feat: 水印功能

This commit is contained in:
pany
2023-08-31 17:28:05 +08:00
parent cc87ab3ccc
commit d2952e0d16
5 changed files with 145 additions and 95 deletions

View File

@@ -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> = 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<HTMLElement>(document.body)
/**
* 创建水印
* 1. 可以选择传入挂载水印的容器元素,默认时 body
* 2. 做了水印防御,能有效防御别人打开控制台删除或隐藏水印
*/
export function useWatermark(parentEl: Ref<HTMLElement | null> = bodyEl) {
/** 备份文本 */
let backupText: string
/** 最终配置 */
let mergeConfig: DefaultConfig
/** 水印元素 */
const watermarkEl = shallowRef<HTMLElement | null>(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<DefaultConfig> = {}) => {
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<HTMLElement | null> = 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<DefaultConfig> = {}) => {
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<HTMLElement>) => {
if (targetNode._mutationObserver || targetNode._resizeObserver || !watermarkEl.value) return
// 监听 mutation 变化DOM 变化)
/** 监听水印容器的变化DOM 变化 & DOM 大小变化) */
const addParentElListener = (targetNode: TargetNode<HTMLElement>) => {
// 防止重复添加监听
if (targetNode._mutationObserver || targetNode._resizeObserver) return
// 监听 DOM 变化
addMutationListener(targetNode)
// 监听 resize 变化(DOM 大小变化
// 监听 DOM 大小变化
addResizeListener(targetNode)
}
const removeDomListener = (targetNode: TargetNode<HTMLElement>) => {
// 取消 mutation 监听
/** 移除对水印容器的监听 */
const removeParentElListener = (targetNode: TargetNode<HTMLElement>) => {
// 移除 mutation 监听
targetNode._mutationObserver?.disconnect()
targetNode._mutationObserver = undefined
// 取消 resize 监听
// 移除 resize 监听
targetNode._resizeObserver?.unobserve(targetNode)
targetNode._resizeObserver = undefined
}
/** 监听 DOM 变化 */
const addMutationListener = (targetNode: TargetNode<HTMLElement>) => {
// 观察器的配置(需要观察什么变动)
const observerOptions: MutationObserverInit = {
// 观察属性变动
// 观察器的配置(需要观察哪些变动)
const mutationObserverOptions: MutationObserverInit = {
// 观察目标节点属性变动
attributes: true,
// 观察目标子节点的变化,是否有添加或者删除
// 观察目标子节点是否有添加或者删除
childList: true,
// 观察后代节点,默认为 false
// 拓展到观察所有后代节点,默认为 false
subtree: true
}
// 当观察到变动时执行的回调
@@ -137,15 +161,11 @@ export function useWatermark(parentEl: Ref<HTMLElement | null> = 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<HTMLElement | null> = bodyEl) {
// 创建一个观察器实例并传入回调
const mutationObserver = new MutationObserver(mutationCallback)
// 以上述配置开始观察目标节点
mutationObserver.observe(targetNode, observerOptions)
mutationObserver.observe(targetNode, mutationObserverOptions)
targetNode._mutationObserver = mutationObserver
}
/** 监听 DOM 大小变化 */
const addResizeListener = (targetNode: TargetNode<HTMLElement>) => {
// 当 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 }
}