@didaoktv/didaoui-uniapp 1.0.0 → 1.2.0

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 (114) hide show
  1. package/README.md +16 -7
  2. package/components/dd-action-sheet/dd-action-sheet.vue +3 -3
  3. package/components/dd-album/album.js +19 -0
  4. package/components/dd-album/dd-album.vue +345 -0
  5. package/components/dd-album/props.js +89 -0
  6. package/components/dd-avatar/avatar.js +19 -0
  7. package/components/dd-avatar/dd-avatar.vue +172 -160
  8. package/components/dd-avatar/props.js +84 -0
  9. package/components/dd-avatar-group/avatarGroup.js +14 -0
  10. package/components/dd-avatar-group/dd-avatar-group.vue +110 -0
  11. package/components/dd-avatar-group/props.js +57 -0
  12. package/components/dd-badge/dd-badge.vue +2 -2
  13. package/components/dd-barcode/dd-barcode.vue +1030 -0
  14. package/components/dd-bill-detail/dd-bill-detail.vue +265 -0
  15. package/components/dd-button/dd-button.vue +17 -17
  16. package/components/dd-calendar/dd-calendar.vue +357 -0
  17. package/components/dd-canvas/dd-canvas.vue +1088 -0
  18. package/components/dd-capsule-button/dd-capsule-button.vue +6 -6
  19. package/components/dd-car-keyboard/carKeyboard.js +6 -0
  20. package/components/dd-car-keyboard/dd-car-keyboard.vue +315 -0
  21. package/components/dd-car-keyboard/props.js +19 -0
  22. package/components/dd-cascader/dd-cascader.vue +405 -0
  23. package/components/dd-cell/dd-cell.vue +50 -23
  24. package/components/dd-cell-group/dd-cell-group.vue +32 -25
  25. package/components/dd-champion-card/dd-champion-card.vue +4 -4
  26. package/components/dd-checkbox/dd-checkbox.vue +30 -39
  27. package/components/dd-checkbox-group/dd-checkbox-group.vue +67 -0
  28. package/components/dd-copy/dd-copy.vue +74 -0
  29. package/components/dd-coupon/dd-coupon.vue +368 -0
  30. package/components/dd-cropper/dd-cropper.vue +1297 -0
  31. package/components/dd-dialog/dd-dialog.vue +1 -1
  32. package/components/dd-divider/dd-divider.vue +3 -3
  33. package/components/dd-dropdown-item/dd-dropdown-item.vue +5 -5
  34. package/components/dd-empty-state/dd-empty-state.vue +1 -1
  35. package/components/dd-field/dd-field.vue +531 -106
  36. package/components/dd-form/dd-form.vue +116 -0
  37. package/components/dd-form/rules.ts +84 -0
  38. package/components/dd-goods-sku/dd-goods-sku.vue +434 -0
  39. package/components/dd-icon/dd-icon.vue +2 -2
  40. package/components/dd-image/dd-image.vue +4 -4
  41. package/components/dd-input/dd-input.vue +5 -5
  42. package/components/dd-keyboard/dd-keyboard.vue +178 -0
  43. package/components/dd-keyboard/keyboard.js +22 -0
  44. package/components/dd-keyboard/props.js +89 -0
  45. package/components/dd-link/dd-link.vue +87 -0
  46. package/components/dd-link/link.js +18 -0
  47. package/components/dd-link/props.js +44 -0
  48. package/components/dd-list-cell/dd-list-cell.vue +4 -4
  49. package/components/dd-loading/dd-loading.vue +1 -1
  50. package/components/dd-loadmore/dd-loadmore.vue +2 -2
  51. package/components/dd-markdown/dd-markdown.vue +344 -0
  52. package/components/dd-markdown/marked.esm.mjs +7 -0
  53. package/components/dd-mini-program-navbar/dd-mini-program-navbar.vue +9 -9
  54. package/components/dd-navigation/dd-navigation.vue +5 -5
  55. package/components/dd-number-keyboard/dd-number-keyboard.vue +200 -0
  56. package/components/dd-number-keyboard/numberKeyboard.js +8 -0
  57. package/components/dd-number-keyboard/props.js +24 -0
  58. package/components/dd-parse/dd-parse.vue +534 -0
  59. package/components/dd-parse/node/handler.wxs +26 -0
  60. package/components/dd-parse/node/node.vue +603 -0
  61. package/components/dd-parse/parse.js +13 -0
  62. package/components/dd-parse/parser.js +1400 -0
  63. package/components/dd-parse/props.js +57 -0
  64. package/components/dd-popover/dd-popover.vue +10 -10
  65. package/components/dd-popover-item/dd-popover-item.vue +3 -3
  66. package/components/dd-popup/dd-popup.vue +5 -5
  67. package/components/dd-poster/dd-poster.vue +695 -0
  68. package/components/dd-progress/dd-progress.vue +3 -3
  69. package/components/dd-pull-refresh/dd-pull-refresh.vue +3 -3
  70. package/components/dd-qrcode/dd-qrcode.vue +509 -0
  71. package/components/dd-qrcode/qrcode.js +1283 -0
  72. package/components/dd-radio/dd-radio.vue +19 -20
  73. package/components/dd-radio-group/dd-radio-group.vue +53 -0
  74. package/components/dd-rate/dd-rate.vue +1 -1
  75. package/components/dd-room-card/dd-room-card.vue +3 -3
  76. package/components/dd-search-bar/dd-search-bar.vue +8 -8
  77. package/components/dd-segmented-tab/dd-segmented-tab.vue +6 -6
  78. package/components/dd-signature/dd-signature.vue +513 -0
  79. package/components/dd-skeleton/dd-skeleton.vue +1 -1
  80. package/components/dd-slider/dd-slider.vue +4 -4
  81. package/components/dd-stat-card/dd-stat-card.vue +5 -5
  82. package/components/dd-step/dd-step.vue +11 -11
  83. package/components/dd-stepper/dd-stepper.vue +6 -6
  84. package/components/dd-swipe-action/dd-swipe-action.vue +1 -1
  85. package/components/dd-swipeable-tab/dd-swipeable-tab.vue +1 -1
  86. package/components/dd-tabbar-item/dd-tabbar-item.vue +8 -8
  87. package/components/dd-tag/dd-tag.vue +17 -1
  88. package/components/dd-text/dd-text.vue +250 -0
  89. package/components/dd-text/props.js +120 -0
  90. package/components/dd-text/text.js +30 -0
  91. package/components/dd-text/value.js +87 -0
  92. package/components/dd-toast/dd-toast.vue +2 -2
  93. package/components/dd-upload/dd-upload.vue +4 -4
  94. package/components/dd-workorder-card/dd-workorder-card.vue +237 -0
  95. package/index.ts +50 -9
  96. package/libs/config/color.js +16 -0
  97. package/libs/config/config.js +25 -0
  98. package/libs/config/props.js +185 -0
  99. package/libs/config/zIndex.js +20 -0
  100. package/libs/function/digit.js +167 -0
  101. package/libs/function/index.js +932 -0
  102. package/libs/function/test.js +327 -0
  103. package/libs/i18n/index.d.ts +2 -0
  104. package/libs/i18n/index.js +89 -0
  105. package/libs/i18n/locales/zh-Hans.js +86 -0
  106. package/libs/mixin/button.js +18 -0
  107. package/libs/mixin/mixin.js +44 -0
  108. package/libs/mixin/mpMixin.js +13 -0
  109. package/libs/mixin/openType.js +27 -0
  110. package/libs/utils.ts +462 -0
  111. package/libs/vue.js +3 -0
  112. package/package.json +1 -1
  113. package/scss/_dd-shared.scss +40 -0
  114. package/scss/_variables.scss +4 -1
