import { type Ref, onBeforeUnmount, ref } from "vue" import { debounce } from "lodash-es" type Observer = { watermarkElMutationObserver?: MutationObserver parentElMutationObserver?: MutationObserver parentElResizeObserver?: ResizeObserver } type DefaultConfig = typeof defaultConfig /** 默认配置 */ const defaultConfig = { /** 防御(默认开启,能防御水印被删除或隐藏,但可能会有性能损耗) */ defense: true, /** 文本颜色 */ color: "#c0c4cc", /** 文本透明度 */ opacity: 0.5, /** 文本字体大小 */ size: 16, /** 文本字体 */ family: "serif", /** 文本倾斜角度 */ angle: -20, /** 一处水印所占宽度(数值越大水印密度越低) */ 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 /** 水印元素 */ let watermarkEl: HTMLElement | null = null /** 观察器 */ const observer: Observer = { watermarkElMutationObserver: undefined, parentElMutationObserver: undefined, parentElResizeObserver: undefined } /** 设置水印 */ const setWatermark = (text: string, config: Partial = {}) => { if (!parentEl.value) { console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印") return } // 备份文本 backupText = text // 合并配置 mergeConfig = { ...defaultConfig, ...config } // 创建或更新水印元素 watermarkEl ? updateWatermarkEl() : createWatermarkEl() // 监听水印元素和容器元素的变化 addElListener(parentEl.value) } /** 创建水印元素 */ const createWatermarkEl = () => { watermarkEl = document.createElement("div") watermarkEl.style.pointerEvents = "none" watermarkEl.style.top = "0" watermarkEl.style.left = "0" watermarkEl.style.position = "absolute" watermarkEl.style.zIndex = "99999" const { clientWidth, clientHeight } = parentEl.value! updateWatermarkEl({ width: clientWidth, height: clientHeight }) // 设置水印容器为相对定位 parentEl.value!.style.position = "relative" // 将水印元素添加到水印容器中 parentEl.value!.appendChild(watermarkEl) } /** 更新水印元素 */ 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 const ctx = canvasEl.getContext("2d") if (ctx) { ctx.fillStyle = color ctx.globalAlpha = opacity ctx.font = `${size}px ${family}` ctx.rotate((Math.PI / 180) * angle) ctx.fillText(backupText, 0, height / 2) } return canvasEl.toDataURL() } /** 清除水印 */ const clearWatermark = () => { if (!parentEl.value || !watermarkEl) return // 移除对水印元素和容器元素的监听 removeListener() // 移除水印元素 try { parentEl.value.removeChild(watermarkEl) } catch { // 比如在无防御情况下,用户打开控制台删除了这个元素 console.warn("水印元素已不存在,请重新创建") } finally { watermarkEl = null } } /** 刷新水印(防御时调用) */ const updateWatermark = debounce(() => { clearWatermark() createWatermarkEl() addElListener(parentEl.value!) }, 100) /** 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化) */ const addElListener = (targetNode: HTMLElement) => { // 判断是否开启防御 if (mergeConfig.defense) { // 防止重复添加监听 if (!observer.watermarkElMutationObserver && !observer.parentElMutationObserver) { // 监听 DOM 变化 addMutationListener(targetNode) } } else { // 无防御时不需要 mutation 监听 removeListener("mutation") } // 防止重复添加监听 if (!observer.parentElResizeObserver) { // 监听 DOM 大小变化 addResizeListener(targetNode) } } /** 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听 */ const removeListener = (kind: "mutation" | "resize" | "all" = "all") => { // 移除 mutation 监听 if (kind === "mutation" || kind === "all") { observer.watermarkElMutationObserver?.disconnect() observer.watermarkElMutationObserver = undefined observer.parentElMutationObserver?.disconnect() observer.parentElMutationObserver = undefined } // 移除 resize 监听 if (kind === "resize" || kind === "all") { observer.parentElResizeObserver?.disconnect() observer.parentElResizeObserver = undefined } } /** 监听 DOM 变化 */ const addMutationListener = (targetNode: HTMLElement) => { // 当观察到变动时执行的回调 const mutationCallback = debounce((mutationList: MutationRecord[]) => { // 水印的防御(防止用户手动删除水印元素或通过 CSS 隐藏水印) mutationList.forEach( debounce((mutation: MutationRecord) => { switch (mutation.type) { case "attributes": mutation.target === watermarkEl && updateWatermark() break case "childList": mutation.removedNodes.forEach((item) => { item === watermarkEl && targetNode.appendChild(watermarkEl) }) break } }, 100) ) }, 100) // 创建观察器实例并传入回调 observer.watermarkElMutationObserver = new MutationObserver(mutationCallback) observer.parentElMutationObserver = new MutationObserver(mutationCallback) // 以上述配置开始观察目标节点 observer.watermarkElMutationObserver.observe(watermarkEl!, { // 观察目标节点属性是否变动,默认为 true attributes: true, // 观察目标子节点是否有添加或者删除,默认为 false childList: false, // 是否拓展到观察所有后代节点,默认为 false subtree: false }) observer.parentElMutationObserver.observe(targetNode, { attributes: false, childList: true, subtree: false }) } /** 监听 DOM 大小变化 */ const addResizeListener = (targetNode: HTMLElement) => { // 当 targetNode 元素大小变化时去更新整个水印的大小 const resizeCallback = debounce(() => { const { clientWidth, clientHeight } = targetNode updateWatermarkEl({ width: clientWidth, height: clientHeight }) }, 500) // 创建一个观察器实例并传入回调 observer.parentElResizeObserver = new ResizeObserver(resizeCallback) // 开始观察目标节点 observer.parentElResizeObserver.observe(targetNode) } /** 在组件卸载前移除水印以及各种监听 */ onBeforeUnmount(() => { clearWatermark() }) return { setWatermark, clearWatermark } }