@didaoktv/didaoui-uniapp 1.2.1 → 1.2.3

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,6 +1,5 @@
1
1
  <template>
2
2
  <view class="dd-cropper">
3
- <!-- <image :src="imgSrc.imgSrc" @click="select" :style="[ imgStyle ]" class="my-avatar"></image> -->
4
3
  <dd-canvas ref="avatarCanvas" :canvas-id="'avatar-canvas-' + instanceId" class="my-canvas" :width="windowWidth || 0" :height="windowHeight || 0" :style="{ top: styleTop, height: cvsStyleHeight }" bg-color="transparent"></dd-canvas>
5
4
  <dd-canvas ref="operCanvas" :canvas-id="'oper-canvas-' + instanceId" class="oper-canvas" :width="windowWidth || 0" :height="windowHeight || 0" :style="{ top: styleTop, height: cvsStyleHeight }" bg-color="transparent" @touchstart="start" @touchmove="move" @touchend="end"></dd-canvas>
6
5
  <dd-canvas ref="prvCanvas" :canvas-id="'prv-canvas-' + instanceId" class="prv-canvas" :width="windowWidth || 0" :height="windowHeight || 0" bg-color="transparent" @touchstart="hideImg" :style="{ height: cvsStyleHeight, top: prvTop }"></dd-canvas>
@@ -8,65 +7,154 @@
8
7
  <view class="oper">
9
8
  <view class="btn-wrapper" v-if="showOper">
10
9
  <view @click="select" hover-class="hover" :style="{ width: btnWidth }">
11
- <text>{{ t("dd.common.re-select") }}</text>
10
+ <text>重选</text>
12
11
  </view>
13
12
  <view @click="close" hover-class="hover" :style="{ width: btnWidth }">
14
- <text>{{ t("dd.common.close") }}</text>
13
+ <text>关闭</text>
15
14
  </view>
16
15
  <view @click="rotate" hover-class="hover" :style="{ width: btnWidth, display: btnDsp }">
17
- <text>{{ t("dd.common.rotate") }}</text>
16
+ <text>旋转</text>
18
17
  </view>
19
18
  <view @click="preview" hover-class="hover" :style="{ width: btnWidth }">
20
- <text>{{ t("dd.common.preview") }}</text>
19
+ <text>预览</text>
21
20
  </view>
22
21
  <view @click="confirm" hover-class="hover" :style="{ width: btnWidth }">
23
- <text>{{ t("dd.common.confirm") }}</text>
22
+ <text>确定</text>
24
23
  </view>
25
24
  </view>
26
25
  <view class="clr-wrapper" v-else>
27
26
  <slider class="my-slider" @change="colorChange" block-size="25" value="0" min="-100" max="100" activeColor="red" backgroundColor="green" block-color="grey" show-value></slider>
28
27
  <view @click="prvUpload" hover-class="hover" :style="{ width: btnWidth }">
29
- <text>{{ t("dd.common.confirm") }}</text>
28
+ <text>确定</text>
30
29
  </view>
31
30
  </view>
32
31
  </view>
33
32
  </view>
34
33
  <view @click="chooseImage(0, {})" v-if="styleDisplay == 'none'">
35
- <slot>
36
-
37
- </slot>
34
+ <slot></slot>
38
35
  </view>
39
36
  </view>
40
37
  </template>
41
38
 
42
39
  <script>
43
- import { t } from '../../libs/i18n'
40
+ // 画布系组件(dd-canvas / dd-signature / dd-qrcode 同族)统一采用 Options API,
41
+ // 命令式 canvas 状态挂在 this 上不走响应式,避免触摸高频重绘的代理开销。
42
+
43
+ // 底部操作条高度(px),与样式 .oper-wrapper 的 height 保持一致
44
44
  const tabHeight = 50;
