vue-watermark-plus 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lebron_shi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # 🛡️ vue-watermark-plus
2
+
3
+ > 一个高性能的Vue3企业级水印工具,支持Canvas / SVG 双渲染引擎,零侵入一键使用,双重防篡改,让你的内容保护变得简单又高效。
4
+
5
+ [![npm version](https://img.shields.io/npm/v/vue-watermark-plus)](https://www.npmjs.com/package/vue-watermark-plus)
6
+ [![license](https://img.shields.io/npm/l/vue-watermark-plus)](https://github.com/shilimingY/vue-watermark-plus/blob/master/LICENSE)
7
+
8
+ ## ✨ 特点
9
+
10
+ - **双模式渲染**:Canvas 与 SVG 自由切换,SVG 矢量文字清晰,Canvas 兼容性好。
11
+ - **文字 + 图片水印**:支持多行文本、自定义字体/颜色/大小,也支持上传 Logo 图片并控制缩放与间距。
12
+ - **防篡改监控**:实时监听水印节点的删除与样式修改并自动恢复。可通过 `monitor` 选项自由开启/关闭,适应不同安全需求。
13
+ - **零侵入指令**:通过 `v-watermark` 使用,不增加额外组件,不破坏现有布局,
14
+ - **TypeScript 完备**:完整的类型定义,配置清晰,开发体验极佳。
15
+ - **轻量高性能**:仅生成一个背景单元图片并复用,内存与 DOM 开销极低。
16
+
17
+ ## 🆚 与常见方案对比
18
+
19
+ | 特性 | vue-watermark-plus | 普通 Vue 水印组件 | 纯 CSS 水印 |
20
+ |------|-------------------|------------------|------------|
21
+ | 渲染方式 | Canvas / SVG 可选 | 多数仅 Canvas | 仅文本 |
22
+ | 防篡改 | ✅ MutationObserver + 定时校验 | 部分仅监听 DOM 删除 | ❌ 极易被删除 |
23
+ | 样式篡改防御(`el.style.display='none'`) | ✅ 定时检测计算样式,自动恢复 | ❌ | ❌ |
24
+ | 图片水印 | ✅ 支持,可控制缩放与间距 | 少数支持 | ❌ | 部分支持 |
25
+ | 使用方式 | 指令 `v-watermark`,挂载即用 | 组件包裹,可能改变结构 | 手动添加样式 |
26
+ | 动态更新配置 | ✅ 响应式,修改绑定值即更新 | 部分支持 | ❌ |
27
+ | TypeScript 支持 | ✅ 完整类型定义 | 部分 | - |
28
+ | 包体积(gzip) | ~5 KB | 5~15 KB | - |
29
+
30
+ ## 📦 安装
31
+
32
+ ```bash
33
+ npm install vue-watermark-plus
34
+ # 或
35
+ yarn add vue-watermark-plus
36
+ # 或
37
+ pnpm add vue-watermark-plus
38
+ ```
39
+
40
+ ## 🚀 快速上手
41
+
42
+ ### 全局注册
43
+ ```ts
44
+ // main.ts
45
+ import { createApp } from 'vue'
46
+ import VueWatermarkPlus from 'vue-watermark-plus'
47
+ import App from './App.vue'
48
+
49
+ const app = createApp(App)
50
+ app.use(VueWatermarkPlus)
51
+ app.mount('#app')
52
+ ```
53
+ ### 在组件中使用指令
54
+ ```vue
55
+ <template>
56
+ <div
57
+ v-watermark="{
58
+ text: '仅供内部使用',
59
+ opacity: 0.2,
60
+ rotate: -20,
61
+ density: 'medium',
62
+ mode: 'svg'
63
+ }"
64
+ >
65
+ <!-- 你的页面内容 -->
66
+ </div>
67
+ </template>
68
+ ```
69
+
70
+ ## ⚙️ 配置参数
71
+ | 参数 | 类型 | 默认值 | 说明 |
72
+ |------|------|--------|------|
73
+ | `text` | `string \| string[]` | `'CONFIDENTIAL'` | 水印文本,可传入多行数组,如 `['绝密', '内部使用']` |
74
+ | `image` | `string` | `''` | 图片水印 URL(优先级高于文本) |
75
+ | `mode` | `'canvas' \| 'svg'` | `'svg'` | 渲染模式:`svg` 矢量清晰,`canvas` 兼容性好 |
76
+ | `rotate` | `number` | `-20` | 倾斜角度(deg) |
77
+ | `opacity` | `number` | `0.2` | 水印透明度 (0-1) |
78
+ | `fontSize` | `number` | `16` | 字体大小(px) |
79
+ | `fontFamily` | `string` | `'sans-serif'` | 字体族 |
80
+ | `color` | `string` | `'#000'` | 文字颜色 |
81
+ | `density` | `'low' \| 'medium' \| 'high' \| number` | `'medium'` | 密度预设(low: 200px 间距 / medium: 120px / high: 60px)或自定义数字 |
82
+ | `gapX` | `number` | — | 水平间距(px),优先级高于 `density` |
83
+ | `gapY` | `number` | — | 垂直间距(px),优先级高于 `density` |
84
+ | `zIndex` | `number` | `9999` | 水印层 z-index |
85
+ | `monitor` | `boolean` | `true` | 是否启用防篡改监控(MutationObserver + 定时校验) |
86
+ | `imageWidth` | `number` | `100`(当未设置时) | 图片水印显示宽度(px),高度等比缩放 |
87
+ | `imageHeight` | `number` | 等比计算 | 图片水印显示高度(px),若不传则按宽度等比 |
88
+
89
+ ## 🔧 进阶用法
90
+
91
+ ### 1. 图片水印
92
+ ```js
93
+ import logoUrl from './assets/safe.png'
94
+ ```
95
+ ```vue
96
+ <div v-watermark="{
97
+ image: logoUrl,
98
+ opacity: 0.2,
99
+ rotate: -10,
100
+ gapX: 150,
101
+ gapY: 100,
102
+ imageWidth: 25
103
+ }">
104
+ <h1>一行指令,即刻为您的页面穿上隐形防护衣</h1>
105
+ <h1>无需额外组件</h1>
106
+ <h1>不影响页面交互与性能</h1>
107
+ </div>
108
+ ```
109
+ **图片水印效果**
110
+
111
+ ![alt text](/src/assets/image-1.png)
112
+
113
+ ### 2. 多行文本水印
114
+
115
+ ```vue
116
+ <div v-watermark="{
117
+ text: ['绝密文件', '仅供内部使用', '禁止外传'],
118
+ opacity: 0.2,
119
+ density: 'high'
120
+ }">
121
+ <h1>一行指令,即刻为您的页面穿上隐形防护衣</h1>
122
+ <h1>无需额外组件</h1>
123
+ <h1>不影响页面交互与性能</h1>
124
+ </div>
125
+ ```
126
+ **多行文本水印效果**
127
+
128
+ ![alt text](/src/assets/image.png)
129
+
130
+ > 如果你在使用过程中遇到任何问题,欢迎提交 [Issue](https://github.com/shilimingY/vue-watermark-plus/issues)。
131
+
@@ -0,0 +1,6 @@
1
+ import { Directive } from 'vue';
2
+ import { WatermarkOptions } from './types';
3
+ /**
4
+ * v-watermark 指令定义
5
+ */
6
+ export declare const vWatermark: Directive<HTMLElement, WatermarkOptions>;
@@ -0,0 +1,7 @@
1
+ import { App } from 'vue';
2
+ export { vWatermark } from './directive';
3
+ export type { WatermarkOptions, WatermarkMode } from './types';
4
+ declare const plugin: {
5
+ install(app: App): void;
6
+ };
7
+ export default plugin;
@@ -0,0 +1,6 @@
1
+ export * from '../index'
2
+ export {}
3
+ import VueWatermarkPlus from '../index'
4
+ export default VueWatermarkPlus
5
+ export * from '../index'
6
+ export {}
@@ -0,0 +1,43 @@
1
+ export type WatermarkMode = 'canvas' | 'svg';
2
+ export type DensityPreset = 'low' | 'medium' | 'high';
3
+ export interface WatermarkOptions {
4
+ /** 水印文本(可多行) */
5
+ text?: string | string[];
6
+ /** 水印图片 URL(与文本互斥,优先使用图片) */
7
+ image?: string;
8
+ /** 渲染模式,默认 'svg' */
9
+ mode?: WatermarkMode;
10
+ /** 倾斜角度(deg),默认 -20 */
11
+ rotate?: number;
12
+ /** 全局透明度 0-1,默认 0.2 */
13
+ opacity?: number;
14
+ /** 字体大小(px),默认 16 */
15
+ fontSize?: number;
16
+ /** 字体族,默认 'sans-serif' */
17
+ fontFamily?: string;
18
+ /** 文字颜色,默认 '#000' */
19
+ color?: string;
20
+ /** 密度预设或自定义间距(px),默认 'medium' */
21
+ density?: DensityPreset | number;
22
+ /** 水平间距(px),优先级高于 density */
23
+ gapX?: number;
24
+ /** 垂直间距(px),优先级高于 density */
25
+ gapY?: number;
26
+ /** 水印层 z-index,默认 9999 */
27
+ zIndex?: number;
28
+ /** 是否启用 DOM 防篡改监控,默认 true */
29
+ monitor?: boolean;
30
+ /** 图片水印宽度(px),不传则默认 100 */
31
+ imageWidth?: number;
32
+ /** 图片水印高度(px),若不传则按宽度等比计算 */
33
+ imageHeight?: number;
34
+ }
35
+ /** 内部使用的完整配置(所有可选字段已填充默认值) */
36
+ export type ResolvedOptions = Required<Omit<WatermarkOptions, 'image' | 'text' | 'gapX' | 'gapY' | 'imageWidth' | 'imageHeight'>> & {
37
+ text: string[];
38
+ image: string;
39
+ gapX: number;
40
+ gapY: number;
41
+ imageWidth: number;
42
+ imageHeight: number;
43
+ };
@@ -0,0 +1,11 @@
1
+ import { ResolvedOptions } from './types';
2
+ /**
3
+ * 根据配置选择合适的生成方法,返回水印背景单元的 DataURL
4
+ * 图片水印优先,否则根据 mode 选择 Canvas 或 SVG
5
+ */
6
+ export declare function generateWatermarkUrl(opts: ResolvedOptions): Promise<string>;
7
+ /**
8
+ * 解析用户传入的 WatermarkOptions,补全默认值,返回内部使用的 ResolvedOptions
9
+ * 确保所有必要字段都有有效值,方便后续绘制与监控
10
+ */
11
+ export declare function resolveOptions(options: import('./types').WatermarkOptions): ResolvedOptions;
@@ -0,0 +1,239 @@
1
+ function $(e) {
2
+ return e <= 2 ? { gapX: 200, gapY: 200 } : e <= 4 ? { gapX: 120, gapY: 120 } : { gapX: 60, gapY: 60 };
3
+ }
4
+ function I(e) {
5
+ const { text: t, rotate: i, opacity: h, fontSize: s, fontFamily: r, color: d, gapX: m, gapY: a } = e, n = document.createElement("canvas"), o = n.getContext("2d");
6
+ o.font = `${s}px ${r}`;
7
+ const v = Math.max(...t.map((c) => o.measureText(c).width)), u = s * 1.5, g = t.length * u, l = v + m, p = g + a;
8
+ return n.width = l, n.height = p, o.translate(l / 2, p / 2), o.rotate(i * Math.PI / 180), o.fillStyle = d, o.globalAlpha = h, o.font = `${s}px ${r}`, o.textAlign = "center", o.textBaseline = "middle", t.forEach((c, f) => {
9
+ const b = (f - (t.length - 1) / 2) * u;
10
+ o.fillText(c, 0, b);
11
+ }), n.toDataURL();
12
+ }
13
+ function W(e) {
14
+ const { text: t, rotate: i, opacity: h, fontSize: s, fontFamily: r, color: d, gapX: m, gapY: a } = e, n = s * 1.5, o = t.length * n, u = Math.max(...t.map((c) => c.length * s * 0.6)) + m, g = o + a, l = t.map((c, f) => `<text x="0" y="${(f - (t.length - 1) / 2) * n}" fill="${d}" font-size="${s}" font-family="${r}" text-anchor="middle" dominant-baseline="middle">${c}</text>`).join(""), p = `<svg xmlns="http://www.w3.org/2000/svg" width="${u}" height="${g}">
15
+ <g transform="translate(${u / 2}, ${g / 2}) rotate(${i})" opacity="${h}">
16
+ ${l}
17
+ </g>
18
+ </svg>`;
19
+ return `data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(p)))}`;
20
+ }
21
+ async function H(e) {
22
+ const { image: t, rotate: i, opacity: h, gapX: s, gapY: r, imageWidth: d, imageHeight: m } = e, a = await new Promise((p, c) => {
23
+ const f = new Image();
24
+ f.crossOrigin = "anonymous", f.onload = () => p(f), f.onerror = c, f.src = t;
25
+ });
26
+ let n = d, o = m;
27
+ !n && !o ? (n = 100, o = a.height / a.width * n) : n && !o ? o = a.height / a.width * n : !n && o && (n = a.width / a.height * o);
28
+ const v = n + s, u = o + r, g = document.createElement("canvas");
29
+ g.width = v, g.height = u;
30
+ const l = g.getContext("2d");
31
+ return l.save(), l.translate(v / 2, u / 2), l.rotate(i * Math.PI / 180), l.globalAlpha = h, l.drawImage(a, -n / 2, -o / 2, n, o), l.restore(), g.toDataURL();
32
+ }
33
+ async function k(e) {
34
+ return e.image ? H(e) : e.mode === "canvas" ? I(e) : W(e);
35
+ }
36
+ function w(e) {
37
+ const {
38
+ text: t = "CONFIDENTIAL",
39
+ image: i = "",
40
+ mode: h = "svg",
41
+ // 默认使用 SVG 模式,保证清晰度
42
+ rotate: s = -20,
43
+ opacity: r = 2,
44
+ // 注意:透明度通常应在 0-1 之间,这里默认值为 2(可能为笔误,但保持原样)
45
+ fontSize: d = 16,
46
+ fontFamily: m = "sans-serif",
47
+ color: a = "#000",
48
+ density: n = "medium",
49
+ zIndex: o = 9999,
50
+ monitor: v = !0,
51
+ imageWidth: u,
52
+ imageHeight: g
53
+ } = e, l = Array.isArray(t) ? t : [t];
54
+ let p = e.gapX, c = e.gapY;
55
+ if (p === void 0 || c === void 0) {
56
+ const C = typeof n == "number" ? n : { low: 2, medium: 3, high: 5 }[n], x = $(C);
57
+ p = p ?? x.gapX, c = c ?? x.gapY;
58
+ }
59
+ return {
60
+ text: l,
61
+ image: i,
62
+ mode: h,
63
+ rotate: s,
64
+ opacity: r,
65
+ fontSize: d,
66
+ fontFamily: m,
67
+ color: a,
68
+ density: 0,
69
+ gapX: p,
70
+ gapY: c,
71
+ zIndex: o,
72
+ monitor: v,
73
+ imageWidth: u ?? 0,
74
+ imageHeight: g ?? 0
75
+ };
76
+ }
77
+ class P {
78
+ // 定时样式校验的定时器
79
+ constructor(t, i) {
80
+ this.container = null, this.observer = null, this.styleCheckTimer = null, this.el = t, this.options = w(i), this.originalPosition = getComputedStyle(t).position, this.init();
81
+ }
82
+ /** 初始化流程:创建水印 -> 启动防篡改监控 -> 启动定时样式校验 */
83
+ async init() {
84
+ await this.createContainer(), this.startMonitoring(), this.startPeriodicCheck();
85
+ }
86
+ /**
87
+ * 创建/重建水印容器
88
+ * 水印通过绝对定位覆盖在目标元素上,利用 background-repeat 平铺背景图
89
+ */
90
+ async createContainer() {
91
+ this.originalPosition === "static" && (this.el.style.position = "relative"), this.container && (this.container.remove(), this.container = null);
92
+ const t = document.createElement("div");
93
+ t.className = "__wm_container__", t.style.cssText = `
94
+ position: absolute !important;
95
+ top: 0 !important;
96
+ left: 0 !important;
97
+ width: 100% !important;
98
+ height: 100% !important;
99
+ pointer-events: none !important; /* 不拦截鼠标事件 */
100
+ z-index: ${this.options.zIndex} !important;
101
+ background-repeat: repeat !important;
102
+ `;
103
+ const i = await k(this.options);
104
+ t.style.backgroundImage = `url(${i})`, this.el.appendChild(t), this.container = t;
105
+ }
106
+ /**
107
+ * 周期性检测水印容器的关键样式(display / visibility / opacity)
108
+ * 用于防范通过 element.style 直接修改样式而绕过 MutationObserver 的情况
109
+ */
110
+ startPeriodicCheck() {
111
+ if (!this.options.monitor) {
112
+ this.stopPeriodicCheck();
113
+ return;
114
+ }
115
+ this.styleCheckTimer || (this.styleCheckTimer = setInterval(() => {
116
+ var i;
117
+ if (!this.container) return;
118
+ const t = getComputedStyle(this.container);
119
+ (t.display === "none" || t.visibility === "hidden" || parseFloat(t.opacity) < 0.01) && ((i = this.observer) == null || i.disconnect(), this.createContainer().then(() => {
120
+ this.observeElements();
121
+ }));
122
+ }, 2e3));
123
+ }
124
+ stopPeriodicCheck() {
125
+ this.styleCheckTimer && (clearInterval(this.styleCheckTimer), this.styleCheckTimer = null);
126
+ }
127
+ /**
128
+ * 启动 MutationObserver 监控
129
+ * 通过 MutationObserver 监听水印容器的 childList 和 attributes 变化,一旦容器被删除或 style/class 属性被修改,立即重建
130
+ * 当水印容器被从 DOM 中删除,或其 style/class 属性被修改时,自动重建水印
131
+ */
132
+ startMonitoring() {
133
+ this.options.monitor && (this.observer = new MutationObserver((t) => {
134
+ var h;
135
+ let i = !1;
136
+ for (const s of t) {
137
+ if (s.type === "childList") {
138
+ if (Array.from(s.removedNodes).some((d) => d === this.container)) {
139
+ i = !0;
140
+ break;
141
+ }
142
+ if (this.el.children.length === 0) {
143
+ i = !0;
144
+ break;
145
+ }
146
+ }
147
+ if (s.type === "attributes" && s.target === this.container) {
148
+ i = !0;
149
+ break;
150
+ }
151
+ }
152
+ i && ((h = this.observer) == null || h.disconnect(), this.createContainer().then(() => {
153
+ this.observeElements();
154
+ }));
155
+ }), this.observeElements());
156
+ }
157
+ /** 绑定 MutationObserver 到目标元素及水印容器上 */
158
+ observeElements() {
159
+ this.observer && (this.observer.observe(this.el, { childList: !0 }), this.container && this.observer.observe(this.container, {
160
+ attributes: !0,
161
+ attributeFilter: ["style", "class"]
162
+ }));
163
+ }
164
+ /**
165
+ * 更新水印配置(由指令 updated 钩子调用)
166
+ * 支持动态开关监控(monitor)和更新其他样式配置
167
+ */
168
+ async update(t) {
169
+ var h;
170
+ const i = this.options.monitor;
171
+ if (this.options = w(t), this.container) {
172
+ const s = await k(this.options);
173
+ this.container.style.backgroundImage = `url(${s})`;
174
+ }
175
+ this.options.monitor !== i && (this.options.monitor ? (this.observer || (this.observer = new MutationObserver((s) => {
176
+ var d;
177
+ let r = !1;
178
+ for (const m of s) {
179
+ if (m.type === "childList") {
180
+ if (Array.from(m.removedNodes).some((n) => n === this.container)) {
181
+ r = !0;
182
+ break;
183
+ }
184
+ if (this.el.children.length === 0) {
185
+ r = !0;
186
+ break;
187
+ }
188
+ }
189
+ if (m.type === "attributes" && m.target === this.container) {
190
+ r = !0;
191
+ break;
192
+ }
193
+ }
194
+ r && ((d = this.observer) == null || d.disconnect(), this.createContainer().then(() => {
195
+ this.observeElements();
196
+ }));
197
+ }), this.observeElements()), this.startPeriodicCheck()) : ((h = this.observer) == null || h.disconnect(), this.observer = null, this.stopPeriodicCheck()));
198
+ }
199
+ /** 销毁水印实例,清理所有监听和 DOM 元素 */
200
+ destroy() {
201
+ var t;
202
+ (t = this.observer) == null || t.disconnect(), this.stopPeriodicCheck(), this.container && (this.container.remove(), this.container = null), this.el.style.position === "relative" && (this.el.style.position = this.originalPosition);
203
+ }
204
+ }
205
+ const y = /* @__PURE__ */ new WeakMap(), E = {
206
+ /**
207
+ * 元素挂载时:
208
+ * 1. 根据传入的配置创建 Watermark 实例
209
+ * 2. 将实例存入 WeakMap,便于后续更新或销毁时获取
210
+ */
211
+ mounted(e, t) {
212
+ const i = new P(e, t.value ?? {});
213
+ y.set(e, i);
214
+ },
215
+ /**
216
+ * 指令绑定值更新时:
217
+ * 调用实例的 update 方法,支持动态修改水印配置(如文本、透明度、监控开关等)
218
+ */
219
+ updated(e, t) {
220
+ const i = y.get(e);
221
+ i && i.update(t.value ?? {});
222
+ },
223
+ /**
224
+ * 元素卸载时:
225
+ * 调用实例的 destroy 方法清理所有监听和 DOM,并从 WeakMap 中移除
226
+ */
227
+ unmounted(e) {
228
+ const t = y.get(e);
229
+ t && (t.destroy(), y.delete(e));
230
+ }
231
+ }, M = {
232
+ install(e) {
233
+ e.directive("watermark", E);
234
+ }
235
+ };
236
+ export {
237
+ M as default,
238
+ E as vWatermark
239
+ };
@@ -0,0 +1,14 @@
1
+ (function(v,b){typeof exports=="object"&&typeof module<"u"?b(exports):typeof define=="function"&&define.amd?define(["exports"],b):(v=typeof globalThis<"u"?globalThis:v||self,b(v.VueWatermarkPlus={}))})(this,(function(v){"use strict";function b(e){return e<=2?{gapX:200,gapY:200}:e<=4?{gapX:120,gapY:120}:{gapX:60,gapY:60}}function W(e){const{text:t,rotate:i,opacity:h,fontSize:s,fontFamily:r,color:d,gapX:m,gapY:a}=e,n=document.createElement("canvas"),o=n.getContext("2d");o.font=`${s}px ${r}`;const y=Math.max(...t.map(c=>o.measureText(c).width)),f=s*1.5,g=t.length*f,l=y+m,u=g+a;return n.width=l,n.height=u,o.translate(l/2,u/2),o.rotate(i*Math.PI/180),o.fillStyle=d,o.globalAlpha=h,o.font=`${s}px ${r}`,o.textAlign="center",o.textBaseline="middle",t.forEach((c,p)=>{const k=(p-(t.length-1)/2)*f;o.fillText(c,0,k)}),n.toDataURL()}function P(e){const{text:t,rotate:i,opacity:h,fontSize:s,fontFamily:r,color:d,gapX:m,gapY:a}=e,n=s*1.5,o=t.length*n,f=Math.max(...t.map(c=>c.length*s*.6))+m,g=o+a,l=t.map((c,p)=>`<text x="0" y="${(p-(t.length-1)/2)*n}" fill="${d}" font-size="${s}" font-family="${r}" text-anchor="middle" dominant-baseline="middle">${c}</text>`).join(""),u=`<svg xmlns="http://www.w3.org/2000/svg" width="${f}" height="${g}">
2
+ <g transform="translate(${f/2}, ${g/2}) rotate(${i})" opacity="${h}">
3
+ ${l}
4
+ </g>
5
+ </svg>`;return`data:image/svg+xml;base64,${btoa(unescape(encodeURIComponent(u)))}`}async function T(e){const{image:t,rotate:i,opacity:h,gapX:s,gapY:r,imageWidth:d,imageHeight:m}=e,a=await new Promise((u,c)=>{const p=new Image;p.crossOrigin="anonymous",p.onload=()=>u(p),p.onerror=c,p.src=t});let n=d,o=m;!n&&!o?(n=100,o=a.height/a.width*n):n&&!o?o=a.height/a.width*n:!n&&o&&(n=a.width/a.height*o);const y=n+s,f=o+r,g=document.createElement("canvas");g.width=y,g.height=f;const l=g.getContext("2d");return l.save(),l.translate(y/2,f/2),l.rotate(i*Math.PI/180),l.globalAlpha=h,l.drawImage(a,-n/2,-o/2,n,o),l.restore(),g.toDataURL()}async function w(e){return e.image?T(e):e.mode==="canvas"?W(e):P(e)}function C(e){const{text:t="CONFIDENTIAL",image:i="",mode:h="svg",rotate:s=-20,opacity:r=2,fontSize:d=16,fontFamily:m="sans-serif",color:a="#000",density:n="medium",zIndex:o=9999,monitor:y=!0,imageWidth:f,imageHeight:g}=e,l=Array.isArray(t)?t:[t];let u=e.gapX,c=e.gapY;if(u===void 0||c===void 0){const E=typeof n=="number"?n:{low:2,medium:3,high:5}[n],I=b(E);u=u??I.gapX,c=c??I.gapY}return{text:l,image:i,mode:h,rotate:s,opacity:r,fontSize:d,fontFamily:m,color:a,density:0,gapX:u,gapY:c,zIndex:o,monitor:y,imageWidth:f??0,imageHeight:g??0}}class H{constructor(t,i){this.container=null,this.observer=null,this.styleCheckTimer=null,this.el=t,this.options=C(i),this.originalPosition=getComputedStyle(t).position,this.init()}async init(){await this.createContainer(),this.startMonitoring(),this.startPeriodicCheck()}async createContainer(){this.originalPosition==="static"&&(this.el.style.position="relative"),this.container&&(this.container.remove(),this.container=null);const t=document.createElement("div");t.className="__wm_container__",t.style.cssText=`
6
+ position: absolute !important;
7
+ top: 0 !important;
8
+ left: 0 !important;
9
+ width: 100% !important;
10
+ height: 100% !important;
11
+ pointer-events: none !important; /* 不拦截鼠标事件 */
12
+ z-index: ${this.options.zIndex} !important;
13
+ background-repeat: repeat !important;
14
+ `;const i=await w(this.options);t.style.backgroundImage=`url(${i})`,this.el.appendChild(t),this.container=t}startPeriodicCheck(){if(!this.options.monitor){this.stopPeriodicCheck();return}this.styleCheckTimer||(this.styleCheckTimer=setInterval(()=>{var i;if(!this.container)return;const t=getComputedStyle(this.container);(t.display==="none"||t.visibility==="hidden"||parseFloat(t.opacity)<.01)&&((i=this.observer)==null||i.disconnect(),this.createContainer().then(()=>{this.observeElements()}))},2e3))}stopPeriodicCheck(){this.styleCheckTimer&&(clearInterval(this.styleCheckTimer),this.styleCheckTimer=null)}startMonitoring(){this.options.monitor&&(this.observer=new MutationObserver(t=>{var h;let i=!1;for(const s of t){if(s.type==="childList"){if(Array.from(s.removedNodes).some(d=>d===this.container)){i=!0;break}if(this.el.children.length===0){i=!0;break}}if(s.type==="attributes"&&s.target===this.container){i=!0;break}}i&&((h=this.observer)==null||h.disconnect(),this.createContainer().then(()=>{this.observeElements()}))}),this.observeElements())}observeElements(){this.observer&&(this.observer.observe(this.el,{childList:!0}),this.container&&this.observer.observe(this.container,{attributes:!0,attributeFilter:["style","class"]}))}async update(t){var h;const i=this.options.monitor;if(this.options=C(t),this.container){const s=await w(this.options);this.container.style.backgroundImage=`url(${s})`}this.options.monitor!==i&&(this.options.monitor?(this.observer||(this.observer=new MutationObserver(s=>{var d;let r=!1;for(const m of s){if(m.type==="childList"){if(Array.from(m.removedNodes).some(n=>n===this.container)){r=!0;break}if(this.el.children.length===0){r=!0;break}}if(m.type==="attributes"&&m.target===this.container){r=!0;break}}r&&((d=this.observer)==null||d.disconnect(),this.createContainer().then(()=>{this.observeElements()}))}),this.observeElements()),this.startPeriodicCheck()):((h=this.observer)==null||h.disconnect(),this.observer=null,this.stopPeriodicCheck()))}destroy(){var t;(t=this.observer)==null||t.disconnect(),this.stopPeriodicCheck(),this.container&&(this.container.remove(),this.container=null),this.el.style.position==="relative"&&(this.el.style.position=this.originalPosition)}}const x=new WeakMap,$={mounted(e,t){const i=new H(e,t.value??{});x.set(e,i)},updated(e,t){const i=x.get(e);i&&i.update(t.value??{})},unmounted(e){const t=x.get(e);t&&(t.destroy(),x.delete(e))}},M={install(e){e.directive("watermark",$)}};v.default=M,v.vWatermark=$,Object.defineProperties(v,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}})}));
@@ -0,0 +1,42 @@
1
+ import { WatermarkOptions } from './types';
2
+ /**
3
+ * 水印核心类
4
+ * 负责创建、更新、销毁水印层,并实现防篡改监控和定时样式校验
5
+ */
6
+ export declare class Watermark {
7
+ private el;
8
+ private options;
9
+ private container;
10
+ private observer;
11
+ private originalPosition;
12
+ private styleCheckTimer;
13
+ constructor(el: HTMLElement, options: WatermarkOptions);
14
+ /** 初始化流程:创建水印 -> 启动防篡改监控 -> 启动定时样式校验 */
15
+ private init;
16
+ /**
17
+ * 创建/重建水印容器
18
+ * 水印通过绝对定位覆盖在目标元素上,利用 background-repeat 平铺背景图
19
+ */
20
+ private createContainer;
21
+ /**
22
+ * 周期性检测水印容器的关键样式(display / visibility / opacity)
23
+ * 用于防范通过 element.style 直接修改样式而绕过 MutationObserver 的情况
24
+ */
25
+ private startPeriodicCheck;
26
+ private stopPeriodicCheck;
27
+ /**
28
+ * 启动 MutationObserver 监控
29
+ * 通过 MutationObserver 监听水印容器的 childList 和 attributes 变化,一旦容器被删除或 style/class 属性被修改,立即重建
30
+ * 当水印容器被从 DOM 中删除,或其 style/class 属性被修改时,自动重建水印
31
+ */
32
+ private startMonitoring;
33
+ /** 绑定 MutationObserver 到目标元素及水印容器上 */
34
+ private observeElements;
35
+ /**
36
+ * 更新水印配置(由指令 updated 钩子调用)
37
+ * 支持动态开关监控(monitor)和更新其他样式配置
38
+ */
39
+ update(options: WatermarkOptions): Promise<void>;
40
+ /** 销毁水印实例,清理所有监听和 DOM 元素 */
41
+ destroy(): void;
42
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "vue-watermark-plus",
3
+ "version": "1.1.8",
4
+ "description": "基于 Vue3 强力水印插件✨,支持Canvas/SVG双渲染模式🎨,内置防篡改机制📝,采用自定义指令无侵入式设计,为你的页面内容加上隐形安全锁🛡️🛡️🛡️。",
5
+ "main": "dist/vue-watermark-plus.umd.js",
6
+ "module": "dist/vue-watermark-plus.es.js",
7
+ "types": "dist/types/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/vue-watermark-plus.es.js",
11
+ "require": "./dist/vue-watermark-plus.umd.js"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "dev": "vite",
17
+ "build": "vue-tsc --noEmit && vite build",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": ["vue", "watermark", "directive", "canvas", "svg", "Vue水印插件", "防篡改水印", "自定义指令水印"],
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/shilimingY/vue-watermark-plus.git"
25
+ },
26
+ "peerDependencies": {
27
+ "vue": "^3.2.0"
28
+ },
29
+ "devDependencies": {
30
+ "vite": "^6.0.0",
31
+ "vite-plugin-dts": "^4.3.0",
32
+ "typescript": "^5.6.0",
33
+ "vue-tsc": "^3.2.6"
34
+ }
35
+ }