Skip to content

Commit d026c3e

Browse files
Wesley-0808liweijie0812anlyyao
authored
feat(Watermark): Add Watermark Component (#804)
* feat: api and common * feat: watermark * feat: demo * chore: config and docs * chore: test * fix: test * chore: overview docs Co-authored-by: liweijie0812 <[email protected]> * feat(Watermark): update demo --------- Co-authored-by: liweijie0812 <[email protected]> Co-authored-by: anlyyao <[email protected]>
1 parent 8b53f51 commit d026c3e

27 files changed

+865
-4
lines changed

site/docs/overview.en-US.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,6 +375,13 @@ spline: explain
375375
<p class="name">Tag</p>
376376
</a>
377377
</div>
378+
<div class="image-wrapper">
379+
<a class="item" href="./components/watermark">
380+
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark.png" />
381+
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark-dark.png" />
382+
<p class="name">Watermark</p>
383+
</a>
384+
</div>
378385
</section>
379386

380387
<h3>Feedback<em class="tag">13</em></h3>

site/docs/overview.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ spline: explain
240240
</div>
241241
</section>
242242

243-
<h3>数据展示<em class="tag">19</em></h3>
243+
<h3>数据展示<em class="tag">20</em></h3>
244244
<section class="image-group">
245245
<div class="image-wrapper">
246246
<a class="item" href="./components/avatar">
@@ -375,6 +375,13 @@ spline: explain
375375
<p class="name">Tag 标签</p>
376376
</a>
377377
</div>
378+
<div class="image-wrapper">
379+
<a class="item" href="./components/watermark">
380+
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark.png" />
381+
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark-dark.png" />
382+
<p class="name">Watermark 水印</p>
383+
</a>
384+
</div>
378385
</section>
379386

380387
<h3>反馈<em class="tag">13</em></h3>

site/mobile/mobile.config.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,5 +410,11 @@ export default {
410410
name: 'date-time-picker',
411411
component: () => import('tdesign-mobile-react/date-time-picker/_example/index.tsx'),
412412
},
413+
{
414+
title: 'Watermark 水印',
415+
titleEn: 'Watermark',
416+
name: 'watermark',
417+
component: () => import('tdesign-mobile-react/watermark/_example/index.tsx'),
418+
},
413419
],
414420
};

site/style/mobile/demo.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
line-height: 22px;
4242
}
4343

