diff --git a/src/config/layouts.ts b/src/config/layouts.ts index 8728431d..044ef17c 100644 --- a/src/config/layouts.ts +++ b/src/config/layouts.ts @@ -22,6 +22,8 @@ export interface LayoutSettings { showSearchMenu: boolean /** 是否缓存标签栏 */ cacheTagsView: boolean + /** 开启系统水印 */ + showWatermark: boolean /** 是否显示灰色模式 */ showGreyMode: boolean /** 是否显示色弱模式 */ @@ -40,6 +42,7 @@ const defaultSettings: LayoutSettings = { showScreenfull: true, showSearchMenu: true, cacheTagsView: false, + showWatermark: true, showGreyMode: false, showColorWeakness: false } diff --git a/src/hooks/useWatermark.ts b/src/hooks/useWatermark.ts index a7bca6fe..655a194e 100644 --- a/src/hooks/useWatermark.ts +++ b/src/hooks/useWatermark.ts @@ -1,5 +1,5 @@ -import { type Ref, getCurrentInstance, onBeforeUnmount, ref, shallowRef } from "vue" -import { throttle } from "lodash-es" +import { type Ref, onBeforeUnmount, ref } from "vue" +import { debounce } from "lodash-es" type TargetNode = T & { _mutationObserver?: MutationObserver; _resizeObserver?: ResizeObserver } @@ -12,28 +12,93 @@ const defaultConfig = { /** 文本透明度 */ opacity: 0.5, /** 文本字体大小 */ - size: "16px", + size: 16, /** 文本字体 */ family: "serif", /** 文本倾斜角度 */ angle: -20, - /** canvas 宽度 */ - width: 200, - /** canvas 高度 */ - height: 150 + /** 一处水印所占宽度(数值越大水印密度越低) */ + width: 300, + /** 一处水印所占高度(数值越大水印密度越低) */ + height: 200 } /** body 元素 */ const bodyEl = ref(document.body) +/** + * 创建水印 + * 1. 可以选择传入挂载水印的容器元素,默认时 body + * 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印 + */ export function useWatermark(parentEl: Ref = bodyEl) { + /** 备份文本 */ + let backupText: string /** 最终配置 */ let mergeConfig: DefaultConfig /** 水印元素 */ - const watermarkEl = shallowRef(null) + let watermarkEl: HTMLElement | null = null - const createBase64 = (text: string) => { - const { color, opacity, size, family, angle, width, height } = mergeConfig ?? defaultConfig + /** 设置水印 */ + const setWatermark = (text: string, config: Partial = {}) => { + if (!parentEl.value) { + console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印") + return + } + // 备份文本 + backupText = text + // 合并配置 + mergeConfig = { ...defaultConfig, ...config } + // 创建水印元素 + createWatermarkEl() + // 监听水印容器变化 + addParentElListener(parentEl.value) + } + + /** 刷新水印(防御时调用) */ + const updateWatermark = () => { + clearWatermark() + createWatermarkEl() + addParentElListener(parentEl.value!) + } + + /** 创建水印元素 */ + const createWatermarkEl = () => { + if (watermarkEl) { + updateWatermarkEl() + return + } + const div = document.createElement("div") + div.style.pointerEvents = "none" + div.style.top = "0" + div.style.left = "0" + div.style.position = "absolute" + div.style.zIndex = "99999" + watermarkEl = div + const { clientWidth, clientHeight } = parentEl.value! + updateWatermarkEl({ width: clientWidth, height: clientHeight }) + // 设置水印容器为相对定位 + parentEl.value!.style.position = "relative" + // 将水印元素添加到水印容器中 + parentEl.value!.appendChild(div) + } + + /** 更新水印元素 */ + const updateWatermarkEl = ( + options: Partial<{ + width: number + height: number + }> = {} + ) => { + if (!watermarkEl) return + backupText && (watermarkEl.style.background = `url(${createBase64()}) left top repeat`) + options.width && (watermarkEl.style.width = `${options.width}px`) + options.height && (watermarkEl.style.height = `${options.height}px`) + } + + /** 创建 base64 图片 */ + const createBase64 = () => { + const { color, opacity, size, family, angle, width, height } = mergeConfig const canvasEl = document.createElement("canvas") canvasEl.width = width canvasEl.height = height @@ -41,93 +106,52 @@ export function useWatermark(parentEl: Ref = bodyEl) { if (ctx) { ctx.fillStyle = color ctx.globalAlpha = opacity - ctx.font = size + " " + family + ctx.font = `${size}px ${family}` ctx.rotate((Math.PI / 180) * angle) - ctx.clearRect(0, 0, width, height) - ctx.fillText(text, 0, height / 2) + ctx.fillText(backupText, 0, height / 2) } return canvasEl.toDataURL() } /** 清除水印 */ - const clear = () => { - if (!parentEl.value || !watermarkEl.value) return - parentEl.value.removeChild(watermarkEl.value) - watermarkEl.value = null - // 移除对水印容器的监听(DOM 变化 & DOM 大小变化) - removeDomListener(parentEl.value) + const clearWatermark = () => { + if (!parentEl.value || !watermarkEl) return + // 移除水印元素 + parentEl.value.removeChild(watermarkEl) + watermarkEl = null + // 移除对水印容器的监听 + removeParentElListener(parentEl.value) } - const updateWatermark = ( - options: Partial<{ - width: number - height: number - text: string - }> = {} - ) => { - if (!watermarkEl.value) return - options.width && (watermarkEl.value.style.width = `${options.width}px`) - options.height && (watermarkEl.value.style.height = `${options.height}px`) - options.text && (watermarkEl.value.style.background = `url(${createBase64(options.text)}) left top repeat`) - } - - const createWatermark = (text: string) => { - if (!parentEl.value) return - if (watermarkEl.value) { - updateWatermark({ text }) - return - } - const div = document.createElement("div") - watermarkEl.value = div - div.style.pointerEvents = "none" - div.style.top = "0" - div.style.left = "0" - div.style.position = "absolute" - div.style.zIndex = "9999" - const { clientWidth, clientHeight } = parentEl.value - updateWatermark({ width: clientWidth, height: clientHeight, text }) - parentEl.value.appendChild(div) - } - - const setWatermark = (text: string, config: Partial = {}) => { - if (!parentEl.value) return - // 设置水印容器为相对定位 - parentEl.value.style.position = "relative" - // 合并配置 - mergeConfig = { ...defaultConfig, ...config } - // 创建水印 - createWatermark(text) - // 监听水印容器变化(DOM 变化 & DOM 大小变化) - addDomListener(parentEl.value) - // 当前组件实例卸载前删除水印 - getCurrentInstance() && onBeforeUnmount(clear) - } - - const addDomListener = (targetNode: TargetNode) => { - if (targetNode._mutationObserver || targetNode._resizeObserver || !watermarkEl.value) return - // 监听 mutation 变化(DOM 变化) + /** 监听水印容器的变化(DOM 变化 & DOM 大小变化) */ + const addParentElListener = (targetNode: TargetNode) => { + // 防止重复添加监听 + if (targetNode._mutationObserver || targetNode._resizeObserver) return + // 监听 DOM 变化 addMutationListener(targetNode) - // 监听 resize 变化(DOM 大小变化) + // 监听 DOM 大小变化 addResizeListener(targetNode) } - const removeDomListener = (targetNode: TargetNode) => { - // 取消 mutation 监听 + /** 移除对水印容器的监听 */ + const removeParentElListener = (targetNode: TargetNode) => { + // 移除 mutation 监听 targetNode._mutationObserver?.disconnect() targetNode._mutationObserver = undefined - // 取消 resize 监听 + // 移除 resize 监听 targetNode._resizeObserver?.unobserve(targetNode) targetNode._resizeObserver = undefined } + /** 监听 DOM 变化 */ const addMutationListener = (targetNode: TargetNode) => { - // 观察器的配置(需要观察什么变动) - const observerOptions: MutationObserverInit = { - // 观察属性变动 + // 观察器的配置(需要观察哪些变动) + const mutationObserverOptions: MutationObserverInit = { + // 观察目标节点属性变动 attributes: true, - // 观察目标子节点的变化,是否有添加或者删除 + // 观察目标子节点是否有添加或者删除 childList: true, - // 观察后代节点,默认为 false + // 拓展到观察所有后代节点,默认为 false subtree: true } // 当观察到变动时执行的回调 @@ -137,15 +161,11 @@ export function useWatermark(parentEl: Ref = bodyEl) { switch (mutation.type) { case "childList": mutation.removedNodes.forEach((item) => { - item === watermarkEl.value && targetNode.appendChild(watermarkEl.value) + item === watermarkEl && targetNode.appendChild(watermarkEl) }) break case "attributes": - if (watermarkEl.value!.style.display === "none") { - watermarkEl.value!.style.display = "block" - } else if (watermarkEl.value!.style.visibility === "hidden") { - watermarkEl.value!.style.visibility = "visible" - } + updateWatermark() break } }) @@ -153,20 +173,28 @@ export function useWatermark(parentEl: Ref = bodyEl) { // 创建一个观察器实例并传入回调 const mutationObserver = new MutationObserver(mutationCallback) // 以上述配置开始观察目标节点 - mutationObserver.observe(targetNode, observerOptions) + mutationObserver.observe(targetNode, mutationObserverOptions) targetNode._mutationObserver = mutationObserver } + /** 监听 DOM 大小变化 */ const addResizeListener = (targetNode: TargetNode) => { // 当 targetNode 元素大小变化时去更新整个水印的大小 - const resizeCallback = throttle(() => { + const resizeCallback = debounce(() => { const { clientWidth, clientHeight } = targetNode - updateWatermark({ width: clientWidth, height: clientHeight }) + updateWatermarkEl({ width: clientWidth, height: clientHeight }) }, 500) + // 创建一个观察器实例并传入回调 const resizeObserver = new ResizeObserver(resizeCallback) + // 开始观察目标节点 resizeObserver.observe(targetNode) targetNode._resizeObserver = resizeObserver } - return { setWatermark, clear } + /** 在组件卸载前移除水印以及各种监听 */ + onBeforeUnmount(() => { + clearWatermark() + }) + + return { setWatermark, clearWatermark } } diff --git a/src/layouts/components/Settings/index.vue b/src/layouts/components/Settings/index.vue index 5f2fb9c8..55f11fbd 100644 --- a/src/layouts/components/Settings/index.vue +++ b/src/layouts/components/Settings/index.vue @@ -19,6 +19,7 @@ const { showScreenfull, showSearchMenu, cacheTagsView, + showWatermark, showGreyMode, showColorWeakness } = storeToRefs(settingsStore) @@ -33,6 +34,7 @@ const switchSettings = { 显示全屏按钮: showScreenfull, 显示搜索按钮: showSearchMenu, 是否缓存标签栏: cacheTagsView, + 开启系统水印: showWatermark, 显示灰色模式: showGreyMode, 显示色弱模式: showColorWeakness } diff --git a/src/layouts/index.vue b/src/layouts/index.vue index e5ae971a..1f5dcd0b 100644 --- a/src/layouts/index.vue +++ b/src/layouts/index.vue @@ -4,6 +4,7 @@ import { storeToRefs } from "pinia" import { useAppStore } from "@/store/modules/app" import { useSettingsStore } from "@/store/modules/settings" import useResize from "./hooks/useResize" +import { useWatermark } from "@/hooks/useWatermark" import LeftMode from "./LeftMode.vue" import TopMode from "./TopMode.vue" import LeftTopMode from "./LeftTopMode.vue" @@ -13,11 +14,13 @@ import { getCssVariableValue, setCssVariableValue } from "@/utils" /** Layout 布局响应式 */ useResize() +const { setWatermark, clearWatermark } = useWatermark() const appStore = useAppStore() const settingsStore = useSettingsStore() -const { showSettings, layoutMode, showTagsView, showGreyMode, showColorWeakness } = storeToRefs(settingsStore) +const { showSettings, layoutMode, showTagsView, showWatermark, showGreyMode, showColorWeakness } = + storeToRefs(settingsStore) const classes = computed(() => { return { @@ -35,6 +38,11 @@ watchEffect(() => { : setCssVariableValue(cssVariableName, "0px") }) //#endregion + +/** 开启或关闭系统水印 */ +watchEffect(() => { + showWatermark.value ? setWatermark(import.meta.env.VITE_APP_TITLE) : clearWatermark() +})