@uxda/appkit 4.3.6 → 4.3.8

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 (135) hide show
  1. package/.eslintrc.mjs +7 -7
  2. package/COMPONENT_USAGE.md +1523 -1523
  3. package/PROJECT_DOCS.md +142 -142
  4. package/README.md +187 -187
  5. package/babel.config.js +12 -12
  6. package/dist/appkit.css +15 -63
  7. package/dist/assets/asset-DcH8Kg-2 +1 -0
  8. package/dist/index.js +259 -806
  9. package/package.json +79 -79
  10. package/project.config.json +15 -15
  11. package/project.tt.json +13 -13
  12. package/rollup.config.mjs +78 -78
  13. package/src/Appkit.ts +72 -72
  14. package/src/balance/api/endpoints.ts +133 -133
  15. package/src/balance/api/index.ts +118 -118
  16. package/src/balance/components/AccountView.vue +770 -770
  17. package/src/balance/components/BalanceCard.vue +210 -210
  18. package/src/balance/components/BalanceReminder.vue +84 -84
  19. package/src/balance/components/ConsumptionFilter.vue +218 -218
  20. package/src/balance/components/ConsumptionRules.vue +68 -68
  21. package/src/balance/components/DateFilter.vue +259 -259
  22. package/src/balance/components/DateRange.vue +111 -111
  23. package/src/balance/components/ListFilter.vue +62 -62
  24. package/src/balance/components/ListFilterPicker.vue +191 -191
  25. package/src/balance/components/PromoterCard.vue +307 -308
  26. package/src/balance/components/SecondBalance.vue +77 -77
  27. package/src/balance/components/Tip.vue +45 -45
  28. package/src/balance/components/index.ts +8 -8
  29. package/src/balance/types.ts +99 -99
  30. package/src/components/bt-cropper/index.vue +730 -730
  31. package/src/components/bt-cropper/utils/calcCropper.js +42 -42
  32. package/src/components/bt-cropper/utils/calcImagePosition.js +23 -23
  33. package/src/components/bt-cropper/utils/calcImageSize.js +37 -37
  34. package/src/components/bt-cropper/utils/calcPointDistance.js +12 -12
  35. package/src/components/bt-cropper/utils/calcRightAndBottom.js +7 -7
  36. package/src/components/bt-cropper/utils/ratio.js +3 -3
  37. package/src/components/bt-cropper/utils/tools.js +25 -25
  38. package/src/components/dd-area/index.vue +225 -225
  39. package/src/components/dd-icon/doc.md +21 -21
  40. package/src/components/dd-icon/index.vue +23 -23
  41. package/src/components/dd-notice-bar/index.vue +78 -78
  42. package/src/components/dd-search/doc.md +34 -34
  43. package/src/components/dd-search/index.vue +168 -168
  44. package/src/components/dd-selector/index.vue +124 -124
  45. package/src/components/dd-skeleton/doc.md +19 -19
  46. package/src/components/dd-skeleton/index.vue +36 -36
  47. package/src/global.ts +6 -6
  48. package/src/index.ts +101 -101
  49. package/src/main.scss +1 -1
  50. package/src/notice/api/endpoints.ts +54 -54
  51. package/src/notice/api/index.ts +121 -121
  52. package/src/notice/components/NoticeBanner.vue +247 -247
  53. package/src/notice/components/NoticeEntry.vue +99 -99
  54. package/src/notice/components/NoticeList.vue +311 -311
  55. package/src/notice/components/NoticeList2.vue +400 -399
  56. package/src/notice/components/NoticePopup.vue +163 -163
  57. package/src/notice/components/index.ts +6 -6
  58. package/src/notice/components/useCommonList.ts +86 -87
  59. package/src/notice/components/useNotice.ts +35 -35
  60. package/src/notice/index.ts +1 -1
  61. package/src/notice/types.ts +25 -25
  62. package/src/payment/api/config.ts +7 -7
  63. package/src/payment/api/endpoints.ts +96 -98
  64. package/src/payment/api/index.ts +107 -108
  65. package/src/payment/components/AmountPicker.vue +90 -90
  66. package/src/payment/components/RechargeResult.vue +69 -68
  67. package/src/payment/components/RechargeView.vue +191 -191
  68. package/src/payment/components/RightsPicker.vue +105 -105
  69. package/src/payment/components/TradeView.vue +363 -571
  70. package/src/payment/components/UserAgreement.vue +234 -234
  71. package/src/payment/components/index.ts +22 -22
  72. package/src/payment/index.ts +5 -5
  73. package/src/payment/services/index.ts +16 -16
  74. package/src/payment/services/invoke-recharge.ts +25 -25
  75. package/src/payment/services/request-payment.ts +130 -132
  76. package/src/payment/types.ts +33 -34
  77. package/src/register/components/SelfRegistration.vue +233 -233
  78. package/src/register/components/index.ts +2 -2
  79. package/src/scenarios/components/SharePoster.vue +364 -364
  80. package/src/scenarios/components/index.ts +2 -2
  81. package/src/scenarios/components/poster-paste.vue +93 -93
  82. package/src/scenarios/components/share-poster.md +273 -273
  83. package/src/shared/components/AppDrawer.vue +53 -53
  84. package/src/shared/components/AppVerify.vue +128 -137
  85. package/src/shared/components/DeviceVersion.vue +78 -78
  86. package/src/shared/components/EmptyView.vue +33 -33
  87. package/src/shared/components/OcrBusinessLicense.vue +137 -120
  88. package/src/shared/components/OcrIcon.vue +229 -267
  89. package/src/shared/components/PageHeader.vue +84 -84
  90. package/src/shared/components/index.ts +8 -10
  91. package/src/shared/composables/index.ts +9 -10
  92. package/src/shared/composables/useAmount.ts +46 -46
  93. package/src/shared/composables/useCountdown.ts +46 -46
  94. package/src/shared/composables/useCrypto.ts +76 -76
  95. package/src/shared/composables/useDeviceEnv.ts +26 -26
  96. package/src/shared/composables/useDragBox.ts +97 -97
  97. package/src/shared/composables/useEncode.ts +43 -43
  98. package/src/shared/composables/useLogger.ts +144 -144
  99. package/src/shared/composables/useSafeArea.ts +46 -46
  100. package/src/shared/composables/useTabbar.ts +24 -24
  101. package/src/shared/composables/useUpload.ts +61 -106
  102. package/src/shared/composables/useValidator.ts +32 -32
  103. package/src/shared/composables/useWxAuth.ts +48 -48
  104. package/src/shared/http/Http.ts +148 -149
  105. package/src/shared/http/index.ts +1 -1
  106. package/src/shared/http/types.ts +163 -163
  107. package/src/shared/index.ts +9 -9
  108. package/src/shared/tracking/directives/index.ts +40 -40
  109. package/src/shared/tracking/examples/page-tracking-template.vue +27 -27
  110. package/src/shared/tracking/tracking-sdk.ts +1 -0
  111. package/src/shared/weixin/index.ts +9 -9
  112. package/src/shared/weixin/jssdk.ts +103 -104
  113. package/src/shared/weixin/payment.ts +38 -38
  114. package/src/styles/vars.scss +3 -3
  115. package/src/user/api/endpoints.ts +17 -17
  116. package/src/user/api/index.ts +123 -123
  117. package/src/user/components/LoginSetting.vue +114 -114
  118. package/src/user/components/UserAuth.vue +218 -218
  119. package/src/user/components/UserBinding.vue +277 -277
  120. package/src/user/components/UserBindingSuccess.vue +80 -80
  121. package/src/user/components/UserEntry.vue +139 -139
  122. package/src/user/components/UserFeedback.vue +427 -428
  123. package/src/user/components/UserFeedbackEntry.vue +175 -175
  124. package/src/user/components/UserHeadCrop.vue +65 -65
  125. package/src/user/components/UserInfo.vue +709 -711
  126. package/src/user/components/UserResourceEmpty.vue +75 -75
  127. package/src/user/components/index.ts +23 -23
  128. package/src/user/index.ts +1 -1
  129. package/src/utils/utils.ts +33 -33
  130. package/tsconfig.json +30 -30
  131. package/types/global.d.ts +22 -24
  132. package/types/vue.d.ts +10 -10
  133. package/src/shared/components/OcrBank.vue +0 -202
  134. package/src/shared/components/OcrInvoice.vue +0 -218
  135. package/src/shared/composables/useCompress.ts +0 -64
@@ -1,730 +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 :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, showToast } 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
- 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>
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, showToast } 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
+ 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>