Browse Source

feat(watermark): 支持暗黑模式并增强水印防篡改能力

添加暗黑模式支持,根据主题切换水印颜色
实现MutationObserver监听DOM变化防止水印被移除或隐藏
重构水印配置管理逻辑,提高代码可维护性
pull/7849/head
Lgf 5 months ago
parent
commit
0a058b8e44
  1. 205
      packages/effects/hooks/src/use-watermark.ts
  2. 4
      packages/effects/layouts/src/basic/layout.vue

205
packages/effects/hooks/src/use-watermark.ts

@ -1,74 +1,199 @@
import type { Watermark, WatermarkOptions } from 'watermark-js-plus'; import type { Watermark, WatermarkOptions } from 'watermark-js-plus';
import { nextTick, onUnmounted, readonly, ref } from 'vue'; import { nextTick, onUnmounted, readonly, ref, watch } from 'vue';
const watermark = ref<Watermark>(); const watermark = ref<Watermark>();
const unmountedHooked = ref<boolean>(false); const unmountedHooked = ref<boolean>(false);
const cachedOptions = ref<Partial<WatermarkOptions>>({ const mutationObserver = ref<MutationObserver>();
advancedStyle: { const isWatermarkEnabled = ref<boolean>(false);
colorStops: [
{ const DEFAULT_DARK_COLOR = 'rgba(255, 255, 255, 0.8)';
color: 'gray', const DEFAULT_LIGHT_COLOR = 'rgba(0, 0, 0, 0.8)';
offset: 0,
}, interface WatermarkConfig {
{ content: string;
color: 'gray', globalAlpha: number;
offset: 1, isDark: boolean;
}, }
],
type: 'linear', const currentConfig = ref<WatermarkConfig>({
},
// fontSize: '20px',
content: '', content: '',
contentType: 'multi-line-text',
globalAlpha: 0.25, globalAlpha: 0.25,
gridLayoutOptions: { isDark: false,
cols: 2,
gap: [20, 20],
matrix: [
[1, 0],
[0, 1],
],
rows: 2,
},
height: 200,
layout: 'grid',
rotate: 30,
width: 160,
}); });
function getColorStops(isDark: boolean): { color: string; offset: number }[] {
const color = isDark ? DEFAULT_DARK_COLOR : DEFAULT_LIGHT_COLOR;
return [
{ color, offset: 0 },
{ color, offset: 1 },
];
}
function createBaseOptions(config: WatermarkConfig): Partial<WatermarkOptions> {
return {
advancedStyle: {
colorStops: getColorStops(config.isDark),
type: 'linear',
},
content: config.content,
contentType: 'multi-line-text',
globalAlpha: config.globalAlpha,
gridLayoutOptions: {
cols: 2,
gap: [20, 20],
matrix: [
[1, 0],
[0, 1],
],
rows: 2,
},
height: 200,
layout: 'grid',
rotate: 30,
width: 160,
};
}
function setupMutationObserver() {
if (mutationObserver.value) {
mutationObserver.value.disconnect();
}
const observer = new MutationObserver((mutations) => {
if (!isWatermarkEnabled.value || !watermark.value) return;
let shouldRecreate = false;
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const removedNodes = Array.from(mutation.removedNodes);
for (const node of removedNodes) {
if (
node instanceof HTMLElement &&
(node.classList.contains('watermark-container') ||
node.classList.contains('watermark-item'))
) {
shouldRecreate = true;
break;
}
}
} else if (mutation.type === 'attributes') {
const target = mutation.target as HTMLElement;
if (
target.classList.contains('watermark-container') ||
target.classList.contains('watermark-item')
) {
if (mutation.attributeName === 'style') {
const style = target.getAttribute('style') || '';
if (
style.includes('display: none') ||
style.includes('visibility: hidden') ||
style.includes('opacity: 0') ||
style.includes('z-index: -1')
) {
shouldRecreate = true;
}
} else if (mutation.attributeName === 'class') {
if (
target.classList.contains('hidden') ||
target.classList.contains('invisible')
) {
shouldRecreate = true;
}
}
}
}
}
if (shouldRecreate) {
recreateWatermark();
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['style', 'class'],
});
mutationObserver.value = observer;
}
async function recreateWatermark() {
if (!isWatermarkEnabled.value) return;
try {
if (watermark.value) {
watermark.value.destroy();
watermark.value = undefined;
}
await nextTick();
const options = createBaseOptions(currentConfig.value);
const { Watermark } = await import('watermark-js-plus');
watermark.value = new Watermark(options);
await watermark.value?.create();
} catch (e) {
console.error('Failed to recreate watermark:', e);
}
}
export function useWatermark() { export function useWatermark() {
async function initWatermark(options: Partial<WatermarkOptions>) { async function initWatermark(options: Partial<WatermarkOptions>) {
const { Watermark } = await import('watermark-js-plus'); const { Watermark } = await import('watermark-js-plus');
cachedOptions.value = { const mergedOptions = {
...cachedOptions.value, ...createBaseOptions(currentConfig.value),
...options, ...options,
}; };
watermark.value = new Watermark(cachedOptions.value);
watermark.value = new Watermark(mergedOptions);
await watermark.value?.create(); await watermark.value?.create();
isWatermarkEnabled.value = true;
setupMutationObserver();
} }
async function updateWatermark(options: Partial<WatermarkOptions>) { async function updateWatermark(options: Partial<WatermarkOptions> & { isDark?: boolean }) {
if (options.isDark !== undefined) {
currentConfig.value.isDark = options.isDark;
}
if (options.content !== undefined) {
currentConfig.value.content = options.content;
}
if (options.globalAlpha !== undefined) {
currentConfig.value.globalAlpha = options.globalAlpha;
}
const mergedOptions = createBaseOptions(currentConfig.value);
if (watermark.value) { if (watermark.value) {
await nextTick(); await nextTick();
await watermark.value?.changeOptions({ await watermark.value?.changeOptions(mergedOptions);
...cachedOptions.value,
...options,
});
} else { } else {
await initWatermark(options); await initWatermark(mergedOptions);
} }
isWatermarkEnabled.value = true;
} }
function destroyWatermark() { function destroyWatermark() {
isWatermarkEnabled.value = false;
if (mutationObserver.value) {
mutationObserver.value.disconnect();
mutationObserver.value = undefined;
}
if (watermark.value) { if (watermark.value) {
watermark.value.destroy(); watermark.value.destroy();
watermark.value = undefined; watermark.value = undefined;
} }
} }
// 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印
if (!unmountedHooked.value) { if (!unmountedHooked.value) {
unmountedHooked.value = true; unmountedHooked.value = true;
onUnmounted(() => { onUnmounted(() => {

4
packages/effects/layouts/src/basic/layout.vue

@ -214,12 +214,14 @@ watch(
() => preferences.app.watermark, () => preferences.app.watermark,
() => preferences.app.watermarkOpacity, () => preferences.app.watermarkOpacity,
watermarkContent, watermarkContent,
isDark,
], ],
([isEnabled, opacity, content]) => { ([isEnabled, opacity, content, isDarkMode]) => {
if (isEnabled) { if (isEnabled) {
updateWatermark({ updateWatermark({
content, content,
globalAlpha: opacity as number, globalAlpha: opacity as number,
isDark: isDarkMode as boolean,
}); });
} else { } else {
destroyWatermark(); destroyWatermark();

Loading…
Cancel
Save