hy-app 0.6.4 → 0.6.6

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.
Files changed (106) hide show
  1. package/attributes.json +1 -1
  2. package/components/hy-address-picker/hy-address-picker.vue +249 -249
  3. package/components/hy-address-picker/props.ts +103 -103
  4. package/components/hy-button/hy-button.vue +320 -289
  5. package/components/hy-button/props.ts +143 -143
  6. package/components/hy-button/typing.d.ts +43 -35
  7. package/components/hy-calendar/header.vue +58 -58
  8. package/components/hy-calendar/hy-calendar.vue +8 -6
  9. package/components/hy-calendar/month.vue +402 -402
  10. package/components/hy-calendar/props.ts +169 -169
  11. package/components/hy-calendar/typing.d.ts +47 -45
  12. package/components/hy-cell-item/hy-cell-item.vue +161 -161
  13. package/components/hy-cell-item/props.ts +59 -59
  14. package/components/hy-check-button/hy-check-button.vue +135 -135
  15. package/components/hy-code-input/hy-code-input.vue +231 -231
  16. package/components/hy-code-input/props.ts +90 -90
  17. package/components/hy-config-provider/hy-config-provider.vue +53 -53
  18. package/components/hy-config-provider/props.ts +30 -30
  19. package/components/hy-coupon/hy-coupon.vue +183 -183
  20. package/components/hy-coupon/props.ts +108 -108
  21. package/components/hy-datetime-picker/hy-datetime-picker.vue +41 -55
  22. package/components/hy-datetime-picker/props.ts +144 -144
  23. package/components/hy-datetime-picker/typing.d.ts +2 -0
  24. package/components/hy-divider/props.ts +83 -83
  25. package/components/hy-empty/icon.ts +72 -72
  26. package/components/hy-folding-panel/hy-folding-panel-group.vue +162 -162
  27. package/components/hy-form/hy-form.vue +220 -220
  28. package/components/hy-icon/hy-icon.vue +112 -112
  29. package/components/hy-index-bar/hy-index-bar.vue +185 -185
  30. package/components/hy-index-bar/index.scss +64 -64
  31. package/components/hy-index-bar/props.ts +94 -94
  32. package/components/hy-index-bar/typing.d.ts +36 -36
  33. package/components/hy-input/hy-input.vue +333 -333
  34. package/components/hy-input/props.ts +186 -186
  35. package/components/hy-modal/hy-modal.vue +211 -211
  36. package/components/hy-modal/props.ts +94 -94
  37. package/components/hy-modal/typing.d.ts +16 -16
  38. package/components/hy-notice-bar/hy-row-notice.vue +121 -121
  39. package/components/hy-notify/hy-notify.vue +174 -174
  40. package/components/hy-number-step/hy-number-step.vue +367 -367
  41. package/components/hy-overlay/hy-overlay.vue +61 -61
  42. package/components/hy-overlay/props.ts +38 -38
  43. package/components/hy-pagination/hy-pagination.vue +136 -136
  44. package/components/hy-pagination/props.ts +58 -58
  45. package/components/hy-parse/hy-parse.vue +550 -550
  46. package/components/hy-parse/node/node.vue +781 -781
  47. package/components/hy-parse/parser.js +1455 -1455
  48. package/components/hy-parse/props.ts +19 -19
  49. package/components/hy-parse/typing.d.ts +68 -68
  50. package/components/hy-picker/hy-picker.vue +435 -435
  51. package/components/hy-picker/props.ts +122 -122
  52. package/components/hy-picker/typing.d.ts +38 -38
  53. package/components/hy-qrcode/props.ts +72 -72
  54. package/components/hy-qrcode/qrcode.js.bak +1433 -1433
  55. package/components/hy-radio/props.ts +97 -97
  56. package/components/hy-read-more/props.ts +48 -48
  57. package/components/hy-search/props.ts +133 -133
  58. package/components/hy-signature/canvasHelper.ts +51 -51
  59. package/components/hy-signature/props.ts +121 -121
  60. package/components/hy-skeleton/hy-skeleton.vue +142 -142
  61. package/components/hy-skeleton/props.ts +46 -46
  62. package/components/hy-skeleton/typing.d.ts +31 -31
  63. package/components/hy-steps/hy-steps.vue +275 -275
  64. package/components/hy-steps/typing.d.ts +25 -25
  65. package/components/hy-swiper/hy-swiper.vue +3 -3
  66. package/components/hy-swiper/index.scss +5 -5
  67. package/components/hy-swiper/props.ts +0 -1
  68. package/components/hy-table/hy-table.vue +630 -630
  69. package/components/hy-table/props.ts +62 -62
  70. package/components/hy-table/typing.d.ts +29 -29
  71. package/components/hy-tabs/hy-tabs.vue +336 -335
  72. package/components/hy-tabs/props.ts +84 -77
  73. package/components/hy-tag/hy-tag.vue +173 -173
  74. package/components/hy-tag/props.ts +89 -89
  75. package/components/hy-text/hy-text.vue +237 -237
  76. package/components/hy-text/props.ts +115 -115
  77. package/components/hy-textarea/hy-textarea.vue +198 -198
  78. package/components/hy-toast/hy-toast.vue +200 -200
  79. package/components/hy-toast/props.ts +3 -3
  80. package/components/hy-transition/hy-transition.vue +157 -157
  81. package/components/hy-transition/props.ts +32 -32
  82. package/components/hy-upload/hy-upload.vue +384 -384
  83. package/components/hy-watermark/hy-watermark.vue +1058 -1058
  84. package/components/hy-watermark/props.ts +109 -109
  85. package/global.d.ts +94 -94
  86. package/libs/api/http.ts +119 -119
  87. package/libs/composables/index.ts +8 -8
  88. package/libs/composables/useMessage.ts +149 -149
  89. package/libs/composables/useToast.ts +45 -45
  90. package/libs/composables/useTranslate.ts +10 -10
  91. package/libs/css/_config.scss +5 -5
  92. package/libs/index.ts +8 -8
  93. package/libs/locale/index.ts +32 -32
  94. package/libs/locale/lang/en-US.ts +84 -84
  95. package/libs/locale/lang/zh-CN.ts +87 -87
  96. package/libs/typing/index.ts +2 -2
  97. package/libs/typing/modules/common.d.ts +139 -139
  98. package/libs/typing/modules/form.ts +176 -176
  99. package/libs/typing/modules/http.d.ts +19 -19
  100. package/libs/typing/modules/index.d.ts +12 -12
  101. package/libs/utils/inside.ts +340 -340
  102. package/libs/utils/inspect.ts +140 -140
  103. package/libs/utils/utils.ts +525 -525
  104. package/package.json +81 -81
  105. package/tags.json +1 -1
  106. package/web-types.json +1 -1
