前端实现水印效果

今天逛github,后台管理系统的时候,看见满多系统前端页面都有水印效果,就尝试实现一下,直接上代码

react版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import React, {useState, useEffect, useRef} from 'react';

const WatermarkStaggered = ({
text = "Mr.彭涛",
textColor = "rgba(180, 180, 180, 0.6)", // 水印文字颜色和透明度
fontSize = 18, // 水印文字大小
angle = -30, // 旋转角度
rowHeight = 150, // 每个水印“行”的近似高度 (px)
zIndex = -1, // z-index
}) => {
const [watermarkItems, setWatermarkItems] = useState([]);
const containerRef = useRef(null);
useEffect(() => {
const calculateAndSetWatermarks = () => {
if (!containerRef.current) {
return;
}

const {clientWidth, clientHeight} = containerRef.current;
if (clientWidth === 0 || clientHeight === 0) {
return;
}
const newItems = [];
const numEffectiveRows = Math.ceil(clientHeight / rowHeight);
let itemKey = 0;
for (let i = 0; i < numEffectiveRows; i++) {
const isFourItemsRow = i % 2 === 0; // 0, 2, 4...行是4个;1, 3, 5...行是3个
const itemsInThisRow = isFourItemsRow ? 4 : 3;
// 计算当前行水印的Y轴中心位置
const currentY = (i + 0.5) * rowHeight;
for (let j = 0; j < itemsInThisRow; j++) {
// 计算当前水印在行内的X轴中心位置 (百分比)
const currentXPercent = ((j + 0.5) / itemsInThisRow) * 100;
newItems.push({
id: `staggered-wm-${itemKey++}`,
style: {
position: 'absolute',
top: `${currentY}px`,
left: `${currentXPercent}%`,
transform: `translate(-50%, -50%) rotate(${angle}deg)`, // 使计算点为文本中心
fontSize: `${fontSize}px`,
color: textColor,
whiteSpace: 'nowrap', // 防止文本换行
userSelect: 'none', // 禁止选中文本
pointerEvents: 'none', // 允许点击穿透
},
});
}
}
setWatermarkItems(newItems);
};


// 延迟初次计算,确保容器尺寸稳定后再执行水印布局计算。
const timerId = setTimeout(calculateAndSetWatermarks, 0);

// 创建 ResizeObserver 实例,用于监听容器DOM元素的尺寸变化。
const resizeObserver = new ResizeObserver(entries => {
// 当容器尺寸变化时,entries[0]会包含变化信息。
if (entries && entries[0]) {
calculateAndSetWatermarks(); // 重新计算并设置水印布局。
}
});

// 如果容器的ref已经绑定到DOM元素,则开始观察其尺寸。
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}

// 清理函数:在组件卸载或此Effect下一次运行前执行。
return () => {
clearTimeout(timerId); // 清除可能还未执行的延迟计算。

if (containerRef.current) {
// 停止对容器DOM元素的尺寸监听,防止内存泄漏。
// eslint-disable-next-line react-hooks/exhaustive-deps
resizeObserver.unobserve(containerRef.current);
}
// 或者直接调用 resizeObserver.disconnect(); 来停止所有观察。
};
}, [text, textColor, fontSize, angle, rowHeight]); // 依赖项数组:

return (
<div
ref={containerRef}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden', // 防止旋转的文本溢出导致滚动条
zIndex: zIndex,
pointerEvents: 'none', // 容器本身也应允许事件穿透
}}
>
{watermarkItems.map(wm => (
<div key={wm.id} style={wm.style}>
{text}
</div>
))}
</div>
);
};

export default WatermarkStaggered;

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// src/App.js
import React from 'react';
import Watermark from './components/Watermark';
import SamplePage from './pages/SamplePage';
import './App.css';

function App() {
return (
<div className="app-container">
<Watermark text="Mr.彭涛" zIndex={10}/>
<div className="content-wrapper">
<header className="app-header">
<h1>后台管理系统</h1>
</header>
<main className="app-main">
<SamplePage title="最新更新" />
<SamplePage title="数据分析" customContent="这里是数据分析页面的特定内容。" />
{/* 在这里可以集成 React Router 来管理多个页面 */}
</main>
<footer className="app-footer">
<p>&copy; 2025 Your Company</p>
</footer>
</div>
</div>
);
}

