@uxda/appkit 4.2.14 → 4.2.16

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 (118) hide show
  1. package/.eslintrc.mjs +7 -7
  2. package/README.md +187 -187
  3. package/babel.config.js +12 -12
  4. package/dist/appkit.css +3 -7
  5. package/dist/index.js +1078 -1161
  6. package/package.json +79 -79
  7. package/project.config.json +15 -15
  8. package/project.tt.json +13 -13
  9. package/rollup.config.mjs +67 -67
  10. package/src/Appkit.ts +66 -66
  11. package/src/balance/api/endpoints.ts +133 -133
  12. package/src/balance/api/index.ts +106 -106
  13. package/src/balance/components/AccountView.vue +750 -750
  14. package/src/balance/components/BalanceCard.vue +215 -215
  15. package/src/balance/components/BalanceReminder.vue +85 -85
  16. package/src/balance/components/ConsumptionFilter.vue +218 -218
  17. package/src/balance/components/ConsumptionRules.vue +68 -68
  18. package/src/balance/components/DateFilter.vue +250 -250
  19. package/src/balance/components/DateRange.vue +80 -80
  20. package/src/balance/components/ListFilter.vue +62 -62
  21. package/src/balance/components/ListFilterPicker.vue +191 -191
  22. package/src/balance/components/PromoterCard.vue +237 -237
  23. package/src/balance/components/SecondBalance.vue +71 -71
  24. package/src/balance/components/Tip.vue +45 -45
  25. package/src/balance/components/index.ts +8 -8
  26. package/src/balance/types.ts +97 -97
  27. package/src/components/bt-cropper/index.vue +774 -774
  28. package/src/components/bt-cropper/utils/calcCropper.js +42 -42
  29. package/src/components/bt-cropper/utils/calcImagePosition.js +23 -23
  30. package/src/components/bt-cropper/utils/calcImageSize.js +37 -37
  31. package/src/components/bt-cropper/utils/calcPointDistance.js +12 -12
  32. package/src/components/bt-cropper/utils/calcRightAndBottom.js +7 -7
  33. package/src/components/bt-cropper/utils/ratio.js +3 -3
  34. package/src/components/bt-cropper/utils/tools.js +25 -25
  35. package/src/components/dd-area/index.vue +225 -225
  36. package/src/components/dd-icon/doc.md +21 -21
  37. package/src/components/dd-icon/index.vue +23 -23
  38. package/src/components/dd-notice-bar/index.vue +78 -78
  39. package/src/components/dd-search/doc.md +34 -34
  40. package/src/components/dd-search/index.vue +168 -168
  41. package/src/components/dd-selector/index.vue +124 -124
  42. package/src/components/dd-skeleton/doc.md +19 -19
  43. package/src/components/dd-skeleton/index.vue +36 -36
  44. package/src/global.ts +6 -6
  45. package/src/index.ts +89 -89
  46. package/src/main.scss +1 -1
  47. package/src/notice/api/endpoints.ts +17 -17
  48. package/src/notice/api/index.ts +106 -106
  49. package/src/notice/components/NoticeBanner.vue +243 -243
  50. package/src/notice/components/NoticeEntry.vue +99 -99
  51. package/src/notice/components/NoticeList.vue +315 -315
  52. package/src/notice/components/NoticePopup.vue +162 -162
  53. package/src/notice/components/index.ts +5 -5
  54. package/src/notice/components/useCommonList.ts +86 -86
  55. package/src/notice/components/useNotice.ts +35 -35
  56. package/src/notice/index.ts +1 -1
  57. package/src/notice/types.ts +25 -25
  58. package/src/payment/api/config.ts +7 -7
  59. package/src/payment/api/endpoints.ts +103 -103
  60. package/src/payment/api/index.ts +100 -100
  61. package/src/payment/components/AmountPicker.vue +90 -90
  62. package/src/payment/components/RechargeResult.vue +69 -69
  63. package/src/payment/components/RechargeView.vue +155 -155
  64. package/src/payment/components/RightsPicker.vue +105 -105
  65. package/src/payment/components/TradeView.vue +317 -317
  66. package/src/payment/components/UserAgreement.vue +234 -234
  67. package/src/payment/components/index.ts +22 -22
  68. package/src/payment/index.ts +5 -5
  69. package/src/payment/services/index.ts +16 -16
  70. package/src/payment/services/invoke-recharge.ts +25 -25
  71. package/src/payment/services/request-payment.ts +58 -58
  72. package/src/payment/types.ts +28 -28
  73. package/src/register/components/SelfRegistration.vue +233 -233
  74. package/src/register/components/index.ts +2 -2
  75. package/src/shared/components/AppDrawer.vue +54 -58
  76. package/src/shared/components/AppVerify.vue +128 -128
  77. package/src/shared/components/DeviceVersion.vue +68 -68
  78. package/src/shared/components/EmptyView.vue +33 -33
  79. package/src/shared/components/OcrBusinessLicense.vue +130 -130
  80. package/src/shared/components/OcrIcon.vue +202 -202
  81. package/src/shared/components/PageHeader.vue +79 -79
  82. package/src/shared/components/index.ts +8 -8
  83. package/src/shared/composables/index.ts +8 -8
  84. package/src/shared/composables/useAmount.ts +46 -46
  85. package/src/shared/composables/useCountdown.ts +46 -46
  86. package/src/shared/composables/useCrypto.ts +76 -76
  87. package/src/shared/composables/useDragBox.ts +97 -97
  88. package/src/shared/composables/useEncode.ts +43 -43
  89. package/src/shared/composables/useLogger.ts +123 -123
  90. package/src/shared/composables/useSafeArea.ts +46 -46
  91. package/src/shared/composables/useTabbar.ts +24 -24
  92. package/src/shared/composables/useUpload.ts +54 -54
  93. package/src/shared/composables/useValidator.ts +32 -32
  94. package/src/shared/http/Http.ts +136 -136
  95. package/src/shared/http/index.ts +1 -1
  96. package/src/shared/http/types.ts +157 -157
  97. package/src/shared/index.ts +3 -3
  98. package/src/shared/weixin/payment.ts +38 -38
  99. package/src/styles/vars.scss +3 -3
  100. package/src/user/api/endpoints.ts +17 -17
  101. package/src/user/api/index.ts +111 -111
  102. package/src/user/components/LoginSetting.vue +114 -114
  103. package/src/user/components/UserAuth.vue +257 -257
  104. package/src/user/components/UserBinding.vue +307 -307
  105. package/src/user/components/UserBindingSuccess.vue +80 -80
  106. package/src/user/components/UserEntry.vue +133 -133
  107. package/src/user/components/UserFeedback.vue +431 -431
  108. package/src/user/components/UserFeedbackEntry.vue +192 -192
  109. package/src/user/components/UserHeadCrop.vue +65 -65
  110. package/src/user/components/UserInfo.vue +826 -824
  111. package/src/user/components/UserResourceEmpty.vue +75 -75
  112. package/src/user/components/index.ts +23 -23
  113. package/src/user/index.ts +1 -1
  114. package/tsconfig.json +30 -30
  115. package/types/global.d.ts +21 -21
  116. package/types/vue.d.ts +10 -10
  117. package/dist/assets/asset-3B_CoPto +0 -1
  118. package/stats.html +0 -4842
