hy-app 0.6.2 → 0.6.4

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.
@@ -1,978 +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 } 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 canvasId = ref<string>(`watermark--${guid()}`) // canvas 组件的唯一标识符
51
- const waterMarkUrl = ref<string>('') // canvas生成base64水印
52
- const canvasOffScreenable = ref<boolean>(
53
- uni.canIUse('createOffscreenCanvas') && Boolean(uni.createOffscreenCanvas)
54
- ) // 是否可以使用离屏canvas
55
- const pixelRatio = ref<number>(uni.getSystemInfoSync().pixelRatio) // 像素比
56
- const canvasHeight = ref<number>((props.height + props.gutterY) * pixelRatio.value) // canvas画布高度
57
- const canvasWidth = ref<number>((props.width + props.gutterX) * pixelRatio.value) // canvas画布宽度
58
- const showCanvas = ref<boolean>(true) // 是否展示canvas
59
-
60
- /**
61
- * @description 水印css类
62
- */
63
- const rootClass = computed(() => {
64
- const classes: string[] = ['hy-watermark']
65
- if (props.fullScreen) {
66
- classes.push('is-fullscreen')
67
- }
68
- return classes
69
- })
70
-
71
- /**
72
- * @description 水印样式
73
- */
74
- const rootStyle = computed(() => {
75
- const style: CSSProperties = {
76
- opacity: props.opacity,
77
- backgroundSize: addUnit(props.width + props.gutterX)
78
- }
79
- if (waterMarkUrl.value) {
80
- style['backgroundImage'] = `url('${waterMarkUrl.value}')`
81
- }
82
- return style
83
- })
84
-
85
- onMounted(() => {
86
- doInit()
87
- })
88
-
89
- function doReset() {
90
- showCanvas.value = true
91
- canvasHeight.value = (props.height + props.gutterY) * pixelRatio.value
92
- canvasWidth.value = (props.width + props.gutterX) * pixelRatio.value
93
- nextTick(() => {
94
- doInit()
95
- })
96
- }
97
-
98
- function doInit() {
99
- // #ifdef H5
100
- // h5使用document.createElement创建canvas,不用展示canvas标签
101
- showCanvas.value = false
102
- // #endif
103
- const {
104
- width,
105
- height,
106
- color,
107
- size,
108
- fontStyle,
109
- fontWeight,
110
- fontFamily,
111
- content,
112
- rotate,
113
- gutterX,
114
- gutterY,
115
- image,
116
- imageHeight,
117
- imageWidth,
118
- title
119
- } = props
120
-
121
- // 创建水印
122
- createWaterMark(
123
- width,
124
- height,
125
- color,
126
- size,
127
- fontStyle,
128
- fontWeight,
129
- fontFamily,
130
- content,
131
- rotate,
132
- gutterX,
133
- gutterY,
134
- image,
135
- imageHeight,
136
- imageWidth,
137
- title
138
- )
139
- }
140
-
141
- /**
142
- * 创建水印图片
143
- * @param width canvas宽度
144
- * @param height canvas高度
145
- * @param color canvas字体颜色
146
- * @param size canvas字体大小
147
- * @param fontStyle canvas字体样式
148
- * @param fontWeight canvas字体字重
149
- * @param fontFamily canvas字体系列
150
- * @param content canvas内容
151
- * @param rotate 倾斜角度
152
- * @param gutterX X轴间距
153
- * @param gutterY Y轴间距
154
- * @param image canvas图片
155
- * @param imageHeight canvas图片高度
156
- * @param imageWidth canvas图片宽度
157
- * @param title 标题
158
- */
159
- function createWaterMark(
160
- width: number,
161
- height: number,
162
- color: string,
163
- size: number,
164
- fontStyle: string,
165
- fontWeight: number | string,
166
- fontFamily: string,
167
- content: string,
168
- rotate: number,
169
- gutterX: number,
170
- gutterY: number,
171
- image: string,
172
- imageHeight: number,
173
- imageWidth: number,
174
- title: string
175
- ) {
176
- const canvasHeight = (height + gutterY) * pixelRatio.value
177
- const canvasWidth = (width + gutterX) * pixelRatio.value
178
- const contentWidth = width * pixelRatio.value
179
- const contentHeight = height * pixelRatio.value
180
- const fontSize = size * pixelRatio.value
181
- // 标题字体大小:如果设置了titleSize则使用titleSize,否则使用size的1.2倍
182
- const titleFontSize = props.titleSize > 0 ? props.titleSize * pixelRatio.value : fontSize * 1.2
183
-
184
- // #ifndef H5
185
- if (canvasOffScreenable.value) {
186
- createOffscreenCanvas(
187
- canvasHeight,
188
- canvasWidth,
189
- contentWidth,
190
- contentHeight,
191
- rotate,
192
- fontSize,
193
- fontFamily,
194
- fontStyle,
195
- fontWeight,
196
- color,
197
- content,
198
- image,
199
- imageHeight,
200
- imageWidth,
201
- title,
202
- titleFontSize
203
- )
204
- } else {
205
- createCanvas(
206
- canvasHeight,
207
- contentWidth,
208
- rotate,
209
- fontSize,
210
- color,
211
- content,
212
- image,
213
- imageHeight,
214
- imageWidth,
215
- title,
216
- titleFontSize
217
- )
218
- }
219
- // #endif
220
- // #ifdef H5
221
- createH5Canvas(
222
- canvasHeight,
223
- canvasWidth,
224
- contentWidth,
225
- contentHeight,
226
- rotate,
227
- fontSize,
228
- fontFamily,
229
- fontStyle,
230
- fontWeight,
231
- color,
232
- content,
233
- image,
234
- imageHeight,
235
- imageWidth,
236
- title,
237
- titleFontSize
238
- )
239
- // #endif
240
- }
241
-
242
- /**
243
- * 创建离屏canvas
244
- * @param canvasHeight canvas高度
245
- * @param canvasWidth canvas宽度
246
- * @param contentWidth 内容宽度
247
- * @param contentHeight 内容高度
248
- * @param rotate 内容倾斜角度
249
- * @param fontSize 字体大小
250
- * @param fontFamily 字体系列
251
- * @param fontStyle 字体样式
252
- * @param fontWeight 字体字重
253
- * @param color 字体颜色
254
- * @param content 内容
255
- * @param image canvas图片
256
- * @param imageHeight canvas图片高度
257
- * @param imageWidth canvas图片宽度
258
- */
259
- function createOffscreenCanvas(
260
- canvasHeight: number,
261
- canvasWidth: number,
262
- contentWidth: number,
263
- contentHeight: number,
264
- rotate: number,
265
- fontSize: number,
266
- fontFamily: string,
267
- fontStyle: string,
268
- fontWeight: string | number,
269
- color: string,
270
- content: string,
271
- image: string,
272
- imageHeight: number,
273
- imageWidth: number,
274
- title: string,
275
- titleFontSize: number
276
- ) {
277
- // 创建离屏canvas
278
- const canvas: any = uni.createOffscreenCanvas({
279
- height: canvasHeight,
280
- width: canvasWidth,
281
- type: '2d'
282
- })
283
- const ctx: any = canvas.getContext('2d')
284
- if (ctx) {
285
- if (image && (title || content)) {
286
- // 图片和文字同时显示
287
- const img = canvas.createImage() as HTMLImageElement
288
- drawImageAndTextOffScreen(
289
- ctx,
290
- img,
291
- image,
292
- imageHeight,
293
- imageWidth,
294
- title,
295
- content,
296
- rotate,
297
- contentWidth,
298
- contentHeight,
299
- fontSize,
300
- titleFontSize,
301
- fontFamily,
302
- fontStyle,
303
- fontWeight,
304
- color,
305
- canvas
306
- )
307
- } else if (image) {
308
- const img = canvas.createImage() as HTMLImageElement
309
- drawImageOffScreen(
310
- ctx,
311
- img,
312
- image,
313
- imageHeight,
314
- imageWidth,
315
- rotate,
316
- contentWidth,
317
- contentHeight,
318
- canvas
319
- )
320
- } else {
321
- drawTextOffScreen(
322
- ctx,
323
- title,
324
- contentWidth,
325
- contentHeight,
326
- rotate,
327
- fontSize,
328
- fontFamily,
329
- fontStyle,
330
- fontWeight,
331
- color,
332
- canvas,
333
- content,
334
- titleFontSize
335
- )
336
- }
337
- } else {
338
- console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
339
- }
340
- }
341
-
342
- /**
343
- * 非H5创建canvas
344
- * 不支持创建离屏canvas时调用
345
- * @param contentHeight 内容高度
346
- * @param contentWidth 内容宽度
347
- * @param rotate 内容倾斜角度
348
- * @param fontSize 字体大小
349
- * @param color 字体颜色
350
- * @param content 内容
351
- * @param image canvas图片
352
- * @param imageHeight canvas图片高度
353
- * @param imageWidth canvas图片宽度
354
- */
355
- function createCanvas(
356
- contentHeight: number,
357
- contentWidth: number,
358
- rotate: number,
359
- fontSize: number,
360
- color: string,
361
- content: string,
362
- image: string,
363
- imageHeight: number,
364
- imageWidth: number,
365
- title: string,
366
- titleFontSize: number
367
- ) {
368
- const ctx = uni.createCanvasContext(canvasId.value)
369
- if (ctx) {
370
- if (image && (title || content)) {
371
- // 图片和文字同时显示
372
- drawImageAndTextOnScreen(
373
- ctx,
374
- image,
375
- imageHeight,
376
- imageWidth,
377
- title,
378
- content,
379
- rotate,
380
- contentWidth,
381
- contentHeight,
382
- fontSize,
383
- titleFontSize,
384
- color
385
- )
386
- } else if (image) {
387
- drawImageOnScreen(
388
- ctx,
389
- image,
390
- imageHeight,
391
- imageWidth,
392
- rotate,
393
- contentWidth,
394
- contentHeight
395
- )
396
- } else {
397
- drawTextOnScreen(
398
- ctx,
399
- title,
400
- contentWidth,
401
- rotate,
402
- fontSize,
403
- color,
404
- content,
405
- titleFontSize
406
- )
407
- }
408
- } else {
409
- console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
410
- }
411
- }
412
-
413
- /**
414
- * h5创建canvas
415
- * @param canvasHeight canvas高度
416
- * @param canvasWidth canvas宽度
417
- * @param contentWidth 水印内容宽度
418
- * @param contentHeight 水印内容高度
419
- * @param rotate 水印内容倾斜角度
420
- * @param fontSize 水印字体大小
421
- * @param fontFamily 水印字体系列
422
- * @param fontStyle 水印字体样式
423
- * @param fontWeight 水印字体字重
424
- * @param color 水印字体颜色
425
- * @param content 水印内容
426
- * @param image canvas图片
427
- * @param imageHeight canvas图片高度
428
- * @param imageWidth canvas图片宽度
429
- */
430
- function createH5Canvas(
431
- canvasHeight: number,
432
- canvasWidth: number,
433
- contentWidth: number,
434
- contentHeight: number,
435
- rotate: number,
436
- fontSize: number,
437
- fontFamily: string,
438
- fontStyle: string,
439
- fontWeight: string | number,
440
- color: string,
441
- content: string,
442
- image: string,
443
- imageHeight: number,
444
- imageWidth: number,
445
- title: string,
446
- titleFontSize: number
447
- ) {
448
- const canvas = document.createElement('canvas')
449
- const ctx = canvas.getContext('2d')
450
- canvas.setAttribute('width', `${canvasWidth}px`)
451
- canvas.setAttribute('height', `${canvasHeight}px`)
452
- if (ctx) {
453
- if (image && (title || content)) {
454
- // 图片和文字同时显示
455
- const img = new Image()
456
- drawImageAndTextOffScreen(
457
- ctx,
458
- img,
459
- image,
460
- imageHeight,
461
- imageWidth,
462
- title,
463
- content,
464
- rotate,
465
- contentWidth,
466
- contentHeight,
467
- fontSize,
468
- titleFontSize,
469
- fontFamily,
470
- fontStyle,
471
- fontWeight,
472
- color,
473
- canvas
474
- )
475
- } else if (image) {
476
- const img = new Image()
477
- drawImageOffScreen(
478
- ctx,
479
- img,
480
- image,
481
- imageHeight,
482
- imageWidth,
483
- rotate,
484
- contentWidth,
485
- contentHeight,
486
- canvas
487
- )
488
- } else {
489
- drawTextOffScreen(
490
- ctx,
491
- title,
492
- contentWidth,
493
- contentHeight,
494
- rotate,
495
- fontSize,
496
- fontFamily,
497
- fontStyle,
498
- fontWeight,
499
- color,
500
- canvas,
501
- content,
502
- titleFontSize
503
- )
504
- }
505
- } else {
506
- console.error('无法获取canvas上下文,请确认当前环境是否支持canvas')
507
- }
508
- }
509
-
510
- /**
511
- * 绘制离屏文字canvas
512
- * @param ctx canvas上下文
513
- * @param content 水印内容
514
- * @param contentWidth 水印宽度
515
- * @param contentHeight 水印高度
516
- * @param rotate 水印内容倾斜角度
517
- * @param fontSize 水印字体大小
518
- * @param fontFamily 水印字体系列
519
- * @param fontStyle 水印字体样式
520
- * @param fontWeight 水印字体字重
521
- * @param color 水印字体颜色
522
- * @param canvas canvas实例
523
- */
524
- // 测量文本宽度并自动换行
525
- function wrapText(ctx: CanvasRenderingContext2D, text: string, maxWidth: number, fontSize: number) {
526
- const words = text.split('')
527
- const lines: string[] = []
528
- let currentLine = ''
529
-
530
- for (let i = 0; i < words.length; i++) {
531
- const testLine = currentLine + words[i]
532
- const metrics = ctx.measureText(testLine)
533
- const testWidth = metrics.width
534
-
535
- // 当文字宽度超过容器宽度的80%时换行
536
- if (testWidth > maxWidth * 0.8 && currentLine !== '') {
537
- lines.push(currentLine)
538
- currentLine = words[i]
539
- } else {
540
- currentLine = testLine
541
- }
542
- }
543
- lines.push(currentLine)
544
- return lines
545
- }
546
-
547
- function drawTextOffScreen(
548
- ctx: CanvasRenderingContext2D,
549
- title: string,
550
- contentWidth: number,
551
- contentHeight: number,
552
- rotate: number,
553
- fontSize: number,
554
- fontFamily: string,
555
- fontStyle: string,
556
- fontWeight: string | number,
557
- color: string,
558
- canvas: HTMLCanvasElement,
559
- content: string = '',
560
- titleFontSize: number = 0
561
- ) {
562
- ctx.textBaseline = 'middle'
563
- ctx.textAlign = 'center'
564
- ctx.translate(contentWidth / 2, contentHeight / 2)
565
- ctx.rotate((Math.PI / 180) * rotate)
566
-
567
- // 计算总高度
568
- let totalTextHeight = titleFontSize
569
- if (content) {
570
- totalTextHeight += fontSize + 5 // 标题和副标题之间的间距
571
- }
572
-
573
- // 起始Y坐标
574
- let startY = -totalTextHeight / 2
575
-
576
- // 绘制主标题(支持自动换行)
577
- if (title) {
578
- ctx.font = `${fontStyle} normal ${fontWeight} ${titleFontSize}px/${contentHeight}px ${fontFamily}`
579
- // 使用titleColor或默认color
580
- ctx.fillStyle = props.titleColor || color
581
- const titleLines = wrapText(ctx, title, contentWidth, titleFontSize)
582
- const titleLineHeight = titleFontSize * 1.2
583
-
584
- for (let i = 0; i < titleLines.length; i++) {
585
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
586
- }
587
-
588
- startY += titleLines.length * titleLineHeight + 5
589
- }
590
-
591
- // 绘制副标题(支持自动换行)
592
- if (content) {
593
- ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
594
- ctx.fillStyle = color
595
- const contentLines = wrapText(ctx, content, contentWidth, fontSize)
596
- const contentLineHeight = fontSize * 1.2
597
-
598
- for (let i = 0; i < contentLines.length; i++) {
599
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
600
- }
601
- }
602
-
603
- ctx.restore()
604
- waterMarkUrl.value = canvas.toDataURL()
605
- }
606
-
607
- /**
608
- * 绘制在屏文字canvas
609
- * @param ctx canvas上下文
610
- * @param content 水印内容
611
- * @param contentWidth 水印宽度
612
- * @param rotate 水印内容倾斜角度
613
- * @param fontSize 水印字体大小
614
- * @param color 水印字体颜色
615
- */
616
- // 简化版本的文字换行(UniApp CanvasContext不支持measureText)
617
- function simpleWrapText(text: string, maxLength: number) {
618
- const lines: string[] = []
619
- let currentLine = ''
620
-
621
- // 基于字符数估算换行(适用于UniApp CanvasContext)
622
- for (let i = 0; i < text.length; i++) {
623
- currentLine += text[i]
624
- if (currentLine.length >= maxLength) {
625
- lines.push(currentLine)
626
- currentLine = ''
627
- }
628
- }
629
- if (currentLine) {
630
- lines.push(currentLine)
631
- }
632
- return lines
633
- }
634
-
635
- function drawTextOnScreen(
636
- ctx: UniApp.CanvasContext,
637
- title: string,
638
- contentWidth: number,
639
- rotate: number,
640
- fontSize: number,
641
- color: string,
642
- content: string = '',
643
- titleFontSize: number = 0
644
- ) {
645
- ctx.setTextBaseline('middle')
646
- ctx.setTextAlign('center')
647
- ctx.translate(contentWidth / 2, contentWidth / 2)
648
- ctx.rotate((Math.PI / 180) * rotate)
649
-
650
- // 估算每行最大字符数
651
- const maxChars = Math.floor(contentWidth / (fontSize * 0.5))
652
-
653
- // 计算总高度
654
- let totalTextHeight = titleFontSize
655
- if (content) {
656
- totalTextHeight += fontSize + 5
657
- }
658
-
659
- // 起始Y坐标
660
- let startY = -totalTextHeight / 2
661
-
662
- // 绘制主标题(支持自动换行)
663
- if (title) {
664
- // 使用titleColor或默认color
665
- ctx.setFillStyle(props.titleColor || color)
666
- ctx.setFontSize(titleFontSize)
667
- const titleLines = simpleWrapText(title, maxChars)
668
- const titleLineHeight = titleFontSize * 1.2
669
-
670
- for (let i = 0; i < titleLines.length; i++) {
671
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
672
- }
673
-
674
- startY += titleLines.length * titleLineHeight + 5
675
- }
676
-
677
- // 绘制副标题(支持自动换行)
678
- if (content) {
679
- ctx.setFillStyle(color)
680
- ctx.setFontSize(fontSize)
681
- const contentLines = simpleWrapText(content, maxChars)
682
- const contentLineHeight = fontSize * 1.2
683
-
684
- for (let i = 0; i < contentLines.length; i++) {
685
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
686
- }
687
- }
688
-
689
- ctx.restore()
690
- ctx.draw()
691
- // #ifdef MP-DINGTALK
692
- // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
693
- ;(ctx as any).toTempFilePath({
694
- success(res: any) {
695
- showCanvas.value = false
696
- waterMarkUrl.value = res.filePath
697
- }
698
- })
699
- // #endif
700
- // #ifndef MP-DINGTALK
701
- uni.canvasToTempFilePath({
702
- canvasId: canvasId.value,
703
- success: (res) => {
704
- showCanvas.value = false
705
- waterMarkUrl.value = res.tempFilePath
706
- }
707
- })
708
- // #endif
709
- }
710
-
711
- /**
712
- * 绘制离屏图片canvas
713
- * @param ctx canvas上下文
714
- * @param img 水印图片对象
715
- * @param image 水印图片地址
716
- * @param imageHeight 水印图片高度
717
- * @param imageWidth 水印图片宽度
718
- * @param rotate 水印内容倾斜角度
719
- * @param contentWidth 水印宽度
720
- * @param contentHeight 水印高度
721
- * @param canvas canvas实例
722
- */
723
- async function drawImageOffScreen(
724
- ctx: CanvasRenderingContext2D,
725
- img: HTMLImageElement,
726
- image: string,
727
- imageHeight: number,
728
- imageWidth: number,
729
- rotate: number,
730
- contentWidth: number,
731
- contentHeight: number,
732
- canvas: HTMLCanvasElement
733
- ) {
734
- ctx.translate(contentWidth / 2, contentHeight / 2)
735
- ctx.rotate((Math.PI / 180) * Number(rotate))
736
- img.crossOrigin = 'anonymous'
737
- img.referrerPolicy = 'no-referrer'
738
-
739
- img.src = image
740
- img.onload = () => {
741
- ctx.drawImage(
742
- img,
743
- (-imageWidth * pixelRatio.value) / 2,
744
- (-imageHeight * pixelRatio.value) / 2,
745
- imageWidth * pixelRatio.value,
746
- imageHeight * pixelRatio.value
747
- )
748
- ctx.restore()
749
- waterMarkUrl.value = canvas.toDataURL()
750
- }
751
- }
752
-
753
- // 绘制图片和文字(离屏)
754
- async function drawImageAndTextOffScreen(
755
- ctx: CanvasRenderingContext2D,
756
- img: HTMLImageElement,
757
- image: string,
758
- imageHeight: number,
759
- imageWidth: number,
760
- title: string,
761
- content: string,
762
- rotate: number,
763
- contentWidth: number,
764
- contentHeight: number,
765
- fontSize: number,
766
- titleFontSize: number,
767
- fontFamily: string,
768
- fontStyle: string,
769
- fontWeight: string | number,
770
- color: string,
771
- canvas: HTMLCanvasElement
772
- ) {
773
- ctx.translate(contentWidth / 2, contentHeight / 2)
774
- ctx.rotate((Math.PI / 180) * Number(rotate))
775
- img.crossOrigin = 'anonymous'
776
- img.referrerPolicy = 'no-referrer'
777
-
778
- const imgHeight = imageHeight * pixelRatio.value
779
- const imgWidth = imageWidth * pixelRatio.value
780
-
781
- img.src = image
782
- img.onload = () => {
783
- // 计算总高度
784
- let totalHeight = imgHeight
785
- const textSpacing = 10
786
-
787
- if (title) totalHeight += textSpacing + titleFontSize
788
- if (content) totalHeight += fontSize
789
-
790
- // 起始Y坐标
791
- let startY = -totalHeight / 2
792
-
793
- // 绘制图片
794
- ctx.drawImage(img, -imgWidth / 2, startY, imgWidth, imgHeight)
795
-
796
- startY += imgHeight + textSpacing
797
-
798
- // 设置文字样式
799
- ctx.textBaseline = 'top'
800
- ctx.textAlign = 'center'
801
-
802
- // 绘制主标题
803
- if (title) {
804
- ctx.font = `${fontStyle} normal ${fontWeight} ${titleFontSize}px/${contentHeight}px ${fontFamily}`
805
- // 使用titleColor或默认color
806
- ctx.fillStyle = props.titleColor || color
807
- const titleLines = wrapText(ctx, title, contentWidth * 0.9, titleFontSize)
808
- const titleLineHeight = titleFontSize * 1.2
809
-
810
- for (let i = 0; i < titleLines.length; i++) {
811
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
812
- }
813
-
814
- startY += titleLines.length * titleLineHeight + 5
815
- }
816
-
817
- // 绘制副标题
818
- if (content) {
819
- ctx.font = `${fontStyle} normal ${fontWeight} ${fontSize}px/${contentHeight}px ${fontFamily}`
820
- ctx.fillStyle = color
821
- const contentLines = wrapText(ctx, content, contentWidth * 0.9, fontSize)
822
- const contentLineHeight = fontSize * 1.2
823
-
824
- for (let i = 0; i < contentLines.length; i++) {
825
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
826
- }
827
- }
828
-
829
- ctx.restore()
830
- waterMarkUrl.value = canvas.toDataURL()
831
- }
832
- }
833
-
834
- // 绘制图片和文字(在屏)
835
- function drawImageAndTextOnScreen(
836
- ctx: UniApp.CanvasContext,
837
- image: string,
838
- imageHeight: number,
839
- imageWidth: number,
840
- title: string,
841
- content: string,
842
- rotate: number,
843
- contentWidth: number,
844
- contentHeight: number,
845
- fontSize: number,
846
- titleFontSize: number,
847
- color: string
848
- ) {
849
- ctx.setTextBaseline('top')
850
- ctx.setTextAlign('center')
851
- ctx.translate(contentWidth / 2, contentWidth / 2)
852
- ctx.rotate((Math.PI / 180) * Number(rotate))
853
-
854
- const imgHeight = imageHeight * pixelRatio.value
855
- const imgWidth = imageWidth * pixelRatio.value
856
- const maxChars = Math.floor(contentWidth / (fontSize * 0.5))
857
-
858
- // 计算总高度
859
- let totalHeight = imgHeight
860
- const textSpacing = 10
861
-
862
- if (title) totalHeight += textSpacing + titleFontSize
863
- if (content) totalHeight += fontSize
864
-
865
- // 起始Y坐标
866
- let startY = -totalHeight / 2
867
-
868
- // 绘制图片
869
- ctx.drawImage(image, -imgWidth / 2, startY, imgWidth, imgHeight)
870
-
871
- startY += imgHeight + textSpacing
872
-
873
- // 绘制主标题
874
- if (title) {
875
- // 使用titleColor或默认color
876
- ctx.setFillStyle(props.titleColor || color)
877
- ctx.setFontSize(titleFontSize)
878
- const titleLines = simpleWrapText(title, maxChars)
879
- const titleLineHeight = titleFontSize * 1.2
880
-
881
- for (let i = 0; i < titleLines.length; i++) {
882
- ctx.fillText(titleLines[i], 0, startY + i * titleLineHeight)
883
- }
884
-
885
- startY += titleLines.length * titleLineHeight + 5
886
- }
887
-
888
- // 绘制副标题
889
- if (content) {
890
- ctx.setFillStyle(color)
891
- ctx.setFontSize(fontSize)
892
- const contentLines = simpleWrapText(content, maxChars)
893
- const contentLineHeight = fontSize * 1.2
894
-
895
- for (let i = 0; i < contentLines.length; i++) {
896
- ctx.fillText(contentLines[i], 0, startY + i * contentLineHeight)
897
- }
898
- }
899
-
900
- ctx.restore()
901
- ctx.draw(false, () => {
902
- // #ifdef MP-DINGTALK
903
- // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
904
- ;(ctx as any).toTempFilePath({
905
- success(res: any) {
906
- showCanvas.value = false
907
- waterMarkUrl.value = res.filePath
908
- }
909
- })
910
- // #endif
911
- // #ifndef MP-DINGTALK
912
- uni.canvasToTempFilePath({
913
- canvasId: canvasId.value,
914
- success: (res) => {
915
- showCanvas.value = false
916
- waterMarkUrl.value = res.tempFilePath
917
- }
918
- })
919
- // #endif
920
- })
921
- }
922
-
923
- /**
924
- * 绘制在屏图片canvas
925
- * @param ctx canvas上下文
926
- * @param image 水印图片地址
927
- * @param imageHeight 水印图片高度
928
- * @param imageWidth 水印图片宽度
929
- * @param rotate 水印内容倾斜角度
930
- * @param contentWidth 水印宽度
931
- * @param contentHeight 水印高度
932
- */
933
- function drawImageOnScreen(
934
- ctx: UniApp.CanvasContext,
935
- image: string,
936
- imageHeight: number,
937
- imageWidth: number,
938
- rotate: number,
939
- contentWidth: number,
940
- contentHeight: number
941
- ) {
942
- ctx.translate(contentWidth / 2, contentHeight / 2)
943
- ctx.rotate((Math.PI / 180) * Number(rotate))
944
-
945
- ctx.drawImage(
946
- image,
947
- (-imageWidth * pixelRatio.value) / 2,
948
- (-imageHeight * pixelRatio.value) / 2,
949
- imageWidth * pixelRatio.value,
950
- imageHeight * pixelRatio.value
951
- )
952
- ctx.restore()
953
- ctx.draw(false, () => {
954
- // #ifdef MP-DINGTALK
955
- // 钉钉小程序的canvasToTempFilePath接口与其他平台不一样
956
- ;(ctx as any).toTempFilePath({
957
- success(res: any) {
958
- showCanvas.value = false
959
- waterMarkUrl.value = res.filePath
960
- }
961
- })
962
- // #endif
963
- // #ifndef MP-DINGTALK
964
- uni.canvasToTempFilePath({
965
- canvasId: canvasId.value,
966
- success: (res) => {
967
- showCanvas.value = false
968
- waterMarkUrl.value = res.tempFilePath
969
- }
970
- })
971
- // #endif
972
- })
973
- }
974
- </script>
975
-
976
- <style lang="scss" scoped>
977
- @import './index.scss';
978
- </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 && 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>