export default App;

vue版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
<template>
<div
ref="containerRef"
:style="{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
overflow: 'hidden', // 防止旋转的文本溢出导致滚动条
zIndex: props.zIndex,
pointerEvents: 'none', // 容器本身也应允许事件穿透
}"
>
<div
v-for="wm in watermarkItems"
:key="wm.id"
:style="wm.style"
>
{{ props.text }}
</div>
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';

// 1. 定义 Props,与 React 版本保持一致
const props = defineProps({
text: {
type: String,
default: "Mr.彭涛",
},
textColor: {
type: String,
default: "rgba(180, 180, 180, 0.6)", // 与React代码中的默认值一致
},
fontSize: {
type: Number,
default: 18,
},
angle: {
type: Number,
default: -30,
},
rowHeight: {
type: Number,
default: 150,
},
zIndex: {
type: Number,
default: -1,
},
});

// 2. 定义响应式状态和模板引用
const watermarkItems = ref([]); // 存储计算出的水印项
const containerRef = ref(null); // 用于获取容器DOM元素的引用

// 3. 水印计算逻辑 (与React版本核心逻辑相同)
const calculateAndSetWatermarks = () => {
if (!containerRef.value) {
// 容器DOM元素尚未准备好
return;
}

const { clientWidth, clientHeight } = containerRef.value;
if (clientWidth === 0 || clientHeight === 0) {
// 容器尺寸为0,无法计算
return;
}

const newItems = [];
const numEffectiveRows = Math.ceil(clientHeight / props.rowHeight);
let itemKey = 0;

for (let i = 0; i < numEffectiveRows; i++) {
const isFourItemsRow = i % 2 === 0;
const itemsInThisRow = isFourItemsRow ? 4 : 3;
const currentY = (i + 0.5) * props.rowHeight;

for (let j = 0; j < itemsInThisRow; j++) {
const currentXPercent = ((j + 0.5) / itemsInThisRow) * 100;
newItems.push({
id: `staggered-wm-${itemKey++}`,
style: {
position: 'absolute',
top: `${currentY}px`,
left: `${currentXPercent}%`,
transform: `translate(-50%, -50%) rotate(${props.angle}deg)`,
fontSize: `${props.fontSize}px`,
color: props.textColor,
whiteSpace: 'nowrap',
userSelect: 'none',
pointerEvents: 'none',
},
});
}
}
watermarkItems.value = newItems;
};

// 4. 处理生命周期和响应式更新
let resizeObserverInstance = null;

onMounted(() => {
// 组件挂载后,DOM元素可用
// 使用 nextTick 确保在DOM完全渲染和尺寸计算稳定后再执行初次计算
// 这类似于React中useEffect内使用setTimeout(fn, 0)的效果
nextTick(() => {
calculateAndSetWatermarks();
});

// 创建并启动 ResizeObserver
if (containerRef.value) {
resizeObserverInstance = new ResizeObserver(() => {
// 当容器尺寸变化时,重新计算水印
calculateAndSetWatermarks();
});
resizeObserverInstance.observe(containerRef.value);
}
});

onUnmounted(() => {
// 组件卸载前,清理 ResizeObserver
if (resizeObserverInstance) {
if (containerRef.value) { // 确保元素仍存在,尽管通常observer会自己处理
resizeObserverInstance.unobserve(containerRef.value);
}
resizeObserverInstance.disconnect(); // 更彻底的清理
resizeObserverInstance = null;
}
});

// 监听影响布局的props的变化
watch(
// 监听的源:一个返回包含所有相关props的数组的getter函数
() => [props.text, props.textColor, props.fontSize, props.angle, props.rowHeight],
() => {
// 当任何一个被监听的prop变化时,重新计算水印
// 同样使用nextTick,以防prop变化引起DOM reflow影响尺寸读取
nextTick(() => {
calculateAndSetWatermarks();
});
},
{
// deep: false, // 对于这些基本类型和顶层对象属性,不需要深度监听
// immediate: false // 不在watcher创建时立即执行,onMounted已处理首次加载
}
);

</script>

<style scoped>

</style>