@@ -1,774 +1,774 @@
1
- <template>
2
- <view class="bt-container">
3
- <view class="iconfont icon-replay" @click.stop="resetImage"></view>
4
- <view @touchend="onTouchEnd" @touchstart="onTouchStart" class="mainContent">
5
- <image
6
- :src="imageSrc"
7
- mode="aspectFit"
8
- data-type="image"
9
- @touchmove.stop.prevent="onImageMove"
10
- :style="[imageStyle]"
11
- class="image"
12
- ></image>
13
- <view
14
- class="cropper"
15
- v-if="imageSrc && imageInfo"
16
- :style="{
17
- width: cropperPosition.width + 'px',
18
- height: cropperPosition.height + 'px',
19
- left: cropperPosition.left - 1 + 'px',
20
- top: cropperPosition.top - 1 + 'px',
21
- transition,
22
- }"
23
- >
24
- <template v-if="showGrid">
25
- <view class="line row row1"></view>
26
- <view class="line row row2"></view>
27
- <view class="line col col1"></view>
28
- <view class="line col col2"></view>
29
- </template>
30
- </view>
31
- <view
32
- @touchmove.stop.prevent="onHandleResize(-1, 0, $event)"
33
- class="controller vertical"
34
- :style="[controllerPosition.left]"
35
- />
36
- <view
37
- @touchmove.stop.prevent="onHandleResize(1, 0, $event)"
38
- class="controller vertical"
39
- :style="[controllerPosition.right]"
40
- />
41
- <view
42
- @touchmove.stop.prevent="onHandleResize(0, -1, $event)"
43
- class="controller horizon"
44
- :style="[controllerPosition.top]"
45
- />
46
- <view
47
- @touchmove.stop.prevent="onHandleResize(0, 1, $event)"
48
- class="controller horizon"
49
- :style="[controllerPosition.bottom]"
50
- />
51
- <template v-if="ratio == 0">
52
- <view
53
- @touchmove.stop.prevent="onHandleResize(-1, -1, $event)"
54
- class="controller controller_dot"
55
- :style="[controllerPosition.leftTop]"
56
- />
57
- <view
58
- @touchmove.stop.prevent="onHandleResize(1, -1, $event)"
59
- class="controller controller_dot"
60
- :style="[controllerPosition.rightTop]"
61
- />
62
- <view
63
- @touchmove.stop.prevent="onHandleResize(-1, 1, $event)"
64
- class="controller controller_dot"
65
- :style="[controllerPosition.leftBottom]"
66
- />
67
- <view
68
- @touchmove.stop.prevent="onHandleResize(1, 1, $event)"
69
- class="controller controller_dot"
70
- :style="[controllerPosition.rightBottom]"
71
- />
72
- </template>
73
- </view>
74
- <view class="slot">
75
- <slot />
76
- </view>
77
- <canvas
78
- v-if="isWeapp"
79
- type="2d"
80
- class="bt-canvas"
81
- :style="{
82
- width: dSize.width + 'px',
83
- height: dSize.height + 'px',
84
- }"
85
- ></canvas>
86
- <canvas
87
- v-if="!isWeapp && showCanvas"
88
- canvas-id="bt-canvas"
89
- class="bt-canvas"
90
- :style="{
91
- width: dSize.width + 'px',
92
- height: dSize.height + 'px',
93
- }"
94
- ></canvas>
95
- </view>
96
- </template>
97
-
98
- <script>
99
- /**
100
- * better-cropper 图片裁切插件
101
- */
102
- import Taro from '@tarojs/taro'
103
- import calcImageSize from './utils/calcImageSize.js'
104
- import calcImagePosition from './utils/calcImagePosition.js'
105
- import calcCropper from './utils/calcCropper.js'
106
- import calcRightAndBottom from './utils/calcRightAndBottom.js'
107
- import calcPointDistance from './utils/calcPointDistance.js'
108
- import { getTouchPoints, sleep } from './utils/tools.js'
109
-
110
- var startOffsetX = 0,
111
- startOffsetY = 0,
112
- startTouchsDistance = 0,
113
- startChangeLeft = 0,
114
- startChangeTop = 0,
115
- startChangeWidth = 0,
116
- startChangeHeight = 0,
117
- initScale = 1,
118
- startTouches = [],
119
- timer = null
120
- export default {
121
- name: 'bt-cropper',
122
- props: {
123
- // 图片路径,支持网络路径和本地路径
124
- imageSrc: {
125
- type: String,
126
- default: '',
127
- },
128
- // 输出图片的格式,默认jpg
129
- fileType: {
130
- type: String,
131
- default: 'jpg',
132
- },
133
- // 生成的图片的宽度
134
- dWidth: {
135
- type: Number,
136
- default: 1000,
137
- },
138
- // 裁切比例,0表示自由
139
- ratio: {
140
- type: Number,
141
- default: 0,
142
- },
143
- // 是否展示网格
144
- showGrid: {
145
- type: Boolean,
146
- default: false,
147
- },
148
- // 图片质量,0-1 越大质量越好
149
- quality: {
150
- type: Number,
151
- default: 1,
152
- },
153
- },
154
- data() {
155
- return {
156
- imageInfo: '',
157
- containerRect: '',
158
- offsetX: 0,
159
- offsetY: 0,
160
- changeWidth: 0,
161
- changeHeight: 0,
162
- windowWidth: 375,
163
- dpr: 2,
164
- forceChangeWidth: 0,
165
- forceChangeHeight: 0,
166
- animate: false,
167
- imgScale: 1,
168
- imgTranslateX: 0,
169
- imgTranslateY: 0,
170
- // 这个变量不要删掉,不然会造成性能严重下降,这是uni-app框架的Bug
171
- showCanvas: false,
172
- cropperPosition: {
173
- left: 0,
174
- top: 0,
175
- width: 0,
176
- height: 0,
177
- },
178
- imageBoundingRect: {
179
- left: 0,
180
- top: 0,
181
- width: 0,
182
- height: 0,
183
- },
184
- }
185
- },
186
- watch: {
187
- imageSrc: {
188
- handler(src) {
189
- this.imageInfo = ''
190
- this.resetImage()
191
- setTimeout(() => {
192
- this.getImageInfo()
193
- }, 150)
194
- },
195
- immediate: true,
196
- },
197
- ratio: {
198
- handler(ratio) {
199
- this.resetRatio()
200
- this.applyAnim()
201
- },
202
- immediate: false,
203
- },
204
- },
205
- computed: {
206
- isWeapp() {
207
- return Taro.getEnv() === 'WEAPP'
208
- },
209
- // 生成图片的大小
210
- dSize() {
211
- return {
212
- width: this.dWidth,
213
- height: this.dWidth * (this.cropperPosition.height / this.cropperPosition.width),
214
- }
215
- },
216
- imageStyle() {
217
- let style = {
218
- left: this.imageBoundingRect.left + 'px',
219
- top: this.imageBoundingRect.top + 'px',
220
- width: this.imageBoundingRect.width + 'px',
221
- height: this.imageBoundingRect.height + 'px',
222
- transition: this.transition,
223
- transform: `matrix(${this.imgScale}, 0, 0, ${this.imgScale}, ${this.imgTranslateX}, ${this.imgTranslateY})`,
224
- }
225
- return style
226
- },
227
- transition() {
228
- return this.animate ? '0.2s' : 'none'
229
- },
230
- // 四个控制点的位置
231
- controllerPosition() {
232
- const up40 = 20,
233
- up30 = 15,
234
- up20 = 10
235
- const transition = this.transition
236
- return {
237
- left: {
238
- left: this.cropperPosition.left - up30 + 'px',
239
- top: this.cropperPosition.top + this.cropperPosition.height / 2 - up40 + 'px',
240
- transition,
241
- },
242
- right: {
243
- left: this.cropperPosition.left + this.cropperPosition.width - up20 + 'px',
244
- top: this.cropperPosition.top + this.cropperPosition.height / 2 - up40 + 'px',
245
- transition,
246
- },
247
- top: {
248
- left: this.cropperPosition.left + this.cropperPosition.width / 2 - up40 + 'px',
249
- top: this.cropperPosition.top - up30 + 'px',
250
- transition,
251
- },
252
- bottom: {
253
- left: this.cropperPosition.left + this.cropperPosition.width / 2 - up40 + 'px',
254
- top: this.cropperPosition.top + this.cropperPosition.height - up20 + 'px',
255
- transition,
256
- },
257
- leftTop: {
258
- left: this.cropperPosition.left - up40 + 'px',
259
- top: this.cropperPosition.top - up40 + 'px',
260
- transition,
261
- },
262
- rightTop: {
263
- left: this.cropperPosition.left + this.cropperPosition.width - up40 + 'px',
264
- top: this.cropperPosition.top - up40 + 'px',
265
- transition,
266
- },
267
- leftBottom: {
268
- left: this.cropperPosition.left - up40 + 'px',
269
- top: this.cropperPosition.top + this.cropperPosition.height - up40 + 'px',
270
- transition,
271
- },
272
- rightBottom: {
273
- left: this.cropperPosition.left + this.cropperPosition.width - up40 + 'px',
274
- top: this.cropperPosition.top + this.cropperPosition.height - up40 + 'px',
275
- transition,
276
- },
277
- }
278
- },
279
- },
280
- methods: {
281
- applyAnim() {
282
- this.animate = true
283
- clearTimeout(timer)
284
- timer = setTimeout(() => {
285
- this.animate = false
286
- }, 200)
287
- },
288
- async getContainer() {
289
- const systemInfo = Taro.getSystemInfoSync()
290
- this.windowWidth = systemInfo.windowWidth
291
- this.dpr = systemInfo.pixelRatio
292
- return new Promise((resolve) => {
293
- Taro.createSelectorQuery()
294
- .select('.mainContent')
295
- .boundingClientRect((rect) => {
296
- this.containerRect = rect
297
- resolve(rect)
298
- })
299
- .exec()
300
- })
301
- },
302
- async getImageInfo() {
303
- await this.getContainer()
304
- Taro.getImageInfo({
305
- src: this.imageSrc,
306
- success: (res) => {
307
- this.imageInfo = res
308
- calcImageSize(this.imageInfo, this.containerRect).then((res) => {
309
- this.imageBoundingRect = res
310
- this.resetRatio()
311
- })
312
- },
313
- fail: (err) => {
314
- console.error(err)
315
- this.imageInfo = ''
316
- Taro.showToast({
317
- title: '下载图片失败',
318
- icon: 'none',
319
- })
320
- },
321
- })
322
- },
323
- resetImage() {
324
- this.imgTranslateX = 0
325
- this.imgTranslateY = 0
326
- this.imgScale = 1
327
- this.resetRatio()
328
- },
329
- // 重置裁剪框
330
- resetRatio() {
331
- this.$nextTick(() => {
332
- if (this.imageInfo) {
333
- this.cropperPosition = calcCropper(this.imageBoundingRect, {
334
- width: this.ratio,
335
- height: 1,
336
- })
337
- this.checkImagePosition()
338
- }
339
- })
340
- },
341
- onTouchStart(ev) {
342
- this.animate = false
343
- if (timer) {
344
- clearTimeout(timer)
345
- }
346
- startTouches = Array.from(ev.touches)
347
- if (ev.target.dataset.type === 'image') {
348
- startOffsetX = this.imgTranslateX
349
- startOffsetY = this.imgTranslateY
350
- if (ev.touches.length == 2) {
351
- initScale = this.imgScale
352
- startTouchsDistance = calcPointDistance(...getTouchPoints(startTouches))
353
- }
354
- } else {
355
- startChangeLeft = this.cropperPosition.left
356
- startChangeTop = this.cropperPosition.top
357
- startChangeWidth = this.cropperPosition.width
358
- startChangeHeight = this.cropperPosition.height
359
- }
360
- },
361
- onTouchEnd() {
362
- startTouches = []
363
- this.checkImagePosition()
364
- if (timer) {
365
- clearTimeout(timer)
366
- }
367
- timer = setTimeout(this.zoom, 1000)
368
- },
369
- getImagePosition() {
370
- return calcImagePosition(this.imageBoundingRect, {
371
- imgTranslateX: this.imgTranslateX,
372
- imgTranslateY: this.imgTranslateY,
373
- imgScale: this.imgScale,
374
- })
375
- },
376
- getCropperPosition() {
377
- return calcRightAndBottom(this.cropperPosition)
378
- },
379
- // 检查图片位置
380
- checkImagePosition() {
381
- const imagePosition = this.getImagePosition()
382
- const cropperPosition = this.getCropperPosition()
383
- // 如果裁剪框大于图像大小,就放大到裁剪框大小
384
- const widthScale = cropperPosition.width / imagePosition.width
385
- const heightScale = cropperPosition.height / imagePosition.height
386
- const scale = Math.max(widthScale, heightScale)
387
- if (scale > 1) {
388
- this.imageZoom(
389
- {
390
- left: cropperPosition.left + cropperPosition.width / 2,
391
- top: cropperPosition.top + cropperPosition.height / 2,
392
- },
393
- scale
394
- )
395
- this.applyAnim()
396
- }
397
- // 判断是否超出边界
398
- if (imagePosition.left > cropperPosition.left) {
399
- this.imgTranslateX = this.imgTranslateX - (imagePosition.left - cropperPosition.left)
400
- } else if (imagePosition.right < cropperPosition.right) {
401
- this.imgTranslateX = this.imgTranslateX + (cropperPosition.right - imagePosition.right)
402
- }
403
- if (imagePosition.top > cropperPosition.top) {
404
- this.imgTranslateY = this.imgTranslateY - (imagePosition.top - cropperPosition.top)
405
- } else if (imagePosition.bottom < cropperPosition.bottom) {
406
- this.imgTranslateY = this.imgTranslateY + (cropperPosition.bottom - imagePosition.bottom)
407
- }
408
- },
409
- zoom() {
410
- // 容器比例
411
- const containerRatio = this.containerRect.width / this.containerRect.height
412
- // 移动后的裁剪框比例
413
- const cropperRatio = this.cropperPosition.width / this.cropperPosition.height
414
- // 放大比例
415
- let scale = 1
416
- if (cropperRatio > containerRatio) {
417
- scale = this.containerRect.width / this.cropperPosition.width
418
- } else {
419
- scale = this.containerRect.height / this.cropperPosition.height
420
- }
421
- // 放大裁剪框
422
- this.cropperPosition.width *= scale
423
- this.cropperPosition.height *= scale
424
- // // 移动图像
425
- this.imageZoom(
426
- {
427
- left: this.cropperPosition.left,
428
- top: this.cropperPosition.top,
429
- },
430
- scale
431
- )
432
- // 将裁剪框上下居中
433
- const cropperTop = (this.containerRect.height - this.cropperPosition.height) / 2
434
- // 需要上下移动的距离
435
- const moveTop = cropperTop - this.cropperPosition.top
436
- // 将裁剪框左右居中
437
- const cropperLeft = (this.containerRect.width - this.cropperPosition.width) / 2
438
- // 需要左右移动的距离
439
- const moveLeft = cropperLeft - this.cropperPosition.left
440
- this.cropperPosition.left = cropperLeft
441
- this.cropperPosition.top = cropperTop
442
-
443
- // 移动图像使之与裁剪框对齐
444
- this.imgTranslateX += moveLeft
445
- this.imgTranslateY += moveTop
446
- this.checkImagePosition()
447
- this.applyAnim()
448
- },
449
- imageZoom(
450
- center = {
451
- left: 0,
452
- top: 0,
453
- },
454
- scale = 1
455
- ) {
456
- const imagePosition = this.getImagePosition()
457
- this.imgScale = this.imgScale * scale
458
- const offsetLeftPercent = (center.left - imagePosition.left) / imagePosition.width
459
- this.imgTranslateX =
460
- this.imgTranslateX + ((imagePosition.width * (scale - 1)) / 2) * (1 - offsetLeftPercent * 2)
461
- const offsetTopPercent = (center.top - imagePosition.top) / imagePosition.height
462
- this.imgTranslateY =
463
- this.imgTranslateY + ((imagePosition.height * (scale - 1)) / 2) * (1 - offsetTopPercent * 2)
464
- },
465
- onImageMove(ev) {
466
- if (ev.touches.length == 2 && startTouches.length == 2) {
467
- const points = getTouchPoints(ev.touches)
468
- const imgScale = (initScale * calcPointDistance(...points)) / startTouchsDistance
469
- this.imageZoom(
470
- {
471
- left: this.cropperPosition.left + this.cropperPosition.width / 2,
472
- top: this.cropperPosition.top + this.cropperPosition.height / 2,
473
- },
474
- imgScale / this.imgScale
475
- )
476
- } else if (ev.touches.length == 1 && startTouches.length == 1) {
477
- const [startClientX, startClientY] = getTouchPoints(startTouches)[0]
478
- const [clientX, clientY] = getTouchPoints(ev.touches)[0]
479
- this.imgTranslateX = startOffsetX + clientX - startClientX
480
- this.imgTranslateY = startOffsetY + clientY - startClientY
481
- }
482
- },
483
- // 调整裁剪框大小
484
- onHandleResize(pX, pY, ev) {
485
- const [startClientX, startClientY] = getTouchPoints(startTouches)[0]
486
- const [clientX, clientY] = getTouchPoints(ev.touches)[0]
487
- const cropperBoundingRect = this.getCropperPosition()
488
- const imageBoundingRect = this.getImagePosition()
489
- const changeX = clientX - startClientX
490
- const changeY = clientY - startClientY
491
- const minSize = {
492
- width: 50,
493
- height: 50,
494
- }
495
- const imageRemainHeight = imageBoundingRect.bottom - cropperBoundingRect.top
496
- const cropperRemainHeight = this.containerRect.bottom - cropperBoundingRect.top
497
- const maxHeight = Math.min(imageRemainHeight, cropperRemainHeight)
498
- const imageRemainWidth = imageBoundingRect.right - cropperBoundingRect.left
499
- const cropperRemainWidth = this.containerRect.right - cropperBoundingRect.left
500
- const maxWidth = Math.min(imageRemainWidth, cropperRemainWidth)
501
- let width = 0
502
- switch (pX) {
503
- case 1:
504
- width = startChangeWidth + changeX
505
- if (width < maxWidth) {
506
- if (width > minSize.width) {
507
- this.cropperPosition.width = width
508
- }
509
- }
510
- break
511
- case -1:
512
- const left = startChangeLeft + changeX
513
- const minLeft = Math.min(imageBoundingRect.left, cropperBoundingRect.left)
514
- width = startChangeWidth - changeX
515
- if (left > minLeft) {
516
- if (width > minSize.width) {
517
- this.cropperPosition.left = left
518
- this.cropperPosition.width = width
519
- }
520
- }
521
- break
522
- case 0:
523
- if (this.ratio != 0) this.cropperPosition.width = this.cropperPosition.height * this.ratio
524
- break
525
- }
526
- switch (pY) {
527
- case 1:
528
- const height = startChangeHeight + changeY
529
- if (height < maxHeight && height > minSize.height) {
530
- this.cropperPosition.height = height
531
- }
532
- break
533
- case -1:
534
- const top = startChangeTop + changeY
535
- const minTop = Math.min(imageBoundingRect.top, cropperBoundingRect.top)
536
- if (top > minTop) {
537
- const height = startChangeHeight - changeY
538
- if (height > minSize.height) {
539
- this.cropperPosition.top = top
540
- this.cropperPosition.height = height
541
- }
542
- }
543
- break
544
- case 0:
545
- if (this.ratio != 0) this.cropperPosition.height = this.cropperPosition.width / this.ratio
546
- break
547
- }
548
- },
549
- // 开始裁剪
550
- async crop() {
551
- this.showCanvas = true
552
- return new Promise((resolve) => {
553
- this.$nextTick(() => {
554
- this.onCrop().then((res) => {
555
- resolve(res)
556
- })
557
- })
558
- })
559
- },
560
- // 开始裁切
561
- async onCrop() {
562
- let canvas, image, ctx, err, res
563
- if (Taro.getEnv() === 'WEAPP') {
564
- canvas = await new Promise((resolve) => {
565
- Taro.createSelectorQuery()
566
- .select('.bt-canvas')
567
- .node((res) => {
568
- resolve(res.node)
569
- })
570
- .exec()
571
- })
572
- console.warn('在小程序模拟器上可能会裁剪失败,真机无此问题,放心使用')
573
- image = canvas.createImage()
574
- image.src = this.imageInfo.path
575
- await new Promise((resolve) => (image.onload = resolve))
576
- canvas.width = this.dSize.width
577
- canvas.height = this.dSize.height
578
- ctx = canvas.getContext('2d')
579
- } else {
580
- image = this.imageInfo.path
581
- ctx = Taro.createCanvasContext('bt-canvas', this)
582
- }
583
-
584
- const imagePosition = this.getImagePosition()
585
- const cropperPosition = this.getCropperPosition()
586
- const scale = imagePosition.width / this.imageInfo.width
587
- const offsetLeft = (cropperPosition.left - imagePosition.left) / scale
588
- const offsetTop = (cropperPosition.top - imagePosition.top) / scale
589
- const cropperWidth = cropperPosition.width / scale
590
- const cropperHeight = cropperPosition.height / scale
591
- ctx.drawImage(
592
- image,
593
- offsetLeft,
594
- offsetTop,
595
- cropperWidth,
596
- cropperHeight,
597
- 0,
598
- 0,
599
- this.dSize.width,
600
- this.dSize.height
601
- )
602
- ctx.fillStyle = '#000'
603
-
604
- // if (Taro.getEnv() === 'WEAPP') {
605
- // await new Promise((resolve) => ctx.draw(true, resolve))
606
- // }
607
- // 等待一段时间,不然ios会裁剪失败
608
- await sleep(200)
609
- // 在vue3里面,只能写成这种回调形式,否则报错
610
- ;[err, res] = await new Promise((resolve) => {
611
- Taro.canvasToTempFilePath({
612
- // #ifdef MP-WEIXIN
613
- canvas,
614
- // #endif
615
- // #ifndef MP-WEIXIN
616
- canvasId: 'bt-canvas',
617
- // #endif
618
- fileType: this.fileType,
619
- destWidth: this.dSize.width,
620
- destHeight: this.dSize.height,
621
- quality: this.quality,
622
- success(res) {
623
- console.log('裁剪成功')
624
- resolve([null, res])
625
- },
626
- fail(err) {
627
- console.log('裁剪失败', err)
628
- resolve([err, null])
629
- },
630
- complete: () => {
631
- this.showCanvas = false
632
- },
633
- })
634
- })
635
- return [err, res]
636
- },
637
- },
638
- }
639
- </script>
640
-
641
- <style lang="scss">
642
- .bt-container {
643
- display: flex;
644
- flex-direction: column;
645
- justify-content: space-between;
646
- height: 100%;
647
- box-sizing: border-box;
648
- background-color: #0e1319;
649
- position: relative;
650
- overflow: hidden;
651
-
652
- .iconfont {
653
- position: absolute;
654
- z-index: 999;
655
- top: 20px;
656
- font-size: 15px;
657
- padding: 5px;
658
- background-color: rgba(255, 255, 255, 0.2);
659
- border-radius: 50%;
660
- color: #ffffff;
661
-
662
- &.active {
663
- color: #007aff;
664
- }
665
- }
666
-
667
- .icon-replay {
668
- right: 20px;
669
- }
670
-
671
- .bt-canvas {
672
- position: absolute;
673
- left: 100%;
674
- top: 0;
675
- width: 300px;
676
- height: 300px;
677
- }
678
-
679
- .mainContent {
680
- flex: 1;
681
- margin: 30px;
682
- position: relative;
683
-
684
- .image {
685
- position: absolute;
686
- transform-origin: center center;
687
- }
688
-
689
- .controller {
690
- position: absolute;
691
- z-index: 99;
692
- padding: 10px;
693
-
694
- &::after {
695
- display: block;
696
- content: '';
697
- box-shadow: 0 0 5px #333;
698
- background-color: #e4e7ed;
699
- }
700
-
701
- &.controller_dot {
702
- &::after {
703
- width: 20px;
704
- height: 20px;
705
- border-radius: 99px;
706
- }
707
- }
708
-
709
- &.vertical {
710
- &::after {
711
- width: 5px;
712
- height: 20px;
713
- }
714
- }
715
-
716
- &.horizon {
717
- &::after {
718
- width: 20px;
719
- height: 5px;
720
- }
721
- }
722
- }
723
-
724
- .cropper {
725
- position: absolute;
726
- border: 1px solid #eee;
727
- box-sizing: content-box;
728
- transform-origin: center center;
729
- outline: 999px solid rgba(0, 0, 0, 0.5);
730
- will-change: transform;
731
- display: contain;
732
- pointer-events: none;
733
-
734
- .line {
735
- position: absolute;
736
- }
737
-
738
- .row {
739
- width: 100%;
740
- height: 0px;
741
- left: 0;
742
- border-top: 1px dashed #007aff;
743
- }
744
-
745
- .col {
746
- height: 100%;
747
- width: 0px;
748
- border-left: 1px dashed #007aff;
749
- }
750
-
751
- .row1 {
752
- top: 33%;
753
- }
754
-
755
- .row2 {
756
- top: 66%;
757
- }
758
-
759
- .col1 {
760
- left: 33%;
761
- }
762
-
763
- .col2 {
764
- left: 66%;
765
- }
766
- }
767
- }
768
-
769
- .slot {
770
- position: relative;
771
- padding-top: 10px;
772
- }
773
- }
774
- </style>
1
+ <template>
2
+ <view class="bt-container">
3
+ <view class="iconfont icon-replay" @click.stop="resetImage"></view>
4
+ <view @touchend="onTouchEnd" @touchstart="onTouchStart" class="mainContent">
5
+ <image
6
+ :src="imageSrc"
7
+ mode="aspectFit"
8
+ data-type="image"
9
+ @touchmove.stop.prevent="onImageMove"
10
+ :style="[imageStyle]"
11
+ class="image"
12
+ ></image>
13
+ <view
14
+ class="cropper"
15
+ v-if="imageSrc && imageInfo"
16
+ :style="{
17
+ width: cropperPosition.width + 'px',
18
+ height: cropperPosition.height + 'px',
19
+ left: cropperPosition.left - 1 + 'px',
20
+ top: cropperPosition.top - 1 + 'px',
21
+ transition,
22
+ }"
23
+ >
24
+ <template v-if="showGrid">
25
+ <view class="line row row1"></view>
26
+ <view class="line row row2"></view>
27
+ <view class="line col col1"></view>
28
+ <view class="line col col2"></view>
29
+ </template>
30
+ </view>
31
+ <view
32
+ @touchmove.stop.prevent="onHandleResize(-1, 0, $event)"
33
+ class="controller vertical"
34
+ :style="[controllerPosition.left]"
35
+ />
36
+ <view
37
+ @touchmove.stop.prevent="onHandleResize(1, 0, $event)"
38
+ class="controller vertical"
39
+ :style="[controllerPosition.right]"
40
+ />
41
+ <view
42
+ @touchmove.stop.prevent="onHandleResize(0, -1, $event)"
43
+ class="controller horizon"
44
+ :style="[controllerPosition.top]"
45
+ />
46
+ <view
47
+ @touchmove.stop.prevent="onHandleResize(0, 1, $event)"
48
+ class="controller horizon"
49
+ :style="[controllerPosition.bottom]"
50
+ />
51
+ <template v-if="ratio == 0">
52
+ <view
53
+ @touchmove.stop.prevent="onHandleResize(-1, -1, $event)"
54
+ class="controller controller_dot"
55
+ :style="[controllerPosition.leftTop]"
56
+ />
57
+ <view
58
+ @touchmove.stop.prevent="onHandleResize(1, -1, $event)"
59
+ class="controller controller_dot"
60
+ :style="[controllerPosition.rightTop]"
61
+ />
62
+ <view
63
+ @touchmove.stop.prevent="onHandleResize(-1, 1, $event)"
64
+ class="controller controller_dot"
65
+ :style="[controllerPosition.leftBottom]"
66
+ />
67
+ <view
68
+ @touchmove.stop.prevent="onHandleResize(1, 1, $event)"
69
+ class="controller controller_dot"
70
+ :style="[controllerPosition.rightBottom]"
71
+ />
72
+ </template>
73
+ </view>
74
+ <view class="slot">
75
+ <slot />
76
+ </view>
77
+ <canvas
78
+ v-if="isWeapp"
79
+ type="2d"
80
+ class="bt-canvas"
81
+ :style="{
82
+ width: dSize.width + 'px',
83
+ height: dSize.height + 'px',
84
+ }"
85
+ ></canvas>
86
+ <canvas
87
+ v-if="!isWeapp && showCanvas"
88
+ canvas-id="bt-canvas"
89
+ class="bt-canvas"
90
+ :style="{
91
+ width: dSize.width + 'px',
92
+ height: dSize.height + 'px',
93
+ }"
94
+ ></canvas>
95
+ </view>
96
+ </template>
97
+
98
+ <script>
99
+ /**
100
+ * better-cropper 图片裁切插件
101
+ */
102
+ import Taro from '@tarojs/taro'
103
+ import calcImageSize from './utils/calcImageSize.js'
104
+ import calcImagePosition from './utils/calcImagePosition.js'
105
+ import calcCropper from './utils/calcCropper.js'
106
+ import calcRightAndBottom from './utils/calcRightAndBottom.js'
107
+ import calcPointDistance from './utils/calcPointDistance.js'
108
+ import { getTouchPoints, sleep } from './utils/tools.js'
109
+
110
+ var startOffsetX = 0,
111
+ startOffsetY = 0,
112
+ startTouchsDistance = 0,
113
+ startChangeLeft = 0,
114
+ startChangeTop = 0,
115
+ startChangeWidth = 0,
116
+ startChangeHeight = 0,
117
+ initScale = 1,
118
+ startTouches = [],
119
+ timer = null
120
+ export default {
121
+ name: 'bt-cropper',
122
+ props: {
123
+ // 图片路径,支持网络路径和本地路径
124
+ imageSrc: {
125
+ type: String,
126
+ default: '',
127
+ },
128
+ // 输出图片的格式,默认jpg
129
+ fileType: {
130
+ type: String,
131
+ default: 'jpg',
132
+ },
133
+ // 生成的图片的宽度
134
+ dWidth: {
135
+ type: Number,
136
+ default: 1000,
137
+ },
138
+ // 裁切比例,0表示自由
139
+ ratio: {
140
+ type: Number,
141
+ default: 0,
142
+ },
143
+ // 是否展示网格
144
+ showGrid: {
145
+ type: Boolean,
146
+ default: false,
147
+ },
148
+ // 图片质量,0-1 越大质量越好
149
+ quality: {
150
+ type: Number,
151
+ default: 1,
152
+ },
153
+ },
154
+ data() {
155
+ return {
156
+ imageInfo: '',
157
+ containerRect: '',
158
+ offsetX: 0,
159
+ offsetY: 0,
160
+ changeWidth: 0,
161
+ changeHeight: 0,
162
+ windowWidth: 375,
163
+ dpr: 2,
164
+ forceChangeWidth: 0,
165
+ forceChangeHeight: 0,
166
+ animate: false,
167
+ imgScale: 1,
168
+ imgTranslateX: 0,
169
+ imgTranslateY: 0,
170
+ // 这个变量不要删掉,不然会造成性能严重下降,这是uni-app框架的Bug
171
+ showCanvas: false,
172
+ cropperPosition: {
173
+ left: 0,
174
+ top: 0,
175
+ width: 0,
176
+ height: 0,
177
+ },
178
+ imageBoundingRect: {
179
+ left: 0,
180
+ top: 0,
181
+ width: 0,
182
+ height: 0,
183
+ },
184
+ }
185
+ },
186
+ watch: {
187
+ imageSrc: {
188
+ handler(src) {
189
+ this.imageInfo = ''
190
+ this.resetImage()
191
+ setTimeout(() => {
192
+ this.getImageInfo()
193
+ }, 150)
194
+ },
195
+ immediate: true,
196
+ },
197
+ ratio: {
198
+ handler(ratio) {
199
+ this.resetRatio()
200
+ this.applyAnim()
201
+ },
202
+ immediate: false,
203
+ },
204
+ },
205
+ computed: {
206
+ isWeapp() {
207
+ return Taro.getEnv() === 'WEAPP'
208
+ },
209
+ // 生成图片的大小
210
+ dSize() {
211
+ return {
212
+ width: this.dWidth,
213
+ height: this.dWidth * (this.cropperPosition.height / this.cropperPosition.width),
214
+ }
215
+ },
216
+ imageStyle() {
217
+ let style = {
218
+ left: this.imageBoundingRect.left + 'px',
219
+ top: this.imageBoundingRect.top + 'px',
220
+ width: this.imageBoundingRect.width + 'px',
221
+ height: this.imageBoundingRect.height + 'px',
222
+ transition: this.transition,
223
+ transform: `matrix(${this.imgScale}, 0, 0, ${this.imgScale}, ${this.imgTranslateX}, ${this.imgTranslateY})`,
224
+ }
225
+ return style
226
+ },
227
+ transition() {
228
+ return this.animate ? '0.2s' : 'none'
229
+ },
230
+ // 四个控制点的位置
231
+ controllerPosition() {
232
+ const up40 = 20,
233
+ up30 = 15,
234
+ up20 = 10
235
+ const transition = this.transition
236
+ return {
237
+ left: {
238
+ left: this.cropperPosition.left - up30 + 'px',
239
+ top: this.cropperPosition.top + this.cropperPosition.height / 2 - up40 + 'px',
240
+ transition,
241
+ },
242
+ right: {
243
+ left: this.cropperPosition.left + this.cropperPosition.width - up20 + 'px',
244
+ top: this.cropperPosition.top + this.cropperPosition.height / 2 - up40 + 'px',
245
+ transition,
246
+ },
247
+ top: {
248
+ left: this.cropperPosition.left + this.cropperPosition.width / 2 - up40 + 'px',
249
+ top: this.cropperPosition.top - up30 + 'px',
250
+ transition,
251
+ },
252
+ bottom: {
253
+ left: this.cropperPosition.left + this.cropperPosition.width / 2 - up40 + 'px',
254
+ top: this.cropperPosition.top + this.cropperPosition.height - up20 + 'px',
255
+ transition,
256
+ },
257
+ leftTop: {
258
+ left: this.cropperPosition.left - up40 + 'px',
259
+ top: this.cropperPosition.top - up40 + 'px',
260
+ transition,
261
+ },
262
+ rightTop: {
263
+ left: this.cropperPosition.left + this.cropperPosition.width - up40 + 'px',
264
+ top: this.cropperPosition.top - up40 + 'px',
265
+ transition,
266
+ },
267
+ leftBottom: {
268
+ left: this.cropperPosition.left - up40 + 'px',
269
+ top: this.cropperPosition.top + this.cropperPosition.height - up40 + 'px',
270
+ transition,
271
+ },
272
+ rightBottom: {
273
+ left: this.cropperPosition.left + this.cropperPosition.width - up40 + 'px',
274
+ top: this.cropperPosition.top + this.cropperPosition.height - up40 + 'px',
275
+ transition,
276
+ },
277
+ }
278
+ },
279
+ },
280
+ methods: {
281
+ applyAnim() {
282
+ this.animate = true
283
+ clearTimeout(timer)
284
+ timer = setTimeout(() => {
285
+ this.animate = false
286
+ }, 200)
287
+ },
288
+ async getContainer() {
289
+ const systemInfo = Taro.getSystemInfoSync()
290
+ this.windowWidth = systemInfo.windowWidth
291
+ this.dpr = systemInfo.pixelRatio
292
+ return new Promise((resolve) => {
293
+ Taro.createSelectorQuery()
294
+ .select('.mainContent')
295
+ .boundingClientRect((rect) => {
296
+ this.containerRect = rect
297
+ resolve(rect)
298
+ })
299
+ .exec()
300
+ })
301
+ },
302
+ async getImageInfo() {
303
+ await this.getContainer()
304
+ Taro.getImageInfo({
305
+ src: this.imageSrc,
306
+ success: (res) => {
307
+ this.imageInfo = res
308
+ calcImageSize(this.imageInfo, this.containerRect).then((res) => {
309
+ this.imageBoundingRect = res
310
+ this.resetRatio()
311
+ })
312
+ },
313
+ fail: (err) => {
314
+ console.error(err)
315
+ this.imageInfo = ''
316
+ Taro.showToast({
317
+ title: '下载图片失败',
318
+ icon: 'none',
319
+ })
320
+ },
321
+ })
322
+ },
323
+ resetImage() {
324
+ this.imgTranslateX = 0
325
+ this.imgTranslateY = 0
326
+ this.imgScale = 1
327
+ this.resetRatio()
328
+ },
329
+ // 重置裁剪框
330
+ resetRatio() {
331
+ this.$nextTick(() => {
332
+ if (this.imageInfo) {
333
+ this.cropperPosition = calcCropper(this.imageBoundingRect, {
334
+ width: this.ratio,
335
+ height: 1,
336
+ })
337
+ this.checkImagePosition()
338
+ }
339
+ })
340
+ },
341
+ onTouchStart(ev) {
342
+ this.animate = false
343
+ if (timer) {
344
+ clearTimeout(timer)
345
+ }
346
+ startTouches = Array.from(ev.touches)
347
+ if (ev.target.dataset.type === 'image') {
348
+ startOffsetX = this.imgTranslateX
349
+ startOffsetY = this.imgTranslateY
350
+ if (ev.touches.length == 2) {
351
+ initScale = this.imgScale
352
+ startTouchsDistance = calcPointDistance(...getTouchPoints(startTouches))
353
+ }
354
+ } else {
355
+ startChangeLeft = this.cropperPosition.left
356
+ startChangeTop = this.cropperPosition.top
357
+ startChangeWidth = this.cropperPosition.width
358
+ startChangeHeight = this.cropperPosition.height
359
+ }
360
+ },
361
+ onTouchEnd() {
362
+ startTouches = []
363
+ this.checkImagePosition()
364
+ if (timer) {
365
+ clearTimeout(timer)
366
+ }
367
+ timer = setTimeout(this.zoom, 1000)
368
+ },
369
+ getImagePosition() {
370
+ return calcImagePosition(this.imageBoundingRect, {
371
+ imgTranslateX: this.imgTranslateX,
372
+ imgTranslateY: this.imgTranslateY,
373
+ imgScale: this.imgScale,
374
+ })
375
+ },
376
+ getCropperPosition() {
377
+ return calcRightAndBottom(this.cropperPosition)
378
+ },
379
+ // 检查图片位置
380
+ checkImagePosition() {
381
+ const imagePosition = this.getImagePosition()
382
+ const cropperPosition = this.getCropperPosition()
383
+ // 如果裁剪框大于图像大小,就放大到裁剪框大小
384
+ const widthScale = cropperPosition.width / imagePosition.width
385
+ const heightScale = cropperPosition.height / imagePosition.height
386
+ const scale = Math.max(widthScale, heightScale)
387
+ if (scale > 1) {
388
+ this.imageZoom(
389
+ {
390
+ left: cropperPosition.left + cropperPosition.width / 2,
391
+ top: cropperPosition.top + cropperPosition.height / 2,
392
+ },
393
+ scale
394
+ )
395
+ this.applyAnim()
396
+ }
397
+ // 判断是否超出边界
398
+ if (imagePosition.left > cropperPosition.left) {
399
+ this.imgTranslateX = this.imgTranslateX - (imagePosition.left - cropperPosition.left)
400
+ } else if (imagePosition.right < cropperPosition.right) {
401
+ this.imgTranslateX = this.imgTranslateX + (cropperPosition.right - imagePosition.right)
402
+ }
403
+ if (imagePosition.top > cropperPosition.top) {
404
+ this.imgTranslateY = this.imgTranslateY - (imagePosition.top - cropperPosition.top)
405
+ } else if (imagePosition.bottom < cropperPosition.bottom) {
406
+ this.imgTranslateY = this.imgTranslateY + (cropperPosition.bottom - imagePosition.bottom)
407
+ }
408
+ },
409
+ zoom() {
410
+ // 容器比例
411
+ const containerRatio = this.containerRect.width / this.containerRect.height
412
+ // 移动后的裁剪框比例
413
+ const cropperRatio = this.cropperPosition.width / this.cropperPosition.height
414
+ // 放大比例
415
+ let scale = 1
416
+ if (cropperRatio > containerRatio) {
417
+ scale = this.containerRect.width / this.cropperPosition.width
418
+ } else {
419
+ scale = this.containerRect.height / this.cropperPosition.height
420
+ }
421
+ // 放大裁剪框
422
+ this.cropperPosition.width *= scale
423
+ this.cropperPosition.height *= scale
424
+ // // 移动图像
425
+ this.imageZoom(
426
+ {
427
+ left: this.cropperPosition.left,
428
+ top: this.cropperPosition.top,
429
+ },
430
+ scale
431
+ )
432
+ // 将裁剪框上下居中
433
+ const cropperTop = (this.containerRect.height - this.cropperPosition.height) / 2
434
+ // 需要上下移动的距离
435
+ const moveTop = cropperTop - this.cropperPosition.top
436
+ // 将裁剪框左右居中
437
+ const cropperLeft = (this.containerRect.width - this.cropperPosition.width) / 2
438
+ // 需要左右移动的距离
439
+ const moveLeft = cropperLeft - this.cropperPosition.left
440
+ this.cropperPosition.left = cropperLeft
441
+ this.cropperPosition.top = cropperTop
442
+
443
+ // 移动图像使之与裁剪框对齐
444
+ this.imgTranslateX += moveLeft
445
+ this.imgTranslateY += moveTop
446
+ this.checkImagePosition()
447
+ this.applyAnim()
448
+ },
449
+ imageZoom(
450
+ center = {
451
+ left: 0,
452
+ top: 0,
453
+ },
454
+ scale = 1
455
+ ) {
456
+ const imagePosition = this.getImagePosition()
457
+ this.imgScale = this.imgScale * scale
458
+ const offsetLeftPercent = (center.left - imagePosition.left) / imagePosition.width
459
+ this.imgTranslateX =
460
+ this.imgTranslateX + ((imagePosition.width * (scale - 1)) / 2) * (1 - offsetLeftPercent * 2)
461
+ const offsetTopPercent = (center.top - imagePosition.top) / imagePosition.height
462
+ this.imgTranslateY =
463
+ this.imgTranslateY + ((imagePosition.height * (scale - 1)) / 2) * (1 - offsetTopPercent * 2)
464
+ },
465
+ onImageMove(ev) {
466
+ if (ev.touches.length == 2 && startTouches.length == 2) {
467
+ const points = getTouchPoints(ev.touches)
468
+ const imgScale = (initScale * calcPointDistance(...points)) / startTouchsDistance
469
+ this.imageZoom(
470
+ {
471
+ left: this.cropperPosition.left + this.cropperPosition.width / 2,
472
+ top: this.cropperPosition.top + this.cropperPosition.height / 2,
473
+ },
474
+ imgScale / this.imgScale
475
+ )
476
+ } else if (ev.touches.length == 1 && startTouches.length == 1) {
477
+ const [startClientX, startClientY] = getTouchPoints(startTouches)[0]
478
+ const [clientX, clientY] = getTouchPoints(ev.touches)[0]
479
+ this.imgTranslateX = startOffsetX + clientX - startClientX
480
+ this.imgTranslateY = startOffsetY + clientY - startClientY
481
+ }
482
+ },
483
+ // 调整裁剪框大小
484
+ onHandleResize(pX, pY, ev) {
485
+ const [startClientX, startClientY] = getTouchPoints(startTouches)[0]
486
+ const [clientX, clientY] = getTouchPoints(ev.touches)[0]
487
+ const cropperBoundingRect = this.getCropperPosition()
488
+ const imageBoundingRect = this.getImagePosition()
489
+ const changeX = clientX - startClientX
490
+ const changeY = clientY - startClientY
491
+ const minSize = {
492
+ width: 50,
493
+ height: 50,
494
+ }
495
+ const imageRemainHeight = imageBoundingRect.bottom - cropperBoundingRect.top
496
+ const cropperRemainHeight = this.containerRect.bottom - cropperBoundingRect.top
497
+ const maxHeight = Math.min(imageRemainHeight, cropperRemainHeight)
498
+ const imageRemainWidth = imageBoundingRect.right - cropperBoundingRect.left
499
+ const cropperRemainWidth = this.containerRect.right - cropperBoundingRect.left
500
+ const maxWidth = Math.min(imageRemainWidth, cropperRemainWidth)
501
+ let width = 0
502
+ switch (pX) {
503
+ case 1:
504
+ width = startChangeWidth + changeX
505
+ if (width < maxWidth) {
506
+ if (width > minSize.width) {
507
+ this.cropperPosition.width = width
508
+ }
509
+ }
510
+ break
511
+ case -1:
512
+ const left = startChangeLeft + changeX
513
+ const minLeft = Math.min(imageBoundingRect.left, cropperBoundingRect.left)
514
+ width = startChangeWidth - changeX
515
+ if (left > minLeft) {
516
+ if (width > minSize.width) {
517
+ this.cropperPosition.left = left
518
+ this.cropperPosition.width = width
519
+ }
520
+ }
521
+ break
522
+ case 0:
523
+ if (this.ratio != 0) this.cropperPosition.width = this.cropperPosition.height * this.ratio
524
+ break
525
+ }
526
+ switch (pY) {
527
+ case 1:
528
+ const height = startChangeHeight + changeY
529
+ if (height < maxHeight && height > minSize.height) {
530
+ this.cropperPosition.height = height
531
+ }
532
+ break
533
+ case -1:
534
+ const top = startChangeTop + changeY
535
+ const minTop = Math.min(imageBoundingRect.top, cropperBoundingRect.top)
536
+ if (top > minTop) {
537
+ const height = startChangeHeight - changeY
538
+ if (height > minSize.height) {
539
+ this.cropperPosition.top = top
540
+ this.cropperPosition.height = height
541
+ }
542
+ }
543
+ break
544
+ case 0:
545
+ if (this.ratio != 0) this.cropperPosition.height = this.cropperPosition.width / this.ratio
546
+ break
547
+ }
548
+ },
549
+ // 开始裁剪
550
+ async crop() {
551
+ this.showCanvas = true
552
+ return new Promise((resolve) => {
553
+ this.$nextTick(() => {
554
+ this.onCrop().then((res) => {
555
+ resolve(res)
556
+ })
557
+ })
558
+ })
559
+ },
560
+ // 开始裁切
561
+ async onCrop() {
562
+ let canvas, image, ctx, err, res
563
+ if (Taro.getEnv() === 'WEAPP') {
564
+ canvas = await new Promise((resolve) => {
565
+ Taro.createSelectorQuery()
566
+ .select('.bt-canvas')
567
+ .node((res) => {
568
+ resolve(res.node)
569
+ })
570
+ .exec()
571
+ })
572
+ console.warn('在小程序模拟器上可能会裁剪失败,真机无此问题,放心使用')
573
+ image = canvas.createImage()
574
+ image.src = this.imageInfo.path
575
+ await new Promise((resolve) => (image.onload = resolve))
576
+ canvas.width = this.dSize.width
577
+ canvas.height = this.dSize.height
578
+ ctx = canvas.getContext('2d')
579
+ } else {
580
+ image = this.imageInfo.path
581
+ ctx = Taro.createCanvasContext('bt-canvas', this)
582
+ }
583
+
584
+ const imagePosition = this.getImagePosition()
585
+ const cropperPosition = this.getCropperPosition()
586
+ const scale = imagePosition.width / this.imageInfo.width
587
+ const offsetLeft = (cropperPosition.left - imagePosition.left) / scale
588
+ const offsetTop = (cropperPosition.top - imagePosition.top) / scale
589
+ const cropperWidth = cropperPosition.width / scale
590
+ const cropperHeight = cropperPosition.height / scale
591
+ ctx.drawImage(
592
+ image,
593
+ offsetLeft,
594
+ offsetTop,
595
+ cropperWidth,
596
+ cropperHeight,
597
+ 0,
598
+ 0,
599
+ this.dSize.width,
600
+ this.dSize.height
601
+ )
602
+ ctx.fillStyle = '#000'
603
+
604
+ // if (Taro.getEnv() === 'WEAPP') {
605
+ // await new Promise((resolve) => ctx.draw(true, resolve))
606
+ // }
607
+ // 等待一段时间,不然ios会裁剪失败
608
+ await sleep(200)
609
+ // 在vue3里面,只能写成这种回调形式,否则报错
610
+ ;[err, res] = await new Promise((resolve) => {
611
+ Taro.canvasToTempFilePath({
612
+ // #ifdef MP-WEIXIN
613
+ canvas,
614
+ // #endif
615
+ // #ifndef MP-WEIXIN
616
+ canvasId: 'bt-canvas',
617
+ // #endif
618
+ fileType: this.fileType,
619
+ destWidth: this.dSize.width,
620
+ destHeight: this.dSize.height,
621
+ quality: this.quality,
622
+ success(res) {
623
+ console.log('裁剪成功')
624
+ resolve([null, res])
625
+ },
626
+ fail(err) {
627
+ console.log('裁剪失败', err)
628
+ resolve([err, null])
629
+ },
630
+ complete: () => {
631
+ this.showCanvas = false
632
+ },
633
+ })
634
+ })
635
+ return [err, res]
636
+ },
637
+ },
638
+ }
639
+ </script>
640
+
641
+ <style lang="scss">
642
+ .bt-container {
643
+ display: flex;
644
+ flex-direction: column;
645
+ justify-content: space-between;
646
+ height: 100%;
647
+ box-sizing: border-box;
648
+ background-color: #0e1319;
649
+ position: relative;
650
+ overflow: hidden;
651
+
652
+ .iconfont {
653
+ position: absolute;
654
+ z-index: 999;
655
+ top: 20px;
656
+ font-size: 15px;
657
+ padding: 5px;
658
+ background-color: rgba(255, 255, 255, 0.2);
659
+ border-radius: 50%;
660
+ color: #ffffff;
661
+
662
+ &.active {
663
+ color: #007aff;
664
+ }
665
+ }
666
+
667
+ .icon-replay {
668
+ right: 20px;
669
+ }
670
+
671
+ .bt-canvas {
672
+ position: absolute;
673
+ left: 100%;
674
+ top: 0;
675
+ width: 300px;
676
+ height: 300px;
677
+ }
678
+
679
+ .mainContent {
680
+ flex: 1;
681
+ margin: 30px;
682
+ position: relative;
683
+
684
+ .image {
685
+ position: absolute;
686
+ transform-origin: center center;
687
+ }
688
+
689
+ .controller {
690
+ position: absolute;
691
+ z-index: 99;
692
+ padding: 10px;
693
+
694
+ &::after {
695
+ display: block;
696
+ content: '';
697
+ box-shadow: 0 0 5px #333;
698
+ background-color: #e4e7ed;
699
+ }
700
+
701
+ &.controller_dot {
702
+ &::after {
703
+ width: 20px;
704
+ height: 20px;
705
+ border-radius: 99px;
706
+ }
707
+ }
708
+
709
+ &.vertical {
710
+ &::after {
711
+ width: 5px;
712
+ height: 20px;
713
+ }
714
+ }
715
+
716
+ &.horizon {
717
+ &::after {
718
+ width: 20px;
719
+ height: 5px;
720
+ }
721
+ }
722
+ }
723
+
724
+ .cropper {
725
+ position: absolute;
726
+ border: 1px solid #eee;
727
+ box-sizing: content-box;
728
+ transform-origin: center center;
729
+ outline: 999px solid rgba(0, 0, 0, 0.5);
730
+ will-change: transform;
731
+ display: contain;
732
+ pointer-events: none;
733
+
734
+ .line {
735
+ position: absolute;
736
+ }
737
+
738
+ .row {
739
+ width: 100%;
740
+ height: 0px;
741
+ left: 0;
742
+ border-top: 1px dashed #007aff;
743
+ }
744
+
745
+ .col {
746
+ height: 100%;
747
+ width: 0px;
748
+ border-left: 1px dashed #007aff;
749
+ }
750
+
751
+ .row1 {
752
+ top: 33%;
753
+ }
754
+
755
+ .row2 {
756
+ top: 66%;
757
+ }
758
+
759
+ .col1 {
760
+ left: 33%;
761
+ }
762
+
763
+ .col2 {
764
+ left: 66%;
765
+ }
766
+ }
767
+ }
768
+
769
+ .slot {
770
+ position: relative;
771
+ padding-top: 10px;
772
+ }
773
+ }
774
+ </style>