From 0a058b8e442f688d90ef520c8578f0230f7b209d Mon Sep 17 00:00:00 2001 From: Lgf <2246221508@qq.com> Date: Sat, 25 Apr 2026 17:01:36 +0800 Subject: [PATCH] =?UTF-8?q?feat(watermark):=20=E6=94=AF=E6=8C=81=E6=9A=97?= =?UTF-8?q?=E9=BB=91=E6=A8=A1=E5=BC=8F=E5=B9=B6=E5=A2=9E=E5=BC=BA=E6=B0=B4?= =?UTF-8?q?=E5=8D=B0=E9=98=B2=E7=AF=A1=E6=94=B9=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加暗黑模式支持,根据主题切换水印颜色 实现MutationObserver监听DOM变化防止水印被移除或隐藏 重构水印配置管理逻辑,提高代码可维护性 --- packages/effects/hooks/src/use-watermark.ts | 205 ++++++++++++++---- packages/effects/layouts/src/basic/layout.vue | 4 +- 2 files changed, 168 insertions(+), 41 deletions(-) diff --git a/packages/effects/hooks/src/use-watermark.ts b/packages/effects/hooks/src/use-watermark.ts index 8484a0824..f898bb816 100644 --- a/packages/effects/hooks/src/use-watermark.ts +++ b/packages/effects/hooks/src/use-watermark.ts @@ -1,74 +1,199 @@ 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(); const unmountedHooked = ref(false); -const cachedOptions = ref>({ - advancedStyle: { - colorStops: [ - { - color: 'gray', - offset: 0, - }, - { - color: 'gray', - offset: 1, - }, - ], - type: 'linear', - }, - // fontSize: '20px', +const mutationObserver = ref(); +const isWatermarkEnabled = ref(false); + +const DEFAULT_DARK_COLOR = 'rgba(255, 255, 255, 0.8)'; +const DEFAULT_LIGHT_COLOR = 'rgba(0, 0, 0, 0.8)'; + +interface WatermarkConfig { + content: string; + globalAlpha: number; + isDark: boolean; +} + +const currentConfig = ref({ content: '', - contentType: 'multi-line-text', globalAlpha: 0.25, - gridLayoutOptions: { - cols: 2, - gap: [20, 20], - matrix: [ - [1, 0], - [0, 1], - ], - rows: 2, - }, - height: 200, - layout: 'grid', - rotate: 30, - width: 160, + isDark: false, }); +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 { + 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() { async function initWatermark(options: Partial) { const { Watermark } = await import('watermark-js-plus'); - cachedOptions.value = { - ...cachedOptions.value, + const mergedOptions = { + ...createBaseOptions(currentConfig.value), ...options, }; - watermark.value = new Watermark(cachedOptions.value); + + watermark.value = new Watermark(mergedOptions); await watermark.value?.create(); + + isWatermarkEnabled.value = true; + setupMutationObserver(); } - async function updateWatermark(options: Partial) { + async function updateWatermark(options: Partial & { 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) { await nextTick(); - await watermark.value?.changeOptions({ - ...cachedOptions.value, - ...options, - }); + await watermark.value?.changeOptions(mergedOptions); } else { - await initWatermark(options); + await initWatermark(mergedOptions); } + + isWatermarkEnabled.value = true; } function destroyWatermark() { + isWatermarkEnabled.value = false; + + if (mutationObserver.value) { + mutationObserver.value.disconnect(); + mutationObserver.value = undefined; + } + if (watermark.value) { watermark.value.destroy(); watermark.value = undefined; } } - // 只在第一次调用时注册卸载钩子,防止重复注册以致于在路由切换时销毁了水印 if (!unmountedHooked.value) { unmountedHooked.value = true; onUnmounted(() => { diff --git a/packages/effects/layouts/src/basic/layout.vue b/packages/effects/layouts/src/basic/layout.vue index fb14a452e..b64fffa43 100644 --- a/packages/effects/layouts/src/basic/layout.vue +++ b/packages/effects/layouts/src/basic/layout.vue @@ -214,12 +214,14 @@ watch( () => preferences.app.watermark, () => preferences.app.watermarkOpacity, watermarkContent, + isDark, ], - ([isEnabled, opacity, content]) => { + ([isEnabled, opacity, content, isDarkMode]) => { if (isEnabled) { updateWatermark({ content, globalAlpha: opacity as number, + isDark: isDarkMode as boolean, }); } else { destroyWatermark();