45
+
45
46
  export default {
46
47
  name: "dd-cropper",
48
+ emits: ['avtinit', 'confirm', 'cancel'],
49
+ props: {
50
+ // 最小缩放倍数
51
+ minScale: {
52
+ type: [Number, String],
53
+ default: 0.3
54
+ },
55
+ // 最大缩放倍数
56
+ maxScale: {
57
+ type: [Number, String],
58
+ default: 4
59
+ },
60
+ // 是否允许双指缩放
61
+ canScale: {
62
+ type: Boolean,
63
+ default: true
64
+ },
65
+ // 是否允许旋转(inner 模式下不可用)
66
+ canRotate: {
67
+ type: Boolean,
68
+ default: true
69
+ },
70
+ lockWidth: {
71
+ type: String,
72
+ default: ''
73
+ },
74
+ lockHeight: {
75
+ type: String,
76
+ default: ''
77
+ },
78
+ // 拉伸模式:x / y / long / short / longSel / shortSel
79
+ stretch: {
80
+ type: String,
81
+ default: ''
82
+ },
83
+ // 锁定模式:x / y / long / short / longSel / shortSel
84
+ lock: {
85
+ type: String,
86
+ default: ''
87
+ },
88
+ // 页面无底部 tabbar 时保持 true
89
+ noTab: {
90
+ type: Boolean,
91
+ default: true
92
+ },
93
+ // 内部模式:隐藏旋转按钮、按钮加宽
94
+ inner: {
95
+ type: Boolean,
96
+ default: false
97
+ },
98
+ // 导出图片质量(0-1),仅对压缩格式生效
99
+ quality: {
100
+ type: [Number, String],
101
+ default: 0.9
102
+ },
103
+ // 实例标识,confirm 事件原样回传
104
+ index: {
105
+ type: [Number, String],
106
+ default: ''
107
+ },
108
+ // 是否允许拖拽四角控制点调整裁剪框大小
109
+ canChangeSize: {
110
+ type: Boolean,
111
+ default: false
112
+ },
113
+ // 裁剪区域宽度,如 '300rpx'
114
+ areaWidth: {
115
+ type: String,
116
+ default: '300rpx'
117
+ },
118
+ // 裁剪区域高度,如 '300rpx'
119
+ areaHeight: {
120
+ type: String,
121
+ default: '300rpx'
122
+ },
123
+ // 导出图片宽度,如 '260rpx'
124
+ exportWidth: {
125
+ type: String,
126
+ default: '260rpx'
127
+ },
128
+ // 导出图片高度,如 '260rpx'
129
+ exportHeight: {
130
+ type: String,
131
+ default: '260rpx'
132
+ },
133
+ // 画布填充色,默认透明,可传 'black'、'#ffffff' 等
134
+ fillColor: {
135
+ type: String,
136
+ default: 'transparent'
137
+ },
138
+ },
47
139
  data() {
48
140
  return {
49
- // 添加实例ID用于区分不同实例
50
- instanceId: Date.now() + '-' + Math.random().toString(36).substr(2, 9),
141
+ // 实例ID用于区分不同实例的 canvas-id
142
+ instanceId: Date.now() + '-' + Math.random().toString(36).substring(2, 11),
51
143
  cvsStyleHeight: '0px',
52
144
  styleDisplay: 'none',
53
145
  styleTop: '-10000px',
54
146
  prvTop: '-10000px',
55
- imgStyle: {},
56
147
  selStyle: {},
57
148
  showOper: true,
149
+ // confirm 回载荷(avatar 字段)
58
150
  imgSrc: {
59
151
  imgSrc: ''
60
152
  },
61
153
  btnWidth: '19%',
62
154
  btnDsp: 'flex',
63
- // 裁剪区域宽度,用于设置选择区域的宽度
64
- arWidth: '',
65
- // 裁剪区域高度,用于设置选择区域的高度
66
- arHeight: '',
67
- // 导出图片宽度,用于设置最终导出图片的宽度
155
+ // 导出图片宽度(px,经 rpx 换算)
68
156
  expWidth: '',
69
- // 导出图片高度,用于设置最终导出图片的高度
157
+ // 导出图片高度(px,经 rpx 换算)
70
158
  expHeight: '',
71
159
  // 是否允许调整裁剪框大小
72
160
  letChangeSize: false,
@@ -74,48 +162,13 @@
74
162
  safeAreaInsetsBottom: 0,
75
163
  };
76
164
  },
77
- watch: {
78
- avatarSrc() {
79
- this.imgSrc.imgSrc = this.avatarSrc;
80
- }
81
- },
82
- emits: ['avtinit', 'confirm'],
83
- props: {
84
- minScale: '',
85
- maxScale: '',
86
- canScale: true,
87
- canRotate: true,
88
- lockWidth: '',
89
- lockHeight: '',
90
- stretch: '',
91
- lock: '',
92
- noTab: true,
93
- inner: false,
94
- quality: '',
95
- index: '',
96
- canChangeSize: false,
97
- areaWidth: '300rpx',
98
- // 裁剪区域高度,用于设置选择区域的高度
99
- areaHeight: '300rpx',
100
- // 导出图片宽度,用于设置最终导出图片的宽度
101
- exportWidth: '260rpx',
102
- // 导出图片高度,用于设置最终导出图片的高度
103
- exportHeight: '260rpx',
104
- // 画布填充色,默认透明,可传 'black'、'#ffffff' 等
105
- fillColor: {
106
- type: String,
107
- default: 'transparent'
108
- },
109
- },
110
165
  created() {
111
166
  this.ctxCanvas = null;
112
167
  this.ctxCanvasOper = null;
113
168
  this.ctxCanvasPrv = null;
114
169
  this.qlty = parseInt(this.quality) || 0.9;
115
- this.imgSrc.imgSrc = this.imageSrc;
116
170
  this.letRotate = (this.canRotate === false || this.inner === true) ? 0 : 1;
117
171
  this.letScale = this.canScale === false ? 0 : 1;
118
- // 是否允许调整裁剪框大小,false表示不允许,其他值表示允许
119
172
  this.letChangeSize = this.canChangeSize;
120
173
  this.isin = this.inner === true ? 1 : 0;
121
174
  this.indx = this.index || undefined;
@@ -148,7 +201,6 @@
148
201
  this.initCanvasRefs(true);
149
202
  },
150
203
  methods: {
151
- t,
152
204
  async initCanvasRefs(force = false) {
153
205
  await this.$nextTick();
154
206
  const avatarCanvas = this.$refs.avatarCanvas;
@@ -191,28 +243,6 @@
191
243
  // #endif
192
244
  this.pxRatio = this.windowWidth / 750;
193
245
 
194
- let style = this.avatarStyle;
195
- if (style && style !== true && (style = style.trim())) {
196
- style = style.split(';');
197
- let obj = {};
198
- for (let v of style) {
199
- if (!v) continue;
200
- v = v.trim().split(':');
201
- if (v[1].indexOf('rpx') >= 0) {
202
- let arr = v[1].trim().split(' ');
203
- for (let k in arr) {
204
- if (!arr[k]) continue;
205
- if (arr[k].indexOf('rpx') >= 0) {
206
- arr[k] = parseFloat(arr[k]) * this.pxRatio + 'px';
207
- }
208
- }
209
- v[1] = arr.join(' ');
210
- }
211
- obj[v[0].trim()] = v[1].trim();
212
- }
213
- this.imgStyle = obj;
214
- }
215
-
216
246
  this.expWidth && (this.expWidth = this.expWidth.indexOf('rpx') >= 0 ? parseInt(this.expWidth) * this.pxRatio : parseInt(this.expWidth));
217
247
  this.expHeight && (this.expHeight = this.expHeight.indexOf('rpx') >= 0 ? parseInt(this.expHeight) * this.pxRatio : parseInt(this.expHeight));
218
248
 
@@ -225,7 +255,6 @@
225
255
  if (this.fSelecting) return;
226
256
  this.fSelecting = true;
227
257
  setTimeout(() => { this.fSelecting = false; }, 500);
228
- const self = this
229
258
  uni.chooseImage({
230
259
  count: 1,
231
260
  sizeType: ['original', 'compressed'],
@@ -241,16 +270,16 @@
241
270
  this.path = path;
242
271
  if (!this.hasSel) {
243
272
  let style = this.selStyle || {};
244
- if (this.arWidth && this.arHeight) {
245
- let areaWidth = this.arWidth.indexOf('rpx') >= 0 ? parseInt(this.arWidth) * this.pxRatio : parseInt(this.arWidth),
246
- areaHeight = this.arHeight.indexOf('rpx') >= 0 ? parseInt(this.arHeight) * this.pxRatio : parseInt(this.arHeight);
273
+ if (this.areaWidth && this.areaHeight) {
274
+ let areaWidth = this.areaWidth.indexOf('rpx') >= 0 ? parseInt(this.areaWidth) * this.pxRatio : parseInt(this.areaWidth),
275
+ areaHeight = this.areaHeight.indexOf('rpx') >= 0 ? parseInt(this.areaHeight) * this.pxRatio : parseInt(this.areaHeight);
247
276
  style.width = areaWidth + 'px';
248
277
  style.height = areaHeight + 'px';
249
278
  style.top = (this.windowHeight - areaHeight - tabHeight) / 2 + 'px';
250
279
  style.left = (this.windowWidth - areaWidth) / 2 + 'px';
251
280
  } else {
252
281
  uni.showModal({
253
- title: t("dd.cropper.emptyWidhtOrHeight"),
282
+ title: "裁剪框的宽或高没有设置",
254
283
  showCancel: false
255
284
  })
256
285
  return;
@@ -270,7 +299,7 @@
270
299
  },
271
300
  fail: () => {
272
301
  uni.showToast({
273
- title: "error3",
302
+ title: "读取图片失败",
274
303
  duration: 2000,
275
304
  })
276
305
  },
@@ -278,8 +307,9 @@
278
307
  uni.hideLoading();
279
308
  }
280
309
  });
281
- }, fail(err) {
282
- self.$emit('cancel')
310
+ },
311
+ fail: () => {
312
+ this.$emit('cancel')
283
313
  }
284
314
  })
285
315
  },