44-
&__slot {
44+
&__slot:not(:empty) {
4545
margin-top: 16px;
4646

4747
&.with-padding {

site/web/site.config.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,14 @@ export const docs = [
479479
component: () => import('tdesign-mobile-react/tag/tag.md'),
480480
componentEn: () => import('tdesign-mobile-react/tag/tag.en-US.md'),
481481
},
482+
{
483+
title: 'Watermark 水印',
484+
titleEn: 'Watermark',
485+
name: 'watermark',
486+
path: '/mobile-react/components/watermark',
487+
component: () => import('tdesign-mobile-react/watermark/watermark.md'),
488+
componentEn: () => import('tdesign-mobile-react/watermark/watermark.en-US.md'),
489+
},
482490
],
483491
},
484492
{

src/hooks/useVariables.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { useMemo, useState } from 'react';
2+
import { THEME_MODE } from '../_common/js/common';
3+
import getColorTokenColor from '../_common/js/utils/getColorTokenColor';
4+
import useMutationObservable from './useMutationObserver';
5+
import { canUseDocument } from '../_util/dom';
6+
7+
const DEFAULT_OPTIONS = {
8+
debounceTime: 250,
9+
config: {
10+
attributes: true,
11+
},
12+
};
13+
14+
/**
15+
* useVariables Hook - 监听CSS变量变化并返回实时值
16+
* @param variables CSS 变量名对象,键为返回对象的属性名,值为CSS变量名(带--前缀)
17+
* @param targetElement 监听的元素,默认为 document?.documentElement
18+
* @returns 包含每个变量当前值的ref对象
19+
* @example
20+
* const { textColor, brandColor } = useVariables({
21+
* textColor: '--td-text-color-primary',
22+
* brandColor: '--td-brand-color',
23+
* });
24+
*
25+
* // 使用变量值
26+
* console.log(textColor.current); // 获取当前文本颜色
27+
*/
28+
function useVariables<T extends Record<string, string>>(
29+
variables: T,
30+
targetElement?: HTMLElement,
31+
): Record<keyof T, string> {
32+
const [, forceUpdate] = useState<Record<string, never>>({});
33+
34+
if (canUseDocument && !targetElement) {
35+
// eslint-disable-next-line no-param-reassign
36+
targetElement = document?.documentElement;
37+
}
38+
39+
// 确保 variables 参数有效
40+
if (!variables || Object.keys(variables).length === 0) {
41+
throw new Error('useVariables: variables parameter cannot be empty');
42+
}
43+
44+
const refs = useMemo(() => {
45+
const values = {} as Record<keyof T, string>;
46+
47+
// 为每个变量创建ref并获取初始值
48+
Object.entries(variables).forEach(([key, varName]) => {
49+
try {
50+
const initialValue = getColorTokenColor(varName);
51+
values[key as keyof T] = initialValue;
52+
} catch (error) {
53+
console.warn(`Failed to get initial value for CSS variable ${varName}:`, error);
54+
values[key as keyof T] = '';
55+
}
56+
});
57+
58+
return values;
59+
}, [variables]);
60+
61+
// 缓存更新函数,避免每次渲染都创建新函数
62+
const updateVariables = () => {
63+
try {
64+
Object.entries(variables).forEach(([key, varName]) => {
65+
const newValue = getColorTokenColor(varName);
66+
if (refs[key as keyof T] && refs[key as keyof T] !== newValue) {
67+
refs[key as keyof T] = newValue;
68+
}
69+
});
70+
forceUpdate({});
71+
} catch (error) {
72+
console.warn('Failed to update CSS variables:', error);
73+
}
74+
};
75+
76+
useMutationObservable(
77+
targetElement,
78+
(mutationsList) => {
79+
// 使用 for 循环而不是 some,提高性能
80+
for (const mutation of mutationsList) {
81+
if (mutation.type === 'attributes' && mutation.attributeName === THEME_MODE) {
82+
updateVariables();
83+
return;
84+
}
85+
}
86+
},
87+
DEFAULT_OPTIONS,
88+
);
89+
90+
// @ts-expect-error
91+
if (!canUseDocument) return {};
92+
93+
return refs;
94+
}
95+
96+
export default useVariables;

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export * from './tree-select';
4141
export * from './upload';
4242

4343
/**
44-
* 数据展示(19个
44+
* 数据展示(20个
4545
*/
4646
export * from './avatar';
4747
export * from './badge';
@@ -62,6 +62,7 @@ export * from './sticky';
6262
export * from './swiper';
6363
export * from './table';
6464
export * from './tag';
65+
export * from './watermark';
6566

6667
/**
6768
* 反馈(13个)

src/watermark/Watermark.tsx

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/* eslint-disable no-nested-ternary */
2+
import React, { useCallback, useState, useEffect, useRef } from 'react';
3+
import classNames from 'classnames';
4+
import generateBase64Url from '../_common/js/watermark/generateBase64Url';
5+
import randomMovingStyle from '../_common/js/watermark/randomMovingStyle';
6+
import injectStyle from '../_common/js/utils/injectStyle';
7+
import { StyledProps } from '../common';
8+
import useConfig from '../hooks/useConfig';
9+
import useMutationObserver from '../hooks/useMutationObserver';
10+
import { TdWatermarkProps } from './type';
11+
import { watermarkDefaultProps } from './defaultProps';
12+
import { getStyleStr } from './utils';
13+
import useDefaultProps from '../hooks/useDefaultProps';
14+
import useVariables from '../hooks/useVariables';
15+
import parseTNode from '../_util/parseTNode';
16+
17+
export interface WatermarkProps extends TdWatermarkProps, StyledProps {}
18+
19+
const Watermark: React.FC<WatermarkProps> = (originalProps) => {
20+
const props = useDefaultProps<WatermarkProps>(originalProps, watermarkDefaultProps);
21+
const {
22+
alpha,
23+
x = 200,
24+
y = 210,
25+
width = 120,
26+
height = 60,
27+
rotate: tempRotate,
28+
zIndex = 10,
29+
lineSpace,
30+
isRepeat,
31+
removable,
32+
movable,
33+
moveInterval,
34+
offset = [],
35+
content,
36+
children,
37+
watermarkContent,
38+
layout,
39+
className,
40+
style = {},
41+
} = props;
42+
43+
const { classPrefix } = useConfig();
44+
45+
let gapX = x;
46+
let gapY = y;
47+
let rotate = tempRotate;
48+
if (movable) {
49+
gapX = 0;
50+
gapY = 0;
51+
rotate = 0;
52+
}
53+
const clsName = `${classPrefix}-watermark`;
54+
const [base64Url, setBase64Url] = useState('');
55+
const styleStr = useRef('');
56+
const watermarkRef = useRef<HTMLDivElement>(null);
57+
const watermarkImgRef = useRef<HTMLDivElement>(null);
58+
const stopObservation = useRef(false);
59+
const offsetLeft = offset[0] || gapX / 2;
60+
const offsetTop = offset[1] || gapY / 2;
61+
const backgroundSize = useRef(`${gapX + width}px`);
62+
63+
const { fontColor } = useVariables({
64+
fontColor: '--td-text-color-watermark',
65+
});
66+
67+
// 水印节点 - 背景base64
68+
useEffect(() => {
69+
generateBase64Url(
70+
{
71+
width,
72+
height,
73+
rotate,
74+
lineSpace,
75+
alpha,
76+
gapX,
77+
gapY,
78+
watermarkContent,
79+
offsetLeft,
80+
offsetTop,
81+
fontColor,
82+
layout,
83+
},
84+
(url, { width }) => {
85+
backgroundSize.current = width ? `${width}px` : null;
86+
setBase64Url(url);
87+
},
88+
);
89+
}, [
90+
width,
91+
height,
92+
rotate,
93+
zIndex,
94+
lineSpace,
95+
alpha,
96+
offsetLeft,
97+
offsetTop,
98+
gapX,
99+
gapY,
100+
watermarkContent,
101+
fontColor,
102+
layout,
103+
]);
104+
105+
// 水印节点 - styleStr
106+
useEffect(() => {
107+
styleStr.current = getStyleStr({
108+
zIndex,
109+
position: 'absolute',
110+
left: 0,
111+
right: 0,
112+
top: 0,
113+
bottom: 0,
114+
width: movable ? `${width}px` : '100%',
115+
height: movable ? `${height}px` : '100%',
116+
backgroundSize: backgroundSize.current || `${gapX + width}px`,
117+
pointerEvents: 'none',
118+
backgroundRepeat: movable ? 'no-repeat' : isRepeat ? 'repeat' : 'no-repeat',
119+
backgroundImage: `url('${base64Url}')`,
120+
animation: movable ? `watermark infinite ${(moveInterval * 4) / 60}s` : 'none',
121+
...style,
122+
});
123+
}, [zIndex, gapX, width, movable, isRepeat, base64Url, moveInterval, style, height, backgroundSize]);
124+
125+
// 水印节点 - 渲染
126+
const renderWatermark = useCallback(() => {
127+
// 停止监听
128+
stopObservation.current = true;
129+
// 删除之前
130+
watermarkImgRef.current?.remove?.();
131+
watermarkImgRef.current = undefined;
132+
// 创建新的
133+
watermarkImgRef.current = document.createElement('div');
134+
watermarkImgRef.current.setAttribute('style', styleStr.current);
135+
watermarkRef.current?.append(watermarkImgRef.current);
136+
// 继续监听
137+
setTimeout(() => {
138+
stopObservation.current = false;
139+
});
140+
}, []);
141+
142+
// 水印节点 - 初始化渲染
143+
useEffect(() => {
144+
renderWatermark();
145+
}, [renderWatermark, zIndex, gapX, width, movable, isRepeat, base64Url, moveInterval, style, className]);
146+
147+
// 水印节点 - 变化时重新渲染
148+
useMutationObserver(watermarkRef.current, (mutations) => {
149+
if (stopObservation.current) return;
150+
if (removable) return;
151+
mutations.forEach((mutation) => {
152+
// 水印节点被删除
153+
if (mutation.type === 'childList') {
154+
const removeNodes = mutation.removedNodes;
155+
removeNodes.forEach((node) => {
156+
const element = node as HTMLElement;
157+
if (element === watermarkImgRef.current) {
158+
renderWatermark();
159+
}
160+
});
161+
}
162+
// 水印节点其他变化
163+
if (mutation.target === watermarkImgRef.current) {
164+
renderWatermark();
165+
}
166+
});
167+
});
168+
169+
// 组件父节点 - 增加keyframes
170+
const parent = useRef<HTMLElement>(null);
171+
useEffect(() => {
172+
parent.current = watermarkRef.current.parentElement;
173+
const keyframesStyle = randomMovingStyle();
174+
injectStyle(keyframesStyle);
175+
}, []);
176+
177+
// 水印节点的父节点 - 防删除
178+
useMutationObserver(typeof document !== 'undefined' ? document.body : null, (mutations) => {
179+
if (removable) return;
180+
mutations.forEach((mutation) => {
181+
if (mutation.type === 'childList') {
182+
const removeNodes = mutation.removedNodes;
183+
removeNodes.forEach((node) => {
184+
const element = node as HTMLElement;
185+
if (element === watermarkRef.current) {
186+
parent.current.appendChild(element);
187+
}
188+
});
189+
}
190+
});
191+
});
192+
193+
return (
194+
<div className={classNames([clsName, className])} ref={watermarkRef}>
195+
{parseTNode(children || content)}
196+
</div>
197+
);
198+
};
199+
200+
export default Watermark;

0 commit comments

Comments
 (0)