From e1949bd7fa86923a19d507146744bc126ad5057b Mon Sep 17 00:00:00 2001 From: qingruiliu Date: Wed, 28 Jan 2026 21:33:34 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20watermark=20=E6=B0=B4=E5=8D=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/index.tsx | 1 + components/types.ts | 1 + components/watermark/PropsType.tsx | 19 + .../__tests__/__snapshots__/demo.test.js.snap | 450 ++++++++++++++++++ components/watermark/__tests__/demo.test.js | 3 + components/watermark/demo/basic.md | 105 ++++ components/watermark/demo/basic.tsx | 93 ++++ components/watermark/index.en-US.md | 30 ++ components/watermark/index.tsx | 129 +++++ components/watermark/index.zh-CN.md | 31 ++ components/watermark/style/index.tsx | 33 ++ rn-kitchen-sink/demoList.js | 6 + tests/__snapshots__/index.test.js.snap | 1 + 13 files changed, 902 insertions(+) create mode 100644 components/watermark/PropsType.tsx create mode 100644 components/watermark/__tests__/__snapshots__/demo.test.js.snap create mode 100644 components/watermark/__tests__/demo.test.js create mode 100644 components/watermark/demo/basic.md create mode 100644 components/watermark/demo/basic.tsx create mode 100644 components/watermark/index.en-US.md create mode 100644 components/watermark/index.tsx create mode 100644 components/watermark/index.zh-CN.md create mode 100644 components/watermark/style/index.tsx diff --git a/components/index.tsx b/components/index.tsx index 4d07b20d3..0e1436e64 100644 --- a/components/index.tsx +++ b/components/index.tsx @@ -50,6 +50,7 @@ export { default as TextareaItem } from './textarea-item/index' export { default as Toast } from './toast/index' export { default as Tooltip } from './tooltip/index' export { default as View } from './view/index' +export { default as Watermark } from './watermark/index' export { default as WhiteSpace } from './white-space/index' export { default as WingBlank } from './wing-blank/index' /** diff --git a/components/types.ts b/components/types.ts index 82dcdf182..4d103fe05 100644 --- a/components/types.ts +++ b/components/types.ts @@ -63,5 +63,6 @@ export type { TextareaItemProps } from './textarea-item/index' export type { ToastProps } from './toast/index' export type { TooltipProps } from './tooltip/PropsType' export type { ViewInterface as ViewProps } from './view/index' +export type { WatermarkProps } from './watermark/PropsType' export type { WhiteSpaceProps } from './white-space/index' export type { WingBlankProps } from './wing-blank/index' diff --git a/components/watermark/PropsType.tsx b/components/watermark/PropsType.tsx new file mode 100644 index 000000000..87c66254e --- /dev/null +++ b/components/watermark/PropsType.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { ImageStyle, StyleProp, TextStyle, ViewStyle } from 'react-native' +import { WatermarkStyle } from './style' + +export interface WatermarkProps { + content?: string | string[] + contentStyle?: StyleProp + image?: string | React.ReactNode + imageStyle?: StyleProp + width?: number + height?: number + gapX?: number + gapY?: number + rotate?: number + foreground?: boolean + children?: React.ReactNode + style?: ViewStyle + styles?: Partial +} diff --git a/components/watermark/__tests__/__snapshots__/demo.test.js.snap b/components/watermark/__tests__/__snapshots__/demo.test.js.snap new file mode 100644 index 000000000..79798efd5 --- /dev/null +++ b/components/watermark/__tests__/__snapshots__/demo.test.js.snap @@ -0,0 +1,450 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`renders ./components/watermark/demo/basic.tsx correctly 1`] = ` + + + + + + + + + 单行 + + + + + + + + + + + 多行 + + + + + + + + + + + 图片 + + + + + + + + + + + 背景层 + + + + + + + + +`; diff --git a/components/watermark/__tests__/demo.test.js b/components/watermark/__tests__/demo.test.js new file mode 100644 index 000000000..d013b2c4c --- /dev/null +++ b/components/watermark/__tests__/demo.test.js @@ -0,0 +1,3 @@ +import rnDemoTest from '../../../tests/shared/demoTest' + +rnDemoTest('watermark') diff --git a/components/watermark/demo/basic.md b/components/watermark/demo/basic.md new file mode 100644 index 000000000..11840a8bd --- /dev/null +++ b/components/watermark/demo/basic.md @@ -0,0 +1,105 @@ +--- +order: 0 +title: + zh-CN: 基本 + en-US: Basic +--- + +[Demo Source Code](https://github.com/ant-design/ant-design-mobile-rn/blob/master/components/watermark/demo/basic.tsx) + +```jsx +import React, { useState } from 'react' +import { Dimensions, StyleSheet, View } from 'react-native' +import { Button, Watermark, WhiteSpace } from '@ant-design/react-native' + +const { height: windowHeight } = Dimensions.get('window') + +export default class WatermarkExample extends React.Component { + constructor(props: any) { + super(props) + this.state = { + props: {}, + foreground: false, + } + } + render() { + const textProps = { + content: ['AntD Mobile'], + width: 100, + height: 40, + } + + const rowsTextProps = { + content: ['AntD Mobile', 'AntD'], + width: 100, + height: 40, + gapY: 10, + } + + const imageProps = { + image: + 'https://gw.alipayobjects.com/zos/rmsportal/BGcxWbIWmgBlIChNOpqp.png', + gapX: 10, + gapY: 10, + imageStyle: { + width: 132, + height: 40, + }, + } + return ( + + + + + + + + + + + + + + + + + ) + } +} + +const styles = StyleSheet.create({ + root: { + width: 200, + marginLeft: 60, + height: windowHeight, + }, +}) + +``` diff --git a/components/watermark/demo/basic.tsx b/components/watermark/demo/basic.tsx new file mode 100644 index 000000000..f77d9ea85 --- /dev/null +++ b/components/watermark/demo/basic.tsx @@ -0,0 +1,93 @@ +import React from 'react' +import { Dimensions, StyleSheet, View } from 'react-native' +import { Button, Watermark, WhiteSpace } from '../..' + +const { height: windowHeight } = Dimensions.get('window') + +export default class WatermarkExample extends React.Component { + constructor(props: any) { + super(props) + this.state = { + props: {}, + foreground: false, + } + } + render() { + const textProps = { + content: ['AntD Mobile'], + width: 100, + height: 40, + } + + const rowsTextProps = { + content: ['AntD Mobile', 'AntD'], + width: 100, + height: 40, + gapY: 10, + } + + const imageProps = { + image: + 'https://gw.alipayobjects.com/zos/rmsportal/BGcxWbIWmgBlIChNOpqp.png', + gapX: 10, + gapY: 10, + imageStyle: { + width: 132, + height: 40, + }, + } + return ( + + + + + + + + + + + + + + + + + ) + } +} + +const styles = StyleSheet.create({ + root: { + width: 200, + marginLeft: 60, + height: windowHeight, + }, +}) diff --git a/components/watermark/index.en-US.md b/components/watermark/index.en-US.md new file mode 100644 index 000000000..9d2c7bc80 --- /dev/null +++ b/components/watermark/index.en-US.md @@ -0,0 +1,30 @@ +--- +category: Components +type: Data Display +title: Watermark +--- + +Add watermark to pages or components for copyright identification or information protection. + +### Rule + +- Useful for scenarios that need to protect content copyright. +- Support both text and image watermark forms. +- Watermark will automatically tile to fill the entire container. + +## API + +| Name | Description | Type | Default | +| ------------- | ------------------------------------------------------------------------------- | ----------------------- | ------- | +| content | Watermark text content, supports string or string array | `string \| string[]` | - | +| contentStyle | Watermark text style | `StyleProp` | - | +| image | Watermark image | `string \| React.ReactNode` | - | +| imageStyle | Watermark image style | `StyleProp` | - | +| width | Width of a single watermark | `number` | `120` | +| height | Height of a single watermark | `number` | `64` | +| gapX | Horizontal spacing between watermarks | `number` | `0` | +| gapY | Vertical spacing between watermarks | `number` | `0` | +| rotate | Rotation angle of watermark (unit: degrees) | `number` | `-22` | +| foreground | Whether to display watermark in foreground layer (`true` means above content) | `boolean` | `true` | +| children | Content that needs watermark | `React.ReactNode` | - | +| style | Container style | `ViewStyle` | - | diff --git a/components/watermark/index.tsx b/components/watermark/index.tsx new file mode 100644 index 000000000..a9375d424 --- /dev/null +++ b/components/watermark/index.tsx @@ -0,0 +1,129 @@ +import React, { useCallback, useMemo, useState } from 'react' +import { Dimensions, Image, LayoutChangeEvent, Text, View } from 'react-native' +import { useTheme } from '../style' + +import WatermarkStyle from './style' + +import { WatermarkProps } from './PropsType' + +const { width: windowWidth, height: windowHeight } = Dimensions.get('window') + +const Watermark: React.FC = ({ + content, + contentStyle, + image, + imageStyle, + height = 64, + width = 120, + gapX = 0, + gapY = 0, + rotate = -22, + foreground = true, + children, + style, + ...rest +}) => { + const styles = useTheme({ + styles: rest.styles, + themeStyles: WatermarkStyle, + }) + const [layout, setLayout] = useState<{ width: number; height: number }>({ + width: windowWidth, + height: windowHeight, + }) + const onChildrenLayout = useCallback((event: LayoutChangeEvent) => { + setLayout(event.nativeEvent.layout) + }, []) + + const _width = width + gapX * 2 + const _height = height + gapY * 2 + const count = + parseInt(String(layout.width / _width), 10) * + parseInt(String(layout.height / _height), 10) + + // ========== watermarkDom ============ + const watermarkDom = useMemo(() => { + let innerDom = null + const _content = typeof content === 'string' ? [content] : content + if (Array.isArray(_content) && _content.length > 0) { + innerDom = ( + <> + {_content.map((item, index) => { + return ( + + {item} + + ) + })} + + ) + } else { + if (typeof image === 'string') { + innerDom = ( + + ) + } else if (React.isValidElement(image)) { + innerDom = image + } + } + return innerDom + }, [content, contentStyle, image, imageStyle]) + + // ========== renderWatermark ============ + const renderWatermark = useCallback(() => { + if (!watermarkDom || isNaN(count) || count <= 0) { + return null + } + const _zIndex = foreground ? 1 : -1 + return ( + + {Array.from({ length: count }).map((_, index) => { + return ( + + {watermarkDom} + + ) + })} + + ) + }, [ + watermarkDom, + count, + foreground, + styles.wmContainer, + styles.wmItem, + _height, + _width, + rotate, + ]) + + return ( + + {renderWatermark()} + + {children} + + + ) +} + +export default Watermark diff --git a/components/watermark/index.zh-CN.md b/components/watermark/index.zh-CN.md new file mode 100644 index 000000000..f344bbf23 --- /dev/null +++ b/components/watermark/index.zh-CN.md @@ -0,0 +1,31 @@ +--- +category: Components +type: Data Display +title: Watermark +subtitle: 水印 +--- + +给页面或组件添加水印,用于标识版权或防止信息泄露。 + +### 规则 + +- 适用于需要保护内容版权的场景。 +- 支持文字和图片两种水印形式。 +- 水印会自动平铺填充整个容器。 + +## API + +| 属性 | 说明 | 类型 | 默认值 | +| ------------- | --------------------------------------------------------- | ----------------------- | ------- | +| content | 水印文字内容,支持字符串或字符串数组 | `string \| string[]` | - | +| contentStyle | 水印文字样式 | `StyleProp` | - | +| image | 水印图片 | `string \| React.ReactNode` | - | +| imageStyle | 水印图片样式 | `StyleProp` | - | +| width | 单个水印的宽度 | `number` | `120` | +| height | 单个水印的高度 | `number` | `64` | +| gapX | 水印之间的水平间距 | `number` | `0` | +| gapY | 水印之间的垂直间距 | `number` | `0` | +| rotate | 水印的旋转角度(单位:度) | `number` | `-22` | +| foreground | 是否将水印显示在前景层(`true` 时水印在内容上方) | `boolean` | `true` | +| children | 需要添加水印的内容 | `React.ReactNode` | - | +| style | 容器样式 | `ViewStyle` | - | diff --git a/components/watermark/style/index.tsx b/components/watermark/style/index.tsx new file mode 100644 index 000000000..37c1527ff --- /dev/null +++ b/components/watermark/style/index.tsx @@ -0,0 +1,33 @@ +import { StyleSheet, ViewStyle } from 'react-native' + +export interface WatermarkStyle { + container: ViewStyle + wmContainer: ViewStyle + wmItem: ViewStyle +} + +export default () => + StyleSheet.create({ + container: { + flex: 1, + position: 'relative', + }, + wmContainer: { + position: 'absolute', + left: 0, + right: 0, + top: 0, + bottom: 0, + backgroundColor: 'rgba(0,0,0,0)', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + alignContent: 'center', + flexWrap: 'wrap', + }, + wmItem: { + backgroundColor: 'rgba(0,0,0,0)', + justifyContent: 'center', + alignItems: 'center', + }, + }) diff --git a/rn-kitchen-sink/demoList.js b/rn-kitchen-sink/demoList.js index d692fdb40..d014f2f97 100644 --- a/rn-kitchen-sink/demoList.js +++ b/rn-kitchen-sink/demoList.js @@ -265,6 +265,12 @@ module.exports = { icon: 'https://os.alipayobjects.com/rmsportal/DUkfOYZVcLctGot.png', module: require('../components/view/demo/basic'), }, + { + title: 'Watermark', + description: '水印', + icon: 'https://os.alipayobjects.com/rmsportal/daARhPjKcxlSuuZ.png', + module: require('../components/watermark/demo/basic'), + }, { title: 'WhiteSpace', description: '上下留白', diff --git a/tests/__snapshots__/index.test.js.snap b/tests/__snapshots__/index.test.js.snap index 123cab0c2..2a8378491 100644 --- a/tests/__snapshots__/index.test.js.snap +++ b/tests/__snapshots__/index.test.js.snap @@ -48,6 +48,7 @@ Array [ "Toast", "Tooltip", "View", + "Watermark", "WhiteSpace", "WingBlank", "ImagePicker", From eafc66850609a2190c7c425d07f9c52c2e472550 Mon Sep 17 00:00:00 2001 From: qingruiliu Date: Thu, 29 Jan 2026 20:55:39 +0800 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20AI=20Review=20=E6=84=8F=E8=A7=81?= =?UTF-8?q?=E9=87=87=E7=BA=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- components/watermark/demo/basic.md | 3 +-- components/watermark/demo/basic.tsx | 5 +---- components/watermark/index.en-US.md | 13 +++++++++++++ components/watermark/index.tsx | 16 +++++----------- components/watermark/index.zh-CN.md | 12 ++++++++++++ components/watermark/style/index.tsx | 12 +++++++++++- 6 files changed, 43 insertions(+), 18 deletions(-) diff --git a/components/watermark/demo/basic.md b/components/watermark/demo/basic.md index 11840a8bd..0646aad3c 100644 --- a/components/watermark/demo/basic.md +++ b/components/watermark/demo/basic.md @@ -8,7 +8,7 @@ title: [Demo Source Code](https://github.com/ant-design/ant-design-mobile-rn/blob/master/components/watermark/demo/basic.tsx) ```jsx -import React, { useState } from 'react' +import React from 'react' import { Dimensions, StyleSheet, View } from 'react-native' import { Button, Watermark, WhiteSpace } from '@ant-design/react-native' @@ -48,7 +48,6 @@ export default class WatermarkExample extends React.Component { } return ( diff --git a/components/watermark/demo/basic.tsx b/components/watermark/demo/basic.tsx index f77d9ea85..fb12a681e 100644 --- a/components/watermark/demo/basic.tsx +++ b/components/watermark/demo/basic.tsx @@ -37,10 +37,7 @@ export default class WatermarkExample extends React.Component { }, } return ( - +