@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.
- package/README.md +16 -7
- package/components/dd-action-sheet/dd-action-sheet.vue +3 -3
- package/components/dd-album/album.js +19 -0
- package/components/dd-album/dd-album.vue +345 -0
- package/components/dd-album/props.js +89 -0
- package/components/dd-avatar/avatar.js +19 -0
- package/components/dd-avatar/dd-avatar.vue +172 -160
- package/components/dd-avatar/props.js +84 -0
- package/components/dd-avatar-group/avatarGroup.js +14 -0
- package/components/dd-avatar-group/dd-avatar-group.vue +110 -0
- package/components/dd-avatar-group/props.js +57 -0
- package/components/dd-badge/dd-badge.vue +2 -2
- package/components/dd-barcode/dd-barcode.vue +1030 -0
- package/components/dd-bill-detail/dd-bill-detail.vue +265 -0
- package/components/dd-button/dd-button.vue +17 -17
- package/components/dd-calendar/dd-calendar.vue +357 -0
- package/components/dd-canvas/dd-canvas.vue +1088 -0
- package/components/dd-capsule-button/dd-capsule-button.vue +6 -6
- package/components/dd-car-keyboard/carKeyboard.js +6 -0
- package/components/dd-car-keyboard/dd-car-keyboard.vue +315 -0
- package/components/dd-car-keyboard/props.js +19 -0
- package/components/dd-cascader/dd-cascader.vue +405 -0
- package/components/dd-cell/dd-cell.vue +50 -23
- package/components/dd-cell-group/dd-cell-group.vue +32 -25
- package/components/dd-champion-card/dd-champion-card.vue +4 -4
- package/components/dd-checkbox/dd-checkbox.vue +30 -39
- package/components/dd-checkbox-group/dd-checkbox-group.vue +67 -0
- package/components/dd-copy/dd-copy.vue +74 -0
- package/components/dd-coupon/dd-coupon.vue +368 -0
- package/components/dd-cropper/dd-cropper.vue +1297 -0
- package/components/dd-dialog/dd-dialog.vue +1 -1
- package/components/dd-divider/dd-divider.vue +3 -3
- package/components/dd-dropdown-item/dd-dropdown-item.vue +5 -5
- package/components/dd-empty-state/dd-empty-state.vue +1 -1
- package/components/dd-field/dd-field.vue +531 -106
- package/components/dd-form/dd-form.vue +116 -0
- package/components/dd-form/rules.ts +84 -0
- package/components/dd-goods-sku/dd-goods-sku.vue +434 -0
- package/components/dd-icon/dd-icon.vue +2 -2
- package/components/dd-image/dd-image.vue +4 -4
- package/components/dd-input/dd-input.vue +5 -5
- package/components/dd-keyboard/dd-keyboard.vue +178 -0
- package/components/dd-keyboard/keyboard.js +22 -0
- package/components/dd-keyboard/props.js +89 -0
- package/components/dd-link/dd-link.vue +87 -0
- package/components/dd-link/link.js +18 -0
- package/components/dd-link/props.js +44 -0
- package/components/dd-list-cell/dd-list-cell.vue +4 -4
- package/components/dd-loading/dd-loading.vue +1 -1
- package/components/dd-loadmore/dd-loadmore.vue +2 -2
- package/components/dd-markdown/dd-markdown.vue +344 -0
- package/components/dd-markdown/marked.esm.mjs +7 -0
- package/components/dd-mini-program-navbar/dd-mini-program-navbar.vue +9 -9
- package/components/dd-navigation/dd-navigation.vue +5 -5
- package/components/dd-number-keyboard/dd-number-keyboard.vue +200 -0
- package/components/dd-number-keyboard/numberKeyboard.js +8 -0
- package/components/dd-number-keyboard/props.js +24 -0
- package/components/dd-parse/dd-parse.vue +534 -0
- package/components/dd-parse/node/handler.wxs +26 -0
- package/components/dd-parse/node/node.vue +603 -0
- package/components/dd-parse/parse.js +13 -0
- package/components/dd-parse/parser.js +1400 -0
- package/components/dd-parse/props.js +57 -0
- package/components/dd-popover/dd-popover.vue +10 -10
- package/components/dd-popover-item/dd-popover-item.vue +3 -3
- package/components/dd-popup/dd-popup.vue +5 -5
- package/components/dd-poster/dd-poster.vue +695 -0
- package/components/dd-progress/dd-progress.vue +3 -3
- package/components/dd-pull-refresh/dd-pull-refresh.vue +3 -3
- package/components/dd-qrcode/dd-qrcode.vue +509 -0
- package/components/dd-qrcode/qrcode.js +1283 -0
- package/components/dd-radio/dd-radio.vue +19 -20
- package/components/dd-radio-group/dd-radio-group.vue +53 -0
- package/components/dd-rate/dd-rate.vue +1 -1
- package/components/dd-room-card/dd-room-card.vue +3 -3
- package/components/dd-search-bar/dd-search-bar.vue +8 -8
- package/components/dd-segmented-tab/dd-segmented-tab.vue +6 -6
- package/components/dd-signature/dd-signature.vue +513 -0
- package/components/dd-skeleton/dd-skeleton.vue +1 -1
- package/components/dd-slider/dd-slider.vue +4 -4
- package/components/dd-stat-card/dd-stat-card.vue +5 -5
- package/components/dd-step/dd-step.vue +11 -11
- package/components/dd-stepper/dd-stepper.vue +6 -6
- package/components/dd-swipe-action/dd-swipe-action.vue +1 -1
- package/components/dd-swipeable-tab/dd-swipeable-tab.vue +1 -1
- package/components/dd-tabbar-item/dd-tabbar-item.vue +8 -8
- package/components/dd-tag/dd-tag.vue +17 -1
- package/components/dd-text/dd-text.vue +250 -0
- package/components/dd-text/props.js +120 -0
- package/components/dd-text/text.js +30 -0
- package/components/dd-text/value.js +87 -0
- package/components/dd-toast/dd-toast.vue +2 -2
- package/components/dd-upload/dd-upload.vue +4 -4
- package/components/dd-workorder-card/dd-workorder-card.vue +237 -0
- package/index.ts +50 -9
- package/libs/config/color.js +16 -0
- package/libs/config/config.js +25 -0
- package/libs/config/props.js +185 -0
- package/libs/config/zIndex.js +20 -0
- package/libs/function/digit.js +167 -0
- package/libs/function/index.js +932 -0
- package/libs/function/test.js +327 -0
- package/libs/i18n/index.d.ts +2 -0
- package/libs/i18n/index.js +89 -0
- package/libs/i18n/locales/zh-Hans.js +86 -0
- package/libs/mixin/button.js +18 -0
- package/libs/mixin/mixin.js +44 -0
- package/libs/mixin/mpMixin.js +13 -0
- package/libs/mixin/openType.js +27 -0
- package/libs/utils.ts +462 -0
- package/libs/vue.js +3 -0
- package/package.json +1 -1
- package/scss/_dd-shared.scss +40 -0
- package/scss/_variables.scss +4 -1
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<view class="dd-poster">
|
|
3
|
+
<!-- canvas用于绘制海报 -->
|
|
4
|
+
<view v-if="showCanvas" class="dd-poster__renderer">
|
|
5
|
+
<dd-canvas
|
|
6
|
+
ref="posterCanvas"
|
|
7
|
+
class="dd-poster__hidden-canvas"
|
|
8
|
+
:canvas-id="canvasId"
|
|
9
|
+
:width="canvasWidth"
|
|
10
|
+
:height="canvasHeight"
|
|
11
|
+
bg-color="transparent"
|
|
12
|
+
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }">
|
|
13
|
+
</dd-canvas>
|
|
14
|
+
</view>
|
|
15
|
+
<!-- 隐藏的二维码组件,用于生成二维码图片 -->
|
|
16
|
+
<view class="dd-poster__renderer">
|
|
17
|
+
<dd-qrcode
|
|
18
|
+
ref="qrCode"
|
|
19
|
+
:val="qrCodeValue"
|
|
20
|
+
:size="qrCodeSize"
|
|
21
|
+
:margin="0"
|
|
22
|
+
:loadMake="false"
|
|
23
|
+
:onval="false"
|
|
24
|
+
background="#ffffff"
|
|
25
|
+
foreground="#000000"
|
|
26
|
+
:class="['dd-poster__hidden-qrcode', qrCodeShow ? '' : 'dd-poster__hidden-qrcode--hidden']"
|
|
27
|
+
/>
|
|
28
|
+
</view>
|
|
29
|
+
</view>
|
|
30
|
+
</template>
|
|
31
|
+
|
|
32
|
+
<script>
|
|
33
|
+
/**
|
|
34
|
+
* Poster 海报组件
|
|
35
|
+
* @description 用于生成海报的组件,支持文本、图片、二维码等元素
|
|
36
|
+
*
|
|
37
|
+
* @property {Object} json 海报配置JSON数据
|
|
38
|
+
* @property {Object} json.css 海报容器样式
|
|
39
|
+
* @property {Array} json.views 海报元素列表
|
|
40
|
+
* @property {String} json.views.type 元素类型(text/image/qrcode/view)
|
|
41
|
+
* @property {String} json.views.text 文本内容(仅text类型)
|
|
42
|
+
* @property {String} json.views.src 图片地址(仅image/qrcode类型)
|
|
43
|
+
* @property {Object} json.views.css 元素样式
|
|
44
|
+
*
|
|
45
|
+
* @example <dd-poster :json="posterJson"></dd-poster>
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
import {
|
|
49
|
+
rpx2px
|
|
50
|
+
} from '../../libs/function/index.js';
|
|
51
|
+
export default {
|
|
52
|
+
name: 'dd-poster',
|
|
53
|
+
props: {
|
|
54
|
+
json: {
|
|
55
|
+
type: Object,
|
|
56
|
+
default: () => ({})
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
data() {
|
|
60
|
+
return {
|
|
61
|
+
canvasId: 'dd-poster-canvas-' + Date.now(),
|
|
62
|
+
showCanvas: false,
|
|
63
|
+
canvasWidth: 0,
|
|
64
|
+
canvasHeight: 0,
|
|
65
|
+
// 二维码相关数据
|
|
66
|
+
qrCodeValue: '',
|
|
67
|
+
qrCodeSize: 200,
|
|
68
|
+
qrCodeShow: false,
|
|
69
|
+
// 存储多个二维码的数据
|
|
70
|
+
qrCodeMap: new Map()
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
computed: {
|
|
74
|
+
// 根据传入的css生成文本样式
|
|
75
|
+
getTextStyle() {
|
|
76
|
+
return (css) => {
|
|
77
|
+
const style = {};
|
|
78
|
+
if (css.color) style.color = css.color;
|
|
79
|
+
if (css.fontSize) style.fontSize = css.fontSize;
|
|
80
|
+
if (css.fontWeight) style.fontWeight = css.fontWeight;
|
|
81
|
+
if (css.lineHeight) style.lineHeight = css.lineHeight;
|
|
82
|
+
if (css.textAlign) style.textAlign = css.textAlign;
|
|
83
|
+
return style;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
methods: {
|
|
88
|
+
/**
|
|
89
|
+
* 导出海报图片
|
|
90
|
+
* @description 根据json配置生成海报并导出为临时图片路径
|
|
91
|
+
* @returns {Promise<Object>} 返回包含图片信息的对象
|
|
92
|
+
* @author jry ijry@qq.com
|
|
93
|
+
*/
|
|
94
|
+
async exportImage() {
|
|
95
|
+
let timeoutId = null;
|
|
96
|
+
try {
|
|
97
|
+
const timeoutPromise = new Promise((resolve, reject) => {
|
|
98
|
+
timeoutId = setTimeout(() => reject(new Error('导出图片超时')), 20000);
|
|
99
|
+
});
|
|
100
|
+
const exportPromise = (async () => {
|
|
101
|
+
// 获取海报尺寸信息
|
|
102
|
+
const posterSize = this.json.css || {};
|
|
103
|
+
const width = this.convertRpxToPx(posterSize.width || '750rpx');
|
|
104
|
+
const height = this.convertRpxToPx(posterSize.height || '1114rpx');
|
|
105
|
+
|
|
106
|
+
this.canvasWidth = width;
|
|
107
|
+
this.canvasHeight = height;
|
|
108
|
+
this.showCanvas = true;
|
|
109
|
+
await this.$nextTick();
|
|
110
|
+
|
|
111
|
+
const posterCanvas = await this.getPosterCanvas();
|
|
112
|
+
const ctx = posterCanvas;
|
|
113
|
+
if (posterSize.background) {
|
|
114
|
+
if (posterSize.background.includes('linear-gradient') || posterSize.background.includes('radial-gradient')) {
|
|
115
|
+
this.drawGradientBackground(ctx, posterSize, 0, 0, width, height);
|
|
116
|
+
} else {
|
|
117
|
+
ctx.setFillStyle(posterSize.background);
|
|
118
|
+
ctx.fillRect(0, 0, width, height);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
for (const item of this.json.views || []) {
|
|
123
|
+
await this.drawItem(ctx, item, width, height);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await this.flushPosterCanvas(posterCanvas);
|
|
127
|
+
const res = await posterCanvas.toTempFilePath({ width, height });
|
|
128
|
+
const path = res && (res.tempFilePath || res.apFilePath);
|
|
129
|
+
if (!path) {
|
|
130
|
+
throw new Error('导出图片未返回文件路径');
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
path,
|
|
136
|
+
blob: this.dataURLToBlob(path)
|
|
137
|
+
};
|
|
138
|
+
})();
|
|
139
|
+
return await Promise.race([exportPromise, timeoutPromise]);
|
|
140
|
+
} finally {
|
|
141
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
142
|
+
this.showCanvas = false;
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
flushPosterCanvas(posterCanvas) {
|
|
147
|
+
return new Promise((resolve, reject) => {
|
|
148
|
+
let settled = false;
|
|
149
|
+
const finish = (error) => {
|
|
150
|
+
if (settled) return;
|
|
151
|
+
settled = true;
|
|
152
|
+
if (error) reject(error);
|
|
153
|
+
else resolve();
|
|
154
|
+
};
|
|
155
|
+
try {
|
|
156
|
+
const result = posterCanvas.draw(false, () => finish());
|
|
157
|
+
if (result && typeof result.then === 'function') {
|
|
158
|
+
result.then(() => finish(), finish);
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
finish(error);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async getPosterCanvas() {
|
|
167
|
+
await this.$nextTick();
|
|
168
|
+
const posterCanvas = this.$refs.posterCanvas;
|
|
169
|
+
if (!posterCanvas) {
|
|
170
|
+
throw new Error('无法获取海报画布实例');
|
|
171
|
+
}
|
|
172
|
+
const initialized = await posterCanvas.initCanvas();
|
|
173
|
+
if (!initialized || !posterCanvas.getRawContext()) {
|
|
174
|
+
throw new Error('无法初始化海报画布');
|
|
175
|
+
}
|
|
176
|
+
return posterCanvas;
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* 绘制单个元素
|
|
181
|
+
* @description 根据元素类型绘制文本、图片、矩形或二维码到canvas
|
|
182
|
+
* @param {Object} ctx canvas上下文
|
|
183
|
+
* @param {Object} item 元素配置信息
|
|
184
|
+
* @param {Number} canvasWidth canvas宽度
|
|
185
|
+
* @param {Number} canvasHeight canvas高度
|
|
186
|
+
* @returns {Promise} 绘制完成的Promise
|
|
187
|
+
* @author jry ijry@qq.com
|
|
188
|
+
*/
|
|
189
|
+
async drawItem(ctx, item, canvasWidth, canvasHeight) {
|
|
190
|
+
const css = item.css || {};
|
|
191
|
+
const left = this.convertRpxToPx(css.left || '0rpx');
|
|
192
|
+
const top = this.convertRpxToPx(css.top || '0rpx');
|
|
193
|
+
const width = this.convertRpxToPx(css.width || '0rpx');
|
|
194
|
+
const height = this.convertRpxToPx(css.height || '0rpx');
|
|
195
|
+
|
|
196
|
+
switch (item.type) {
|
|
197
|
+
case 'view':
|
|
198
|
+
// 绘制矩形背景
|
|
199
|
+
if (css.background) {
|
|
200
|
+
// 支持渐变背景色
|
|
201
|
+
if (css.background.includes('linear-gradient') || css.background.includes('radial-gradient')) {
|
|
202
|
+
this.drawGradientBackground(ctx, css, left, top, width, height);
|
|
203
|
+
} else {
|
|
204
|
+
ctx.setFillStyle(css.background);
|
|
205
|
+
// 处理圆角
|
|
206
|
+
if (css.radius) {
|
|
207
|
+
const radius = this.convertRpxToPx(css.radius);
|
|
208
|
+
this.drawRoundRect(ctx, left, top, width, height, radius, css.background);
|
|
209
|
+
} else {
|
|
210
|
+
ctx.fillRect(left, top, width, height);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
|
|
216
|
+
case 'text':
|
|
217
|
+
// 设置文本样式
|
|
218
|
+
ctx.setFillStyle(css.color || '#000000');
|
|
219
|
+
if (css.fontSize) {
|
|
220
|
+
const fontSize = this.convertRpxToPx(css.fontSize);
|
|
221
|
+
ctx.setFontSize(fontSize);
|
|
222
|
+
}
|
|
223
|
+
if (css.fontWeight) {
|
|
224
|
+
ctx.setLineWidth(css.fontWeight === 'bold' ? 2 : 1);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 处理文本换行
|
|
228
|
+
if (css.lineClamp) {
|
|
229
|
+
await this.drawTextWithLineClamp(ctx, item.text, left, top, width, css);
|
|
230
|
+
} else {
|
|
231
|
+
// 修复:文本垂直居中对齐问题
|
|
232
|
+
const textBaseLine = css.fontSize ? this.convertRpxToPx(css.fontSize) / 2 : 10;
|
|
233
|
+
ctx.fillText(item.text, left, top + textBaseLine);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case 'image':
|
|
238
|
+
// 绘制图片
|
|
239
|
+
return new Promise((resolve, reject) => {
|
|
240
|
+
uni.getImageInfo({
|
|
241
|
+
src: item.src,
|
|
242
|
+
success: async (res) => {
|
|
243
|
+
let clipped = false;
|
|
244
|
+
try {
|
|
245
|
+
if (css.radius) {
|
|
246
|
+
const radius = this.convertRpxToPx(css.radius);
|
|
247
|
+
this.clipRoundRect(ctx, left, top, width, height, radius);
|
|
248
|
+
clipped = true;
|
|
249
|
+
}
|
|
250
|
+
await ctx.drawImage(res.path, left, top, width, height);
|
|
251
|
+
resolve();
|
|
252
|
+
} catch (error) {
|
|
253
|
+
reject(new Error(`海报图片绘制失败: ${error && error.message ? error.message : error}`));
|
|
254
|
+
} finally {
|
|
255
|
+
if (clipped) ctx.restore();
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
fail: (e) => {
|
|
259
|
+
reject(new Error(`海报图片加载失败: ${e && e.errMsg ? e.errMsg : e}`));
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
case 'qrcode':
|
|
265
|
+
// 绘制二维码
|
|
266
|
+
if (item.text) {
|
|
267
|
+
// 使用 dd-qrcode 生成二维码图片
|
|
268
|
+
const qrCodeImageUrl = await this.generateQRCode(item.text, width, height);
|
|
269
|
+
const drawn = await ctx.drawImage(qrCodeImageUrl, left, top, width, height);
|
|
270
|
+
if (drawn === false) {
|
|
271
|
+
throw new Error('二维码图片无法绘制到海报画布');
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
// 绘制二维码占位符
|
|
275
|
+
ctx.setFillStyle('#f5f5f5');
|
|
276
|
+
ctx.fillRect(left, top, width, height);
|
|
277
|
+
ctx.setFillStyle('#999');
|
|
278
|
+
ctx.setFontSize(12);
|
|
279
|
+
ctx.setTextAlign('center');
|
|
280
|
+
ctx.fillText('QR', left + width/2, top + height/2);
|
|
281
|
+
ctx.setTextAlign('left');
|
|
282
|
+
}
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* 绘制圆角矩形
|
|
289
|
+
* @description 绘制指定位置和尺寸的圆角矩形
|
|
290
|
+
* @param {Object} ctx canvas上下文
|
|
291
|
+
* @param {Number} x x坐标
|
|
292
|
+
* @param {Number} y y坐标
|
|
293
|
+
* @param {Number} width 宽度
|
|
294
|
+
* @param {Number} height 高度
|
|
295
|
+
* @param {Number} radius 圆角半径
|
|
296
|
+
* @param {String} fillColor 填充颜色
|
|
297
|
+
* @author jry ijry@qq.com
|
|
298
|
+
*/
|
|
299
|
+
drawRoundRect(ctx, x, y, width, height, radius, fillColor) {
|
|
300
|
+
ctx.save();
|
|
301
|
+
ctx.beginPath();
|
|
302
|
+
ctx.moveTo(x + radius, y);
|
|
303
|
+
ctx.lineTo(x + width - radius, y);
|
|
304
|
+
ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
|
|
305
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
306
|
+
ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
|
|
307
|
+
ctx.lineTo(x + radius, y + height);
|
|
308
|
+
ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
|
|
309
|
+
ctx.lineTo(x, y + radius);
|
|
310
|
+
ctx.quadraticCurveTo(x, y, x + radius, y);
|
|
311
|
+
ctx.closePath();
|
|
312
|
+
if (fillColor) {
|
|
313
|
+
ctx.setFillStyle(fillColor);
|
|
314
|
+
ctx.fill();
|
|
315
|
+
}
|
|
316
|
+
ctx.restore();
|
|
317
|
+
},
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* 裁剪圆角矩形区域
|
|
321
|
+
* @description 在canvas上创建圆角矩形裁剪区域
|
|
322
|
+
* @param {Object} ctx canvas上下文
|
|
323
|
+
* @param {Number} x x坐标
|
|
324
|
+
* @param {Number} y y坐标
|
|
325
|
+
* @param {Number} width 宽度
|
|
326
|
+
* @param {Number} height 高度
|
|
327
|
+
* @param {Number} radius 圆角半径
|
|
328
|
+
* @author jry ijry@qq.com
|
|
329
|
+
*/
|
|
330
|
+
clipRoundRect(ctx, x, y, width, height, radius) {
|
|
331
|
+
ctx.save();
|
|
332
|
+
ctx.beginPath();
|
|
333
|
+
ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5);
|
|
334
|
+
ctx.lineTo(x + width - radius, y);
|
|
335
|
+
ctx.arc(x + width - radius, y + radius, radius, Math.PI * 1.5, Math.PI * 2);
|
|
336
|
+
ctx.lineTo(x + width, y + height - radius);
|
|
337
|
+
ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI * 0.5);
|
|
338
|
+
ctx.lineTo(x + radius, y + height);
|
|
339
|
+
ctx.arc(x + radius, y + height - radius, radius, Math.PI * 0.5, Math.PI);
|
|
340
|
+
ctx.closePath();
|
|
341
|
+
ctx.clip();
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* 绘制带行数限制的文本
|
|
346
|
+
* @description 绘制可控制最大行数的文本,超出部分显示省略号
|
|
347
|
+
* @param {Object} ctx canvas上下文
|
|
348
|
+
* @param {String} text 文本内容
|
|
349
|
+
* @param {Number} x x坐标
|
|
350
|
+
* @param {Number} y y坐标
|
|
351
|
+
* @param {Number} maxWidth 最大宽度
|
|
352
|
+
* @param {Object} css 样式配置
|
|
353
|
+
* @author jry ijry@qq.com
|
|
354
|
+
*/
|
|
355
|
+
async drawTextWithLineClamp(ctx, text, x, y, maxWidth, css) {
|
|
356
|
+
const lineClamp = Math.max(1, parseInt(css.lineClamp, 10) || 1);
|
|
357
|
+
const lineHeight = css.lineHeight ? this.convertRpxToPx(css.lineHeight) : 20;
|
|
358
|
+
const characters = Array.from(String(text || ''));
|
|
359
|
+
const lines = [];
|
|
360
|
+
const ellipsis = '...';
|
|
361
|
+
const measurementCache = new Map();
|
|
362
|
+
const fontSize = css.fontSize ? this.convertRpxToPx(css.fontSize) : 20;
|
|
363
|
+
// 兜底估算:全角字符约占一个字号,半角约 0.56。测量失败时若按半角估算,
|
|
364
|
+
// 中文会短算 40% 而不换行,所以必须区分宽度。
|
|
365
|
+
const estimateWidth = (value) => Array.from(String(value)).reduce((width, char) => {
|
|
366
|
+
if (/[ᄀ-ᅟ⺀-〾-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰--⦆¢-₩]/.test(char)) {
|
|
367
|
+
return width + fontSize;
|
|
368
|
+
}
|
|
369
|
+
if (/\s/.test(char)) return width + fontSize * 0.28;
|
|
370
|
+
return width + fontSize * 0.56;
|
|
371
|
+
}, 0);
|
|
372
|
+
const measureTextWidth = async (value) => {
|
|
373
|
+
if (measurementCache.has(value)) {
|
|
374
|
+
return measurementCache.get(value);
|
|
375
|
+
}
|
|
376
|
+
let metrics;
|
|
377
|
+
if (ctx && typeof ctx.measureTextAsync === 'function') {
|
|
378
|
+
metrics = await ctx.measureTextAsync(value);
|
|
379
|
+
} else {
|
|
380
|
+
metrics = ctx.measureText(value);
|
|
381
|
+
}
|
|
382
|
+
let measuredWidth = Number(metrics && metrics.width) || 0;
|
|
383
|
+
// 测量不可用时(鸿蒙同步 measureText 恒返回 0)必须改用估算:
|
|
384
|
+
// 把 0 当作真实宽度会让下面的 <= maxWidth 恒成立,整段文本挤成一行。
|
|
385
|
+
if (!(measuredWidth > 0) && value) {
|
|
386
|
+
measuredWidth = estimateWidth(value);
|
|
387
|
+
}
|
|
388
|
+
measurementCache.set(value, measuredWidth);
|
|
389
|
+
return measuredWidth;
|
|
390
|
+
};
|
|
391
|
+
const findTextFitLength = async (values, suffix = '') => {
|
|
392
|
+
let low = 0;
|
|
393
|
+
let high = values.length;
|
|
394
|
+
while (low < high) {
|
|
395
|
+
const middle = Math.ceil((low + high) / 2);
|
|
396
|
+
const candidate = values.slice(0, middle).join('') + suffix;
|
|
397
|
+
if (await measureTextWidth(candidate) <= maxWidth) {
|
|
398
|
+
low = middle;
|
|
399
|
+
} else {
|
|
400
|
+
high = middle - 1;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return low;
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
let offset = 0;
|
|
407
|
+
while (offset < characters.length && lines.length < lineClamp) {
|
|
408
|
+
const remaining = characters.slice(offset);
|
|
409
|
+
const remainingText = remaining.join('');
|
|
410
|
+
if (await measureTextWidth(remainingText) <= maxWidth) {
|
|
411
|
+
lines.push(remainingText);
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const isLastLine = lines.length === lineClamp - 1;
|
|
416
|
+
if (isLastLine) {
|
|
417
|
+
let suffix = ellipsis;
|
|
418
|
+
while (suffix && await measureTextWidth(suffix) > maxWidth) {
|
|
419
|
+
suffix = suffix.slice(0, -1);
|
|
420
|
+
}
|
|
421
|
+
const fitLength = await findTextFitLength(remaining, suffix);
|
|
422
|
+
lines.push(remaining.slice(0, fitLength).join('') + suffix);
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const fitLength = Math.max(1, await findTextFitLength(remaining));
|
|
427
|
+
lines.push(remaining.slice(0, fitLength).join(''));
|
|
428
|
+
offset += fitLength;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
const textBaseLine = css.fontSize ? this.convertRpxToPx(css.fontSize) / 2 : 10;
|
|
432
|
+
for (let i = 0; i < lines.length; i++) {
|
|
433
|
+
ctx.fillText(lines[i], x, y + (i * lineHeight) + textBaseLine);
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* 生成二维码图片
|
|
439
|
+
* @description 根据文本内容生成二维码图片URL
|
|
440
|
+
* @param {String} text 二维码内容
|
|
441
|
+
* @param {Number} width 二维码宽度
|
|
442
|
+
* @param {Number} height 二维码高度
|
|
443
|
+
* @returns {Promise<String>} 二维码图片URL
|
|
444
|
+
* @author jry ijry@qq.com
|
|
445
|
+
*/
|
|
446
|
+
async generateQRCode(text, width, height) {
|
|
447
|
+
const qrCodeKey = `${text}_${width}_${height}`;
|
|
448
|
+
if (this.qrCodeMap.has(qrCodeKey)) {
|
|
449
|
+
return this.qrCodeMap.get(qrCodeKey);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
this.qrCodeValue = text;
|
|
453
|
+
this.qrCodeSize = Math.max(width, height);
|
|
454
|
+
this.qrCodeShow = true;
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
await this.$nextTick();
|
|
458
|
+
const qrCode = this.$refs.qrCode;
|
|
459
|
+
if (!qrCode) {
|
|
460
|
+
throw new Error('无法获取二维码组件实例');
|
|
461
|
+
}
|
|
462
|
+
if (typeof qrCode.refreshCanvas === 'function') {
|
|
463
|
+
await qrCode.refreshCanvas();
|
|
464
|
+
}
|
|
465
|
+
const generatedPath = await qrCode._makeCode();
|
|
466
|
+
let tempFilePath = typeof generatedPath === 'string' ? generatedPath : '';
|
|
467
|
+
if (!tempFilePath) {
|
|
468
|
+
const res = await qrCode.toTempFilePath();
|
|
469
|
+
tempFilePath = res && (res.tempFilePath || res.apFilePath);
|
|
470
|
+
}
|
|
471
|
+
if (!tempFilePath) {
|
|
472
|
+
throw new Error('二维码组件未返回图片路径');
|
|
473
|
+
}
|
|
474
|
+
this.qrCodeMap.set(qrCodeKey, tempFilePath);
|
|
475
|
+
return tempFilePath;
|
|
476
|
+
} catch (error) {
|
|
477
|
+
throw new Error(`二维码生成失败: ${error && error.message ? error.message : error}`);
|
|
478
|
+
} finally {
|
|
479
|
+
this.qrCodeShow = false;
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* 将rpx单位转换为px
|
|
485
|
+
* @description 根据屏幕密度将rpx单位转换为px单位
|
|
486
|
+
* @param {String|Number} rpxValue rpx值
|
|
487
|
+
* @returns {Number} 转换后的px值
|
|
488
|
+
* @author jry ijry@qq.com
|
|
489
|
+
*/
|
|
490
|
+
convertRpxToPx(rpxValue) {
|
|
491
|
+
if (typeof rpxValue === 'number') return rpxValue;
|
|
492
|
+
|
|
493
|
+
// 使用rpx2px方法
|
|
494
|
+
if (typeof rpxValue === 'string' && rpxValue.endsWith('rpx')) {
|
|
495
|
+
const value = parseFloat(rpxValue);
|
|
496
|
+
return value * this.getRpxRatio();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return parseFloat(rpxValue) || 0;
|
|
500
|
+
},
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* 获取 rpx -> px 的统一换算比例
|
|
504
|
+
* @description uni.upx2px 对每个值单独判断设备宽度:当 windowWidth 超过
|
|
505
|
+
* rpxCalcMaxDeviceWidth(默认960)时,只有恰好等于 750 的值使用真实设备宽度,
|
|
506
|
+
* 其余值回退到 rpxCalcBaseDeviceWidth(默认375)。海报需要所有尺寸共用同一
|
|
507
|
+
* 比例,逐值换算会让宽高用上不同基准而破坏宽高比:鸿蒙 windowWidth 上报为
|
|
508
|
+
* 物理像素1084,750rpx算得1084、1114rpx算得557,海报因此变成横向。
|
|
509
|
+
* 这里只取一次比例,且刻意用非 750 的值探测,避开上面的特例分支,
|
|
510
|
+
* 使超宽设备统一走 baseWidth,避免画布被放大到数十MB。
|
|
511
|
+
* @returns {Number} 每 1rpx 对应的 px 值
|
|
512
|
+
* @author jry ijry@qq.com
|
|
513
|
+
*/
|
|
514
|
+
getRpxRatio() {
|
|
515
|
+
const PROBE = 1000; // 不能用 750,750 是 upx2px 的特例值
|
|
516
|
+
const ratio = rpx2px(PROBE) / PROBE;
|
|
517
|
+
return ratio > 0 ? ratio : 1;
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* 绘制渐变背景
|
|
522
|
+
* @description 绘制线性渐变或径向渐变背景
|
|
523
|
+
* @param {Object} ctx canvas上下文
|
|
524
|
+
* @param {Object} css 样式配置
|
|
525
|
+
* @param {Number} left 左边距
|
|
526
|
+
* @param {Number} top 上边距
|
|
527
|
+
* @param {Number} width 宽度
|
|
528
|
+
* @param {Number} height 高度
|
|
529
|
+
* @author jry ijry@qq.com
|
|
530
|
+
*/
|
|
531
|
+
drawGradientBackground(ctx, css, left, top, width, height) {
|
|
532
|
+
const background = css.background;
|
|
533
|
+
let gradient = null;
|
|
534
|
+
|
|
535
|
+
// 处理线性渐变
|
|
536
|
+
if (background.includes('linear-gradient')) {
|
|
537
|
+
// 解析线性渐变角度和颜色
|
|
538
|
+
const angleMatch = background.match(/linear-gradient\((\d+)deg/);
|
|
539
|
+
const angle = angleMatch ? parseInt(angleMatch[1]) : 135;
|
|
540
|
+
|
|
541
|
+
// 根据角度计算渐变起点和终点
|
|
542
|
+
let startX = left, startY = top, endX = left + width, endY = top + height;
|
|
543
|
+
|
|
544
|
+
// 简化的角度处理(支持常见角度)
|
|
545
|
+
if (angle === 0) {
|
|
546
|
+
startX = left;
|
|
547
|
+
startY = top + height;
|
|
548
|
+
endX = left;
|
|
549
|
+
endY = top;
|
|
550
|
+
} else if (angle === 90) {
|
|
551
|
+
startX = left;
|
|
552
|
+
startY = top;
|
|
553
|
+
endX = left + width;
|
|
554
|
+
endY = top;
|
|
555
|
+
} else if (angle === 180) {
|
|
556
|
+
startX = left;
|
|
557
|
+
startY = top;
|
|
558
|
+
endX = left;
|
|
559
|
+
endY = top + height;
|
|
560
|
+
} else if (angle === 270) {
|
|
561
|
+
startX = left + width;
|
|
562
|
+
startY = top;
|
|
563
|
+
endX = left;
|
|
564
|
+
endY = top;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
gradient = ctx.createLinearGradient(startX, startY, endX, endY);
|
|
568
|
+
|
|
569
|
+
// 解析颜色值
|
|
570
|
+
const colorMatches = background.match(/#[0-9a-fA-F]+|rgba?\([^)]+\)/g);
|
|
571
|
+
if (colorMatches && colorMatches.length >= 2) {
|
|
572
|
+
// 添加渐变色点
|
|
573
|
+
colorMatches.forEach((color, index) => {
|
|
574
|
+
const stop = index / (colorMatches.length - 1);
|
|
575
|
+
gradient.addColorStop(stop, color);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
// 处理径向渐变
|
|
580
|
+
else if (background.includes('radial-gradient')) {
|
|
581
|
+
// 径向渐变从中心开始
|
|
582
|
+
const centerX = left + width / 2;
|
|
583
|
+
const centerY = top + height / 2;
|
|
584
|
+
const radius = Math.min(width, height) / 2;
|
|
585
|
+
|
|
586
|
+
gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, radius);
|
|
587
|
+
|
|
588
|
+
// 解析颜色值
|
|
589
|
+
const colorMatches = background.match(/#[0-9a-fA-F]+|rgba?\([^)]+\)/g);
|
|
590
|
+
if (colorMatches && colorMatches.length >= 2) {
|
|
591
|
+
// 添加渐变色点
|
|
592
|
+
colorMatches.forEach((color, index) => {
|
|
593
|
+
const stop = index / (colorMatches.length - 1);
|
|
594
|
+
gradient.addColorStop(stop, color);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (gradient) {
|
|
600
|
+
ctx.setFillStyle(gradient);
|
|
601
|
+
// 处理圆角
|
|
602
|
+
if (css.radius) {
|
|
603
|
+
const radius = this.convertRpxToPx(css.radius);
|
|
604
|
+
this.drawRoundRect(ctx, left, top, width, height, radius, gradient);
|
|
605
|
+
} else {
|
|
606
|
+
ctx.fillRect(left, top, width, height);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* 将dataURL转换为Blob
|
|
613
|
+
* @description H5环境下将base64格式的dataURL转换为Blob对象
|
|
614
|
+
* @param {String} dataURL base64格式的图片数据
|
|
615
|
+
* @returns {Blob} Blob对象
|
|
616
|
+
* @author jry ijry@qq.com
|
|
617
|
+
*/
|
|
618
|
+
dataURLToBlob(dataURL) {
|
|
619
|
+
// 检查是否为H5环境且是base64数据
|
|
620
|
+
// #ifdef H5
|
|
621
|
+
if (dataURL && dataURL.startsWith('data:image')) {
|
|
622
|
+
const parts = dataURL.split(';base64,');
|
|
623
|
+
const contentType = parts[0].split(':')[1];
|
|
624
|
+
const raw = window.atob(parts[1]);
|
|
625
|
+
const rawLength = raw.length;
|
|
626
|
+
const uInt8Array = new Uint8Array(rawLength);
|
|
627
|
+
|
|
628
|
+
for (let i = 0; i < rawLength; ++i) {
|
|
629
|
+
uInt8Array[i] = raw.charCodeAt(i);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return new Blob([uInt8Array], { type: contentType });
|
|
633
|
+
}
|
|
634
|
+
// #endif
|
|
635
|
+
|
|
636
|
+
return null;
|
|
637
|
+
},
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
</script>
|
|
641
|
+
|
|
642
|
+
<style lang="scss" scoped>
|
|
643
|
+
.dd-poster {
|
|
644
|
+
position: relative;
|
|
645
|
+
|
|
646
|
+
&__canvas {
|
|
647
|
+
position: relative;
|
|
648
|
+
overflow: hidden;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/* #ifdef APP-NVUE */
|
|
652
|
+
&__renderer {
|
|
653
|
+
position: fixed;
|
|
654
|
+
top: -1px;
|
|
655
|
+
left: -1px;
|
|
656
|
+
width: 1px;
|
|
657
|
+
height: 1px;
|
|
658
|
+
overflow: hidden;
|
|
659
|
+
z-index: -1;
|
|
660
|
+
opacity: 0.01;
|
|
661
|
+
pointer-events: none;
|
|
662
|
+
}
|
|
663
|
+
/* #endif */
|
|
664
|
+
|
|
665
|
+
/* #ifndef APP-NVUE */
|
|
666
|
+
&__renderer {
|
|
667
|
+
position: fixed;
|
|
668
|
+
top: -10000px;
|
|
669
|
+
left: -10000px;
|
|
670
|
+
width: 1px;
|
|
671
|
+
height: 1px;
|
|
672
|
+
overflow: hidden;
|
|
673
|
+
z-index: -1;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
&__hidden-canvas {
|
|
677
|
+
position: fixed;
|
|
678
|
+
top: -10000px;
|
|
679
|
+
left: -10000px;
|
|
680
|
+
z-index: -1;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
&__hidden-qrcode {
|
|
684
|
+
position: fixed;
|
|
685
|
+
top: -10000px;
|
|
686
|
+
left: -10000px;
|
|
687
|
+
z-index: -1;
|
|
688
|
+
|
|
689
|
+
&--hidden {
|
|
690
|
+
display: none;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
/* #endif */
|
|
694
|
+
}
|
|
695
|
+
</style>
|
|
@@ -199,7 +199,7 @@ function stepStatus(i: number): 'done' | 'current' | 'pending' {
|
|
|
199
199
|
position: relative;
|
|
200
200
|
|
|
201
201
|
&:last-child {
|
|
202
|
-
flex: 0 0
|
|
202
|
+
flex: 0 0 $dd-space-3;
|
|
203
203
|
}
|
|
204
204
|
}
|
|
205
205
|
|
|
@@ -224,8 +224,8 @@ function stepStatus(i: number): 'done' | 'current' | 'pending' {
|
|
|
224
224
|
|
|
225
225
|
&__connector {
|
|
226
226
|
flex: 1;
|
|
227
|
-
height:
|
|
228
|
-
margin: 0
|
|
227
|
+
height: $dd-space-1;
|
|
228
|
+
margin: 0 $dd-space-1;
|
|
229
229
|
background: #2a2a2a;
|
|
230
230
|
@include dd-transition(background 0.3s ease);
|
|
231
231
|
|