2023-08-31 17:28:05 +08:00
|
|
|
|
import { debounce } from "lodash-es"
|
2024-11-18 19:40:44 +08:00
|
|
|
|
import { onBeforeUnmount, type Ref, ref } from "vue"
|
2023-08-30 15:32:34 +08:00
|
|
|
|
|
2024-11-18 19:40:44 +08:00
|
|
|
|
interface Observer {
|
2023-09-01 11:52:57 +08:00
|
|
|
|
watermarkElMutationObserver?: MutationObserver
|
|
|
|
|
parentElMutationObserver?: MutationObserver
|
|
|
|
|
parentElResizeObserver?: ResizeObserver
|
|
|
|
|
}
|
2023-08-30 18:26:49 +08:00
|
|
|
|
|
|
|
|
|
type DefaultConfig = typeof defaultConfig
|
|
|
|
|
|
|
|
|
|
/** 默认配置 */
|
2023-08-30 15:32:34 +08:00
|
|
|
|
const defaultConfig = {
|
2023-09-01 11:52:57 +08:00
|
|
|
|
/** 防御(默认开启,能防御水印被删除或隐藏,但可能会有性能损耗) */
|
|
|
|
|
defense: true,
|
2023-08-30 15:32:34 +08:00
|
|
|
|
/** 文本颜色 */
|
2023-08-30 18:26:49 +08:00
|
|
|
|
color: "#c0c4cc",
|
2023-08-30 15:32:34 +08:00
|
|
|
|
/** 文本透明度 */
|
2023-08-30 18:26:49 +08:00
|
|
|
|
opacity: 0.5,
|
2023-08-30 15:32:34 +08:00
|
|
|
|
/** 文本字体大小 */
|
2023-08-31 17:28:05 +08:00
|
|
|
|
size: 16,
|
2023-08-30 15:32:34 +08:00
|
|
|
|
/** 文本字体 */
|
2023-08-30 18:26:49 +08:00
|
|
|
|
family: "serif",
|
|
|
|
|
/** 文本倾斜角度 */
|
2023-08-30 15:32:34 +08:00
|
|
|
|
angle: -20,
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 一处水印所占宽度(数值越大水印密度越低) */
|
|
|
|
|
width: 300,
|
|
|
|
|
/** 一处水印所占高度(数值越大水印密度越低) */
|
|
|
|
|
height: 200
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-30 18:26:49 +08:00
|
|
|
|
/** body 元素 */
|
|
|
|
|
const bodyEl = ref<HTMLElement>(document.body)
|
2023-08-30 15:32:34 +08:00
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/**
|
|
|
|
|
* 创建水印
|
2023-09-01 00:43:44 +08:00
|
|
|
|
* 1. 可以选择传入挂载水印的容器元素,默认是 body
|
2023-08-31 17:28:05 +08:00
|
|
|
|
* 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
|
|
|
|
|
*/
|
2023-08-30 18:26:49 +08:00
|
|
|
|
export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 备份文本 */
|
|
|
|
|
let backupText: string
|
2023-08-30 18:26:49 +08:00
|
|
|
|
/** 最终配置 */
|
|
|
|
|
let mergeConfig: DefaultConfig
|
|
|
|
|
/** 水印元素 */
|
2023-08-31 17:28:05 +08:00
|
|
|
|
let watermarkEl: HTMLElement | null = null
|
2023-08-31 22:13:36 +08:00
|
|
|
|
/** 观察器 */
|
|
|
|
|
const observer: Observer = {
|
2023-09-01 11:52:57 +08:00
|
|
|
|
watermarkElMutationObserver: undefined,
|
|
|
|
|
parentElMutationObserver: undefined,
|
|
|
|
|
parentElResizeObserver: undefined
|
2023-08-31 22:13:36 +08:00
|
|
|
|
}
|
2023-08-30 15:32:34 +08:00
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 设置水印 */
|
|
|
|
|
const setWatermark = (text: string, config: Partial<DefaultConfig> = {}) => {
|
|
|
|
|
if (!parentEl.value) {
|
|
|
|
|
console.warn("请在 DOM 挂载完成后再调用 setWatermark 方法设置水印")
|
|
|
|
|
return
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 备份文本
|
|
|
|
|
backupText = text
|
|
|
|
|
// 合并配置
|
|
|
|
|
mergeConfig = { ...defaultConfig, ...config }
|
2023-09-01 17:27:19 +08:00
|
|
|
|
// 创建或更新水印元素
|
|
|
|
|
watermarkEl ? updateWatermarkEl() : createWatermarkEl()
|
2023-09-01 17:50:37 +08:00
|
|
|
|
// 监听水印元素和容器元素的变化
|
2023-09-01 17:27:19 +08:00
|
|
|
|
addElListener(parentEl.value)
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 创建水印元素 */
|
|
|
|
|
const createWatermarkEl = () => {
|
2023-09-01 23:01:52 +08:00
|
|
|
|
const isBody = parentEl.value!.tagName.toLowerCase() === bodyEl.value.tagName.toLowerCase()
|
|
|
|
|
const watermarkElPosition = isBody ? "fixed" : "absolute"
|
|
|
|
|
const parentElPosition = isBody ? "" : "relative"
|
2023-09-01 17:27:19 +08:00
|
|
|
|
watermarkEl = document.createElement("div")
|
|
|
|
|
watermarkEl.style.pointerEvents = "none"
|
|
|
|
|
watermarkEl.style.top = "0"
|
|
|
|
|
watermarkEl.style.left = "0"
|
2023-09-01 23:01:52 +08:00
|
|
|
|
watermarkEl.style.position = watermarkElPosition
|
2023-09-01 17:27:19 +08:00
|
|
|
|
watermarkEl.style.zIndex = "99999"
|
2023-08-31 17:28:05 +08:00
|
|
|
|
const { clientWidth, clientHeight } = parentEl.value!
|
|
|
|
|
updateWatermarkEl({ width: clientWidth, height: clientHeight })
|
|
|
|
|
// 设置水印容器为相对定位
|
2023-09-01 23:01:52 +08:00
|
|
|
|
parentEl.value!.style.position = parentElPosition
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 将水印元素添加到水印容器中
|
2023-09-01 17:27:19 +08:00
|
|
|
|
parentEl.value!.appendChild(watermarkEl)
|
2023-08-30 18:26:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 更新水印元素 */
|
|
|
|
|
const updateWatermarkEl = (
|
2023-08-31 08:46:58 +08:00
|
|
|
|
options: Partial<{
|
|
|
|
|
width: number
|
|
|
|
|
height: number
|
|
|
|
|
}> = {}
|
|
|
|
|
) => {
|
2023-08-31 17:28:05 +08:00
|
|
|
|
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`)
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 创建 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)
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
2023-08-31 17:28:05 +08:00
|
|
|
|
return canvasEl.toDataURL()
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 清除水印 */
|
|
|
|
|
const clearWatermark = () => {
|
|
|
|
|
if (!parentEl.value || !watermarkEl) return
|
2023-09-01 11:52:57 +08:00
|
|
|
|
// 移除对水印元素和容器元素的监听
|
|
|
|
|
removeListener()
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 移除水印元素
|
2023-09-01 17:27:19 +08:00
|
|
|
|
try {
|
|
|
|
|
parentEl.value.removeChild(watermarkEl)
|
2024-11-19 10:42:02 +08:00
|
|
|
|
} catch {
|
2023-09-01 17:50:37 +08:00
|
|
|
|
// 比如在无防御情况下,用户打开控制台删除了这个元素
|
2023-09-01 17:27:19 +08:00
|
|
|
|
console.warn("水印元素已不存在,请重新创建")
|
2024-11-19 10:42:02 +08:00
|
|
|
|
} finally {
|
2023-09-01 17:27:19 +08:00
|
|
|
|
watermarkEl = null
|
|
|
|
|
}
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 00:43:44 +08:00
|
|
|
|
/** 刷新水印(防御时调用) */
|
|
|
|
|
const updateWatermark = debounce(() => {
|
|
|
|
|
clearWatermark()
|
|
|
|
|
createWatermarkEl()
|
2023-09-01 11:52:57 +08:00
|
|
|
|
addElListener(parentEl.value!)
|
2023-09-01 00:43:44 +08:00
|
|
|
|
}, 100)
|
|
|
|
|
|
2023-09-01 11:52:57 +08:00
|
|
|
|
/** 监听水印元素和容器元素的变化(DOM 变化 & DOM 大小变化) */
|
|
|
|
|
const addElListener = (targetNode: HTMLElement) => {
|
2023-09-01 17:50:37 +08:00
|
|
|
|
// 判断是否开启防御
|
2023-09-01 17:27:19 +08:00
|
|
|
|
if (mergeConfig.defense) {
|
|
|
|
|
// 防止重复添加监听
|
|
|
|
|
if (!observer.watermarkElMutationObserver && !observer.parentElMutationObserver) {
|
|
|
|
|
// 监听 DOM 变化
|
|
|
|
|
addMutationListener(targetNode)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2023-09-01 17:50:37 +08:00
|
|
|
|
// 无防御时不需要 mutation 监听
|
2023-09-01 17:27:19 +08:00
|
|
|
|
removeListener("mutation")
|
|
|
|
|
}
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 防止重复添加监听
|
2023-09-01 17:27:19 +08:00
|
|
|
|
if (!observer.parentElResizeObserver) {
|
|
|
|
|
// 监听 DOM 大小变化
|
|
|
|
|
addResizeListener(targetNode)
|
2023-09-01 11:52:57 +08:00
|
|
|
|
}
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-01 17:27:19 +08:00
|
|
|
|
/** 移除对水印元素和容器元素的监听,传参可指定要移除哪个监听,不传默认移除全部监听 */
|
|
|
|
|
const removeListener = (kind: "mutation" | "resize" | "all" = "all") => {
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 移除 mutation 监听
|
2023-09-01 17:27:19 +08:00
|
|
|
|
if (kind === "mutation" || kind === "all") {
|
|
|
|
|
observer.watermarkElMutationObserver?.disconnect()
|
|
|
|
|
observer.watermarkElMutationObserver = undefined
|
|
|
|
|
observer.parentElMutationObserver?.disconnect()
|
|
|
|
|
observer.parentElMutationObserver = undefined
|
|
|
|
|
}
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 移除 resize 监听
|
2023-09-01 17:27:19 +08:00
|
|
|
|
if (kind === "resize" || kind === "all") {
|
|
|
|
|
observer.parentElResizeObserver?.disconnect()
|
|
|
|
|
observer.parentElResizeObserver = undefined
|
|
|
|
|
}
|
2023-08-31 08:46:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 监听 DOM 变化 */
|
2023-08-31 22:13:36 +08:00
|
|
|
|
const addMutationListener = (targetNode: HTMLElement) => {
|
2023-08-30 15:32:34 +08:00
|
|
|
|
// 当观察到变动时执行的回调
|
2023-08-31 18:28:24 +08:00
|
|
|
|
const mutationCallback = debounce((mutationList: MutationRecord[]) => {
|
2023-08-31 08:46:58 +08:00
|
|
|
|
// 水印的防御(防止用户手动删除水印元素或通过 CSS 隐藏水印)
|
2023-09-01 11:52:57 +08:00
|
|
|
|
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)
|
|
|
|
|
)
|
2023-09-01 00:43:44 +08:00
|
|
|
|
}, 100)
|
2023-09-01 11:52:57 +08:00
|
|
|
|
// 创建观察器实例并传入回调
|
|
|
|
|
observer.watermarkElMutationObserver = new MutationObserver(mutationCallback)
|
|
|
|
|
observer.parentElMutationObserver = new MutationObserver(mutationCallback)
|
2023-08-30 15:32:34 +08:00
|
|
|
|
// 以上述配置开始观察目标节点
|
2023-09-01 11:52:57 +08:00
|
|
|
|
observer.watermarkElMutationObserver.observe(watermarkEl!, {
|
|
|
|
|
// 观察目标节点属性是否变动,默认为 true
|
|
|
|
|
attributes: true,
|
|
|
|
|
// 观察目标子节点是否有添加或者删除,默认为 false
|
|
|
|
|
childList: false,
|
|
|
|
|
// 是否拓展到观察所有后代节点,默认为 false
|
|
|
|
|
subtree: false
|
|
|
|
|
})
|
|
|
|
|
observer.parentElMutationObserver.observe(targetNode, {
|
|
|
|
|
attributes: false,
|
|
|
|
|
childList: true,
|
|
|
|
|
subtree: false
|
|
|
|
|
})
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 监听 DOM 大小变化 */
|
2023-08-31 22:13:36 +08:00
|
|
|
|
const addResizeListener = (targetNode: HTMLElement) => {
|
2023-08-31 08:46:58 +08:00
|
|
|
|
// 当 targetNode 元素大小变化时去更新整个水印的大小
|
2023-08-31 17:28:05 +08:00
|
|
|
|
const resizeCallback = debounce(() => {
|
2023-08-31 08:46:58 +08:00
|
|
|
|
const { clientWidth, clientHeight } = targetNode
|
2023-08-31 17:28:05 +08:00
|
|
|
|
updateWatermarkEl({ width: clientWidth, height: clientHeight })
|
2023-08-31 08:46:58 +08:00
|
|
|
|
}, 500)
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 创建一个观察器实例并传入回调
|
2023-09-01 11:52:57 +08:00
|
|
|
|
observer.parentElResizeObserver = new ResizeObserver(resizeCallback)
|
2023-08-31 17:28:05 +08:00
|
|
|
|
// 开始观察目标节点
|
2023-09-01 11:52:57 +08:00
|
|
|
|
observer.parentElResizeObserver.observe(targetNode)
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:28:05 +08:00
|
|
|
|
/** 在组件卸载前移除水印以及各种监听 */
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
clearWatermark()
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return { setWatermark, clearWatermark }
|
2023-08-30 15:32:34 +08:00
|
|
|
|
}
|