@@ -297,8 +327,6 @@
297
327
  expHeight = this.expHeight || height;
298
328
 
299
329
  // #ifdef H5
300
- // x *= this.pixelRatio;
301
- // y *= this.pixelRatio;
302
330
  expWidth = width;
303
331
  expHeight = height;
304
332
  // #endif
@@ -327,6 +355,8 @@
327
355
  expWidth = this.expWidth,
328
356
  expHeight = this.expHeight;
329
357
 
358
+ // 立即模式画布:清掉裁剪大图再画导出小图,防止透明像素透出底图
359
+ ctxCanvas.clearRect(0, 0, this.windowWidth, this.windowHeight);
330
360
  await ctxCanvas.drawImage(r, 0, 0, expWidth, expHeight);
331
361
  ctxCanvas.draw(false, () => {
332
362
  ctxCanvas.toTempFilePath({
@@ -346,7 +376,7 @@
346
376
  },
347
377
  fail: () => {
348
378
  uni.showToast({
349
- title: "error0",
379
+ title: "导出图片失败",
350
380
  duration: 2000,
351
381
  })
352
382
  }
@@ -361,9 +391,9 @@
361
391
  this.$emit("confirm", { avatar: this.imgSrc, path: r, index: this.indx, data: this.rtn });
362
392
  // #endif
363
393
  },
364
- fail: (res) => {
394
+ fail: () => {
365
395
  uni.showToast({
366
- title: "error1",
396
+ title: "导出图片失败",
367
397
  duration: 2000,
368
398
  })
369
399
  },
@@ -373,16 +403,13 @@
373
403
  }
374
404
  });
375
405
  },
