joeguang
2024-04-11 9784cdc840336d5c2bb006fa3ababfa7fa4056af
提交 | 用户 | age
a1d956 1 import { getCurrentInstance, onBeforeUnmount, ref, Ref, shallowRef, unref } from 'vue';
bab28a 2 import { useRafThrottle } from '@/utils/domUtils';
X 3 import { addResizeListener, removeResizeListener } from '@/utils/event';
4 import { isDef } from '@/utils/is';
2f6253 5
64b812 6 const watermarkSymbol = 'watermark-dom';
0a1a5f 7 const updateWatermarkText = ref<string | null>(null);
64b812 8
IW 9 type UseWatermarkRes = {
10   setWatermark: (str: string) => void;
11   clear: () => void;
12   clearAll: () => void;
ca3ddd 13   waterMarkOptions?: waterMarkOptionsType;
0a1a5f 14   obInstance?: MutationObserver;
J 15   targetElement?: HTMLElement;
16   parentElement?: HTMLElement;
64b812 17 };
IW 18
ca3ddd 19 type waterMarkOptionsType = {
K 20   // 自定义水印的文字大小
21   fontSize?: number;
22   // 自定义水印的文字颜色
23   fontColor?: string;
24   // 自定义水印的文字字体
25   fontFamily?: string;
26   // 自定义水印的文字对齐方式
27   textAlign?: CanvasTextAlign;
28   // 自定义水印的文字基线
29   textBaseline?: CanvasTextBaseline;
30   // 自定义水印的文字倾斜角度
31   rotate?: number;
32 };
33
64b812 34 const sourceMap = new Map<Symbol, Omit<UseWatermarkRes, 'clearAll'>>();
0a1a5f 35
ca3ddd 36 function findTargetNode(el) {
K 37   return Array.from(sourceMap.values()).find((item) => item.targetElement === el);
38 }
39
40 function createBase64(str: string, waterMarkOptions: waterMarkOptionsType) {
0a1a5f 41   const can = document.createElement('canvas');
J 42   const width = 300;
43   const height = 240;
44   Object.assign(can, { width, height });
45
46   const cans = can.getContext('2d');
47   if (cans) {
ca3ddd 48     const fontFamily = waterMarkOptions?.fontFamily || 'Vedana';
K 49     const fontSize = waterMarkOptions?.fontSize || 15;
50     const fontColor = waterMarkOptions?.fontColor || 'rgba(0, 0, 0, 0.15)';
51     const textAlign = waterMarkOptions?.textAlign || 'left';
52     const textBaseline = waterMarkOptions?.textBaseline || 'middle';
53     const rotate = waterMarkOptions?.rotate || 20;
54     cans.rotate((-rotate * Math.PI) / 180);
55     cans.font = `${fontSize}px ${fontFamily}`;
56     cans.fillStyle = fontColor;
57     cans.textAlign = textAlign;
58     cans.textBaseline = textBaseline;
0a1a5f 59     cans.fillText(str, width / 20, height);
J 60   }
61   return can.toDataURL('image/png');
62 }
ca3ddd 63 const resetWatermarkStyle = (
K 64   element: HTMLElement,
65   watermarkText: string,
66   waterMarkOptions: waterMarkOptionsType,
67 ) => {
0a1a5f 68   element.className = '__' + watermarkSymbol;
J 69   element.style.pointerEvents = 'none';
ca3ddd 70   element.style.display = 'block';
K 71   element.style.visibility = 'visible';
0a1a5f 72   element.style.top = '0px';
J 73   element.style.left = '0px';
74   element.style.position = 'absolute';
75   element.style.zIndex = '100000';
76   element.style.height = '100%';
77   element.style.width = '100%';
78   element.style.background = `url(${createBase64(
79     unref(updateWatermarkText) || watermarkText,
ca3ddd 80     waterMarkOptions,
0a1a5f 81   )}) left top repeat`;
J 82 };
83
84 const obFn = () => {
85   const obInstance = new MutationObserver((mutationRecords) => {
86     for (const mutation of mutationRecords) {
87       for (const node of Array.from(mutation.removedNodes)) {
ca3ddd 88         const target = findTargetNode(node);
0a1a5f 89         if (!target) return;
J 90         const { targetElement, parentElement } = target;
91         // 父元素的子元素水印如果被删除 重新插入被删除的水印(防篡改,插入通过控制台删除的水印)
92         if (!parentElement?.contains(targetElement as Node | null)) {
93           target?.parentElement?.appendChild(node as HTMLElement);
94         }
95       }
9784cd 96       if (mutation.type === 'attributes' && mutation.target) {
J 97         // 修复控制台可以”Hide element” 的问题
0a1a5f 98         const _target = mutation.target as HTMLElement;
ca3ddd 99         const target = findTargetNode(_target);
K 100         if (target) {
9784cd 101           // 禁止改属性 包括class 修改以后 mutation.type 也等于 'attributes'
J 102           // 先解除监听 再加一下
103           clearAll();
104           target.setWatermark(target.targetElement?.['data-watermark-text']);
0a1a5f 105         }
J 106       }
107     }
108   });
109   return obInstance;
110 };
2f6253 111
480cfb 112 export function useWatermark(
56a966 113   appendEl: Ref<HTMLElement | null> = ref(document.body) as Ref<HTMLElement>,
ca3ddd 114   waterMarkOptions: waterMarkOptionsType = {},
64b812 115 ): UseWatermarkRes {
IW 116   const domSymbol = Symbol(watermarkSymbol);
3b0077 117   const appendElRaw = unref(appendEl);
64b812 118   if (appendElRaw && sourceMap.has(domSymbol)) {
IW 119     const { setWatermark, clear } = sourceMap.get(domSymbol) as UseWatermarkRes;
120     return { setWatermark, clear, clearAll };
3b0077 121   }
a1d956 122   const func = useRafThrottle(function () {
Y 123     const el = unref(appendEl);
124     if (!el) return;
125     const { clientHeight: height, clientWidth: width } = el;
126     updateWatermark({ height, width });
127   });
128   const watermarkEl = shallowRef<HTMLElement>();
129   const clear = () => {
130     const domId = unref(watermarkEl);
131     watermarkEl.value = undefined;
132     const el = unref(appendEl);
0a1a5f 133     sourceMap.has(domSymbol) && sourceMap.get(domSymbol)?.obInstance?.disconnect();
64b812 134     sourceMap.delete(domSymbol);
a1d956 135     if (!el) return;
Y 136     domId && el.removeChild(domId);
137     removeResizeListener(el, func);
138   };
2f6253 139
a1d956 140   function updateWatermark(
Y 141     options: {
142       width?: number;
143       height?: number;
144       str?: string;
56a966 145     } = {},
a1d956 146   ) {
Y 147     const el = unref(watermarkEl);
148     if (!el) return;
149     if (isDef(options.width)) {
150       el.style.width = `${options.width}px`;
151     }
152     if (isDef(options.height)) {
153       el.style.height = `${options.height}px`;
154     }
155     if (isDef(options.str)) {
ca3ddd 156       el.style.background = `url(${createBase64(options.str, waterMarkOptions)}) left top repeat`;
a1d956 157     }
Y 158   }
159
160   const createWatermark = (str: string) => {
64b812 161     if (unref(watermarkEl) && sourceMap.has(domSymbol)) {
0a1a5f 162       updateWatermarkText.value = str;
a1d956 163       updateWatermark({ str });
64b812 164       return;
a1d956 165     }
2f6253 166     const div = document.createElement('div');
0a1a5f 167     div['data-watermark-text'] = str; //自定义属性 用于恢复水印
J 168     updateWatermarkText.value = str;
a1d956 169     watermarkEl.value = div;
ca3ddd 170     resetWatermarkStyle(div, str, waterMarkOptions);
2f6253 171     const el = unref(appendEl);
64b812 172     if (!el) return;
a1d956 173     const { clientHeight: height, clientWidth: width } = el;
Y 174     updateWatermark({ str, width, height });
175     el.appendChild(div);
0a1a5f 176     sourceMap.set(domSymbol, {
J 177       setWatermark,
178       clear,
179       parentElement: el,
180       targetElement: div,
181       obInstance: obFn(),
ca3ddd 182       waterMarkOptions,
0a1a5f 183     });
J 184     sourceMap.get(domSymbol)?.obInstance?.observe(el, {
185       childList: true, // 子节点的变动(指新增,删除或者更改)
186       subtree: true, // 该观察器应用于该节点的所有后代节点
187       attributes: true, // 属性的变动
188     });
2f6253 189   };
190
191   function setWatermark(str: string) {
192     createWatermark(str);
a1d956 193     addResizeListener(document.documentElement, func);
2f6253 194     const instance = getCurrentInstance();
195     if (instance) {
196       onBeforeUnmount(() => {
197         clear();
198       });
199     }
200   }
64b812 201   return { setWatermark, clear, clearAll };
IW 202 }
203
204 function clearAll() {
205   Array.from(sourceMap.values()).forEach((item) => {
0a1a5f 206     item?.obInstance?.disconnect();
64b812 207     item.clear();
IW 208   });
2f6253 209 }