diff --git a/site/docs/overview.en-US.md b/site/docs/overview.en-US.md
index 3b54b31a0..8fc934ff3 100644
--- a/site/docs/overview.en-US.md
+++ b/site/docs/overview.en-US.md
@@ -375,6 +375,13 @@ spline: explain
Tag
+
Feedback13
diff --git a/site/docs/overview.md b/site/docs/overview.md
index 2c41db21d..7de7b1234 100644
--- a/site/docs/overview.md
+++ b/site/docs/overview.md
@@ -240,7 +240,7 @@ spline: explain
-数据展示19
+数据展示20
反馈13
diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js
index cc6ed8825..ffb9612ae 100644
--- a/site/mobile/mobile.config.js
+++ b/site/mobile/mobile.config.js
@@ -410,5 +410,11 @@ export default {
name: 'date-time-picker',
component: () => import('tdesign-mobile-react/date-time-picker/_example/index.tsx'),
},
+ {
+ title: 'Watermark 水印',
+ titleEn: 'Watermark',
+ name: 'watermark',
+ component: () => import('tdesign-mobile-react/watermark/_example/index.tsx'),
+ },
],
};
diff --git a/site/style/mobile/demo.less b/site/style/mobile/demo.less
index 1271515a9..184cf6442 100644
--- a/site/style/mobile/demo.less
+++ b/site/style/mobile/demo.less
@@ -41,7 +41,7 @@
line-height: 22px;
}
- &__slot {
+ &__slot:not(:empty) {
margin-top: 16px;
&.with-padding {
diff --git a/site/web/site.config.js b/site/web/site.config.js
index 7b68fd0a8..9b2b9cfa1 100644
--- a/site/web/site.config.js
+++ b/site/web/site.config.js
@@ -479,6 +479,14 @@ export const docs = [
component: () => import('tdesign-mobile-react/tag/tag.md'),
componentEn: () => import('tdesign-mobile-react/tag/tag.en-US.md'),
},
+ {
+ title: 'Watermark 水印',
+ titleEn: 'Watermark',
+ name: 'watermark',
+ path: '/mobile-react/components/watermark',
+ component: () => import('tdesign-mobile-react/watermark/watermark.md'),
+ componentEn: () => import('tdesign-mobile-react/watermark/watermark.en-US.md'),
+ },
],
},
{
diff --git a/src/_common b/src/_common
index 2030d2719..8538ba896 160000
--- a/src/_common
+++ b/src/_common
@@ -1 +1 @@
-Subproject commit 2030d2719e4253ca8a0539a5c67a182274c2c7b6
+Subproject commit 8538ba896ad100fd716edbd7252fe0d06da8447a
diff --git a/src/hooks/useVariables.ts b/src/hooks/useVariables.ts
new file mode 100644
index 000000000..709281b19
--- /dev/null
+++ b/src/hooks/useVariables.ts
@@ -0,0 +1,96 @@
+import { useMemo, useState } from 'react';
+import { THEME_MODE } from '../_common/js/common';
+import getColorTokenColor from '../_common/js/utils/getColorTokenColor';
+import useMutationObservable from './useMutationObserver';
+import { canUseDocument } from '../_util/dom';
+
+const DEFAULT_OPTIONS = {
+ debounceTime: 250,
+ config: {
+ attributes: true,
+ },
+};
+
+/**
+ * useVariables Hook - 监听CSS变量变化并返回实时值
+ * @param variables CSS 变量名对象,键为返回对象的属性名,值为CSS变量名(带--前缀)
+ * @param targetElement 监听的元素,默认为 document?.documentElement
+ * @returns 包含每个变量当前值的ref对象
+ * @example
+ * const { textColor, brandColor } = useVariables({
+ * textColor: '--td-text-color-primary',
+ * brandColor: '--td-brand-color',
+ * });
+ *
+ * // 使用变量值
+ * console.log(textColor.current); // 获取当前文本颜色
+ */
+function useVariables>(
+ variables: T,
+ targetElement?: HTMLElement,
+): Record {
+ const [, forceUpdate] = useState>({});
+
+ if (canUseDocument && !targetElement) {
+ // eslint-disable-next-line no-param-reassign
+ targetElement = document?.documentElement;
+ }
+
+ // 确保 variables 参数有效
+ if (!variables || Object.keys(variables).length === 0) {
+ throw new Error('useVariables: variables parameter cannot be empty');
+ }
+
+ const refs = useMemo(() => {
+ const values = {} as Record;
+
+ // 为每个变量创建ref并获取初始值
+ Object.entries(variables).forEach(([key, varName]) => {
+ try {
+ const initialValue = getColorTokenColor(varName);
+ values[key as keyof T] = initialValue;
+ } catch (error) {
+ console.warn(`Failed to get initial value for CSS variable ${varName}:`, error);
+ values[key as keyof T] = '';
+ }
+ });
+
+ return values;
+ }, [variables]);
+
+ // 缓存更新函数,避免每次渲染都创建新函数
+ const updateVariables = () => {
+ try {
+ Object.entries(variables).forEach(([key, varName]) => {
+ const newValue = getColorTokenColor(varName);
+ if (refs[key as keyof T] && refs[key as keyof T] !== newValue) {
+ refs[key as keyof T] = newValue;
+ }
+ });
+ forceUpdate({});
+ } catch (error) {
+ console.warn('Failed to update CSS variables:', error);
+ }
+ };
+
+ useMutationObservable(
+ targetElement,
+ (mutationsList) => {
+ // 使用 for 循环而不是 some,提高性能
+ for (const mutation of mutationsList) {
+ if (mutation.type === 'attributes' && mutation.attributeName === THEME_MODE) {
+ updateVariables();
+ return;
+ }
+ }
+ },
+ DEFAULT_OPTIONS,
+ );
+
+ // @ts-expect-error
+ if (!canUseDocument) return {};
+
+ return refs;
+}
+
+export default useVariables;
diff --git a/src/index.ts b/src/index.ts
index b41440082..b70d3b4ac 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -41,7 +41,7 @@ export * from './tree-select';
export * from './upload';
/**
- * 数据展示(19个)
+ * 数据展示(20个)
*/
export * from './avatar';
export * from './badge';
@@ -62,6 +62,7 @@ export * from './sticky';
export * from './swiper';
export * from './table';
export * from './tag';
+export * from './watermark';
/**
* 反馈(13个)
diff --git a/src/watermark/Watermark.tsx b/src/watermark/Watermark.tsx
new file mode 100644
index 000000000..183f407ea
--- /dev/null
+++ b/src/watermark/Watermark.tsx
@@ -0,0 +1,200 @@
+/* eslint-disable no-nested-ternary */
+import React, { useCallback, useState, useEffect, useRef } from 'react';
+import classNames from 'classnames';
+import generateBase64Url from '../_common/js/watermark/generateBase64Url';
+import randomMovingStyle from '../_common/js/watermark/randomMovingStyle';
+import injectStyle from '../_common/js/utils/injectStyle';
+import { StyledProps } from '../common';
+import useConfig from '../hooks/useConfig';
+import useMutationObserver from '../hooks/useMutationObserver';
+import { TdWatermarkProps } from './type';
+import { watermarkDefaultProps } from './defaultProps';
+import { getStyleStr } from './utils';
+import useDefaultProps from '../hooks/useDefaultProps';
+import useVariables from '../hooks/useVariables';
+import parseTNode from '../_util/parseTNode';
+
+export interface WatermarkProps extends TdWatermarkProps, StyledProps {}
+
+const Watermark: React.FC = (originalProps) => {
+ const props = useDefaultProps(originalProps, watermarkDefaultProps);
+ const {
+ alpha,
+ x = 200,
+ y = 210,
+ width = 120,
+ height = 60,
+ rotate: tempRotate,
+ zIndex = 10,
+ lineSpace,
+ isRepeat,
+ removable,
+ movable,
+ moveInterval,
+ offset = [],
+ content,
+ children,
+ watermarkContent,
+ layout,
+ className,
+ style = {},
+ } = props;
+
+ const { classPrefix } = useConfig();
+
+ let gapX = x;
+ let gapY = y;
+ let rotate = tempRotate;
+ if (movable) {
+ gapX = 0;
+ gapY = 0;
+ rotate = 0;
+ }
+ const clsName = `${classPrefix}-watermark`;
+ const [base64Url, setBase64Url] = useState('');
+ const styleStr = useRef('');
+ const watermarkRef = useRef(null);
+ const watermarkImgRef = useRef(null);
+ const stopObservation = useRef(false);
+ const offsetLeft = offset[0] || gapX / 2;
+ const offsetTop = offset[1] || gapY / 2;
+ const backgroundSize = useRef(`${gapX + width}px`);
+
+ const { fontColor } = useVariables({
+ fontColor: '--td-text-color-watermark',
+ });
+
+ // 水印节点 - 背景base64
+ useEffect(() => {
+ generateBase64Url(
+ {
+ width,
+ height,
+ rotate,
+ lineSpace,
+ alpha,
+ gapX,
+ gapY,
+ watermarkContent,
+ offsetLeft,
+ offsetTop,
+ fontColor,
+ layout,
+ },
+ (url, { width }) => {
+ backgroundSize.current = width ? `${width}px` : null;
+ setBase64Url(url);
+ },
+ );
+ }, [
+ width,
+ height,
+ rotate,
+ zIndex,
+ lineSpace,
+ alpha,
+ offsetLeft,
+ offsetTop,
+ gapX,
+ gapY,
+ watermarkContent,
+ fontColor,
+ layout,
+ ]);
+
+ // 水印节点 - styleStr
+ useEffect(() => {
+ styleStr.current = getStyleStr({
+ zIndex,
+ position: 'absolute',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
+ width: movable ? `${width}px` : '100%',
+ height: movable ? `${height}px` : '100%',
+ backgroundSize: backgroundSize.current || `${gapX + width}px`,
+ pointerEvents: 'none',
+ backgroundRepeat: movable ? 'no-repeat' : isRepeat ? 'repeat' : 'no-repeat',
+ backgroundImage: `url('${base64Url}')`,
+ animation: movable ? `watermark infinite ${(moveInterval * 4) / 60}s` : 'none',
+ ...style,
+ });
+ }, [zIndex, gapX, width, movable, isRepeat, base64Url, moveInterval, style, height, backgroundSize]);
+
+ // 水印节点 - 渲染
+ const renderWatermark = useCallback(() => {
+ // 停止监听
+ stopObservation.current = true;
+ // 删除之前
+ watermarkImgRef.current?.remove?.();
+ watermarkImgRef.current = undefined;
+ // 创建新的
+ watermarkImgRef.current = document.createElement('div');
+ watermarkImgRef.current.setAttribute('style', styleStr.current);
+ watermarkRef.current?.append(watermarkImgRef.current);
+ // 继续监听
+ setTimeout(() => {
+ stopObservation.current = false;
+ });
+ }, []);
+
+ // 水印节点 - 初始化渲染
+ useEffect(() => {
+ renderWatermark();
+ }, [renderWatermark, zIndex, gapX, width, movable, isRepeat, base64Url, moveInterval, style, className]);
+
+ // 水印节点 - 变化时重新渲染
+ useMutationObserver(watermarkRef.current, (mutations) => {
+ if (stopObservation.current) return;
+ if (removable) return;
+ mutations.forEach((mutation) => {
+ // 水印节点被删除
+ if (mutation.type === 'childList') {
+ const removeNodes = mutation.removedNodes;
+ removeNodes.forEach((node) => {
+ const element = node as HTMLElement;
+ if (element === watermarkImgRef.current) {
+ renderWatermark();
+ }
+ });
+ }
+ // 水印节点其他变化
+ if (mutation.target === watermarkImgRef.current) {
+ renderWatermark();
+ }
+ });
+ });
+
+ // 组件父节点 - 增加keyframes
+ const parent = useRef(null);
+ useEffect(() => {
+ parent.current = watermarkRef.current.parentElement;
+ const keyframesStyle = randomMovingStyle();
+ injectStyle(keyframesStyle);
+ }, []);
+
+ // 水印节点的父节点 - 防删除
+ useMutationObserver(typeof document !== 'undefined' ? document.body : null, (mutations) => {
+ if (removable) return;
+ mutations.forEach((mutation) => {
+ if (mutation.type === 'childList') {
+ const removeNodes = mutation.removedNodes;
+ removeNodes.forEach((node) => {
+ const element = node as HTMLElement;
+ if (element === watermarkRef.current) {
+ parent.current.appendChild(element);
+ }
+ });
+ }
+ });
+ });
+
+ return (
+
+ {parseTNode(children || content)}
+
+ );
+};
+
+export default Watermark;
diff --git a/src/watermark/__tests__/watermark.test.tsx b/src/watermark/__tests__/watermark.test.tsx
new file mode 100644
index 000000000..271909122
--- /dev/null
+++ b/src/watermark/__tests__/watermark.test.tsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { beforeAll, beforeEach, describe, expect, test, vi } from '@test/utils';
+import { render } from '@testing-library/react';
+import Watermark from '../Watermark';
+
+describe('Watermark', () => {
+ const childTestID = 'childTestID';
+ const mockGetCanvasContext = vi.spyOn(HTMLCanvasElement.prototype, 'getContext');
+ const mockGetCanvasToDataURL = vi.spyOn(HTMLCanvasElement.prototype, 'toDataURL');
+
+ beforeAll(() => {
+ mockGetCanvasContext.mockReturnValue({
+ drawImage: vi.fn(),
+ getImageData: vi.fn(),
+ putImageData: vi.fn(),
+ translate: vi.fn(),
+ rotate: vi.fn(),
+ fillRect: vi.fn(),
+ globalAlpha: 0.5,
+ font: '',
+ textAlign: '',
+ textBaseline: '',
+ fillStyle: '',
+ fillText: vi.fn(),
+ });
+ mockGetCanvasToDataURL.mockReturnValue('test');
+ });
+
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ function renderWatermark(watermark) {
+ const { container } = render(watermark);
+ return container.firstChild;
+ }
+
+ test('base', async () => {
+ const watermark = renderWatermark(
+
+
+ ,
+ );
+ expect(watermark).toHaveClass('t-watermark');
+ expect(watermark.lastChild).toHaveStyle({ 'background-repeat': 'repeat' });
+ });
+
+ test('movable ', async () => {
+ const watermark = renderWatermark(
+
+
+ ,
+ );
+ expect(watermark.lastChild).toHaveStyle({ animation: 'watermark infinite 1s' });
+ });
+
+ test('mutationObserver', async () => {
+ const wrapper = render(
+
+
+ ,
+ );
+ const child = document.createElement('div');
+ child.innerText = 'testing';
+ child.id = 'test';
+
+ const watermarkWrapCls = wrapper.container.querySelector('.test-observer');
+ expect(watermarkWrapCls).not.toBeNull();
+ const watermarkWrap = wrapper.container.querySelector('.t-watermark');
+ const watermarkWrapParent = watermarkWrap.parentElement;
+ const watermarkEle = watermarkWrap.querySelectorAll('div')[1];
+
+ // 删除了水印wrap元素,还会被立即追加回去
+ watermarkWrapParent.removeChild(watermarkWrap);
+ const afterWrapRemove = watermarkWrapParent.querySelector('.t-watermark');
+ expect(afterWrapRemove).toBeNull();
+ await vi.advanceTimersByTime(10);
+ const waitAfterWrapRemove = watermarkWrapParent.querySelector('.t-watermark');
+ expect(waitAfterWrapRemove).not.toBeNull();
+
+ // 删除了水印元素,还会被立即追加回去
+ waitAfterWrapRemove.removeChild(watermarkEle);
+ expect(waitAfterWrapRemove.querySelectorAll('div').length).toBe(1);
+ // const afterMarkRemove = waitAfterWrapRemove.querySelectorAll('div')[1];
+ // expect(afterMarkRemove).toBeNull();
+ await vi.advanceTimersByTime(10);
+ const waitAfterMarkRemove = waitAfterWrapRemove.querySelectorAll('div')[1];
+ expect(waitAfterMarkRemove).not.toBeNull();
+
+ // 修改水印元素的属性,会立即还原
+ watermarkEle.setAttribute('any', '11');
+ await vi.advanceTimersByTime(10);
+ const waitAfterAttrChange = watermarkWrap.querySelectorAll('div')[1];
+ expect(waitAfterAttrChange.getAttribute('any')).toBeNull();
+ });
+});
diff --git a/src/watermark/_example/base.tsx b/src/watermark/_example/base.tsx
new file mode 100644
index 000000000..a3256bee9
--- /dev/null
+++ b/src/watermark/_example/base.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function BaseWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/_example/gray.tsx b/src/watermark/_example/gray.tsx
new file mode 100644
index 000000000..0290b7b8a
--- /dev/null
+++ b/src/watermark/_example/gray.tsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function GrayscaleWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/_example/image.tsx b/src/watermark/_example/image.tsx
new file mode 100644
index 000000000..b123dd99a
--- /dev/null
+++ b/src/watermark/_example/image.tsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function ImageWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/_example/index.tsx b/src/watermark/_example/index.tsx
new file mode 100644
index 000000000..446b31f2b
--- /dev/null
+++ b/src/watermark/_example/index.tsx
@@ -0,0 +1,49 @@
+import React from 'react';
+import TDemoBlock from '../../../site/mobile/components/DemoBlock';
+import BaseDemo from './base';
+import ImageDemo from './image';
+import GaryDemo from './gray';
+import MultiLineDemo from './multiLine';
+import MultiLineGaryDemo from './multiLineGray';
+import MovingTextDemo from './movingText';
+import MovingImageDemo from './movingImage';
+import LayoutDemo from './layout';
+
+export default function Base() {
+ return (
+
+
Watermark 标签
+
给页面的某个区域加上水印。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/watermark/_example/layout.tsx b/src/watermark/_example/layout.tsx
new file mode 100644
index 000000000..458b45860
--- /dev/null
+++ b/src/watermark/_example/layout.tsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function LayoutWatermark() {
+ return (
+ <>
+
+ Rectangular Grid 矩形布局
+
+
+
+
+
+
+ Hexagonal Grid 六边形网格
+
+
+
+
+ >
+ );
+}
diff --git a/src/watermark/_example/movingImage.tsx b/src/watermark/_example/movingImage.tsx
new file mode 100644
index 000000000..add40ae74
--- /dev/null
+++ b/src/watermark/_example/movingImage.tsx
@@ -0,0 +1,17 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function MovingImageWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/_example/movingText.tsx b/src/watermark/_example/movingText.tsx
new file mode 100644
index 000000000..c3845a7d6
--- /dev/null
+++ b/src/watermark/_example/movingText.tsx
@@ -0,0 +1,10 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function MovingTextWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/_example/multiLine.tsx b/src/watermark/_example/multiLine.tsx
new file mode 100644
index 000000000..1504425eb
--- /dev/null
+++ b/src/watermark/_example/multiLine.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function MultilineWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/_example/multiLineGray.tsx b/src/watermark/_example/multiLineGray.tsx
new file mode 100644
index 000000000..9342268dc
--- /dev/null
+++ b/src/watermark/_example/multiLineGray.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { Watermark } from 'tdesign-mobile-react';
+
+export default function MultilineWatermark() {
+ return (
+
+
+
+ );
+}
diff --git a/src/watermark/defaultProps.ts b/src/watermark/defaultProps.ts
new file mode 100644
index 000000000..6b8e14f82
--- /dev/null
+++ b/src/watermark/defaultProps.ts
@@ -0,0 +1,16 @@
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TdWatermarkProps } from './type';
+
+export const watermarkDefaultProps: TdWatermarkProps = {
+ alpha: 1,
+ isRepeat: true,
+ layout: 'rectangular',
+ lineSpace: 16,
+ movable: false,
+ moveInterval: 3000,
+ removable: true,
+ rotate: -22,
+};
diff --git a/src/watermark/index.ts b/src/watermark/index.ts
new file mode 100644
index 000000000..739ea3b84
--- /dev/null
+++ b/src/watermark/index.ts
@@ -0,0 +1,10 @@
+import _Watermark from './Watermark';
+
+import './style/index.js';
+
+export type { WatermarkProps } from './Watermark';
+
+export * from './type';
+
+export const Watermark = _Watermark;
+export default Watermark;
diff --git a/src/watermark/style/css.js b/src/watermark/style/css.js
new file mode 100644
index 000000000..6a9a4b132
--- /dev/null
+++ b/src/watermark/style/css.js
@@ -0,0 +1 @@
+import './index.css';
diff --git a/src/watermark/style/index.js b/src/watermark/style/index.js
new file mode 100644
index 000000000..04c9fdd7a
--- /dev/null
+++ b/src/watermark/style/index.js
@@ -0,0 +1 @@
+import '../../_common/style/mobile/components/watermark/_index.less';
diff --git a/src/watermark/type.ts b/src/watermark/type.ts
new file mode 100644
index 000000000..a42173293
--- /dev/null
+++ b/src/watermark/type.ts
@@ -0,0 +1,127 @@
+/* eslint-disable */
+
+/**
+ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
+ * */
+
+import { TNode } from '../common';
+
+export interface TdWatermarkProps {
+ /**
+ * 水印整体透明度,取值范围 [0-1]
+ * @default 1
+ */
+ alpha?: number;
+ /**
+ * 水印所覆盖的内容节点,同 `content`
+ */
+ children?: TNode;
+ /**
+ * 水印所覆盖的内容节点
+ */
+ content?: TNode;
+ /**
+ * 水印高度
+ */
+ height?: number;
+ /**
+ * 水印是否重复出现
+ * @default true
+ */
+ isRepeat?: boolean;
+ /**
+ * 水印的布局方式,rectangular:矩形,即横平竖直的水印;hexagonal:六边形,即错位的水印
+ * @default rectangular
+ */
+ layout?: 'rectangular' | 'hexagonal';
+ /**
+ * 行间距,只作用在多行(`content` 配置为数组)情况下
+ * @default 16
+ */
+ lineSpace?: number;
+ /**
+ * 水印是否可移动
+ * @default false
+ */
+ movable?: boolean;
+ /**
+ * 水印发生运动位移的间隙,单位:毫秒
+ * @default 3000
+ */
+ moveInterval?: number;
+ /**
+ * 水印在画布上绘制的水平和垂直偏移量,正常情况下水印绘制在中间位置,即 `offset = [gapX / 2, gapY / 2]`
+ */
+ offset?: Array;
+ /**
+ * 水印是否可被删除
+ * @default true
+ */
+ removable?: boolean;
+ /**
+ * 水印旋转的角度,单位 °
+ * @default -22
+ */
+ rotate?: number;
+ /**
+ * 水印内容,需要显示多行情况下可配置为数组
+ */
+ watermarkContent?: WatermarkText | WatermarkImage | Array;
+ /**
+ * 水印宽度
+ */
+ width?: number;
+ /**
+ * 水印之间的水平间距
+ */
+ x?: number;
+ /**
+ * 水印之间的垂直间距
+ */
+ y?: number;
+ /**
+ * 水印元素的 `z-index`,默认值写在 CSS 中
+ */
+ zIndex?: number;
+}
+
+export interface WatermarkText {
+ /**
+ * 水印文本文字颜色
+ * @default rgba(0,0,0,0.1)
+ */
+ fontColor?: string;
+ /**
+ * 水印文本文字字体
+ * @default ''
+ */
+ fontFamily?: string;
+ /**
+ * 水印文本文字大小
+ * @default 16
+ */
+ fontSize?: number;
+ /**
+ * 水印文本文字粗细
+ * @default normal
+ */
+ fontWeight?: 'normal' | 'lighter' | 'bold' | 'bolder';
+ /**
+ * 水印文本内容
+ * @default ''
+ */
+ text?: string;
+}
+
+export interface WatermarkImage {
+ /**
+ * 水印图片是否需要灰阶显示
+ * @default false
+ */
+ isGrayscale?: boolean;
+ /**
+ * 水印图片源地址,为了显示清楚,建议导出 2 倍或 3 倍图
+ * @default ''
+ */
+ url?: string;
+}
diff --git a/src/watermark/utils.ts b/src/watermark/utils.ts
new file mode 100644
index 000000000..3e80996bf
--- /dev/null
+++ b/src/watermark/utils.ts
@@ -0,0 +1,7 @@
+const toLowercaseSeparator = (key: string) => key.replace(/([A-Z])/g, '-$1').toLowerCase();
+
+// style对象转字符串
+export const getStyleStr = (style: React.CSSProperties): string =>
+ Object.keys(style)
+ .map((key: keyof React.CSSProperties) => `${toLowercaseSeparator(key)}: ${style[key]};`)
+ .join(' ');
diff --git a/src/watermark/watermark.en-US.md b/src/watermark/watermark.en-US.md
new file mode 100644
index 000000000..8a450ed2d
--- /dev/null
+++ b/src/watermark/watermark.en-US.md
@@ -0,0 +1,45 @@
+:: BASE_DOC ::
+
+## API
+
+
+### Watermark Props
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+className | String | - | className of component | N
+style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
+alpha | Number | 1 | \- | N
+children | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
+content | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
+height | Number | - | \- | N
+isRepeat | Boolean | true | \- | N
+layout | String | rectangular | options: rectangular/hexagonal | N
+lineSpace | Number | 16 | \- | N
+movable | Boolean | false | \- | N
+moveInterval | Number | 3000 | \- | N
+offset | Array | - | Typescript:`Array` | N
+removable | Boolean | true | \- | N
+rotate | Number | -22 | \- | N
+watermarkContent | Object / Array | - | Typescript:`WatermarkText\|WatermarkImage\|Array` | N
+width | Number | - | \- | N
+x | Number | - | \- | N
+y | Number | - | \- | N
+zIndex | Number | - | \- | N
+
+### WatermarkText
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+fontColor | String | rgba(0,0,0,0.1) | \- | N
+fontFamily | String | - | font-family configuration for watermark text | N
+fontSize | Number | 16 | \- | N
+fontWeight | String | normal | options: normal/lighter/bold/bolder | N
+text | String | - | \- | N
+
+### WatermarkImage
+
+name | type | default | description | required
+-- | -- | -- | -- | --
+isGrayscale | Boolean | false | \- | N
+url | String | - | \- | N
diff --git a/src/watermark/watermark.md b/src/watermark/watermark.md
new file mode 100644
index 000000000..a7189ecf3
--- /dev/null
+++ b/src/watermark/watermark.md
@@ -0,0 +1,45 @@
+:: BASE_DOC ::
+
+## API
+
+
+### Watermark Props
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+className | String | - | 类名 | N
+style | Object | - | 样式,TS 类型:`React.CSSProperties` | N
+alpha | Number | 1 | 水印整体透明度,取值范围 [0-1] | N
+children | TNode | - | 水印所覆盖的内容节点,同 `content`。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
+content | TNode | - | 水印所覆盖的内容节点。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
+height | Number | - | 水印高度 | N
+isRepeat | Boolean | true | 水印是否重复出现 | N
+layout | String | rectangular | 水印的布局方式,rectangular:矩形,即横平竖直的水印;hexagonal:六边形,即错位的水印。可选项:rectangular/hexagonal | N
+lineSpace | Number | 16 | 行间距,只作用在多行(`content` 配置为数组)情况下 | N
+movable | Boolean | false | 水印是否可移动 | N
+moveInterval | Number | 3000 | 水印发生运动位移的间隙,单位:毫秒 | N
+offset | Array | - | 水印在画布上绘制的水平和垂直偏移量,正常情况下水印绘制在中间位置,即 `offset = [gapX / 2, gapY / 2]`。TS 类型:`Array` | N
+removable | Boolean | true | 水印是否可被删除 | N
+rotate | Number | -22 | 水印旋转的角度,单位 ° | N
+watermarkContent | Object / Array | - | 水印内容,需要显示多行情况下可配置为数组。TS 类型:`WatermarkText\|WatermarkImage\|Array` | N
+width | Number | - | 水印宽度 | N
+x | Number | - | 水印之间的水平间距 | N
+y | Number | - | 水印之间的垂直间距 | N
+zIndex | Number | - | 水印元素的 `z-index`,默认值写在 CSS 中 | N
+
+### WatermarkText
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+fontColor | String | rgba(0,0,0,0.1) | 水印文本文字颜色 | N
+fontFamily | String | - | 水印文本文字字体 | N
+fontSize | Number | 16 | 水印文本文字大小 | N
+fontWeight | String | normal | 水印文本文字粗细。可选项:normal/lighter/bold/bolder | N
+text | String | - | 水印文本内容 | N
+
+### WatermarkImage
+
+名称 | 类型 | 默认值 | 描述 | 必传
+-- | -- | -- | -- | --
+isGrayscale | Boolean | false | 水印图片是否需要灰阶显示 | N
+url | String | - | 水印图片源地址,为了显示清楚,建议导出 2 倍或 3 倍图 | N