376
- // 用户点击"预览"模式下的"确认"按钮时被调用,用于将预览的裁剪结果上传
406
+ // 预览模式下点击"确认":将预览画布的裁剪结果导出
377
407
  async prvUpload() {
378
408
  if (this.fPrvUploading) return;
379
409
  this.fPrvUploading = true;
380
410
  setTimeout(() => { this.fPrvUploading = false; }, 1000)
381
411
 
382
- let style = this.selStyle,
383
- destWidth = parseInt(style.width),
384
- destHeight = parseInt(style.height),
385
- prvX = this.prvX,
412
+ let prvX = this.prvX,
386
413
  prvY = this.prvY,
387
414
  prvWidth = this.prvWidth,
388
415
  prvHeight = this.prvHeight,
@@ -390,8 +417,6 @@
390
417
  expHeight = this.expHeight || prvHeight;
391
418
 
392
419
  // #ifdef H5
393
- // prvX *= this.pixelRatio;
394
- // prvY *= this.pixelRatio;
395
420
  expWidth = prvWidth;
396
421
  expHeight = prvHeight;
397
422
  // #endif
@@ -421,6 +446,8 @@
421
446
  expWidth = this.expWidth,
422
447
  expHeight = this.expHeight;
423
448
 
449
+ // 立即模式画布:清掉裁剪大图再画导出小图,防止透明像素透出底图
450
+ ctxCanvas.clearRect(0, 0, this.windowWidth, this.windowHeight);
424
451
  await ctxCanvas.drawImage(r, 0, 0, expWidth, expHeight);
425
452
  ctxCanvas.draw(false, () => {
426
453
  ctxCanvas.toTempFilePath({
@@ -440,7 +467,7 @@
440
467
  },
441
468
  fail: () => {
442
469
  uni.showToast({
443
- title: "error0",
470
+ title: "导出图片失败",
444
471
  duration: 2000,
445
472
  })
446
473
  }
@@ -457,7 +484,7 @@
457
484
  },
458
485
  fail: () => {
459
486
  uni.showToast({
460
- title: "error_prv",
487
+ title: "导出图片失败",
461
488
  duration: 2000,
462
489
  })
463
490
  },
@@ -476,7 +503,6 @@
476
503
  imgRadio = imgWidth / imgHeight,
477
504
  useWidth = allWidth - 40,
478
505
  useHeight = allHeight - tabHeight - 80,
479
- pixelRatio = this.pixelRatio,
480
506
  selWidth = parseInt(this.selStyle.width),
481
507
  selHeight = parseInt(this.selStyle.height);
482
508
 
@@ -492,13 +518,8 @@
492
518
  case 'longSel': if (selWidth > selHeight) this.fixWidth = 1; else this.fixHeight = 1; break;
493
519
  case 'shortSel': if (selWidth > selHeight) this.fixHeight = 1; else this.fixWidth = 1; break;
494
520
  }
495
- // lck 用于控制裁剪框的宽度和高度锁定行为
496
- // 'x': 锁定宽度,不允许水平方向调整
497
- // 'y': 锁定高度,不允许垂直方向调整
498
- // 'long': 根据图片长边锁定,如果图片横向较长则锁定宽度,否则锁定高度
499
- // 'short': 根据图片短边锁定,如果图片横向较长则锁定高度,否则锁定宽度
500
- // 'longSel': 根据选择框的长边锁定,如果选择框宽度大于高度则锁定宽度,否则锁定高度
501
- // 'shortSel': 根据选择框的短边锁定,如果选择框宽度大于高度则锁定高度,否则锁定宽度
521
+ // lck 锁定裁剪框的宽/高调整方向:
522
+ // 'x' 锁宽 / 'y' 锁高 / 'long'/'short' 按图片长短边 / 'longSel'/'shortSel' 按选择框长短边
502
523
  switch (this.lck) {
503
524
  case 'x': this.lckWidth = 1; break;
504
525
  case 'y': this.lckHeight = 1; break;
@@ -553,16 +574,17 @@
553
574
  this.useHeight = useHeight;
554
575
 
555
576
  let style = this.selStyle,
556
- left = parseInt(style.left),
557
- top = parseInt(style.top),
558
- width = parseInt(style.width),
559
- height = parseInt(style.height),
560
- canvas = this.canvas,
561
- canvasOper = this.canvasOper,
562
- ctxCanvas = this.ctxCanvas,
563
- ctxCanvasOper = this.ctxCanvasOper;
577
+ left = parseInt(style.left),
578
+ top = parseInt(style.top),
579
+ width = parseInt(style.width),
580
+ height = parseInt(style.height),
581
+ ctxCanvas = this.ctxCanvas,
582
+ ctxCanvasOper = this.ctxCanvasOper;
583
+
584
+ // H5/小程序 type=2d 为立即模式画布,重绘遮罩前须清掉上一帧,否则拖控制点时遮罩残留
585
+ ctxCanvasOper.clearRect(0, 0, this.windowWidth, this.windowHeight);
564
586
 
565
- ctxCanvasOper.setLineWidth(3);
587
+ ctxCanvasOper.setLineWidth(3);
566
588
  ctxCanvasOper.setStrokeStyle('grey');
567
589
  ctxCanvasOper.setGlobalAlpha(0.4);
568
590
  ctxCanvasOper.setFillStyle('black');
@@ -577,21 +599,17 @@
577
599
  ctxCanvasOper.moveTo(left + 20, top + height); ctxCanvasOper.lineTo(left, top + height); ctxCanvasOper.lineTo(left, top + height - 20);
578
600
  ctxCanvasOper.moveTo(left + width - 20, top + height); ctxCanvasOper.lineTo(left + width, top + height); ctxCanvasOper.lineTo(left + width, top + height - 20);
579
601
 
580
- // 绘制控制点(四个角)
602
+ // 四角控制点(canChangeSize 时可拖拽调整裁剪框)
581
603
  const controlPointSize = 10;
582
604
  ctxCanvasOper.setFillStyle('white');
583
605
  ctxCanvasOper.setStrokeStyle('grey');
584
606
  ctxCanvasOper.setLineWidth(1);
585
- // 左上角
586
607
  ctxCanvasOper.fillRect(left - controlPointSize / 2, top - controlPointSize / 2, controlPointSize, controlPointSize);
587
608
  ctxCanvasOper.strokeRect(left - controlPointSize / 2, top - controlPointSize / 2, controlPointSize, controlPointSize);
588
- // 右上角
589
609
  ctxCanvasOper.fillRect(left + width - controlPointSize / 2, top - controlPointSize / 2, controlPointSize, controlPointSize);
590
610
  ctxCanvasOper.strokeRect(left + width - controlPointSize / 2, top - controlPointSize / 2, controlPointSize, controlPointSize);
591
- // 左下角
592
611
  ctxCanvasOper.fillRect(left - controlPointSize / 2, top + height - controlPointSize / 2, controlPointSize, controlPointSize);
593
612
  ctxCanvasOper.strokeRect(left - controlPointSize / 2, top + height - controlPointSize / 2, controlPointSize, controlPointSize);
594
- // 右下角
595
613
  ctxCanvasOper.fillRect(left + width - controlPointSize / 2, top + height - controlPointSize / 2, controlPointSize, controlPointSize);
596
614
  ctxCanvasOper.strokeRect(left + width - controlPointSize / 2, top + height - controlPointSize / 2, controlPointSize, controlPointSize);
597
615
 
@@ -616,22 +634,30 @@
616
634
  this.$emit("avtinit");
617
635
  },
618
636
  async drawImage() {
619
- await this.initCanvasRefs();
620
- let tm_now = Date.now();
621
- if (tm_now - this.drawTm < 20) return;
622
- this.drawTm = tm_now;
623
- let ctxCanvas = this.ctxCanvas;
624
- if (this.fillColor && this.fillColor !== 'transparent') {
625
- ctxCanvas.fillRect(0, 0, this.windowWidth, this.windowHeight - tabHeight);
626
- }
627
- ctxCanvas.save();
628
- ctxCanvas.translate(this.posWidth + this.useWidth / 2, this.posHeight + this.useHeight / 2);
629
- ctxCanvas.scale(this.scaleSize, this.scaleSize);
630
- ctxCanvas.rotate(this.rotateDeg * Math.PI / 180);
631
- await ctxCanvas.drawImage(this.imgPath, -this.useWidth / 2, -this.useHeight / 2, this.useWidth, this.useHeight);
632
- ctxCanvas.restore();
633
- ctxCanvas.draw(false);
634
- },
637
+ await this.initCanvasRefs();
638
+ let tm_now = Date.now();
639
+ if (tm_now - this.drawTm < 20) return;
640
+ this.drawTm = tm_now;
641
+ let ctxCanvas = this.ctxCanvas;
642
+ // 图片对象先取回(dd-canvas 内部有缓存,二次绘制走同步路径);
643
+ // 加载期间若新帧已触发(token 失配)则丢弃本帧,避免旧帧续体把图片画回旧位置
644
+ const token = this._drawToken = (this._drawToken || 0) + 1;
645
+ const image = await ctxCanvas.loadImage(this.imgPath).catch(() => null);
646
+ if (!image || token !== this._drawToken) return;
647
+ // H5/小程序 type=2d 为立即模式画布,ctx.draw() 在 dd-canvas 内是 no-op,
648
+ // 上一帧像素不会自动清空——拖动残影的根因,每帧必须先 clearRect
649
+ ctxCanvas.clearRect(0, 0, this.windowWidth, this.windowHeight);
650
+ if (this.fillColor && this.fillColor !== 'transparent') {
651
+ ctxCanvas.fillRect(0, 0, this.windowWidth, this.windowHeight - tabHeight);
652
+ }
653
+ ctxCanvas.save();
654
+ ctxCanvas.translate(this.posWidth + this.useWidth / 2, this.posHeight + this.useHeight / 2);
655
+ ctxCanvas.scale(this.scaleSize, this.scaleSize);
656
+ ctxCanvas.rotate(this.rotateDeg * Math.PI / 180);
657
+ ctxCanvas.drawImage(image, -this.useWidth / 2, -this.useHeight / 2, this.useWidth, this.useHeight);
658
+ ctxCanvas.restore();
659
+ ctxCanvas.draw(false);
660
+ },
635
661
  hideImg() {
636
662
  this.prvImg = '';
637
663
  this.prvTop = '-10000px';
@@ -640,8 +666,6 @@
640
666
  this.target = null;
641
667
  },
642
668
  close() {
643
- console.log('up-cropper close');
644
-
645
669
  this.styleDisplay = 'none';
646
670
  this.styleTop = '-10000px';
647
671
  this.hasSel = false;
@@ -660,7 +684,6 @@
660
684
  height = parseInt(style.height);
661
685
 
662
686
  uni.showLoading({ mask: true });
663
- // console.log('size', x, y, width, height)
664
687
  await this.initCanvasRefs();
665
688
  this.ctxCanvas.toTempFilePath({
666
689
  x: x,
@@ -670,27 +693,28 @@
670
693
  fileType: 'png',
671
694
  quality: this.qlty,
672
695
  success: async (r) => {
673
- // console.log(r)
674
696
  this.prvImgTmp = r = r.tempFilePath;
675
697
 
676
698
  let ctxCanvasPrv = this.ctxCanvasPrv,
677
- prvX = this.windowWidth,
678
- prvY = parseInt(this.cvsStyleHeight),
679
- prvWidth = parseInt(this.selStyle.width),
680
- prvHeight = parseInt(this.selStyle.height),
681
- useWidth = prvX - 40,
682
- useHeight = prvY - 80,
683
- radio = useWidth / prvWidth,
684
- rHeight = prvHeight * radio;
685
- if (rHeight < useHeight) {
686
- prvWidth = useWidth;
687
- prvHeight = rHeight;
688
- } else {
689
- radio = useHeight / prvHeight;
690
- prvWidth *= radio;
691
- prvHeight = useHeight;
692
- }
693
- if (this.fillColor && this.fillColor !== 'transparent') {
699
+ prvX = this.windowWidth,
700
+ prvY = parseInt(this.cvsStyleHeight),
701
+ prvWidth = parseInt(this.selStyle.width),
702
+ prvHeight = parseInt(this.selStyle.height),
703
+ useWidth = prvX - 40,
704
+ useHeight = prvY - 80,
705
+ radio = useWidth / prvWidth,
706
+ rHeight = prvHeight * radio;
707
+ // 立即模式画布:清掉上一次预览的残影再画新帧
708
+ ctxCanvasPrv.clearRect(0, 0, this.windowWidth, this.windowHeight);
709
+ if (rHeight < useHeight) {
710
+ prvWidth = useWidth;
711
+ prvHeight = rHeight;
712
+ } else {
713
+ radio = useHeight / prvHeight;
714
+ prvWidth *= radio;
715
+ prvHeight = useHeight;
716
+ }
717
+ if (this.fillColor && this.fillColor !== 'transparent') {
694
718
  ctxCanvasPrv.setFillStyle(this.fillColor);
695
719
  ctxCanvasPrv.fillRect(0, 0, prvX, prvY);
696
720
  ctxCanvasPrv.fillRect(x, y, width, height);
@@ -702,7 +726,7 @@
702
726
  await ctxCanvasPrv.drawImage(r, prvX, prvY, prvWidth, prvHeight);
703
727
  ctxCanvasPrv.draw(false, () => {
704
728
  // #ifdef H5
705
- this.btop(this.prvImgTmp).then((r) => {
729
+ this.btop(this.prvImgTmp).then(() => {
706
730
  this.showOper = false;
707
731
  this.prvTop = this.drawTop + 'px';
708
732
  })
@@ -717,7 +741,7 @@
717
741
  },
718
742
  fail: () => {
719
743
  uni.showToast({
720
- title: "error2",
744
+ title: "生成预览失败",
721
745
  duration: 2000,
722
746
  })
723
747
  },
@@ -728,7 +752,6 @@
728
752
  },
729
753
  chooseImage(index = undefined, params = undefined, data = undefined) {
730
754
  if (params) {
731
- console.log(params)
732
755
  let areaWidth = params.areaWidth || this.areaWidth,
733
756
  areaHeight = params.areaHeight || this.areaHeight,
734
757
  expWidth = params.exportWidth || this.exportWidth,
@@ -742,14 +765,12 @@
742
765
  stretch = params.stretch,
743
766
  inner = params.inner,
744
767
  lock = params.lock;
745
- console.log('areaWidth', this.areaWidth)
746
768
 
747
769
  expWidth && (this.expWidth = expWidth.indexOf('rpx') >= 0 ? parseInt(expWidth) * this.pxRatio : parseInt(expWidth));
748
770
  expHeight && (this.expHeight = expHeight.indexOf('rpx') >= 0 ? parseInt(expHeight) * this.pxRatio : parseInt(expHeight));
749
771
  this.isin = inner === true ? 1 : 0;
750
772
  this.letRotate = (canRotate === false || this.isin) ? 0 : 1;
751
773
  this.letScale = canScale === false ? 0 : 1;
752
- // 设置是否允许调整裁剪框大小
753
774
  this.letChangeSize = canChangeSize || false;
754
775
  this.qlty = parseInt(quality) || 0.9;
755
776
  this.mnScale = minScale || 0.3;
@@ -771,7 +792,6 @@
771
792
  this.selStyle.height = areaHeight + 'px';
772
793
  this.selStyle.top = (this.windowHeight - areaHeight - tabHeight) / 2 + 'px';
773
794
  this.selStyle.left = (this.windowWidth - areaWidth) / 2 + 'px';
774
- // console.log(this.selStyle);
775
795
  this.hasSel = true;
776
796
  }
777
797
  }
@@ -788,10 +808,8 @@
788
808
  }
789
809
  // #endif
790
810
 
791
- // if(this.letRotate) {
792
811
  this.rotateDeg += 90 - this.rotateDeg % 90;
793
812
  this.drawImage();
794
- // }
795
813
  },
796
814
  start(e) {
797
815
  let touches = e.touches,
@@ -805,34 +823,30 @@
805
823
  let x = touch1.x - touch0.x,
806
824
  y = touch1.y - touch0.y;
807
825
  this.fgDistance = Math.sqrt(x * x + y * y);
808
- } else {
809
- // 只有在允许调整大小时才检查控制点
810
- if (this.letChangeSize) {
811
- // 检查是否点击在控制点上
812
- const controlPointSize = 20;
813
- const x = touch0.x;
814
- const y = touch0.y;
815
- const style = this.selStyle;
816
- const left = parseInt(style.left);
817
- const top = parseInt(style.top);
818
- const width = parseInt(style.width);
819
- const height = parseInt(style.height);
820
-
821
- // 检查四个控制点
822
- if (Math.abs(x - left) < controlPointSize && Math.abs(y - top) < controlPointSize) {
823
- this.resizeHandle = 'top-left';
824
- } else if (Math.abs(x - (left + width)) < controlPointSize && Math.abs(y - top) < controlPointSize) {
825
- this.resizeHandle = 'top-right';
826
- } else if (Math.abs(x - left) < controlPointSize && Math.abs(y - (top + height)) < controlPointSize) {
827
- this.resizeHandle = 'bottom-left';
828
- } else if (Math.abs(x - (left + width)) < controlPointSize && Math.abs(y - (top + height)) < controlPointSize) {
829
- this.resizeHandle = 'bottom-right';
830
- } else {
831
- this.resizeHandle = null;
832
- }
826
+ } else if (this.letChangeSize) {
827
+ // 单指落在四角控制点附近时进入裁剪框调整模式
828
+ const controlPointSize = 20;
829
+ const x = touch0.x;
830
+ const y = touch0.y;
831
+ const style = this.selStyle;
832
+ const left = parseInt(style.left);
833
+ const top = parseInt(style.top);
834
+ const width = parseInt(style.width);
835
+ const height = parseInt(style.height);
836
+
837
+ if (Math.abs(x - left) < controlPointSize && Math.abs(y - top) < controlPointSize) {
838
+ this.resizeHandle = 'top-left';
839
+ } else if (Math.abs(x - (left + width)) < controlPointSize && Math.abs(y - top) < controlPointSize) {
840
+ this.resizeHandle = 'top-right';
841
+ } else if (Math.abs(x - left) < controlPointSize && Math.abs(y - (top + height)) < controlPointSize) {
842
+ this.resizeHandle = 'bottom-left';
843
+ } else if (Math.abs(x - (left + width)) < controlPointSize && Math.abs(y - (top + height)) < controlPointSize) {
844
+ this.resizeHandle = 'bottom-right';
833
845
  } else {
834
846
  this.resizeHandle = null;
835
847
  }
848
+ } else {
849
+ this.resizeHandle = null;
836
850
  }
837
851
  },
838
852
  move(e) {
@@ -881,8 +895,8 @@
881
895
 
882
896
  this.drawImage();
883
897
  } else if (this.touch0) {
884
- // 只有在允许调整大小时才处理裁剪框大小调整
885
898
  if (this.resizeHandle && this.letChangeSize) {
899
+ // 拖拽控制点调整裁剪框
886
900
  const style = { ...this.selStyle };
887
901
  const left = parseInt(style.left);
888
902
  const top = parseInt(style.top);
@@ -920,7 +934,6 @@
920
934
  let nw = parseInt(style.width);
921
935
  let nh = parseInt(style.height);
922
936
 
923
- // 屏幕边界
924
937
  if (nl < 0) nl = 0;
925
938
  if (nt < 0) nt = 0;
926
939
  if (nl + nw > this.windowWidth) nw = this.windowWidth - nl;
@@ -947,11 +960,10 @@
947
960
  style.width = nw + 'px';
948
961
  style.height = nh + 'px';
949
962
  this.selStyle = style;
950
- // 重新绘制操作层
951
963
  this.drawInit();
952
964
  }
953
965
  } else {
954
- // 原有的移动图片逻辑
966
+ // 单指拖动图片
955
967
  let x = touch0.x - this.touch0.x,
956
968
  y = touch0.y - this.touch0.y,
957
969
  beX = this.posWidth + x,
@@ -997,14 +1009,13 @@
997
1009
  },
998
1010
  end(e) {
999
1011
  let touches = e.touches,
1000
- touch0 = touches && touches[0],
1001
- touch1 = touches && touches[1];
1012
+ touch0 = touches && touches[0];
1002
1013
  if (touch0) {
1003
1014
  this.touch0 = touch0;
1004
1015
  } else {
1005
1016
  this.touch0 = null;
1006
1017
  this.touch1 = null;
1007
- this.resizeHandle = null; // 重置调整手柄
1018
+ this.resizeHandle = null;
1008
1019
  }
1009
1020
  },
1010
1021
  async getImgData() {
@@ -1034,6 +1045,7 @@
1034
1045
  });
1035
1046
  });
1036
1047
  },
1048
+ // 预览模式饱和度滑杆:HSL 调整后 putImageData 回写预览画布
1037
1049
  async colorChange(e) {
1038
1050
  let tm_now = Date.now();
1039
1051
  if (tm_now - this.prvTm < 100) return;
@@ -1042,9 +1054,9 @@
1042
1054
  uni.showLoading({ mask: true });
1043
1055
 
1044
1056
  if (!this.prvImgData) {
1045
- if (!(this.prvImgData = await this.getImgData().catch((res) => {
1057
+ if (!(this.prvImgData = await this.getImgData().catch(() => {
1046
1058
  uni.showToast({
1047
- title: "error_read",
1059
+ title: "读取图片失败",
1048
1060
  duration: 2000,
1049
1061
  })
1050
1062
  }))) return;
@@ -1054,7 +1066,7 @@
1054
1066
  let data = this.prvImgData,
1055
1067
  target = this.target,
1056
1068
  i = e.detail.value,
1057
- r, g, b, a, h, s, l, d, p, q, t, min, max, hK, tR, tG, tB;
1069
+ r, g, b, a, h, s, l, d, p, q, min, max, hK, tR, tG, tB;
1058
1070
 
1059
1071
  if (i === 0) {
1060
1072
  target = data;
@@ -1163,7 +1175,7 @@
1163
1175
  data: target,
1164
1176
  fail() {
1165
1177
  uni.showToast({
1166
- title: 'error_put',
1178
+ title: "导出图片失败",
1167
1179
  duration: 2000
1168
1180
  })
1169
1181
  },
@@ -1172,8 +1184,9 @@
1172
1184
  }
1173
1185
  });
1174
1186
  },
1187
+ // H5 专用:base64 转 blob URL,规避部分浏览器 canvas 跨域限制
1175
1188
  btop(base64) {
1176
- return new Promise(function (resolve, reject) {
1189
+ return new Promise(function (resolve) {
1177
1190
  var arr = base64.split(','),
1178
1191
  mime = arr[0].match(/:(.*?);/)[1],
1179
1192
  bstr = atob(arr[1]),
@@ -1190,36 +1203,32 @@
1190
1203
  </script>
1191
1204
 
1192
1205
  <style lang="scss" scoped>
1206
+ @import '../../scss/variables';
1207
+
1193
1208
  .dd-cropper {
1194
1209
  .my-canvas {
1195
1210
  display: flex;
1196
1211
  position: fixed !important;
1197
- background: #000000;
1212
+ background: $dd-color-black;
1198
1213
  left: 0;
1199
- z-index: 100000;
1214
+ z-index: $dd-z-index-cropper;
1200
1215
  width: 100%;
1201
1216
  }
1202
1217
 
1203
- .my-avatar {
1204
- width: 150rpx;
1205
- height: 150rpx;
1206
- border-radius: 100%;
1207
- }
1208
-
1209
1218
  .oper-canvas {
1210
1219
  display: flex;
1211
1220
  position: fixed !important;
1212
1221
  left: 0;
1213
- z-index: 100001;
1222
+ z-index: $dd-z-index-cropper + 1;
1214
1223
  width: 100%;
1215
1224
  }
1216
1225
 
1217
1226
  .prv-canvas {
1218
1227
  display: flex;
1219
1228
  position: fixed !important;
1220
- background: #000000;
1229
+ background: $dd-color-black;
1221
1230
  left: 0;
1222
- z-index: 200000;
1231
+ z-index: $dd-z-index-cropper * 2;
1223
1232
  width: 100%;
1224
1233
  }
1225
1234
 
@@ -1227,12 +1236,12 @@
1227
1236
  height: 50px;
1228
1237
  position: fixed !important;
1229
1238
  box-sizing: border-box;
1230
- border: 1px solid #F1F1F1;
1231
- background: #ffffff;
1239
+ border: 1px solid $dd-neutral-50;
1240
+ background: $dd-color-white;
1232
1241
  width: 100%;
1233
1242
  left: 0;
1234
1243
  bottom: 0;
1235
- z-index: 100009;
1244
+ z-index: $dd-z-index-cropper + 9;
1236
1245
  flex-direction: row;
1237
1246
  }
1238
1247
 
@@ -1263,14 +1272,14 @@
1263
1272
  display: flex;
1264
1273
  align-items: center;
1265
1274
  justify-content: center;
1266
- font-size: 16px;
1267
- color: #333;
1268
- border: 1px solid #f1f1f1;
1275
+ font-size: $dd-font-size-h4;
1276
+ color: $dd-neutral-700;
1277
+ border: 1px solid $dd-neutral-50;
1269
1278
  border-radius: 6%;
1270
1279
  }
1271
1280
 
1272
1281
  .hover {
1273
- background: #f1f1f1;
1282
+ background: $dd-neutral-50;
1274
1283
  border-radius: 6%;
1275
1284
  }
1276
1285
 
@@ -1284,9 +1293,9 @@
1284
1293
  display: flex;
1285
1294
  align-items: center;
1286
1295
  justify-content: center;
1287
- font-size: 16px;
1288
- color: #333;
1289
- border: 1px solid #f1f1f1;
1296
+ font-size: $dd-font-size-h4;
1297
+ color: $dd-neutral-700;
1298
+ border: 1px solid $dd-neutral-50;
1290
1299
  border-radius: 6%;
1291
1300
  }
1292
1301