@uxda/appkit 4.2.24 → 4.2.28

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