mirror of
https://github.com/un-pany/v3-admin-vite.git
synced 2025-04-20 19:09:21 +08:00
feat: 水印功能
This commit is contained in:
parent
cc87ab3ccc
commit
d2952e0d16
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -1,22 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { ref } from "vue"
|
||||
import { useWatermark } from "@/hooks/useWatermark"
|
||||
|
||||
const localRef = ref<HTMLElement | null>(null)
|
||||
const { setWatermark, clear } = useWatermark(localRef)
|
||||
const { setWatermark: setGlobalWatermark, clear: clearGlobalWatermark } = useWatermark()
|
||||
const { setWatermark, clearWatermark } = useWatermark(localRef)
|
||||
const { setWatermark: setGlobalWatermark, clearWatermark: clearGlobalWatermark } = useWatermark()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<h4>该示例是演示:通过调用 hook,开启或关闭水印(支持局部、全局、自定义样式)</h4>
|
||||
<h4>
|
||||
该示例是演示:通过调用 hook,开启或关闭水印,
|
||||
支持局部、全局、自定义样式(颜色、透明度、字体大小、字体、倾斜角度等), 并自带一定的防删除和自适应功能
|
||||
</h4>
|
||||
<div ref="localRef" class="local" />
|
||||
<el-button-group>
|
||||
<el-button type="primary" @click="setWatermark('局部水印', { color: '#409eff' })">创建局部水印</el-button>
|
||||
<el-button type="danger" @click="clear">清除局部水印</el-button>
|
||||
<el-button type="primary" @click="setWatermark('创建局部水印', { color: '#409eff' })">创建局部水印</el-button>
|
||||
<el-button type="warning" @click="setWatermark('重置局部水印', { color: '#e6a23c' })">重置局部水印</el-button>
|
||||
<el-button type="danger" @click="clearWatermark">清除局部水印</el-button>
|
||||
</el-button-group>
|
||||
<el-button-group>
|
||||
<el-button type="primary" @click="setGlobalWatermark('全局水印')">创建全局水印</el-button>
|
||||
<el-button type="primary" @click="setGlobalWatermark('创建全局水印', { color: '#409eff' })">
|
||||
创建全局水印
|
||||
</el-button>
|
||||
<el-button type="warning" @click="setGlobalWatermark('重置全局水印', { color: '#e6a23c' })">
|
||||
重置全局水印
|
||||
</el-button>
|
||||
<el-button type="danger" @click="clearGlobalWatermark">清除全局水印</el-button>
|
||||
</el-button-group>
|
||||
</div>
|
||||
@ -30,6 +39,6 @@ const { setWatermark: setGlobalWatermark, clear: clearGlobalWatermark } = useWat
|
||||
}
|
||||
|
||||
.el-button-group {
|
||||
margin-right: 10px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
</style>
|
||||
|
Loading…
x
Reference in New Issue
Block a user