Skip to content
This repository has been archived by the owner on Aug 28, 2024. It is now read-only.

Commit

Permalink
feat(demo): add Watermark page(水印-demo #181) (#291)
Browse files Browse the repository at this point in the history
* feat(vbenComponents):  Watermark 添加水印组件

* feat(hooks):  add useWatermark

* feat(demo): add Watermark page(水印-demo #181)

* fix(hooks):  修复useWatermark属性更改时恢复水印

* fix(hooks):  修复useWatermark 自定义属性问题

---------

Co-authored-by: jackhoo_98 <jackhoo_98@foxmail.com>
  • Loading branch information
13982720426 and jackhoo_98 committed Dec 13, 2023
1 parent 7ddab2b commit e28d304
Show file tree
Hide file tree
Showing 8 changed files with 334 additions and 0 deletions.
3 changes: 3 additions & 0 deletions apps/admin/init-components.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ import naive, {
NCollapse,
NCollapseItem,
NSpin,
NWatermark,
} from 'naive-ui'
import { initVbenComponent, setNotice, setMessage } from '@vben/vbencomponents'

Expand Down Expand Up @@ -217,6 +218,8 @@ export async function registerComponents(app) {

Upload: NUpload,
UploadDragger: NUploadDragger,

Watermark: NWatermark,
})
setMessage(useMessage)
setNotice(useNotification)
Expand Down
195 changes: 195 additions & 0 deletions apps/admin/src/hooks/web/useWatermark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import {
getCurrentInstance,
onBeforeUnmount,
ref,
Ref,
shallowRef,
unref,
} from 'vue'
import {
addResizeListener,
removeResizeListener,
useThrottleFn,
isUndefined,
} from '@vben/utils'

const watermarkSymbol = 'watermark-dom'
const updateWatermarkText = ref<string | null>(null)

type UseWatermarkRes = {
setWatermark: (str: string) => void
clear: () => void
clearAll: () => void
obInstance?: MutationObserver
targetElement?: HTMLElement
parentElement?: HTMLElement
}

const sourceMap = new Map<Symbol, Omit<UseWatermarkRes, 'clearAll'>>()

function createBase64(str: string) {
const can = document.createElement('canvas')
const width = 300
const height = 240
Object.assign(can, { width, height })

const cans = can.getContext('2d')
if (cans) {
cans.rotate((-20 * Math.PI) / 180)
cans.font = '15px Vedana'
cans.fillStyle = 'rgba(0, 0, 0, 0.15)'
cans.textAlign = 'left'
cans.textBaseline = 'middle'
cans.fillText(str, width / 20, height)
// todo 自定义水印样式
}
return can.toDataURL('image/png')
}
const resetWatermarkStyle = (element: HTMLElement, watermarkText: string) => {
element.className = '__' + watermarkSymbol
element.style.pointerEvents = 'none'
element.style.top = '0px'
element.style.left = '0px'
element.style.position = 'absolute'
element.style.zIndex = '100000'
element.style.height = '100%'
element.style.width = '100%'
element.style.background = `url(${createBase64(
unref(updateWatermarkText) || watermarkText,
)}) left top repeat`
}

const obFn = () => {
const obInstance = new MutationObserver((mutationRecords) => {
for (const mutation of mutationRecords) {
for (const node of Array.from(mutation.removedNodes)) {
const target = Array.from(sourceMap.values()).find(
(item) => item.targetElement === node,
)
if (!target) return
const { targetElement, parentElement } = target
// 父元素的子元素水印如果被删除 重新插入被删除的水印(防篡改,插入通过控制台删除的水印)
if (!parentElement?.contains(targetElement as Node | null)) {
target?.parentElement?.appendChild(node as HTMLElement)
}
}
if (mutation.attributeName === 'style' && mutation.target) {
const _target = mutation.target as HTMLElement
if (_target.className === '__' + watermarkSymbol) {
resetWatermarkStyle(
_target as HTMLElement,
_target?.['data-watermark-text'],
)
}
}
}
})
return obInstance
}

export function useWatermark(
appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>,
): UseWatermarkRes {
const domSymbol = Symbol(watermarkSymbol)
const appendElRaw = unref(appendEl)
if (appendElRaw && sourceMap.has(domSymbol)) {
const { setWatermark, clear } = sourceMap.get(domSymbol) as UseWatermarkRes
return {
setWatermark,
clear,
clearAll,
}
}
const func = useThrottleFn(function () {
const el = unref(appendEl)
if (!el) return
const { clientHeight: height, clientWidth: width } = el
updateWatermark({ height, width })
})
const watermarkEl = shallowRef<HTMLElement>()
const clear = () => {
const domId = unref(watermarkEl)
watermarkEl.value = undefined
const el = unref(appendEl)
sourceMap.has(domSymbol) &&
sourceMap.get(domSymbol)?.obInstance?.disconnect()
sourceMap.delete(domSymbol)
if (!el) return
domId && el.removeChild(domId)
removeResizeListener(el, func)
}

function updateWatermark(
options: {
width?: number
height?: number
str?: string
} = {},
) {
const el = unref(watermarkEl)
if (!el) return
if (!isUndefined(options.width)) {
el.style.width = `${options.width}px`
}
if (!isUndefined(options.height)) {
el.style.height = `${options.height}px`
}
if (!isUndefined(options.str)) {
el.style.background = `url(${createBase64(options.str)}) left top repeat`
}
}

const createWatermark = (str: string) => {
if (unref(watermarkEl) && sourceMap.has(domSymbol)) {
updateWatermarkText.value = str
updateWatermark({ str })
return
}
const div = document.createElement('div')
div['data-watermark-text'] = str //自定义属性 用于恢复水印
updateWatermarkText.value = str
watermarkEl.value = div
resetWatermarkStyle(div, str)
const el = unref(appendEl)
if (!el) return
const { clientHeight: height, clientWidth: width } = el
updateWatermark({ str, width, height })
el.appendChild(div)
sourceMap.set(domSymbol, {
setWatermark,
clear,
parentElement: el,
targetElement: div,
obInstance: obFn(),
})
sourceMap.get(domSymbol)?.obInstance?.observe(el, {
childList: true, // 子节点的变动(指新增,删除或者更改)
subtree: true, // 该观察器应用于该节点的所有后代节点
attributes: true, // 属性的变动
})
}

function setWatermark(str: string) {
createWatermark(str)
addResizeListener(document.documentElement, func)
const instance = getCurrentInstance()
if (instance) {
onBeforeUnmount(() => {
clear()
})
}
}

return {
setWatermark,
clear,
clearAll,
}
}

function clearAll() {
Array.from(sourceMap.values()).forEach((item) => {
item?.obInstance?.disconnect()
item.clear()
})
}
114 changes: 114 additions & 0 deletions apps/admin/src/pages/demo/feat/watermark.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script lang="ts" setup>
import { ref, onUnmounted, nextTick } from 'vue'
import type { Ref } from 'vue'
import { useWatermark } from '@/hooks/web/useWatermark'
const { setWatermark, clear, clearAll } = useWatermark()
const { setWatermark: setWatermark2 } = useWatermark()
onUnmounted(() => {
clearAll()
})
const show = ref(false)
const content = '我的水印'
const createWatermark = (cardEl) => {
useWatermark(cardEl as Ref<HTMLElement | null>).setWatermark(content)
}
nextTick(() => {
createWatermark(document.getElementById('cardId'))
})
</script>

<template>
<VbenCard title="水印示例">
<VbenWatermark
v-if="show"
content="大家艰苦一下,一切都会有的"
cross
fullscreen
:font-size="16"
:line-height="16"
:width="384"
:height="384"
:x-offset="12"
:y-offset="60"
:rotate="-15"
/>
<VbenGrid x-gap="12" :cols="2">
<VbenGridItem>
<VbenCard title="组件方式" style="height: 100%">
<div class="mb-4">基于Naive UI的水印Watermark组件</div>
<div>
<VbenCard title="全屏水印">
<VbenSwitch v-model:value="show" />
</VbenCard>
<VbenCard title="部分水印">
<VbenWatermark
content="核心机密"
cross
selectable
:font-size="16"
:line-height="16"
:width="192"
:height="128"
:x-offset="12"
:y-offset="28"
:rotate="-15"
>
<VbenCard title="带封面的卡片" hoverable>
<template #cover>
<img src="@/assets/images/cover.png" alt="cover.png" />
</template>
样式丰富了许多,不是吗🥳
</VbenCard>
</VbenWatermark>
</VbenCard>
</div>
</VbenCard>
</VbenGridItem>
<VbenGridItem>
<VbenCard title="函数方式" style="height: 100%">
<div class="mb-4">基于自定义hooks实现(支持防篡改)</div>
<div>
<VbenButton
type="primary"
class="mr-2"
@click="setWatermark('WaterMark Info1')"
>
Create Watermark1
</VbenButton>
<VbenButton
type="primary"
class="mr-2"
@click="setWatermark2('WaterMark Info2')"
>
Create Watermark2
</VbenButton>
<VbenButton type="error" class="mr-2" @click="clear">
Clear Watermark1
</VbenButton>
<VbenButton type="error" class="mr-2" @click="clearAll">
ClearAll
</VbenButton>
<VbenButton
type="warning"
class="mr-2"
@click="setWatermark('WaterMark Info New')"
>
Update Watermark1
</VbenButton>
<VbenCard title="部分水印">
<VbenCard id="cardId" title="带封面的卡片" hoverable>
<template #cover>
<img src="@/assets/images/cover.png" alt="cover.png" />
</template>
样式丰富了许多,不是吗🥳
</VbenCard>
</VbenCard>
</div>
</VbenCard>
</VbenGridItem>
</VbenGrid>
</VbenCard>
</template>
1 change: 1 addition & 0 deletions packages/locale/src/lang/en/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export default {
clickOutSide: 'ClickOutSide',
print: 'Print',
ellipsis: 'EllipsisText',
watermark: 'Watermark',
},
page: {
page: 'Page',
Expand Down
1 change: 1 addition & 0 deletions packages/locale/src/lang/zh-CN/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export default {
clickOutSide: 'ClickOutSide组件',
print: '打印',
ellipsis: '文本省略',
watermark: '水印',
},
page: {
page: '页面',
Expand Down
8 changes: 8 additions & 0 deletions packages/router/src/routes/modules/demo/feat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ const feat: RouteRecordItem = {
title: 'routes.demo.feat.ellipsis',
},
},
{
path: 'watermark',
name: 'Watermark',
component: () => import('@/pages/demo/feat/watermark.vue'),
meta: {
title: 'routes.demo.feat.watermark',
},
},
],
}

Expand Down
Empty file.
12 changes: 12 additions & 0 deletions packages/vbenComponents/src/watermark/src/Watermark.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<script lang="ts" setup name="VbenWatermark">
import { maps } from '#/index'
const Watermark = maps.get('Watermark')
</script>
<template>
<Watermark v-bind="$attrs">
<template #[item]="data" v-for="item in Object.keys($slots)" :key="item">
<slot :name="item" v-bind="data || {}"></slot> </template
></Watermark>
</template>

<style scoped></style>

0 comments on commit e28d304

Please sign in to comment.