Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions site/docs/overview.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,13 @@ spline: explain
<p class="name">Tag</p>
</a>
</div>
<div class="image-wrapper">
<a class="item" href="./components/watermark">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark.png" />
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark-dark.png" />
<p class="name">Watermark</p>
</a>
</div>
</section>

<h3>Feedback<em class="tag">13</em></h3>
Expand Down
9 changes: 8 additions & 1 deletion site/docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ spline: explain
</div>
</section>

<h3>数据展示<em class="tag">19</em></h3>
<h3>数据展示<em class="tag">20</em></h3>
<section class="image-group">
<div class="image-wrapper">
<a class="item" href="./components/avatar">
Expand Down Expand Up @@ -375,6 +375,13 @@ spline: explain
<p class="name">Tag 标签</p>
</a>
</div>
<div class="image-wrapper">
<a class="item" href="./components/watermark">
<img class="__light__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark.png" />
<img class="__dark__" src="https://tdesign.gtimg.com/site/mobile/doc-watermark-dark.png" />
<p class="name">Watermark 水印</p>
</a>
</div>
</section>

<h3>反馈<em class="tag">13</em></h3>
Expand Down
6 changes: 6 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},
],
};
2 changes: 1 addition & 1 deletion site/style/mobile/demo.less
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
line-height: 22px;
}

&__slot {
&__slot:not(:empty) {
margin-top: 16px;

&.with-padding {
Expand Down
8 changes: 8 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
},
],
},
{
Expand Down
96 changes: 96 additions & 0 deletions src/hooks/useVariables.ts
Original file line number Diff line number Diff line change
@@ -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<T extends Record<string, string>>(
variables: T,
targetElement?: HTMLElement,
): Record<keyof T, string> {
const [, forceUpdate] = useState<Record<string, never>>({});

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<keyof T, string>;

// 为每个变量创建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;
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export * from './tree-select';
export * from './upload';

/**
* 数据展示(19个
* 数据展示(20个
*/
export * from './avatar';
export * from './badge';
Expand All @@ -62,6 +62,7 @@ export * from './sticky';
export * from './swiper';
export * from './table';
export * from './tag';
export * from './watermark';

/**
* 反馈(13个)
Expand Down
200 changes: 200 additions & 0 deletions src/watermark/Watermark.tsx
Original file line number Diff line number Diff line change
@@ -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<WatermarkProps> = (originalProps) => {
const props = useDefaultProps<WatermarkProps>(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<HTMLDivElement>(null);
const watermarkImgRef = useRef<HTMLDivElement>(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<HTMLElement>(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 (
<div className={classNames([clsName, className])} ref={watermarkRef}>
{parseTNode(children || content)}
</div>
);
};

export default Watermark;
Loading
Loading