@@ -0,0 +1,1088 @@
1
+ <template>
2
+ <view
3
+ class="dd-canvas"
4
+ :id="rootId"
5
+ :style="{
6
+ width: useRootHeightAndWidth ? '100%' : actualWidth + unit,
7
+ height: useRootHeightAndWidth ? '100%' : actualHeight + unit
8
+ }"
9
+ >
10
+ <!-- #ifdef MP || H5 -->
11
+ <canvas
12
+ class="dd-canvas__canvas"
13
+ :id="canvasId"
14
+ :canvas-id="canvasId"
15
+ type="2d"
16
+ :disable-scroll="disableScroll"
17
+ :style="{ width: actualWidth + unit, height: actualHeight + unit }"
18
+ @touchstart="onTouchStart"
19
+ @touchmove="onTouchMove"
20
+ @touchend="onTouchEnd"
21
+ />
22
+ <!-- #endif -->
23
+
24
+ <!-- #ifdef APP-PLUS || APP-HARMONY -->
25
+ <!-- #ifndef APP-NVUE -->
26
+ <canvas
27
+ class="dd-canvas__canvas"
28
+ :id="canvasId"
29
+ :canvas-id="canvasId"
30
+ :disable-scroll="disableScroll"
31
+ :style="{ width: actualWidth + unit, height: actualHeight + unit }"
32
+ @touchstart="onTouchStart"
33
+ @touchmove="onTouchMove"
34
+ @touchend="onTouchEnd"
35
+ />
36
+ <!-- #endif -->
37
+ <!-- #endif -->
38
+
39
+ <!-- #ifdef APP-NVUE -->
40
+ <web-view
41
+ class="dd-canvas__canvas"
42
+ ref="web"
43
+ src="/static/app-plus/up-canvas/local.html"
44
+ :style="{ width: actualWidth + unit, height: actualHeight + unit }"
45
+ @onPostMessage="onWebViewMessage"
46
+ />
47
+ <!-- #endif -->
48
+ </view>
49
+ </template>
50
+
51
+ <script>
52
+ // #ifdef APP-NVUE
53
+ import {
54
+ createWebViewCanvasBridge,
55
+ createWebViewCanvasContext
56
+ } from '../../libs/util/app-nvue-webview-canvas.js';
57
+ // #endif
58
+
59
+ export default {
60
+ name: 'dd-canvas',
61
+ emits: ['ready', 'touchstart', 'touchmove', 'touchend'],
62
+ props: {
63
+ canvasId: {
64
+ type: String,
65
+ default: () => `dd-canvas${Math.floor(Math.random() * 1000000)}`
66
+ },
67
+ width: {
68
+ type: [String, Number],
69
+ default: 300
70
+ },
71
+ height: {
72
+ type: [String, Number],
73
+ default: 300
74
+ },
75
+ unit: {
76
+ type: String,
77
+ default: 'px'
78
+ },
79
+ useRootHeightAndWidth: {
80
+ type: Boolean,
81
+ default: false
82
+ },
83
+ bgColor: {
84
+ type: String,
85
+ default: '#ffffff'
86
+ },
87
+ disableScroll: {
88
+ type: Boolean,
89
+ default: false
90
+ }
91
+ },
92
+ data() {
93
+ return {
94
+ rootId: `dd-canvas-root-${Math.floor(Math.random() * 1000000)}`,
95
+ ctx: null,
96
+ widthLocal: this.parseSize(this.width),
97
+ heightLocal: this.parseSize(this.height),
98
+ fontSize: 12,
99
+ fontFamily: 'sans-serif',
100
+ fontWeight: 'normal',
101
+ dpr: 1
102
+ };
103
+ },
104
+ computed: {
105
+ actualWidth() {
106
+ return this.useRootHeightAndWidth ? this.widthLocal : this.parseSize(this.width);
107
+ },
108
+ actualHeight() {
109
+ return this.useRootHeightAndWidth ? this.heightLocal : this.parseSize(this.height);
110
+ }
111
+ },
112
+ watch: {
113
+ width() {
114
+ this.refresh();
115
+ },
116
+ height() {
117
+ this.refresh();
118
+ },
119
+ bgColor() {
120
+ this.clearCanvas();
121
+ }
122
+ },
123
+ created() {
124
+ this._canvasNode = null;
125
+ this._selectorResult = null;
126
+ this._canvasElement = null;
127
+ this._imageCache = Object.create(null);
128
+ this._imageDataCache = Object.create(null);
129
+ this._isNvue = false;
130
+ this._initPromise = null;
131
+ this._webViewBridge = null;
132
+ this._webViewContext = null;
133
+ this._webViewInitSignature = null;
134
+ this._webViewSessionId = null;
135
+ },
136
+ mounted() {
137
+ this.$nextTick(() => {
138
+ this.initCanvas();
139
+ });
140
+ },
141
+ beforeUnmount() {
142
+ if (this._webViewBridge) {
143
+ this._webViewBridge.destroy();
144
+ }
145
+ this._webViewBridge = null;
146
+ this._webViewContext = null;
147
+ this.ctx = null;
148
+ },
149
+ methods: {
150
+ parseSize(value) {
151
+ if (typeof value === 'number') {
152
+ return value;
153
+ }
154
+ if (typeof value !== 'string') {
155
+ return 0;
156
+ }
157
+ if (value.endsWith('rpx') || value.endsWith('upx')) {
158
+ return uni.upx2px(parseFloat(value));
159
+ }
160
+ if (value.endsWith('px')) {
161
+ return parseFloat(value) || 0;
162
+ }
163
+ return parseFloat(value) || 0;
164
+ },
165
+ onTouchStart(event) {
166
+ this.$emit('touchstart', event);
167
+ },
168
+ onTouchMove(event) {
169
+ this.$emit('touchmove', event);
170
+ },
171
+ onTouchEnd(event) {
172
+ this.$emit('touchend', event);
173
+ },
174
+ onWebViewMessage(event) {
175
+ if (this._webViewBridge) {
176
+ this._webViewBridge.handleMessage(event);
177
+ }
178
+ },
179
+ onWebViewTouch(message) {
180
+ const eventType = message.eventType;
181
+ if (!['touchstart', 'touchmove', 'touchend'].includes(eventType)) return;
182
+ this.$emit(eventType, {
183
+ type: eventType,
184
+ detail: {
185
+ x: message.x,
186
+ y: message.y
187
+ },
188
+ touches: message.touches || [],
189
+ changedTouches: message.changedTouches || [],
190
+ canvasWidth: message.canvasWidth,
191
+ canvasHeight: message.canvasHeight
192
+ });
193
+ },
194
+ async getCanvasNode(id = this.canvasId, isCanvas = true) {
195
+ return new Promise((resolve) => {
196
+ try {
197
+ uni.createSelectorQuery()
198
+ .in(this)
199
+ .select(`#${id}`)
200
+ .fields(
201
+ {
202
+ node: isCanvas,
203
+ size: true
204
+ },
205
+ (res) => {
206
+ // #ifdef APP-PLUS || APP-HARMONY
207
+ resolve(res || {
208
+ width: this.actualWidth,
209
+ height: this.actualHeight
210
+ });
211
+ return;
212
+ // #endif
213
+
214
+ resolve(res || false);
215
+ }
216
+ )
217
+ .exec();
218
+ } catch (error) {
219
+ console.error('获取画布节点失败:', error);
220
+ resolve(false);
221
+ }
222
+ });
223
+ },
224
+ getCanvasElement() {
225
+ return this._canvasElement || null;
226
+ },
227
+ getRawContext() {
228
+ return this.ctx;
229
+ },
230
+ getCanvasContext() {
231
+ // #ifdef APP-NVUE
232
+ return this._webViewContext;
233
+ // #endif
234
+
235
+ // #ifdef APP-PLUS || APP-HARMONY
236
+ return uni.createCanvasContext(this.canvasId, this);
237
+ // #endif
238
+
239
+ // #ifdef MP || H5
240
+ return this._canvasElement && typeof this._canvasElement.getContext === 'function'
241
+ ? this._canvasElement.getContext('2d')
242
+ : null;
243
+ // #endif
244
+ },
245
+ async setNewSize() {
246
+ const rootNode = await this.getCanvasNode(this.rootId, false);
247
+ if (!rootNode) {
248
+ return;
249
+ }
250
+ if (rootNode.width) {
251
+ this.widthLocal = rootNode.width;
252
+ }
253
+ if (rootNode.height) {
254
+ this.heightLocal = rootNode.height;
255
+ }
256
+ },
257
+ async initCanvas(force = false) {
258
+ if (this._initPromise) {
259
+ return this._initPromise;
260
+ }
261
+
262
+ const initPromise = this._initializeCanvas(force);
263
+ this._initPromise = initPromise;
264
+
265
+ try {
266
+ return await initPromise;
267
+ } finally {
268
+ if (this._initPromise === initPromise) {
269
+ this._initPromise = null;
270
+ }
271
+ }
272
+ },
273
+ async _initializeCanvas(force = false) {
274
+ try {
275
+ if (this.useRootHeightAndWidth) {
276
+ await this.setNewSize();
277
+ }
278
+ // #ifdef APP-NVUE
279
+ if (typeof createWebViewCanvasBridge === 'function') {
280
+ return await this._initializeWebViewCanvas(force);
281
+ }
282
+ // #endif
283
+
284
+ if (this.ctx && !force) {
285
+ this.$emit('ready', {
286
+ width: this.actualWidth,
287
+ height: this.actualHeight
288
+ });
289
+ return true;
290
+ }
291
+
292
+ this._canvasNode = await this.getCanvasNode(this.canvasId);
293
+ if (!this._canvasNode) {
294
+ return false;
295
+ }
296
+
297
+ this._selectorResult = this._canvasNode;
298
+ this._canvasElement = this._isNvue
299
+ ? this._canvasNode
300
+ : (this._canvasNode.node || null);
301
+ const systemInfo = uni.getSystemInfoSync() || {};
302
+ const pixelRatio = Number(systemInfo.pixelRatio) || 1;
303
+ // 微信开发者工具的模拟器预览通常按高密度屏幕缩放,至少使用 2 倍 backing store。
304
+ this.dpr = systemInfo.platform === 'devtools'
305
+ ? Math.max(pixelRatio, 2)
306
+ : pixelRatio;
307
+
308
+ // #ifdef MP
309
+ if (this._canvasElement) {
310
+ this._canvasElement.width = Math.ceil(this.actualWidth * this.dpr);
311
+ this._canvasElement.height = Math.ceil(this.actualHeight * this.dpr);
312
+ }
313
+ // #endif
314
+
315
+ this.ctx = this.getCanvasContext();
316
+ if (!this.ctx) {
317
+ return false;
318
+ }
319
+
320
+ // #ifdef MP
321
+ if (typeof this.ctx.setTransform === 'function') {
322
+ this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
323
+ } else if (typeof this.ctx.scale === 'function') {
324
+ this.ctx.scale(this.dpr, this.dpr);
325
+ }
326
+ // #endif
327
+
328
+ this.applyFont();
329
+ this.clearCanvas();
330
+ this.$emit('ready', {
331
+ width: this.actualWidth,
332
+ height: this.actualHeight
333
+ });
334
+ return true;
335
+ } catch (error) {
336
+ console.error('初始化Canvas失败:', error);
337
+ return false;
338
+ }
339
+ },
340
+ async _initializeWebViewCanvas(force = false) {
341
+ this._isNvue = true;
342
+ const systemInfo = uni.getSystemInfoSync() || {};
343
+ this.dpr = Math.max(1, Number(systemInfo.pixelRatio) || 1);
344
+
345
+ if (!this._webViewBridge) {
346
+ this._webViewBridge = createWebViewCanvasBridge({
347
+ getWebView: () => this.$refs.web,
348
+ onTouch: message => this.onWebViewTouch(message),
349
+ onReady: (message) => {
350
+ if (
351
+ this._webViewSessionId &&
352
+ message.sessionId &&
353
+ this._webViewSessionId !== message.sessionId
354
+ ) {
355
+ this._webViewInitSignature = null;
356
+ }
357
+ this._webViewSessionId = message.sessionId || null;
358
+ },
359
+ onError: error => console.error('Canvas WebView错误:', error)
360
+ });
361
+ this._webViewContext = createWebViewCanvasContext({
362
+ bridge: this._webViewBridge,
363
+ resolveImage: source => this.resolveNvueImageSource(source),
364
+ measureText: (text, state) => {
365
+ const matched = String(state.font || '').match(/(\d+(?:\.\d+)?)px/);
366
+ const fontSize = matched ? Number(matched[1]) : this.fontSize;
367
+ return { width: this.estimateTextWidth(text, fontSize) };
368
+ }
369
+ });
370
+ this.ctx = this._webViewContext;
371
+ }
372
+
373
+ await this._webViewBridge.ready();
374
+ const signature = [
375
+ this.actualWidth,
376
+ this.actualHeight,
377
+ this.dpr,
378
+ this.disableScroll ? 1 : 0
379
+ ].join(':');
380
+ if (force || this._webViewInitSignature !== signature) {
381
+ await this._webViewBridge.request('init', {
382
+ width: this.actualWidth,
383
+ height: this.actualHeight,
384
+ dpr: this.dpr,
385
+ disableScroll: this.disableScroll
386
+ });
387
+ this._webViewInitSignature = signature;
388
+ this.applyFont();
389
+ await this.clearCanvas();
390
+ }
391
+
392
+ this.$emit('ready', {
393
+ width: this.actualWidth,
394
+ height: this.actualHeight
395
+ });
396
+ return true;
397
+ },
398
+ refresh() {
399
+ return this.initCanvas(true);
400
+ },
401
+ getWidth() {
402
+ return this.actualWidth;
403
+ },
404
+ getHeight() {
405
+ return this.actualHeight;
406
+ },
407
+ clearCanvas() {
408
+ if (!this.ctx) return;
409
+ this.clearRect(0, 0, this.actualWidth, this.actualHeight);
410
+ if (this.bgColor && this.bgColor !== 'transparent') {
411
+ this.beginPath();
412
+ this.rect(0, 0, this.actualWidth, this.actualHeight);
413
+ this.setFillStyle(this.bgColor);
414
+ this.fill();
415
+ }
416
+ return this.draw();
417
+ },
418
+ callContext(method, ...args) {
419
+ if (this.ctx && typeof this.ctx[method] === 'function') {
420
+ return this.ctx[method](...args);
421
+ }
422
+ return undefined;
423
+ },
424
+ rect(x, y, width, height) {
425
+ return this.callContext('rect', x, y, width, height);
426
+ },
427
+ clearRect(x, y, width, height) {
428
+ return this.callContext('clearRect', x, y, width, height);
429
+ },
430
+ fillRect(x, y, width, height) {
431
+ return this.callContext('fillRect', x, y, width, height);
432
+ },
433
+ strokeRect(x, y, width, height) {
434
+ return this.callContext('strokeRect', x, y, width, height);
435
+ },
436
+ fill() {
437
+ return this.callContext('fill');
438
+ },
439
+ stroke() {
440
+ return this.callContext('stroke');
441
+ },
442
+ beginPath() {
443
+ return this.callContext('beginPath');
444
+ },
445
+ closePath() {
446
+ return this.callContext('closePath');
447
+ },
448
+ moveTo(x, y) {
449
+ return this.callContext('moveTo', x, y);
450
+ },
451
+ lineTo(x, y) {
452
+ return this.callContext('lineTo', x, y);
453
+ },
454
+ arc(x, y, radius, startAngle, endAngle, anticlockwise = false) {
455
+ return this.callContext('arc', x, y, radius, startAngle, endAngle, anticlockwise);
456
+ },
457
+ arcTo(x1, y1, x2, y2, radius) {
458
+ return this.callContext('arcTo', x1, y1, x2, y2, radius);
459
+ },
460
+ bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) {
461
+ return this.callContext('bezierCurveTo', cp1x, cp1y, cp2x, cp2y, x, y);
462
+ },
463
+ quadraticCurveTo(cpx, cpy, x, y) {
464
+ return this.callContext('quadraticCurveTo', cpx, cpy, x, y);
465
+ },
466
+ ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise = false) {
467
+ return this.callContext(
468
+ 'ellipse',
469
+ x,
470
+ y,
471
+ radiusX,
472
+ radiusY,
473
+ rotation,
474
+ startAngle,
475
+ endAngle,
476
+ anticlockwise
477
+ );
478
+ },
479
+ clip() {
480
+ return this.callContext('clip');
481
+ },
482
+ save() {
483
+ return this.callContext('save');
484
+ },
485
+ restore() {
486
+ return this.callContext('restore');
487
+ },
488
+ translate(x, y) {
489
+ return this.callContext('translate', x, y);
490
+ },
491
+ rotate(angle) {
492
+ return this.callContext('rotate', angle);
493
+ },
494
+ scale(x, y) {
495
+ return this.callContext('scale', x, y);
496
+ },
497
+ setTransform(a, b, c, d, e, f) {
498
+ return this.callContext('setTransform', a, b, c, d, e, f);
499
+ },
500
+ transform(a, b, c, d, e, f) {
501
+ return this.callContext('transform', a, b, c, d, e, f);
502
+ },
503
+ resetTransform() {
504
+ return this.callContext('resetTransform');
505
+ },
506
+ setFillStyle(color) {
507
+ if (!this.ctx) return;
508
+ if (typeof this.ctx.setFillStyle === 'function') {
509
+ this.ctx.setFillStyle(color);
510
+ } else {
511
+ this.ctx.fillStyle = color;
512
+ }
513
+ },
514
+ setStrokeStyle(color) {
515
+ if (!this.ctx) return;
516
+ if (typeof this.ctx.setStrokeStyle === 'function') {
517
+ this.ctx.setStrokeStyle(color);
518
+ } else {
519
+ this.ctx.strokeStyle = color;
520
+ }
521
+ },
522
+ setLineWidth(width) {
523
+ if (!this.ctx) return;
524
+ if (typeof this.ctx.setLineWidth === 'function') {
525
+ this.ctx.setLineWidth(width);
526
+ } else {
527
+ this.ctx.lineWidth = width;
528
+ }
529
+ },
530
+ setLineCap(lineCap = 'round') {
531
+ if (!this.ctx) return;
532
+ if (typeof this.ctx.setLineCap === 'function') {
533
+ this.ctx.setLineCap(lineCap);
534
+ } else {
535
+ this.ctx.lineCap = lineCap;
536
+ }
537
+ },
538
+ setLineJoin(lineJoin = 'round') {
539
+ if (!this.ctx) return;
540
+ if (typeof this.ctx.setLineJoin === 'function') {
541
+ this.ctx.setLineJoin(lineJoin);
542
+ } else {
543
+ this.ctx.lineJoin = lineJoin;
544
+ }
545
+ },
546
+ setMiterLimit(miterLimit) {
547
+ if (!this.ctx) return;
548
+ if (typeof this.ctx.setMiterLimit === 'function') {
549
+ this.ctx.setMiterLimit(miterLimit);
550
+ } else {
551
+ this.ctx.miterLimit = miterLimit;
552
+ }
553
+ },
554
+ setLineDash(segments = []) {
555
+ return this.callContext('setLineDash', segments);
556
+ },
557
+ getLineDash() {
558
+ const result = this.callContext('getLineDash');
559
+ return Array.isArray(result) ? result : [];
560
+ },
561
+ setTextAlign(align = 'left') {
562
+ if (!this.ctx) return;
563
+ if (typeof this.ctx.setTextAlign === 'function') {
564
+ this.ctx.setTextAlign(align);
565
+ } else {
566
+ this.ctx.textAlign = align;
567
+ }
568
+ },
569
+ setTextBaseline(baseline = 'alphabetic') {
570
+ if (!this.ctx) return;
571
+ if (typeof this.ctx.setTextBaseline === 'function') {
572
+ this.ctx.setTextBaseline(baseline);
573
+ } else {
574
+ this.ctx.textBaseline = baseline;
575
+ }
576
+ },
577
+ setFontSize(fontSize) {
578
+ this.fontSize = Number(fontSize) || this.fontSize;
579
+ if (this.ctx && typeof this.ctx.setFontSize === 'function') {
580
+ this.ctx.setFontSize(this.fontSize);
581
+ }
582
+ this.applyFont();
583
+ },
584
+ setFont(font) {
585
+ if (!this.ctx) return;
586
+ if ('font' in this.ctx) {
587
+ this.ctx.font = font;
588
+ return;
589
+ }
590
+ const matched = String(font).match(/(\d+(?:\.\d+)?)px/);
591
+ if (matched) {
592
+ this.fontSize = Number(matched[1]);
593
+ }
594
+ this.applyFont();
595
+ },
596
+ setGlobalAlpha(alpha) {
597
+ if (!this.ctx) return;
598
+ if (typeof this.ctx.setGlobalAlpha === 'function') {
599
+ this.ctx.setGlobalAlpha(alpha);
600
+ } else {
601
+ this.ctx.globalAlpha = alpha;
602
+ }
603
+ },
604
+ setGlobalCompositeOperation(operation) {
605
+ if (!this.ctx) return;
606
+ if (typeof this.ctx.setGlobalCompositeOperation === 'function') {
607
+ this.ctx.setGlobalCompositeOperation(operation);
608
+ } else {
609
+ this.ctx.globalCompositeOperation = operation;
610
+ }
611
+ },
612
+ setShadow(offsetX = 0, offsetY = 0, blur = 0, color = 'rgba(0,0,0,0)') {
613
+ if (!this.ctx) return;
614
+ if (typeof this.ctx.setShadow === 'function') {
615
+ this.ctx.setShadow(offsetX, offsetY, blur, color);
616
+ return;
617
+ }
618
+ this.ctx.shadowOffsetX = offsetX;
619
+ this.ctx.shadowOffsetY = offsetY;
620
+ this.ctx.shadowBlur = blur;
621
+ this.ctx.shadowColor = color;
622
+ },
623
+ setLineStyle(lineColor, lineWidth) {
624
+ this.setLineCap('round');
625
+ this.setLineJoin('round');
626
+ this.setStrokeStyle(lineColor);
627
+ this.setLineWidth(lineWidth);
628
+ },
629
+ applyFont() {
630
+ if (!this.ctx) return;
631
+ const font = `${this.fontWeight === 'normal' ? '' : `${this.fontWeight} `}${this.fontSize}px ${this.fontFamily}`.trim();
632
+ if ('font' in this.ctx) {
633
+ this.ctx.font = font;
634
+ } else if (typeof this.ctx.setFont === 'function') {
635
+ this.ctx.setFont(font);
636
+ } else if (typeof this.ctx.setFontSize === 'function') {
637
+ this.ctx.setFontSize(this.fontSize);
638
+ }
639
+ },
640
+ fillText(text, x, y) {
641
+ return this.callContext('fillText', String(text), x, y);
642
+ },
643
+ strokeText(text, x, y, maxWidth) {
644
+ if (maxWidth === undefined) {
645
+ return this.callContext('strokeText', String(text), x, y);
646
+ }
647
+ return this.callContext('strokeText', String(text), x, y, maxWidth);
648
+ },
649
+ /**
650
+ * 按字形宽度估算文本宽度
651
+ * @description 当平台无法给出真实测量值时的兜底。必须区分全角/半角:
652
+ * 汉字、假名、全角标点约占一个字号,按半角的 0.6 估算会短 40%,
653
+ * 中文海报因此算出"整段都放得下"而不换行。
654
+ * @param {String} text 待测量文本
655
+ * @param {Number} [fontSize] 字号,默认取组件当前字号
656
+ * @returns {Number} 估算宽度(px)
657
+ * @author jry ijry@qq.com
658
+ */
659
+ estimateTextWidth(text, fontSize) {
660
+ const size = Number(fontSize) || Number(this.fontSize) || 12;
661
+ const FULL_WIDTH = /[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-꓏가-힣豈-﫿︰-﹯＀-⦆¢-₩]/;
662
+ return Array.from(String(text)).reduce((width, char) => {
663
+ if (FULL_WIDTH.test(char)) return width + size;
664
+ if (/\s/.test(char)) return width + size * 0.28;
665
+ return width + size * 0.56;
666
+ }, 0);
667
+ },
668
+ measureText(text) {
669
+ if (this.ctx && typeof this.ctx.measureText === 'function') {
670
+ const metrics = this.ctx.measureText(String(text));
671
+ const width = Number(metrics && metrics.width);
672
+ // 鸿蒙同步测量恒为 0(真实值只走 callback),上下文未就绪时其他
673
+ // 平台也可能返回 0。绝不能把 0 交给调用方——换行逻辑会把它当成
674
+ // "宽度足够",于是整段文本挤成一行。
675
+ if (width > 0) return metrics;
676
+ }
677
+ return {
678
+ width: this.estimateTextWidth(text)
679
+ };
680
+ },
681
+ measureTextAsync(text) {
682
+ if (this.ctx && typeof this.ctx.measureTextAsync === 'function') {
683
+ return this.ctx.measureTextAsync(String(text));
684
+ }
685
+
686
+ // #ifdef APP-HARMONY
687
+ // 鸿蒙的 CanvasContext.measureText 同步返回值恒为 0,真实宽度只通过
688
+ // callback(webview.evalJSAsync) 回传,因此必须走回调形式。
689
+ if (this.ctx && typeof this.ctx.measureText === 'function' && !this._harmonyMeasureUnavailable) {
690
+ return new Promise((resolve) => {
691
+ let settled = false;
692
+ // evalJSAsync 并非所有鸿蒙版本都提供,回调可能永远不来,
693
+ // 必须兜底避免上层 await 永久挂起。且一旦超时就标记为不可用:
694
+ // 单次换行要二分测量十几次,逐次干等会把导出拖成十几秒。
695
+ const timer = setTimeout(() => {
696
+ if (settled) return;
697
+ settled = true;
698
+ this._harmonyMeasureUnavailable = true;
699
+ resolve({ width: this.estimateTextWidth(text) });
700
+ }, 300);
701
+ const done = (metrics) => {
702
+ if (settled) return;
703
+ settled = true;
704
+ clearTimeout(timer);
705
+ const width = Number(metrics && metrics.width);
706
+ resolve({ width: width > 0 ? width : this.estimateTextWidth(text) });
707
+ };
708
+ try {
709
+ const sync = this.ctx.measureText(String(text), done);
710
+ // 若该平台其实同步返回了有效值,直接采用
711
+ const syncWidth = Number(sync && sync.width);
712
+ if (syncWidth > 0) done(sync);
713
+ } catch (error) {
714
+ done(null);
715
+ }
716
+ });
717
+ }
718
+ // #endif
719
+
720
+ return Promise.resolve(this.measureText(text));
721
+ },
722
+ createLinearGradient(x0, y0, x1, y1) {
723
+ if (this.ctx && typeof this.ctx.createLinearGradient === 'function') {
724
+ return this.ctx.createLinearGradient(x0, y0, x1, y1);
725
+ }
726
+ return null;
727
+ },
728
+ createRadialGradient(x0, y0, r0, x1, y1, r1) {
729
+ if (this.ctx && typeof this.ctx.createRadialGradient === 'function') {
730
+ return this.ctx.createRadialGradient(x0, y0, r0, x1, y1, r1);
731
+ }
732
+ return null;
733
+ },
734
+ createPattern(image, repetition = 'repeat') {
735
+ if (this.ctx && typeof this.ctx.createPattern === 'function') {
736
+ return this.ctx.createPattern(image, repetition);
737
+ }
738
+ return null;
739
+ },
740
+ async resolveNvueImageSource(src) {
741
+ if (typeof src !== 'string' || src.indexOf('data:image/') === 0) {
742
+ return src;
743
+ }
744
+ if (this._imageDataCache[src]) {
745
+ return this._imageDataCache[src];
746
+ }
747
+
748
+ const task = (async () => {
749
+ const localPath = await new Promise((resolve, reject) => {
750
+ uni.getImageInfo({
751
+ src,
752
+ success: res => resolve(res.path || res.tempFilePath),
753
+ fail: reject
754
+ });
755
+ });
756
+ if (!localPath) {
757
+ throw new Error(`Canvas图片未返回本地路径: ${src}`);
758
+ }
759
+ return this.readNvueFileAsDataURL(localPath);
760
+ })();
761
+ this._imageDataCache[src] = task;
762
+ try {
763
+ const dataUrl = await task;
764
+ this._imageDataCache[src] = dataUrl;
765
+ return dataUrl;
766
+ } catch (error) {
767
+ delete this._imageDataCache[src];
768
+ throw error;
769
+ }
770
+ },
771
+ readNvueFileAsDataURL(path) {
772
+ return new Promise((resolve, reject) => {
773
+ // #ifdef APP-NVUE
774
+ if (typeof plus === 'undefined' || !plus.io) {
775
+ reject(new Error('HTML5+ FileReader不可用'));
776
+ return;
777
+ }
778
+ plus.io.resolveLocalFileSystemURL(path, (entry) => {
779
+ entry.file((file) => {
780
+ const reader = new plus.io.FileReader();
781
+ reader.onloadend = event => resolve(event.target.result);
782
+ reader.onerror = reject;
783
+ reader.readAsDataURL(file);
784
+ }, reject);
785
+ }, reject);
786
+ return;
787
+ // #endif
788
+
789
+ resolve(path);
790
+ });
791
+ },
792
+ loadImage(src) {
793
+ // #ifdef APP-NVUE
794
+ return this.resolveNvueImageSource(src);
795
+ // #endif
796
+
797
+ if (this._imageCache[src]) {
798
+ return Promise.resolve(this._imageCache[src]);
799
+ }
800
+ return new Promise((resolve, reject) => {
801
+ let image = null;
802
+
803
+ // #ifdef MP
804
+ const canvas = this.getCanvasElement();
805
+ if (canvas && typeof canvas.createImage === 'function') {
806
+ image = canvas.createImage();
807
+ }
808
+ // #endif
809
+
810
+ // #ifdef H5
811
+ image = new Image();
812
+ image.crossOrigin = 'anonymous';
813
+ // #endif
814
+
815
+ if (!image) {
816
+ resolve(src);
817
+ return;
818
+ }
819
+
820
+ image.onload = () => {
821
+ this._imageCache[src] = image;
822
+ resolve(image);
823
+ };
824
+ image.onerror = reject;
825
+ image.src = src;
826
+ });
827
+ },
828
+ async drawImage(source, ...args) {
829
+ if (!this.ctx || typeof this.ctx.drawImage !== 'function') {
830
+ return false;
831
+ }
832
+ if (typeof source !== 'string') {
833
+ this.ctx.drawImage(source, ...args);
834
+ return true;
835
+ }
836
+ if (typeof this.ctx.setFillStyle === 'function' && !this._isNvue) {
837
+ this.ctx.drawImage(source, ...args);
838
+ return true;
839
+ }
840
+ const image = await this.loadImage(source);
841
+ this.ctx.drawImage(image, ...args);
842
+ return true;
843
+ },
844
+ draw(isLastDraw = false, callback) {
845
+ if (this.ctx && typeof this.ctx.draw === 'function') {
846
+ return this.ctx.draw(isLastDraw, callback);
847
+ }
848
+ if (typeof callback === 'function') {
849
+ setTimeout(callback, 0);
850
+ }
851
+ return undefined;
852
+ },
853
+ toTempFilePath(options = {}) {
854
+ return new Promise((resolve, reject) => {
855
+ const width = options.width || this.actualWidth;
856
+ const height = options.height || this.actualHeight;
857
+ const hasDestWidth = options.destWidth !== undefined && options.destWidth !== null;
858
+ const hasDestHeight = options.destHeight !== undefined && options.destHeight !== null;
859
+ let destWidth = hasDestWidth ? options.destWidth : width;
860
+ let destHeight = hasDestHeight ? options.destHeight : height;
861
+
862
+ // MP、H5、APP-PLUS 和 APP-HARMONY 保持逻辑绘制坐标,只提升默认导出像素。
863
+ // #ifdef MP || H5 || APP-PLUS || APP-HARMONY
864
+ if (!hasDestWidth) {
865
+ destWidth = Math.round(width * this.dpr);
866
+ }
867
+ if (!hasDestHeight) {
868
+ destHeight = Math.round(height * this.dpr);
869
+ }
870
+ // #endif
871
+
872
+ const defaultFileType = 'png';
873
+ const request = {
874
+ x: options.x || 0,
875
+ y: options.y || 0,
876
+ width,
877
+ height,
878
+ destWidth,
879
+ destHeight,
880
+ fileType: options.fileType === undefined || options.fileType === null
881
+ ? defaultFileType
882
+ : options.fileType,
883
+ quality: options.quality === undefined ? 1 : options.quality
884
+ };
885
+ const success = (res) => {
886
+ if (typeof options.success === 'function') options.success(res);
887
+ resolve(res);
888
+ };
889
+ const fail = (err) => {
890
+ if (typeof options.fail === 'function') options.fail(err);
891
+ reject(err);
892
+ };
893
+ const complete = (res) => {
894
+ if (typeof options.complete === 'function') options.complete(res);
895
+ };
896
+
897
+ // #ifdef H5
898
+ const canvas = this.getCanvasElement() || (this.ctx && this.ctx.canvas);
899
+ if (canvas && typeof canvas.toDataURL === 'function') {
900
+ try {
901
+ let exportCanvas = canvas;
902
+ if (
903
+ request.x !== 0 ||
904
+ request.y !== 0 ||
905
+ request.width !== this.actualWidth ||
906
+ request.height !== this.actualHeight ||
907
+ request.destWidth !== request.width ||
908
+ request.destHeight !== request.height
909
+ ) {
910
+ exportCanvas = document.createElement('canvas');
911
+ exportCanvas.width = request.destWidth;
912
+ exportCanvas.height = request.destHeight;
913
+ const exportCtx = exportCanvas.getContext('2d');
914
+ exportCtx.drawImage(
915
+ canvas,
916
+ request.x * this.dpr,
917
+ request.y * this.dpr,
918
+ request.width * this.dpr,
919
+ request.height * this.dpr,
920
+ 0,
921
+ 0,
922
+ request.destWidth,
923
+ request.destHeight
924
+ );
925
+ }
926
+ const mime = request.fileType === 'jpg' || request.fileType === 'jpeg'
927
+ ? 'image/jpeg'
928
+ : 'image/png';
929
+ const res = {
930
+ tempFilePath: exportCanvas.toDataURL(mime, request.quality)
931
+ };
932
+ success(res);
933
+ complete(res);
934
+ return;
935
+ } catch (error) {
936
+ fail(error);
937
+ complete(error);
938
+ return;
939
+ }
940
+ }
941
+ // #endif
942
+
943
+ // #ifdef APP-NVUE
944
+ if (this.ctx && typeof this.ctx.toTempFilePath === 'function') {
945
+ Promise.resolve(this.ctx.toTempFilePath(request)).then(
946
+ (res) => {
947
+ success(res);
948
+ complete(res);
949
+ },
950
+ (error) => {
951
+ fail(error);
952
+ complete(error);
953
+ }
954
+ );
955
+ return;
956
+ }
957
+ // #endif
958
+
959
+ const canvasNode = this.getCanvasElement();
960
+ const uniOptions = {
961
+ ...request,
962
+ canvasId: this.canvasId,
963
+ success,
964
+ fail,
965
+ complete
966
+ };
967
+ if (canvasNode) {
968
+ uniOptions.canvas = canvasNode;
969
+ }
970
+ uni.canvasToTempFilePath(uniOptions, this);
971
+ });
972
+ },
973
+ async exportImage(fileType = 'png', quality = 1) {
974
+ const res = await this.toTempFilePath({
975
+ fileType,
976
+ quality,
977
+ width: this.actualWidth,
978
+ height: this.actualHeight
979
+ });
980
+ return res.tempFilePath || res.apFilePath;
981
+ },
982
+ getImageData(options = {}) {
983
+ return new Promise((resolve, reject) => {
984
+ const success = (res) => {
985
+ if (typeof options.success === 'function') options.success(res);
986
+ resolve(res);
987
+ };
988
+ const fail = (err) => {
989
+ if (typeof options.fail === 'function') options.fail(err);
990
+ reject(err);
991
+ };
992
+ const complete = (res) => {
993
+ if (typeof options.complete === 'function') options.complete(res);
994
+ };
995
+ // #ifdef APP-NVUE
996
+ if (this.ctx && typeof this.ctx.getImageData === 'function') {
997
+ Promise.resolve(this.ctx.getImageData({
998
+ x: options.x || 0,
999
+ y: options.y || 0,
1000
+ width: options.width || this.actualWidth,
1001
+ height: options.height || this.actualHeight
1002
+ })).then((res) => {
1003
+ success(res);
1004
+ complete(res);
1005
+ }, (error) => {
1006
+ fail(error);
1007
+ complete(error);
1008
+ });
1009
+ return;
1010
+ }
1011
+ // #endif
1012
+
1013
+ const request = {
1014
+ canvasId: this.canvasId,
1015
+ x: options.x || 0,
1016
+ y: options.y || 0,
1017
+ width: options.width || this.actualWidth,
1018
+ height: options.height || this.actualHeight,
1019
+ success,
1020
+ fail,
1021
+ complete
1022
+ };
1023
+ uni.canvasGetImageData(request, this);
1024
+ });
1025
+ },
1026
+ putImageData(options = {}) {
1027
+ return new Promise((resolve, reject) => {
1028
+ const success = (res) => {
1029
+ if (typeof options.success === 'function') options.success(res);
1030
+ resolve(res);
1031
+ };
1032
+ const fail = (err) => {
1033
+ if (typeof options.fail === 'function') options.fail(err);
1034
+ reject(err);
1035
+ };
1036
+ const complete = (res) => {
1037
+ if (typeof options.complete === 'function') options.complete(res);
1038
+ };
1039
+ // #ifdef APP-NVUE
1040
+ if (this.ctx && typeof this.ctx.putImageData === 'function') {
1041
+ Promise.resolve(this.ctx.putImageData({
1042
+ x: options.x || 0,
1043
+ y: options.y || 0,
1044
+ width: options.width || (options.data && options.data.width) || this.actualWidth,
1045
+ height: options.height || (options.data && options.data.height) || this.actualHeight,
1046
+ data: options.data
1047
+ })).then((res) => {
1048
+ success(res);
1049
+ complete(res);
1050
+ }, (error) => {
1051
+ fail(error);
1052
+ complete(error);
1053
+ });
1054
+ return;
1055
+ }
1056
+ // #endif
1057
+
1058
+ const request = {
1059
+ canvasId: this.canvasId,
1060
+ x: options.x || 0,
1061
+ y: options.y || 0,
1062
+ width: options.width || (options.data && options.data.width) || this.actualWidth,
1063
+ height: options.height || (options.data && options.data.height) || this.actualHeight,
1064
+ data: options.data,
1065
+ success,
1066
+ fail,
1067
+ complete
1068
+ };
1069
+ uni.canvasPutImageData(request, this);
1070
+ });
1071
+ }
1072
+ }
1073
+ };
1074
+ </script>
1075
+
1076
+ <style lang="scss" scoped>
1077
+ .dd-canvas {
1078
+ position: relative;
1079
+ overflow: hidden;
1080
+ }
1081
+
1082
+ .dd-canvas__canvas {
1083
+ display: block;
1084
+ width: 100%;
1085
+ height: 100%;
1086
+ }
1087
+ </style>
1088
+