@@ -1,1058 +1,1058 @@
1
- <template>
2
- <view :class="rootClass" :style="rootStyle">
3
- <canvas
4
- v-if="!canvasOffScreenable && showCanvas"
5
- type="2d"
6
- :style="{
7
- height: canvasHeight + 'px',
8
- width: canvasWidth + 'px',
9
- visibility: 'hidden'
10
- }"
11
- :canvas-id="canvasId"
12
- :id="canvasId"
13
- />
14
- </view>
15
- </template>
16
-
17
- <script lang="ts">
18
- export default {
19
- name: 'hy-watermark',
20
- options: {
21
- addGlobalClass: true,
22
- virtualHost: true,
23
- styleIsolation: 'shared'
24
- }
25
- }
26
- </script>
27
-
28
- <script lang="ts" setup>
29
- import { computed, onMounted, ref, watch, nextTick, onUnmounted } from 'vue'
30
- import type { CSSProperties } from 'vue'
31
- import { addUnit, guid } from '../../libs'
32
- import watermarkProps from './props'
33
-
34
- /**
35
- * 在页面或组件上添加指定的图片或文字,可用于版权保护、品牌宣传等场景。
36
- * @displayName hy-watermark
37
- */
38
- defineOptions({})
39
-
40
- const props = defineProps(watermarkProps)
41
-
42
- // watch(
43
- // () => props,
44
- // () => {
45
- // doReset()
46
- // },
47
- // { deep: true }
48
- // )
49
-
50
- const observer = ref<MutationObserver | null>(null)
51
- const WATERMARK_SELECTOR = '.hy-watermark'
52
- const canvasId = ref<string>(`watermark--${guid()}`) // canvas 组件的唯一标识符
53
- const waterMarkUrl = ref<string>('') // canvas生成base64水印
54
- const canvasOffScreenable = ref<boolean>(
55
- uni.canIUse('createOffscreenCanvas') && Boolean(uni.createOffscreenCanvas)
56
- ) // 是否可以使用离屏canvas
57
- const pixelRatio = ref<number>(uni.getSystemInfoSync().pixelRatio) // 像素比
58
- const canvasHeight = ref<number>((props.height + props.gutterY) * pixelRatio.value) // canvas画布高度
59
- const canvasWidth = ref<number>((props.width + props.gutterX) * pixelRatio.value) // canvas画布宽度
60
- const showCanvas = ref<boolean>(true) // 是否展示canvas
61
-
62
- /**
63
- * 水印css类
64
- */
65
- const rootClass = computed(() => {
66
- const classes: string[] = ['hy-watermark']
67
- if (props.fullScreen) {
68
- classes.push('is-fullscreen')
69
- }
70
- return classes
71
- })
72
-
73
- /**
74
- * 水印样式
75
- */
76
- const rootStyle = computed(() => {
77
- const style: CSSProperties = {
78
- // width、height、display, left, top, visibility, transform, margin为了防止在控制台通过修改这些属性导致水印被隐藏
79
- position: 'absolute',
80
- width: '100%',
81
- height: '100%',
82
- left: 0,
83
- top: 0,
84
- pointerEvents: 'none',
85
- visibility: 'visible',
86
- opacity: props.opacity,
87
- zIndex: props.zIndex,
88
- backgroundRepeat: 'repeat',
89
- backgroundPosition: '0px 0px',
90
- backgroundSize: addUnit(props.width + props.gutterX)
91
- }
92
- if (waterMarkUrl.value) {
93
- style['backgroundImage'] = `url('${waterMarkUrl.value}')`
94
- }
95
- return style
96
- })
97
-
98
- function doReset() {
99
- showCanvas.value = true
100
- canvasHeight.value = (props.height + props.gutterY) * pixelRatio.value
101
- canvasWidth.value = (props.width + props.gutterX) * pixelRatio.value
102
- nextTick(() => {
103
- doInit()
104
- })
105
- }
106
-
107
- function doInit() {
108
- // #ifdef H5
109
- // h5使用document.createElement创建canvas,不用展示canvas标签
110
- showCanvas.value = false
111
- // #endif
112
- const {
113
- width,
114
- height,
115
- color,
116
- size,
117
- fontStyle,
118
- fontWeight,
119
- fontFamily,
120
- content,
121
- rotate,
122
- gutterX,
123
- gutterY,
124
- image,
125
- imageHeight,
126
- imageWidth,
127
- title
128
- } = props
129
-
130
- // 创建水印
131
- createWaterMark(
132
- width,
133
- height,
134
- color,
135
- size,
136
- fontStyle,
137
- fontWeight,
138
- fontFamily,
139
- content,
140
- rotate,
141
- gutterX,
142
- gutterY,
143
- image,
144
- imageHeight,
145
- imageWidth,
146
- title
147
- )
148
- }
149
-
150
- /**
151
- * 创建水印图片
152
- * @param width canvas宽度
153
- * @param height canvas高度
154
- * @param color canvas字体颜色
155
- * @param size canvas字体大小
156
- * @param fontStyle canvas字体样式
157
- * @param fontWeight canvas字体字重
158
- * @param fontFamily canvas字体系列
159
- * @param content canvas内容
160
- * @param rotate 倾斜角度
161
- * @param gutterX X轴间距
162
- * @param gutterY Y轴间距
163
- * @param image canvas图片
164
- * @param imageHeight canvas图片高度
165
- * @param imageWidth canvas图片宽度
166
- * @param title 标题
167
- */
168
- function createWaterMark(
169
- width: number,
170
- height: number,
171
- color: string,
172
- size: number,
173
- fontStyle: string,
174
- fontWeight: number | string,
175
- fontFamily: string,
176
- content: string,
177
- rotate: number,
178
- gutterX: number,
179
- gutterY: number,
180
- image: string,
181
- imageHeight: number,
182
- imageWidth: number,
183
- title: string
184
- ) {
185
- const canvasHeight = (height + gutterY) * pixelRatio.value
186
- const canvasWidth = (width + gutterX) * pixelRatio.value
187
- const contentWidth = width * pixelRatio.value
188
- const contentHeight = height * pixelRatio.value
189
- const fontSize = size * pixelRatio.value
190
- // 标题字体大小:如果设置了titleSize则使用titleSize,否则使用size的1.2倍
191
- const titleFontSize = props.titleSize > 0 ? props.titleSize * pixelRatio.value : fontSize * 1.2
192
-
193
- // #ifndef H5
194
- if (canvasOffScreenable.value) {
195
- createOffscreenCanvas(
196
- canvasHeight,
197
- canvasWidth,
198
- contentWidth,
199
- contentHeight,
200
- rotate,
201
- fontSize,
202
- fontFamily,
203
- fontStyle,
204
- fontWeight,
205
- color,
206
- content,
207
- image,
208
- imageHeight,
209
- imageWidth,
210
- title,
211
- titleFontSize
212
- )
213
- } else {
214
- createCanvas(
215
- canvasHeight,
216
- contentWidth,
217
- rotate,
218
- fontSize,
219
- color,
220
- content,
221
- image,
222
- imageHeight,
223
- imageWidth,
224
- title,
225
- titleFontSize
226
- )
227
- }
228
- // #endif
229
- // #ifdef H5
230
- createH5Canvas(
231
- canvasHeight,
232
- canvasWidth,
233
- contentWidth,
234
- contentHeight,
235
- rotate,
236
- fontSize,
237
- fontFamily,
238
- fontStyle,
239
- fontWeight,
240
- color,
241
- content,
242
- image,
243
- imageHeight,
244
- imageWidth,
245
- title,
246
- titleFontSize
247
- )
248
- // #endif
249
- }
250
-
251
- /**
252
- * 创建离屏canvas
253
- * @param canvasHeight canvas高度
254
- * @param canvasWidth canvas宽度
255
- * @param contentWidth 内容宽度
256
- * @param contentHeight 内容高度
257
- * @param rotate 内容倾斜角度
258
- * @param fontSize 字体大小
259
- * @param fontFamily 字体系列
260
- * @param fontStyle 字体样式
261
- * @param fontWeight 字体字重
262
- * @param color 字体颜色
263
- * @param content 内容
264
- * @param image canvas图片
265
- * @param imageHeight canvas图片高度
266
- * @param imageWidth canvas图片宽度
267
- * @param title 标题文本
268
- * @param titleFontSize 标题字体大小
269
- */
270
- function createOffscreenCanvas(
271
- canvasHeight: number,
272
- canvasWidth: number,
273
- contentWidth: number,
274
- contentHeight: number,
275
- rotate: number,
276
- fontSize: number,
277
- fontFamily: string,
278
- fontStyle: string,
279
- fontWeight: string | number,
280
- color: string,
281
- content: string,
282
- image: string,
283
- imageHeight: number,
284
- imageWidth: number,
285
- title: string,
286
- titleFontSize: number
287
- ) {
288
- // 创建离屏canvas
289
- const canvas: any = uni.createOffscreenCanvas({
290
- height: canvasHeight,
291
- width: canvasWidth,
292
- type: '2d'
293
- })
294
- const ctx: any = canvas.getContext('2d')
295
- if (ctx) {
296
- if (image && (title || content)) {
297
- // 图片和文字同时显示
298
- const img = canvas.createImage() as HTMLImageElement
299
- drawImageAndTextOffScreen(
300
- ctx,
301
- img,
302
- image,
303
- imageHeight,
304
- imageWidth,
305
- title,
306
- content,
307
- rotate,
308
- contentWidth,
309
- contentHeight,
310
- fontSize,
311
- titleFontSize,
312
- fontFamily,
313
- fontStyle,
314
- fontWeight,
315
- color,
316
- canvas
317
- )
318
- } else if (image) {
319
- const img = canvas.createImage() as HTMLImageElement
320
- drawImageOffScreen(
321
- ctx,
322
- img,
323
- image,
324
- imageHeight,
325
- imageWidth,
326
- rotate,
327
- contentWidth,
328
- contentHeight,
329
- canvas
330
- )
331
- } else {
332
- drawTextOffScreen(
333
- ctx,
334
- title,
335
- contentWidth,
336
- contentHeight,
337
- rotate,
338
- fontSize,
339
- fontFamily,
340
- fontStyle,
341
- fontWeight,
342
- color,
343
- canvas,
344
- content,
345
- titleFontSize
346
- )
347
- }
348
- } else {
349
- console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
350
- }
351
- }
352
-
353
- /**
354
- * 非H5创建canvas
355
- * 不支持创建离屏canvas时调用
356
- * @param contentHeight 内容高度
357
- * @param contentWidth 内容宽度
358
- * @param rotate 内容倾斜角度
359
- * @param fontSize 字体大小
360
- * @param color 字体颜色
361
- * @param content 内容
362
- * @param image canvas图片
363
- * @param imageHeight canvas图片高度
364
- * @param imageWidth canvas图片宽度
365
- * @param title 标题文本
366
- * @param titleFontSize 标题字体大小
367
- */
368
- function createCanvas(
369
- contentHeight: number,
370
- contentWidth: number,
371
- rotate: number,
372
- fontSize: number,
373
- color: string,
374
- content: string,
375
- image: string,
376
- imageHeight: number,
377
- imageWidth: number,
378
- title: string,
379
- titleFontSize: number
380
- ) {
381
- const ctx = uni.createCanvasContext(canvasId.value)
382
- if (ctx) {
383
- if (image && (title || content)) {
384
- // 图片和文字同时显示
385
- drawImageAndTextOnScreen(
386
- ctx,
387
- image,
388
- imageHeight,
389
- imageWidth,
390
- title,
391
- content,
392
- rotate,
393
- contentWidth,
394
- contentHeight,
395
- fontSize,
396
- titleFontSize,
397
- color
398
- )
399
- } else if (image) {
400
- drawImageOnScreen(
401
- ctx,
402
- image,
403
- imageHeight,
404
- imageWidth,
405
- rotate,
406
- contentWidth,
407
- contentHeight
408
- )
409
- } else {
410
- drawTextOnScreen(
411
- ctx,
412
- title,
413
- contentWidth,
414
- rotate,
415
- fontSize,
416
- color,
417
- content,
418
- titleFontSize
419
- )
420
- }
421
- } else {
422
- console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
423
- }
424
- }
425
-
426
- /**
427
- * h5创建canvas
428
- * @param canvasHeight canvas高度
429
- * @param canvasWidth canvas宽度
430
- * @param contentWidth 水印内容宽度
431
- * @param contentHeight 水印内容高度
432
- * @param rotate 水印内容倾斜角度
433
- * @param fontSize 水印字体大小
434
- * @param fontFamily 水印字体系列
435
- * @param fontStyle 水印字体样式
436
- * @param fontWeight 水印字体字重
437
- * @param color 水印字体颜色
438
- * @param content 水印内容
439
- * @param image canvas图片
440
- * @param imageHeight canvas图片高度
441
- * @param imageWidth canvas图片宽度
442
- * @param title 标题文本
443
- * @param titleFontSize 标题字体大小
444
- */
445
- function createH5Canvas(
446
- canvasHeight: number,
447
- canvasWidth: number,
448
- contentWidth: number,
449
- contentHeight: number,
450
- rotate: number,
451
- fontSize: number,
452
- fontFamily: string,
453
- fontStyle: string,
454
- fontWeight: string | number,
455
- color: string,
456
- content: string,
457
- image: string,
458
- imageHeight: number,
459
- imageWidth: number,
460
- title: string,
461
- titleFontSize: number
462
- ) {
463
- const canvas = document.createElement('canvas')
464
- const ctx = canvas.getContext('2d')
465
- canvas.setAttribute('width', `${canvasWidth}px`)
466
- canvas.setAttribute('height', `${canvasHeight}px`)
467
- if (ctx) {
468
- if (image && (title || content)) {
469
- // 图片和文字同时显示
470
- const img = new Image()
471
- drawImageAndTextOffScreen(
472
- ctx,
473
- img,
474
- image,
475
- imageHeight,
476
- imageWidth,
477
- title,
478
- content,
479
- rotate,
480
- contentWidth,
481
- contentHeight,
482
- fontSize,
483
- titleFontSize,
484
- fontFamily,
485
- fontStyle,
486
- fontWeight,
487
- color,
488
- canvas
489
- )
490
- } else if (image) {
491
- const img = new Image()
492
- drawImageOffScreen(
493
- ctx,
494
- img,
495
- image,
496
- imageHeight,
497
- imageWidth,
498
- rotate,
499
- contentWidth,
500
- contentHeight,
501
- canvas
502
- )
503
- } else {
504
- drawTextOffScreen(
505
- ctx,
506
- title,
507
- contentWidth,
508
- contentHeight,
509
- rotate,
510
- fontSize,
511
- fontFamily,
512
- fontStyle,
513
- fontWeight,
514
- color,
515
- canvas,
516
- content,
517
- titleFontSize
518
- )
519
- }
520
- } else {
521
- console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
522
- }
523
- }
524
-
525
- /**
526
- * 测量文本宽度并自动换行
527
- * @param ctx canvas上下文
528
- * @param text 文本
529
- * @param maxWidth 最大宽度
530
- * @param fontSize 文字大小
531
- */
532
- function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, fontSize: number) {
533
- const words = text.split('')
534
- const lines: string[] = []
535
- let currentLine = ''
536
-
537
- for (let i = 0; i < words.length; i++) {
538
- const testLine = currentLine + words[i]
539
- const metrics = ctx.measureText(testLine)
540
- const testWidth = metrics.width
541
-
542
- // 当文字宽度超过容器宽度的80%时换行
543
- if (testWidth > maxWidth * 0.8 && currentLine !== '') {
544
- lines.push(currentLine)
545
- currentLine = words[i]
546
- } else {
547
- currentLine = testLine
548
- }
549
- }
550
- lines.push(currentLine)
551
- return lines
552
- }
553
-
554
- function drawTextOffScreen(
555
- ctx: CanvasRenderingContext2D,
556
- title: string,
557
- contentWidth: number,
558
- contentHeight: number,
559
- rotate: number,
560
- fontSize: number,
561
- fontFamily: string,
562
- fontStyle: string,
563
- fontWeight: string | number,
564
- color: string,
565
- canvas: HTMLCanvasElement,
566
- content: string = '',
567
- titleFontSize: number = 0
568
- ) {
569
- ctx.textBaseline = 'middle'
570
- ctx.textAlign = 'center'
571
- ctx.translate(contentWidth / 2, contentHeight / 2)
572
- ctx.rotate((Math.PI / 180) * rotate)
573
-
574
- // 计算总高度
575
- let totalTextHeight = titleFontSize
576
- if (content) {
577
- totalTextHeight += fontSize + 5 // 标题和副标题之间的间距
578
- }
579
-
580
- // 起始Y坐标
581
- let startY = -totalTextHeight / 2
582
-
583
- // 绘制主标题(支持自动换行)
584
- if (title) {
585
- ctx.font = `${fontStyle} normal ${fontWeight} ${titleFontSize}px/${contentHeight}px ${fontFamily}`
586
- // 使用titleColor或默认color
587
- ctx.fillStyle = props.titleColor || color
588
- const titleLines = wrapText(ctx, title, contentWidth, titleFontSize)
589
- const titleLineHeight = titleFontSize * 1.2
590
-
591
- for (let i = 0; i < titleLines.length; i++) {
592
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
593
- }
594
-
595
- startY += titleLines.length * titleLineHeight + 5
596
- }
597
-
598
- // 绘制副标题(支持自动换行)
599
- if (content) {
600
- ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
601
- ctx.fillStyle = color
602
- const contentLines = wrapText(ctx, content, contentWidth, fontSize)
603
- const contentLineHeight = fontSize * 1.2
604
-
605
- for (let i = 0; i < contentLines.length; i++) {
606
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
607
- }
608
- }
609
-
610
- ctx.restore()
611
- waterMarkUrl.value = canvas.toDataURL()
612
- }
613
-
614
- // 简化版本的文字换行(UniApp CanvasContext不支持measureText)
615
- function simpleWrapText(text: string, maxLength: number) {
616
- const lines: string[] = []
617
- let currentLine = ''
618
-
619
- // 基于字符数估算换行(适用于UniApp CanvasContext)
620
- for (let i = 0; i < text.length; i++) {
621
- currentLine += text[i]
622
- if (currentLine.length >= maxLength) {
623
- lines.push(currentLine)
624
- currentLine = ''
625
- }
626
- }
627
- if (currentLine) {
628
- lines.push(currentLine)
629
- }
630
- return lines
631
- }
632
-
633
- /**
634
- * 绘制在屏文字canvas
635
- * @param ctx canvas上下文
636
- * @param title 标题
637
- * @param content 水印内容
638
- * @param contentWidth 水印宽度
639
- * @param rotate 水印内容倾斜角度
640
- * @param fontSize 水印字体大小
641
- * @param color 水印字体颜色
642
- * @param titleFontSize 标题字体大小
643
- */
644
- function drawTextOnScreen(
645
- ctx: UniApp.CanvasContext,
646
- title: string,
647
- contentWidth: number,
648
- rotate: number,
649
- fontSize: number,
650
- color: string,
651
- content: string = '',
652
- titleFontSize: number = 0
653
- ) {
654
- ctx.setTextBaseline('middle')
655
- ctx.setTextAlign('center')
656
- ctx.translate(contentWidth / 2, contentWidth / 2)
657
- ctx.rotate((Math.PI / 180) * rotate)
658
-
659
- // 估算每行最大字符数
660
- const maxChars = Math.floor(contentWidth / (fontSize * 0.5))
661
-
662
- // 计算总高度
663
- let totalTextHeight = titleFontSize
664
- if (content) {
665
- totalTextHeight += fontSize + 5
666
- }
667
-
668
- // 起始Y坐标
669
- let startY = -totalTextHeight / 2
670
-
671
- // 绘制主标题(支持自动换行)
672
- if (title) {
673
- // 使用titleColor或默认color
674
- ctx.setFillStyle(props.titleColor || color)
675
- ctx.setFontSize(titleFontSize)
676
- const titleLines = simpleWrapText(title, maxChars)
677
- const titleLineHeight = titleFontSize * 1.2
678
-
679
- for (let i = 0; i < titleLines.length; i++) {
680
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
681
- }
682
-
683
- startY += titleLines.length * titleLineHeight + 5
684
- }
685
-
686
- // 绘制副标题(支持自动换行)
687
- if (content) {
688
- ctx.setFillStyle(color)
689
- ctx.setFontSize(fontSize)
690
- const contentLines = simpleWrapText(content, maxChars)
691
- const contentLineHeight = fontSize * 1.2
692
-
693
- for (let i = 0; i < contentLines.length; i++) {
694
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
695
- }
696
- }
697
-
698
- ctx.restore()
699
- ctx.draw()
700
- // #ifdef MP-DINGTALK
701
- // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
702
- ;(ctx as any).toTempFilePath({
703
- success(res: any) {
704
- showCanvas.value = false
705
- waterMarkUrl.value = res.filePath
706
- }
707
- })
708
- // #endif
709
- // #ifndef MP-DINGTALK
710
- uni.canvasToTempFilePath({
711
- canvasId: canvasId.value,
712
- success: (res) => {
713
- showCanvas.value = false
714
- waterMarkUrl.value = res.tempFilePath
715
- }
716
- })
717
- // #endif
718
- }
719
-
720
- /**
721
- * 绘制离屏图片canvas
722
- * @param ctx canvas上下文
723
- * @param img 水印图片对象
724
- * @param image 水印图片地址
725
- * @param imageHeight 水印图片高度
726
- * @param imageWidth 水印图片宽度
727
- * @param rotate 水印内容倾斜角度
728
- * @param contentWidth 水印宽度
729
- * @param contentHeight 水印高度
730
- * @param canvas canvas实例
731
- */
732
- async function drawImageOffScreen(
733
- ctx: CanvasRenderingContext2D,
734
- img: HTMLImageElement,
735
- image: string,
736
- imageHeight: number,
737
- imageWidth: number,
738
- rotate: number,
739
- contentWidth: number,
740
- contentHeight: number,
741
- canvas: HTMLCanvasElement
742
- ) {
743
- ctx.translate(contentWidth / 2, contentHeight / 2)
744
- ctx.rotate((Math.PI / 180) * Number(rotate))
745
- img.crossOrigin = 'anonymous'
746
- img.referrerPolicy = 'no-referrer'
747
-
748
- img.src = image
749
- img.onload = () => {
750
- ctx.drawImage(
751
- img,
752
- (-imageWidth * pixelRatio.value) / 2,
753
- (-imageHeight * pixelRatio.value) / 2,
754
- imageWidth * pixelRatio.value,
755
- imageHeight * pixelRatio.value
756
- )
757
- ctx.restore()
758
- waterMarkUrl.value = canvas.toDataURL()
759
- }
760
- }
761
-
762
- // 绘制图片和文字(离屏)
763
- async function drawImageAndTextOffScreen(
764
- ctx: CanvasRenderingContext2D,
765
- img: HTMLImageElement,
766
- image: string,
767
- imageHeight: number,
768
- imageWidth: number,
769
- title: string,
770
- content: string,
771
- rotate: number,
772
- contentWidth: number,
773
- contentHeight: number,
774
- fontSize: number,
775
- titleFontSize: number,
776
- fontFamily: string,
777
- fontStyle: string,
778
- fontWeight: string | number,
779
- color: string,
780
- canvas: HTMLCanvasElement
781
- ) {
782
- ctx.translate(contentWidth / 2, contentHeight / 2)
783
- ctx.rotate((Math.PI / 180) * Number(rotate))
784
- img.crossOrigin = 'anonymous'
785
- img.referrerPolicy = 'no-referrer'
786
-
787
- const imgHeight = imageHeight * pixelRatio.value
788
- const imgWidth = imageWidth * pixelRatio.value
789
-
790
- img.src = image
791
- img.onload = () => {
792
- // 计算总高度
793
- let totalHeight = imgHeight
794
- const textSpacing = 10
795
-
796
- if (title) totalHeight += textSpacing + titleFontSize
797
- if (content) totalHeight += fontSize
798
-
799
- // 起始Y坐标
800
- let startY = -totalHeight / 2
801
-
802
- // 绘制图片
803
- ctx.drawImage(img, -imgWidth / 2, startY, imgWidth, imgHeight)
804
-
805
- startY += imgHeight + textSpacing
806
-
807
- // 设置文字样式
808
- ctx.textBaseline = 'top'
809
- ctx.textAlign = 'center'
810
-
811
- // 绘制主标题
812
- if (title) {
813
- ctx.font = `${fontStyle} normal ${fontWeight} ${titleFontSize}px/${contentHeight}px ${fontFamily}`
814
- // 使用titleColor或默认color
815
- ctx.fillStyle = props.titleColor || color
816
- const titleLines = wrapText(ctx, title, contentWidth * 0.9, titleFontSize)
817
- const titleLineHeight = titleFontSize * 1.2
818
-
819
- for (let i = 0; i < titleLines.length; i++) {
820
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
821
- }
822
-
823
- startY += titleLines.length * titleLineHeight + 5
824
- }
825
-
826
- // 绘制副标题
827
- if (content) {
828
- ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
829
- ctx.fillStyle = color
830
- const contentLines = wrapText(ctx, content, contentWidth * 0.9, fontSize)
831
- const contentLineHeight = fontSize * 1.2
832
-
833
- for (let i = 0; i < contentLines.length; i++) {
834
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
835
- }
836
- }
837
-
838
- ctx.restore()
839
- waterMarkUrl.value = canvas.toDataURL()
840
- }
841
- }
842
-
843
- // 绘制图片和文字(在屏)
844
- function drawImageAndTextOnScreen(
845
- ctx: UniApp.CanvasContext,
846
- image: string,
847
- imageHeight: number,
848
- imageWidth: number,
849
- title: string,
850
- content: string,
851
- rotate: number,
852
- contentWidth: number,
853
- contentHeight: number,
854
- fontSize: number,
855
- titleFontSize: number,
856
- color: string
857
- ) {
858
- ctx.setTextBaseline('top')
859
- ctx.setTextAlign('center')
860
- ctx.translate(contentWidth / 2, contentWidth / 2)
861
- ctx.rotate((Math.PI / 180) * Number(rotate))
862
-
863
- const imgHeight = imageHeight * pixelRatio.value
864
- const imgWidth = imageWidth * pixelRatio.value
865
- const maxChars = Math.floor(contentWidth / (fontSize * 0.5))
866
-
867
- // 计算总高度
868
- let totalHeight = imgHeight
869
- const textSpacing = 10
870
-
871
- if (title) totalHeight += textSpacing + titleFontSize
872
- if (content) totalHeight += fontSize
873
-
874
- // 起始Y坐标
875
- let startY = -totalHeight / 2
876
-
877
- // 绘制图片
878
- ctx.drawImage(image, -imgWidth / 2, startY, imgWidth, imgHeight)
879
-
880
- startY += imgHeight + textSpacing
881
-
882
- // 绘制主标题
883
- if (title) {
884
- // 使用titleColor或默认color
885
- ctx.setFillStyle(props.titleColor || color)
886
- ctx.setFontSize(titleFontSize)
887
- const titleLines = simpleWrapText(title, maxChars)
888
- const titleLineHeight = titleFontSize * 1.2
889
-
890
- for (let i = 0; i < titleLines.length; i++) {
891
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
892
- }
893
-
894
- startY += titleLines.length * titleLineHeight + 5
895
- }
896
-
897
- // 绘制副标题
898
- if (content) {
899
- ctx.setFillStyle(color)
900
- ctx.setFontSize(fontSize)
901
- const contentLines = simpleWrapText(content, maxChars)
902
- const contentLineHeight = fontSize * 1.2
903
-
904
- for (let i = 0; i < contentLines.length; i++) {
905
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
906
- }
907
- }
908
-
909
- ctx.restore()
910
- ctx.draw(false, () => {
911
- // #ifdef MP-DINGTALK
912
- // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
913
- ;(ctx as any).toTempFilePath({
914
- success(res: any) {
915
- showCanvas.value = false
916
- waterMarkUrl.value = res.filePath
917
- }
918
- })
919
- // #endif
920
- // #ifndef MP-DINGTALK
921
- uni.canvasToTempFilePath({
922
- canvasId: canvasId.value,
923
- success: (res) => {
924
- showCanvas.value = false
925
- waterMarkUrl.value = res.tempFilePath
926
- }
927
- })
928
- // #endif
929
- })
930
- }
931
-
932
- /**
933
- * 绘制在屏图片canvas
934
- * @param ctx canvas上下文
935
- * @param image 水印图片地址
936
- * @param imageHeight 水印图片高度
937
- * @param imageWidth 水印图片宽度
938
- * @param rotate 水印内容倾斜角度
939
- * @param contentWidth 水印宽度
940
- * @param contentHeight 水印高度
941
- */
942
- function drawImageOnScreen(
943
- ctx: UniApp.CanvasContext,
944
- image: string,
945
- imageHeight: number,
946
- imageWidth: number,
947
- rotate: number,
948
- contentWidth: number,
949
- contentHeight: number
950
- ) {
951
- ctx.translate(contentWidth / 2, contentHeight / 2)
952
- ctx.rotate((Math.PI / 180) * Number(rotate))
953
-
954
- ctx.drawImage(
955
- image,
956
- (-imageWidth * pixelRatio.value) / 2,
957
- (-imageHeight * pixelRatio.value) / 2,
958
- imageWidth * pixelRatio.value,
959
- imageHeight * pixelRatio.value
960
- )
961
- ctx.restore()
962
- ctx.draw(false, () => {
963
- // #ifdef MP-DINGTALK
964
- // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
965
- ;(ctx as any).toTempFilePath({
966
- success(res: any) {
967
- showCanvas.value = false
968
- waterMarkUrl.value = res.filePath
969
- }
970
- })
971
- // #endif
972
- // #ifndef MP-DINGTALK
973
- uni.canvasToTempFilePath({
974
- canvasId: canvasId.value,
975
- success: (res) => {
976
- showCanvas.value = false
977
- waterMarkUrl.value = res.tempFilePath
978
- }
979
- })
980
- // #endif
981
- })
982
- }
983
- /**
984
- * 启动监听
985
- * */
986
- function startObserve() {
987
- // #ifdef H5
988
- const target = document.querySelector(WATERMARK_SELECTOR) as HTMLElement
989
- if (!target || observer.value) return
990
-
991
- // 观察目标节点的属性变化、子节点变化、以及自身被删除
992
- observer.value = new MutationObserver((mutations) => {
993
- mutations.forEach((mutation) => {
994
- let el = document.querySelector('.hy-watermark') as HTMLElement
995
- if (mutation.type === 'attributes' || mutation.removedNodes.length > 0) {
996
- // 检查节点是否被删除
997
- if (!el) {
998
- // 手动创建一个新的 div
999
- el = document.createElement('div') as HTMLElement
1000
- parent.appendChild(target)
1001
- }
1002
- el.className = rootClass.value.join(' ') // 加上你需要的初始类名
1003
- el.style.cssText = Object.entries(rootStyle.value)
1004
- .map(([key, val]) => {
1005
- // 将 camelCase 转为 kebab-case (例如: backgroundImage -> background-image)
1006
- const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
1007
- return `${cssKey}: ${val};` // 强制加上 !important 增加删除难度
1008
- })
1009
- .join(' ')
1010
- console.warn('检测到安全策略违规,正在恢复水印...')
1011
-
1012
- // 停止旧监听,防止死循环
1013
- stopObserve()
1014
- nextTick(() => startObserve())
1015
- }
1016
- })
1017
- })
1018
-
1019
- // 监听父级节点,防止整个 .hy-watermark 被删除
1020
- const parent = target.parentElement || document.body
1021
- observer.value.observe(parent, {
1022
- attributes: true,
1023
- childList: true,
1024
- subtree: true,
1025
- attributeFilter: ['style', 'class'] // 仅监听样式和类名改动
1026
- })
1027
- // #endif
1028
- }
1029
-
1030
- /**
1031
- * 停止监听
1032
- * */
1033
- function stopObserve() {
1034
- if (observer.value) {
1035
- observer.value.disconnect()
1036
- observer.value = null
1037
- }
1038
- }
1039
-
1040
- onMounted(() => {
1041
- doInit()
1042
- // 初始化完成后开启监听
1043
- // #ifdef H5
1044
- props.isAntiTheft && setTimeout(() => startObserve(), 2000)
1045
- // #endif
1046
- })
1047
-
1048
- // 组件销毁前必须断开监听,否则会导致内存泄漏
1049
- onUnmounted(() => {
1050
- // #ifdef H5
1051
- stopObserve()
1052
- // #endif
1053
- })
1054
- </script>
1055
-
1056
- <style lang="scss" scoped>
1057
- @import './index.scss';
1058
- </style>
1
+ <template>
2
+ <view :class="rootClass" :style="rootStyle">
3
+ <canvas
4
+ v-if="!canvasOffScreenable && showCanvas"
5
+ type="2d"
6
+ :style="{
7
+ height: canvasHeight + 'px',
8
+ width: canvasWidth + 'px',
9
+ visibility: 'hidden'
10
+ }"
11
+ :canvas-id="canvasId"
12
+ :id="canvasId"
13
+ />
14
+ </view>
15
+ </template>
16
+
17
+ <script lang="ts">
18
+ export default {
19
+ name: 'hy-watermark',
20
+ options: {
21
+ addGlobalClass: true,
22
+ virtualHost: true,
23
+ styleIsolation: 'shared'
24
+ }
25
+ }
26
+ </script>
27
+
28
+ <script lang="ts" setup>
29
+ import { computed, onMounted, ref, watch, nextTick, onUnmounted } from 'vue'
30
+ import type { CSSProperties } from 'vue'
31
+ import { addUnit, guid } from '../../libs'
32
+ import watermarkProps from './props'
33
+
34
+ /**
35
+ * 在页面或组件上添加指定的图片或文字,可用于版权保护、品牌宣传等场景。
36
+ * @displayName hy-watermark
37
+ */
38
+ defineOptions({})
39
+
40
+ const props = defineProps(watermarkProps)
41
+
42
+ // watch(
43
+ // () => props,
44
+ // () => {
45
+ // doReset()
46
+ // },
47
+ // { deep: true }
48
+ // )
49
+
50
+ const observer = ref<MutationObserver | null>(null)
51
+ const WATERMARK_SELECTOR = '.hy-watermark'
52
+ const canvasId = ref<string>(`watermark--${guid()}`) // canvas 组件的唯一标识符
53
+ const waterMarkUrl = ref<string>('') // canvas生成base64水印
54
+ const canvasOffScreenable = ref<boolean>(
55
+ uni.canIUse('createOffscreenCanvas') && Boolean(uni.createOffscreenCanvas)
56
+ ) // 是否可以使用离屏canvas
57
+ const pixelRatio = ref<number>(uni.getSystemInfoSync().pixelRatio) // 像素比
58
+ const canvasHeight = ref<number>((props.height + props.gutterY) * pixelRatio.value) // canvas画布高度
59
+ const canvasWidth = ref<number>((props.width + props.gutterX) * pixelRatio.value) // canvas画布宽度
60
+ const showCanvas = ref<boolean>(true) // 是否展示canvas
61
+
62
+ /**
63
+ * 水印css类
64
+ */
65
+ const rootClass = computed(() => {
66
+ const classes: string[] = ['hy-watermark']
67
+ if (props.fullScreen) {
68
+ classes.push('is-fullscreen')
69
+ }
70
+ return classes
71
+ })
72
+
73
+ /**
74
+ * 水印样式
75
+ */
76
+ const rootStyle = computed(() => {
77
+ const style: CSSProperties = {
78
+ // width、height、display, left, top, visibility, transform, margin为了防止在控制台通过修改这些属性导致水印被隐藏
79
+ position: 'absolute',
80
+ width: '100%',
81
+ height: '100%',
82
+ left: 0,
83
+ top: 0,
84
+ pointerEvents: 'none',
85
+ visibility: 'visible',
86
+ opacity: props.opacity,
87
+ zIndex: props.zIndex,
88
+ backgroundRepeat: 'repeat',
89
+ backgroundPosition: '0px 0px',
90
+ backgroundSize: addUnit(props.width + props.gutterX)
91
+ }
92
+ if (waterMarkUrl.value) {
93
+ style['backgroundImage'] = `url('${waterMarkUrl.value}')`
94
+ }
95
+ return style
96
+ })
97
+
98
+ function doReset() {
99
+ showCanvas.value = true
100
+ canvasHeight.value = (props.height + props.gutterY) * pixelRatio.value
101
+ canvasWidth.value = (props.width + props.gutterX) * pixelRatio.value
102
+ nextTick(() => {
103
+ doInit()
104
+ })
105
+ }
106
+
107
+ function doInit() {
108
+ // #ifdef H5
109
+ // h5使用document.createElement创建canvas,不用展示canvas标签
110
+ showCanvas.value = false
111
+ // #endif
112
+ const {
113
+ width,
114
+ height,
115
+ color,
116
+ size,
117
+ fontStyle,
118
+ fontWeight,
119
+ fontFamily,
120
+ content,
121
+ rotate,
122
+ gutterX,
123
+ gutterY,
124
+ image,
125
+ imageHeight,
126
+ imageWidth,
127
+ title
128
+ } = props
129
+
130
+ // 创建水印
131
+ createWaterMark(
132
+ width,
133
+ height,
134
+ color,
135
+ size,
136
+ fontStyle,
137
+ fontWeight,
138
+ fontFamily,
139
+ content,
140
+ rotate,
141
+ gutterX,
142
+ gutterY,
143
+ image,
144
+ imageHeight,
145
+ imageWidth,
146
+ title
147
+ )
148
+ }
149
+
150
+ /**
151
+ * 创建水印图片
152
+ * @param width canvas宽度
153
+ * @param height canvas高度
154
+ * @param color canvas字体颜色
155
+ * @param size canvas字体大小
156
+ * @param fontStyle canvas字体样式
157
+ * @param fontWeight canvas字体字重
158
+ * @param fontFamily canvas字体系列
159
+ * @param content canvas内容
160
+ * @param rotate 倾斜角度
161
+ * @param gutterX X轴间距
162
+ * @param gutterY Y轴间距
163
+ * @param image canvas图片
164
+ * @param imageHeight canvas图片高度
165
+ * @param imageWidth canvas图片宽度
166
+ * @param title 标题
167
+ */
168
+ function createWaterMark(
169
+ width: number,
170
+ height: number,
171
+ color: string,
172
+ size: number,
173
+ fontStyle: string,
174
+ fontWeight: number | string,
175
+ fontFamily: string,
176
+ content: string,
177
+ rotate: number,
178
+ gutterX: number,
179
+ gutterY: number,
180
+ image: string,
181
+ imageHeight: number,
182
+ imageWidth: number,
183
+ title: string
184
+ ) {
185
+ const canvasHeight = (height + gutterY) * pixelRatio.value
186
+ const canvasWidth = (width + gutterX) * pixelRatio.value
187
+ const contentWidth = width * pixelRatio.value
188
+ const contentHeight = height * pixelRatio.value
189
+ const fontSize = size * pixelRatio.value
190
+ // 标题字体大小:如果设置了titleSize则使用titleSize,否则使用size的1.2倍
191
+ const titleFontSize = props.titleSize > 0 ? props.titleSize * pixelRatio.value : fontSize * 1.2
192
+
193
+ // #ifndef H5
194
+ if (canvasOffScreenable.value) {
195
+ createOffscreenCanvas(
196
+ canvasHeight,
197
+ canvasWidth,
198
+ contentWidth,
199
+ contentHeight,
200
+ rotate,
201
+ fontSize,
202
+ fontFamily,
203
+ fontStyle,
204
+ fontWeight,
205
+ color,
206
+ content,
207
+ image,
208
+ imageHeight,
209
+ imageWidth,
210
+ title,
211
+ titleFontSize
212
+ )
213
+ } else {
214
+ createCanvas(
215
+ canvasHeight,
216
+ contentWidth,
217
+ rotate,
218
+ fontSize,
219
+ color,
220
+ content,
221
+ image,
222
+ imageHeight,
223
+ imageWidth,
224
+ title,
225
+ titleFontSize
226
+ )
227
+ }
228
+ // #endif
229
+ // #ifdef H5
230
+ createH5Canvas(
231
+ canvasHeight,
232
+ canvasWidth,
233
+ contentWidth,
234
+ contentHeight,
235
+ rotate,
236
+ fontSize,
237
+ fontFamily,
238
+ fontStyle,
239
+ fontWeight,
240
+ color,
241
+ content,
242
+ image,
243
+ imageHeight,
244
+ imageWidth,
245
+ title,
246
+ titleFontSize
247
+ )
248
+ // #endif
249
+ }
250
+
251
+ /**
252
+ * 创建离屏canvas
253
+ * @param canvasHeight canvas高度
254
+ * @param canvasWidth canvas宽度
255
+ * @param contentWidth 内容宽度
256
+ * @param contentHeight 内容高度
257
+ * @param rotate 内容倾斜角度
258
+ * @param fontSize 字体大小
259
+ * @param fontFamily 字体系列
260
+ * @param fontStyle 字体样式
261
+ * @param fontWeight 字体字重
262
+ * @param color 字体颜色
263
+ * @param content 内容
264
+ * @param image canvas图片
265
+ * @param imageHeight canvas图片高度
266
+ * @param imageWidth canvas图片宽度
267
+ * @param title 标题文本
268
+ * @param titleFontSize 标题字体大小
269
+ */
270
+ function createOffscreenCanvas(
271
+ canvasHeight: number,
272
+ canvasWidth: number,
273
+ contentWidth: number,
274
+ contentHeight: number,
275
+ rotate: number,
276
+ fontSize: number,
277
+ fontFamily: string,
278
+ fontStyle: string,
279
+ fontWeight: string | number,
280
+ color: string,
281
+ content: string,
282
+ image: string,
283
+ imageHeight: number,
284
+ imageWidth: number,
285
+ title: string,
286
+ titleFontSize: number
287
+ ) {
288
+ // 创建离屏canvas
289
+ const canvas: any = uni.createOffscreenCanvas({
290
+ height: canvasHeight,
291
+ width: canvasWidth,
292
+ type: '2d'
293
+ })
294
+ const ctx: any = canvas.getContext('2d')
295
+ if (ctx) {
296
+ if (image && (title || content)) {
297
+ // 图片和文字同时显示
298
+ const img = canvas.createImage() as HTMLImageElement
299
+ drawImageAndTextOffScreen(
300
+ ctx,
301
+ img,
302
+ image,
303
+ imageHeight,
304
+ imageWidth,
305
+ title,
306
+ content,
307
+ rotate,
308
+ contentWidth,
309
+ contentHeight,
310
+ fontSize,
311
+ titleFontSize,
312
+ fontFamily,
313
+ fontStyle,
314
+ fontWeight,
315
+ color,
316
+ canvas
317
+ )
318
+ } else if (image) {
319
+ const img = canvas.createImage() as HTMLImageElement
320
+ drawImageOffScreen(
321
+ ctx,
322
+ img,
323
+ image,
324
+ imageHeight,
325
+ imageWidth,
326
+ rotate,
327
+ contentWidth,
328
+ contentHeight,
329
+ canvas
330
+ )
331
+ } else {
332
+ drawTextOffScreen(
333
+ ctx,
334
+ title,
335
+ contentWidth,
336
+ contentHeight,
337
+ rotate,
338
+ fontSize,
339
+ fontFamily,
340
+ fontStyle,
341
+ fontWeight,
342
+ color,
343
+ canvas,
344
+ content,
345
+ titleFontSize
346
+ )
347
+ }
348
+ } else {
349
+ console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
350
+ }
351
+ }
352
+
353
+ /**
354
+ * 非H5创建canvas
355
+ * 不支持创建离屏canvas时调用
356
+ * @param contentHeight 内容高度
357
+ * @param contentWidth 内容宽度
358
+ * @param rotate 内容倾斜角度
359
+ * @param fontSize 字体大小
360
+ * @param color 字体颜色
361
+ * @param content 内容
362
+ * @param image canvas图片
363
+ * @param imageHeight canvas图片高度
364
+ * @param imageWidth canvas图片宽度
365
+ * @param title 标题文本
366
+ * @param titleFontSize 标题字体大小
367
+ */
368
+ function createCanvas(
369
+ contentHeight: number,
370
+ contentWidth: number,
371
+ rotate: number,
372
+ fontSize: number,
373
+ color: string,
374
+ content: string,
375
+ image: string,
376
+ imageHeight: number,
377
+ imageWidth: number,
378
+ title: string,
379
+ titleFontSize: number
380
+ ) {
381
+ const ctx = uni.createCanvasContext(canvasId.value)
382
+ if (ctx) {
383
+ if (image && (title || content)) {
384
+ // 图片和文字同时显示
385
+ drawImageAndTextOnScreen(
386
+ ctx,
387
+ image,
388
+ imageHeight,
389
+ imageWidth,
390
+ title,
391
+ content,
392
+ rotate,
393
+ contentWidth,
394
+ contentHeight,
395
+ fontSize,
396
+ titleFontSize,
397
+ color
398
+ )
399
+ } else if (image) {
400
+ drawImageOnScreen(
401
+ ctx,
402
+ image,
403
+ imageHeight,
404
+ imageWidth,
405
+ rotate,
406
+ contentWidth,
407
+ contentHeight
408
+ )
409
+ } else {
410
+ drawTextOnScreen(
411
+ ctx,
412
+ title,
413
+ contentWidth,
414
+ rotate,
415
+ fontSize,
416
+ color,
417
+ content,
418
+ titleFontSize
419
+ )
420
+ }
421
+ } else {
422
+ console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
423
+ }
424
+ }
425
+
426
+ /**
427
+ * h5创建canvas
428
+ * @param canvasHeight canvas高度
429
+ * @param canvasWidth canvas宽度
430
+ * @param contentWidth 水印内容宽度
431
+ * @param contentHeight 水印内容高度
432
+ * @param rotate 水印内容倾斜角度
433
+ * @param fontSize 水印字体大小
434
+ * @param fontFamily 水印字体系列
435
+ * @param fontStyle 水印字体样式
436
+ * @param fontWeight 水印字体字重
437
+ * @param color 水印字体颜色
438
+ * @param content 水印内容
439
+ * @param image canvas图片
440
+ * @param imageHeight canvas图片高度
441
+ * @param imageWidth canvas图片宽度
442
+ * @param title 标题文本
443
+ * @param titleFontSize 标题字体大小
444
+ */
445
+ function createH5Canvas(
446
+ canvasHeight: number,
447
+ canvasWidth: number,
448
+ contentWidth: number,
449
+ contentHeight: number,
450
+ rotate: number,
451
+ fontSize: number,
452
+ fontFamily: string,
453
+ fontStyle: string,
454
+ fontWeight: string | number,
455
+ color: string,
456
+ content: string,
457
+ image: string,
458
+ imageHeight: number,
459
+ imageWidth: number,
460
+ title: string,
461
+ titleFontSize: number
462
+ ) {
463
+ const canvas = document.createElement('canvas')
464
+ const ctx = canvas.getContext('2d')
465
+ canvas.setAttribute('width', `${canvasWidth}px`)
466
+ canvas.setAttribute('height', `${canvasHeight}px`)
467
+ if (ctx) {
468
+ if (image && (title || content)) {
469
+ // 图片和文字同时显示
470
+ const img = new Image()
471
+ drawImageAndTextOffScreen(
472
+ ctx,
473
+ img,
474
+ image,
475
+ imageHeight,
476
+ imageWidth,
477
+ title,
478
+ content,
479
+ rotate,
480
+ contentWidth,
481
+ contentHeight,
482
+ fontSize,
483
+ titleFontSize,
484
+ fontFamily,
485
+ fontStyle,
486
+ fontWeight,
487
+ color,
488
+ canvas
489
+ )
490
+ } else if (image) {
491
+ const img = new Image()
492
+ drawImageOffScreen(
493
+ ctx,
494
+ img,
495
+ image,
496
+ imageHeight,
497
+ imageWidth,
498
+ rotate,
499
+ contentWidth,
500
+ contentHeight,
501
+ canvas
502
+ )
503
+ } else {
504
+ drawTextOffScreen(
505
+ ctx,
506
+ title,
507
+ contentWidth,
508
+ contentHeight,
509
+ rotate,
510
+ fontSize,
511
+ fontFamily,
512
+ fontStyle,
513
+ fontWeight,
514
+ color,
515
+ canvas,
516
+ content,
517
+ titleFontSize
518
+ )
519
+ }
520
+ } else {
521
+ console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
522
+ }
523
+ }
524
+
525
+ /**
526
+ * 测量文本宽度并自动换行
527
+ * @param ctx canvas上下文
528
+ * @param text 文本
529
+ * @param maxWidth 最大宽度
530
+ * @param fontSize 文字大小
531
+ */
532
+ function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, fontSize: number) {
533
+ const words = text.split('')
534
+ const lines: string[] = []
535
+ let currentLine = ''
536
+
537
+ for (let i = 0; i < words.length; i++) {
538
+ const testLine = currentLine + words[i]
539
+ const metrics = ctx.measureText(testLine)
540
+ const testWidth = metrics.width
541
+
542
+ // 当文字宽度超过容器宽度的80%时换行
543
+ if (testWidth > maxWidth * 0.8 && currentLine !== '') {
544
+ lines.push(currentLine)
545
+ currentLine = words[i]
546
+ } else {
547
+ currentLine = testLine
548
+ }
549
+ }
550
+ lines.push(currentLine)
551
+ return lines
552
+ }
553
+
554
+ function drawTextOffScreen(
555
+ ctx: CanvasRenderingContext2D,
556
+ title: string,
557
+ contentWidth: number,
558
+ contentHeight: number,
559
+ rotate: number,
560
+ fontSize: number,
561
+ fontFamily: string,
562
+ fontStyle: string,
563
+ fontWeight: string | number,
564
+ color: string,
565
+ canvas: HTMLCanvasElement,
566
+ content: string = '',
567
+ titleFontSize: number = 0
568
+ ) {
569
+ ctx.textBaseline = 'middle'
570
+ ctx.textAlign = 'center'
571
+ ctx.translate(contentWidth / 2, contentHeight / 2)
572
+ ctx.rotate((Math.PI / 180) * rotate)
573
+
574
+ // 计算总高度
575
+ let totalTextHeight = titleFontSize
576
+ if (content) {
577
+ totalTextHeight += fontSize + 5 // 标题和副标题之间的间距
578
+ }
579
+
580
+ // 起始Y坐标
581
+ let startY = -totalTextHeight / 2
582
+
583
+ // 绘制主标题(支持自动换行)
584
+ if (title) {
585
+ ctx.font = `${fontStyle} normal ${fontWeight} ${titleFontSize}px/${contentHeight}px ${fontFamily}`
586
+ // 使用titleColor或默认color
587
+ ctx.fillStyle = props.titleColor || color
588
+ const titleLines = wrapText(ctx, title, contentWidth, titleFontSize)
589
+ const titleLineHeight = titleFontSize * 1.2
590
+
591
+ for (let i = 0; i < titleLines.length; i++) {
592
+ ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
593
+ }
594
+
595
+ startY += titleLines.length * titleLineHeight + 5
596
+ }
597
+
598
+ // 绘制副标题(支持自动换行)
599
+ if (content) {
600
+ ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
601
+ ctx.fillStyle = color
602
+ const contentLines = wrapText(ctx, content, contentWidth, fontSize)
603
+ const contentLineHeight = fontSize * 1.2
604
+
605
+ for (let i = 0; i < contentLines.length; i++) {
606
+ ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
607
+ }
608
+ }
609
+
610
+ ctx.restore()
611
+ waterMarkUrl.value = canvas.toDataURL()
612
+ }
613
+
614
+ // 简化版本的文字换行(UniApp CanvasContext不支持measureText)
615
+ function simpleWrapText(text: string, maxLength: number) {
616
+ const lines: string[] = []
617
+ let currentLine = ''
618
+
619
+ // 基于字符数估算换行(适用于UniApp CanvasContext)
620
+ for (let i = 0; i < text.length; i++) {
621
+ currentLine += text[i]
622
+ if (currentLine.length >= maxLength) {
623
+ lines.push(currentLine)
624
+ currentLine = ''
625
+ }
626
+ }
627
+ if (currentLine) {
628
+ lines.push(currentLine)
629
+ }
630
+ return lines
631
+ }
632
+
633
+ /**
634
+ * 绘制在屏文字canvas
635
+ * @param ctx canvas上下文
636
+ * @param title 标题
637
+ * @param content 水印内容
638
+ * @param contentWidth 水印宽度
639
+ * @param rotate 水印内容倾斜角度
640
+ * @param fontSize 水印字体大小
641
+ * @param color 水印字体颜色
642
+ * @param titleFontSize 标题字体大小
643
+ */
644
+ function drawTextOnScreen(
645
+ ctx: UniApp.CanvasContext,
646
+ title: string,
647
+ contentWidth: number,
648
+ rotate: number,
649
+ fontSize: number,
650
+ color: string,
651
+ content: string = '',
652
+ titleFontSize: number = 0
653
+ ) {
654
+ ctx.setTextBaseline('middle')
655
+ ctx.setTextAlign('center')
656
+ ctx.translate(contentWidth / 2, contentWidth / 2)
657
+ ctx.rotate((Math.PI / 180) * rotate)
658
+
659
+ // 估算每行最大字符数
660
+ const maxChars = Math.floor(contentWidth / (fontSize * 0.5))
661
+
662
+ // 计算总高度
663
+ let totalTextHeight = titleFontSize
664
+ if (content) {
665
+ totalTextHeight += fontSize + 5
666
+ }
667
+
668
+ // 起始Y坐标
669
+ let startY = -totalTextHeight / 2
670
+
671
+ // 绘制主标题(支持自动换行)
672
+ if (title) {
673
+ // 使用titleColor或默认color
674
+ ctx.setFillStyle(props.titleColor || color)
675
+ ctx.setFontSize(titleFontSize)
676
+ const titleLines = simpleWrapText(title, maxChars)
677
+ const titleLineHeight = titleFontSize * 1.2
678
+
679
+ for (let i = 0; i < titleLines.length; i++) {
680
+ ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
681
+ }
682
+
683
+ startY += titleLines.length * titleLineHeight + 5
684
+ }
685
+
686
+ // 绘制副标题(支持自动换行)
687
+ if (content) {
688
+ ctx.setFillStyle(color)
689
+ ctx.setFontSize(fontSize)
690
+ const contentLines = simpleWrapText(content, maxChars)
691
+ const contentLineHeight = fontSize * 1.2
692
+
693
+ for (let i = 0; i < contentLines.length; i++) {
694
+ ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
695
+ }
696
+ }
697
+
698
+ ctx.restore()
699
+ ctx.draw()
700
+ // #ifdef MP-DINGTALK
701
+ // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
702
+ ;(ctx as any).toTempFilePath({
703
+ success(res: any) {
704
+ showCanvas.value = false
705
+ waterMarkUrl.value = res.filePath
706
+ }
707
+ })
708
+ // #endif
709
+ // #ifndef MP-DINGTALK
710
+ uni.canvasToTempFilePath({
711
+ canvasId: canvasId.value,
712
+ success: (res) => {
713
+ showCanvas.value = false
714
+ waterMarkUrl.value = res.tempFilePath
715
+ }
716
+ })
717
+ // #endif
718
+ }
719
+
720
+ /**
721
+ * 绘制离屏图片canvas
722
+ * @param ctx canvas上下文
723
+ * @param img 水印图片对象
724
+ * @param image 水印图片地址
725
+ * @param imageHeight 水印图片高度
726
+ * @param imageWidth 水印图片宽度
727
+ * @param rotate 水印内容倾斜角度
728
+ * @param contentWidth 水印宽度
729
+ * @param contentHeight 水印高度
730
+ * @param canvas canvas实例
731
+ */
732
+ async function drawImageOffScreen(
733
+ ctx: CanvasRenderingContext2D,
734
+ img: HTMLImageElement,
735
+ image: string,
736
+ imageHeight: number,
737
+ imageWidth: number,
738
+ rotate: number,
739
+ contentWidth: number,
740
+ contentHeight: number,
741
+ canvas: HTMLCanvasElement
742
+ ) {
743
+ ctx.translate(contentWidth / 2, contentHeight / 2)
744
+ ctx.rotate((Math.PI / 180) * Number(rotate))
745
+ img.crossOrigin = 'anonymous'
746
+ img.referrerPolicy = 'no-referrer'
747
+
748
+ img.src = image
749
+ img.onload = () => {
750
+ ctx.drawImage(
751
+ img,
752
+ (-imageWidth * pixelRatio.value) / 2,
753
+ (-imageHeight * pixelRatio.value) / 2,
754
+ imageWidth * pixelRatio.value,
755
+ imageHeight * pixelRatio.value
756
+ )
757
+ ctx.restore()
758
+ waterMarkUrl.value = canvas.toDataURL()
759
+ }
760
+ }
761
+
762
+ // 绘制图片和文字(离屏)
763
+ async function drawImageAndTextOffScreen(
764
+ ctx: CanvasRenderingContext2D,
765
+ img: HTMLImageElement,
766
+ image: string,
767
+ imageHeight: number,
768
+ imageWidth: number,
769
+ title: string,
770
+ content: string,
771
+ rotate: number,
772
+ contentWidth: number,
773
+ contentHeight: number,
774
+ fontSize: number,
775
+ titleFontSize: number,
776
+ fontFamily: string,
777
+ fontStyle: string,
778
+ fontWeight: string | number,
779
+ color: string,
780
+ canvas: HTMLCanvasElement
781
+ ) {
782
+ ctx.translate(contentWidth / 2, contentHeight / 2)
783
+ ctx.rotate((Math.PI / 180) * Number(rotate))
784
+ img.crossOrigin = 'anonymous'
785
+ img.referrerPolicy = 'no-referrer'
786
+
787
+ const imgHeight = imageHeight * pixelRatio.value
788
+ const imgWidth = imageWidth * pixelRatio.value
789
+
790
+ img.src = image
791
+ img.onload = () => {
792
+ // 计算总高度
793
+ let totalHeight = imgHeight
794
+ const textSpacing = 10
795
+
796
+ if (title) totalHeight += textSpacing + titleFontSize
797
+ if (content) totalHeight += fontSize
798
+
799
+ // 起始Y坐标
800
+ let startY = -totalHeight / 2
801
+
802
+ // 绘制图片
803
+ ctx.drawImage(img, -imgWidth / 2, startY, imgWidth, imgHeight)
804
+
805
+ startY += imgHeight + textSpacing
806
+
807
+ // 设置文字样式
808
+ ctx.textBaseline = 'top'
809
+ ctx.textAlign = 'center'
810
+
811
+ // 绘制主标题
812
+ if (title) {
813
+ ctx.font = `${fontStyle} normal ${fontWeight} ${titleFontSize}px/${contentHeight}px ${fontFamily}`
814
+ // 使用titleColor或默认color
815
+ ctx.fillStyle = props.titleColor || color
816
+ const titleLines = wrapText(ctx, title, contentWidth * 0.9, titleFontSize)
817
+ const titleLineHeight = titleFontSize * 1.2
818
+
819
+ for (let i = 0; i < titleLines.length; i++) {
820
+ ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
821
+ }
822
+
823
+ startY += titleLines.length * titleLineHeight + 5
824
+ }
825
+
826
+ // 绘制副标题
827
+ if (content) {
828
+ ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
829
+ ctx.fillStyle = color
830
+ const contentLines = wrapText(ctx, content, contentWidth * 0.9, fontSize)
831
+ const contentLineHeight = fontSize * 1.2
832
+
833
+ for (let i = 0; i < contentLines.length; i++) {
834
+ ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
835
+ }
836
+ }
837
+
838
+ ctx.restore()
839
+ waterMarkUrl.value = canvas.toDataURL()
840
+ }
841
+ }
842
+
843
+ // 绘制图片和文字(在屏)
844
+ function drawImageAndTextOnScreen(
845
+ ctx: UniApp.CanvasContext,
846
+ image: string,
847
+ imageHeight: number,
848
+ imageWidth: number,
849
+ title: string,
850
+ content: string,
851
+ rotate: number,
852
+ contentWidth: number,
853
+ contentHeight: number,
854
+ fontSize: number,
855
+ titleFontSize: number,
856
+ color: string
857
+ ) {
858
+ ctx.setTextBaseline('top')
859
+ ctx.setTextAlign('center')
860
+ ctx.translate(contentWidth / 2, contentWidth / 2)
861
+ ctx.rotate((Math.PI / 180) * Number(rotate))
862
+
863
+ const imgHeight = imageHeight * pixelRatio.value
864
+ const imgWidth = imageWidth * pixelRatio.value
865
+ const maxChars = Math.floor(contentWidth / (fontSize * 0.5))
866
+
867
+ // 计算总高度
868
+ let totalHeight = imgHeight
869
+ const textSpacing = 10
870
+
871
+ if (title) totalHeight += textSpacing + titleFontSize
872
+ if (content) totalHeight += fontSize
873
+
874
+ // 起始Y坐标
875
+ let startY = -totalHeight / 2
876
+
877
+ // 绘制图片
878
+ ctx.drawImage(image, -imgWidth / 2, startY, imgWidth, imgHeight)
879
+
880
+ startY += imgHeight + textSpacing
881
+
882
+ // 绘制主标题
883
+ if (title) {
884
+ // 使用titleColor或默认color
885
+ ctx.setFillStyle(props.titleColor || color)
886
+ ctx.setFontSize(titleFontSize)
887
+ const titleLines = simpleWrapText(title, maxChars)
888
+ const titleLineHeight = titleFontSize * 1.2
889
+
890
+ for (let i = 0; i < titleLines.length; i++) {
891
+ ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
892
+ }
893
+
894
+ startY += titleLines.length * titleLineHeight + 5
895
+ }
896
+
897
+ // 绘制副标题
898
+ if (content) {
899
+ ctx.setFillStyle(color)
900
+ ctx.setFontSize(fontSize)
901
+ const contentLines = simpleWrapText(content, maxChars)
902
+ const contentLineHeight = fontSize * 1.2
903
+
904
+ for (let i = 0; i < contentLines.length; i++) {
905
+ ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
906
+ }
907
+ }
908
+
909
+ ctx.restore()
910
+ ctx.draw(false, () => {
911
+ // #ifdef MP-DINGTALK
912
+ // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
913
+ ;(ctx as any).toTempFilePath({
914
+ success(res: any) {
915
+ showCanvas.value = false
916
+ waterMarkUrl.value = res.filePath
917
+ }
918
+ })
919
+ // #endif
920
+ // #ifndef MP-DINGTALK
921
+ uni.canvasToTempFilePath({
922
+ canvasId: canvasId.value,
923
+ success: (res) => {
924
+ showCanvas.value = false
925
+ waterMarkUrl.value = res.tempFilePath
926
+ }
927
+ })
928
+ // #endif
929
+ })
930
+ }
931
+
932
+ /**
933
+ * 绘制在屏图片canvas
934
+ * @param ctx canvas上下文
935
+ * @param image 水印图片地址
936
+ * @param imageHeight 水印图片高度
937
+ * @param imageWidth 水印图片宽度
938
+ * @param rotate 水印内容倾斜角度
939
+ * @param contentWidth 水印宽度
940
+ * @param contentHeight 水印高度
941
+ */
942
+ function drawImageOnScreen(
943
+ ctx: UniApp.CanvasContext,
944
+ image: string,
945
+ imageHeight: number,
946
+ imageWidth: number,
947
+ rotate: number,
948
+ contentWidth: number,
949
+ contentHeight: number
950
+ ) {
951
+ ctx.translate(contentWidth / 2, contentHeight / 2)
952
+ ctx.rotate((Math.PI / 180) * Number(rotate))
953
+
954
+ ctx.drawImage(
955
+ image,
956
+ (-imageWidth * pixelRatio.value) / 2,
957
+ (-imageHeight * pixelRatio.value) / 2,
958
+ imageWidth * pixelRatio.value,
959
+ imageHeight * pixelRatio.value
960
+ )
961
+ ctx.restore()
962
+ ctx.draw(false, () => {
963
+ // #ifdef MP-DINGTALK
964
+ // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
965
+ ;(ctx as any).toTempFilePath({
966
+ success(res: any) {
967
+ showCanvas.value = false
968
+ waterMarkUrl.value = res.filePath
969
+ }
970
+ })
971
+ // #endif
972
+ // #ifndef MP-DINGTALK
973
+ uni.canvasToTempFilePath({
974
+ canvasId: canvasId.value,
975
+ success: (res) => {
976
+ showCanvas.value = false
977
+ waterMarkUrl.value = res.tempFilePath
978
+ }
979
+ })
980
+ // #endif
981
+ })
982
+ }
983
+ /**
984
+ * 启动监听
985
+ * */
986
+ function startObserve() {
987
+ // #ifdef H5
988
+ const target = document.querySelector(WATERMARK_SELECTOR) as HTMLElement
989
+ if (!target || observer.value) return
990
+
991
+ // 观察目标节点的属性变化、子节点变化、以及自身被删除
992
+ observer.value = new MutationObserver((mutations) => {
993
+ mutations.forEach((mutation) => {
994
+ let el = document.querySelector('.hy-watermark') as HTMLElement
995
+ if (mutation.type === 'attributes' || mutation.removedNodes.length > 0) {
996
+ // 检查节点是否被删除
997
+ if (!el) {
998
+ // 手动创建一个新的 div
999
+ el = document.createElement('div') as HTMLElement
1000
+ parent.appendChild(target)
1001
+ }
1002
+ el.className = rootClass.value.join(' ') // 加上你需要的初始类名
1003
+ el.style.cssText = Object.entries(rootStyle.value)
1004
+ .map(([key, val]) => {
1005
+ // 将 camelCase 转为 kebab-case (例如: backgroundImage -> background-image)
1006
+ const cssKey = key.replace(/([A-Z])/g, '-$1').toLowerCase()
1007
+ return `${cssKey}: ${val};` // 强制加上 !important 增加删除难度
1008
+ })
1009
+ .join(' ')
1010
+ console.warn('检测到安全策略违规,正在恢复水印...')
1011
+
1012
+ // 停止旧监听,防止死循环
1013
+ stopObserve()
1014
+ nextTick(() => startObserve())
1015
+ }
1016
+ })
1017
+ })
1018
+
1019
+ // 监听父级节点,防止整个 .hy-watermark 被删除
1020
+ const parent = target.parentElement || document.body
1021
+ observer.value.observe(parent, {
1022
+ attributes: true,
1023
+ childList: true,
1024
+ subtree: true,
1025
+ attributeFilter: ['style', 'class'] // 仅监听样式和类名改动
1026
+ })
1027
+ // #endif
1028
+ }
1029
+
1030
+ /**
1031
+ * 停止监听
1032
+ * */
1033
+ function stopObserve() {
1034
+ if (observer.value) {
1035
+ observer.value.disconnect()
1036
+ observer.value = null
1037
+ }
1038
+ }
1039
+
1040
+ onMounted(() => {
1041
+ doInit()
1042
+ // 初始化完成后开启监听
1043
+ // #ifdef H5
1044
+ props.isAntiTheft && nextTick(() => startObserve())
1045
+ // #endif
1046
+ })
1047
+
1048
+ // 组件销毁前必须断开监听,否则会导致内存泄漏
1049
+ onUnmounted(() => {
1050
+ // #ifdef H5
1051
+ stopObserve()
1052
+ // #endif
1053
+ })
1054
+ </script>
1055
+
1056
+ <style lang="scss" scoped>
1057
+ @import './index.scss';
1058
+ </style>