wxapp-poster 1.0.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 ADDED
@@ -0,0 +1,239 @@
1
+ # wxapp-poster
2
+
3
+ 一个高度可配置的微信小程序海报生成组件,支持自定义图片、文字、颜色、字体、间距等所有样式。
4
+
5
+ ## 特性
6
+
7
+ - 🎨 **高度可配置** - 支持自定义所有样式参数
8
+ - 📱 **微信小程序原生组件** - 基于 Component 构建
9
+ - 🖼️ **Canvas 绘制** - 使用 2 倍图提升清晰度
10
+ - 💾 **保存到相册** - 内置保存功能,支持权限处理
11
+ - 📦 **零依赖** - 不依赖任何第三方库
12
+ - 🔧 **低耦合** - 所有配置项均可自定义
13
+
14
+ ## 安装
15
+
16
+ ### 方式一:npm 安装
17
+
18
+ ```bash
19
+ npm install wxapp-poster
20
+ ```
21
+
22
+ ### 方式二:通过微信开发者工具
23
+
24
+ 1. 在微信开发者工具中,点击菜单栏 `工具` -> `构建 npm`
25
+ 2. 构建完成后,在 `node_modules` 目录下会生成 `wxapp-poster` 目录
26
+
27
+ ## 使用
28
+
29
+ ### 1. 在页面或组件的 json 文件中引入组件
30
+
31
+ ```json
32
+ {
33
+ "usingComponents": {
34
+ "poster": "wxapp-poster/poster"
35
+ }
36
+ }
37
+ ```
38
+
39
+ ### 2. 在 wxml 中使用组件
40
+
41
+ ```xml
42
+ <poster
43
+ id="poster"
44
+ background-image="../../static/bg.png"
45
+ qr-image="../../static/qr.png"
46
+ primary-text="邀请您一起加入POPO"
47
+ secondary-text="长按二维码识别"
48
+ bind:drawcomplete="onDrawComplete"
49
+ bind:savesuccess="onSaveSuccess"
50
+ bind:saveerror="onSaveError"
51
+ bind:error="onError"
52
+ />
53
+ ```
54
+
55
+ ### 3. 在 js 中调用保存方法
56
+
57
+ ```javascript
58
+ Page({
59
+ // 保存图片
60
+ saveImage() {
61
+ const poster = this.selectComponent('#poster');
62
+ if (poster) {
63
+ poster.saveImage();
64
+ }
65
+ },
66
+
67
+ // 绘制完成事件
68
+ onDrawComplete(e) {
69
+ console.log('绘制完成:', e.detail.tempImagePath);
70
+ },
71
+
72
+ // 保存成功事件
73
+ onSaveSuccess(e) {
74
+ console.log('保存成功:', e.detail.tempImagePath);
75
+ },
76
+
77
+ // 保存失败事件
78
+ onSaveError(e) {
79
+ console.log('保存失败:', e.detail);
80
+ },
81
+
82
+ // 错误事件
83
+ onError(e) {
84
+ console.error('组件错误:', e.detail);
85
+ }
86
+ });
87
+ ```
88
+
89
+ ## API
90
+
91
+ ### 属性 (Properties)
92
+
93
+ #### 内容相关
94
+
95
+ | 属性名 | 类型 | 默认值 | 说明 |
96
+ |--------|------|--------|------|
97
+ | background-image | String | '' | 背景图片路径(必填) |
98
+ | qr-image | String | '' | 二维码图片路径 |
99
+ | primary-text | String | '邀请您一起加入POPO' | 主文本内容 |
100
+ | secondary-text | String | '长按二维码识别' | 次文本内容 |
101
+
102
+ #### Canvas 相关
103
+
104
+ | 属性名 | 类型 | 默认值 | 说明 |
105
+ |--------|------|--------|------|
106
+ | canvas-background-color | String | '#7e57c2' | Canvas 背景色 |
107
+ | canvas-zoom | Number | 40 | Canvas 缩放比例(用于隐藏 canvas) |
108
+ | image-ratio | Object | {width: 750, height: 1050} | 图片尺寸比例 |
109
+ | white-area-height | Number | 150 | 白色区域高度(rpx) |
110
+
111
+ #### 颜色配置
112
+
113
+ | 属性名 | 类型 | 默认值 | 说明 |
114
+ |--------|------|--------|------|
115
+ | white-area-background-color | String | '#ffffff' | 白色区域背景色 |
116
+ | primary-text-color | String | '#000000' | 主文本颜色 |
117
+ | secondary-text-color | String | '#9C9C9C' | 次文本颜色 |
118
+
119
+ #### 字体配置
120
+
121
+ | 属性名 | 类型 | 默认值 | 说明 |
122
+ |--------|------|--------|------|
123
+ | primary-text-size | Number | 28 | 主文本字体大小(rpx) |
124
+ | secondary-text-size | Number | 24 | 次文本字体大小(rpx) |
125
+ | line-spacing-ratio | Number | 0.3 | 行间距比例(相对于次文本字体大小) |
126
+
127
+ #### 间距配置
128
+
129
+ | 属性名 | 类型 | 默认值 | 说明 |
130
+ |--------|------|--------|------|
131
+ | left-padding | Number | 30 | 左边距(rpx) |
132
+ | right-padding | Number | 30 | 右边距(rpx) |
133
+ | qr-size-ratio | Number | 0.8 | 二维码大小比例(0-1 之间) |
134
+
135
+ #### 提示信息配置
136
+
137
+ | 属性名 | 类型 | 默认值 | 说明 |
138
+ |--------|------|--------|------|
139
+ | loading-text | String | '图片生成中,请稍候' | 图片生成中提示 |
140
+ | image-load-error-text | String | '图片加载失败' | 图片加载失败提示 |
141
+ | permission-modal-title | String | '提示' | 权限提示标题 |
142
+ | permission-modal-content | String | '需要您授权保存相册权限' | 权限提示内容 |
143
+ | permission-modal-confirm-text | String | '去设置' | 权限设置按钮文字 |
144
+ | need-permission-text | String | '需要相册权限' | 需要权限提示 |
145
+ | save-success-text | String | '保存成功' | 保存成功提示 |
146
+ | save-error-text | String | '保存失败' | 保存失败提示 |
147
+
148
+ ### 方法 (Methods)
149
+
150
+ #### saveImage()
151
+
152
+ 保存图片到相册。需要在页面中通过 `selectComponent` 获取组件实例后调用。
153
+
154
+ ```javascript
155
+ const poster = this.selectComponent('#poster');
156
+ poster.saveImage();
157
+ ```
158
+
159
+ ### 事件 (Events)
160
+
161
+ | 事件名 | 说明 | 回调参数 |
162
+ |--------|------|----------|
163
+ | drawcomplete | 绘制完成 | { tempImagePath } |
164
+ | savesuccess | 保存成功 | { tempImagePath } |
165
+ | saveerror | 保存失败 | { err, message } |
166
+ | error | 组件错误 | { err } |
167
+
168
+ ## 完整示例
169
+
170
+ ```xml
171
+ <!-- index.wxml -->
172
+ <view class="container">
173
+ <poster
174
+ id="poster"
175
+ background-image="{{backgroundImage}}"
176
+ qr-image="{{qrImage}}"
177
+ primary-text="{{primaryText}}"
178
+ secondary-text="{{secondaryText}}"
179
+ canvas-background-color="#ff0000"
180
+ primary-text-color="#333333"
181
+ primary-text-size="32"
182
+ bind:drawcomplete="onDrawComplete"
183
+ bind:savesuccess="onSaveSuccess"
184
+ />
185
+
186
+ <button bindtap="saveImage">保存图片</button>
187
+ </view>
188
+ ```
189
+
190
+ ```javascript
191
+ // index.js
192
+ Page({
193
+ data: {
194
+ backgroundImage: '../../static/bg.png',
195
+ qrImage: '../../static/qr.png',
196
+ primaryText: '邀请您一起加入',
197
+ secondaryText: '长按二维码识别'
198
+ },
199
+
200
+ saveImage() {
201
+ const poster = this.selectComponent('#poster');
202
+ if (poster) {
203
+ poster.saveImage();
204
+ }
205
+ },
206
+
207
+ onDrawComplete(e) {
208
+ console.log('绘制完成,临时路径:', e.detail.tempImagePath);
209
+ },
210
+
211
+ onSaveSuccess(e) {
212
+ console.log('保存成功,临时路径:', e.detail.tempImagePath);
213
+ }
214
+ });
215
+ ```
216
+
217
+ ## 注意事项
218
+
219
+ 1. **图片路径**:建议使用网络图片或本地绝对路径,相对路径可能在某些情况下无法正常加载
220
+ 2. **权限处理**:组件会自动处理相册保存权限,首次使用会弹出授权提示
221
+ 3. **Canvas 限制**:由于微信小程序的限制,Canvas 会被隐藏(通过 zoom 属性),实际绘制在后台完成
222
+ 4. **图片尺寸**:默认图片比例为 750:1050,可通过 `image-ratio` 属性自定义
223
+
224
+ ## 更新日志
225
+
226
+ ### 1.0.0
227
+
228
+ - 初始版本发布
229
+ - 支持自定义所有样式参数
230
+ - 支持保存图片到相册
231
+ - 完整的事件回调支持
232
+
233
+ ## 许可证
234
+
235
+ MIT
236
+
237
+ ## 贡献
238
+
239
+ 欢迎提交 Issue 和 Pull Request!
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "wxapp-poster",
3
+ "version": "1.0.0",
4
+ "description": "一个高度可配置的微信小程序海报生成组件,支持自定义图片、文字、颜色、字体、间距等所有样式",
5
+ "main": "poster.js",
6
+ "miniprogram": ".",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [
11
+ "wechat",
12
+ "miniprogram",
13
+ "weixin",
14
+ "poster",
15
+ "canvas",
16
+ "海报",
17
+ "小程序",
18
+ "微信小程序"
19
+ ],
20
+ "author": "joie_pink <cassieye20190909@gmail.com>",
21
+ "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/cassie-ye/wxapp-download-post.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/cassie-ye/wxapp-download-post/issues"
28
+ },
29
+ "homepage": "https://github.com/cassie-ye/wxapp-download-post#readme",
30
+ "files": [
31
+ "poster.js",
32
+ "poster.wxml",
33
+ "poster.wxss",
34
+ "poster.json",
35
+ "README.md"
36
+ ]
37
+ }
package/poster.js ADDED
@@ -0,0 +1,490 @@
1
+ /**
2
+ * 海报生成组件
3
+ *
4
+ * @description 一个高度可配置的海报生成组件,支持自定义图片、文字、颜色、字体、间距等所有样式
5
+ *
6
+ * @example
7
+ * // 基础使用
8
+ * <poster
9
+ * background-image="../../static/bg.png"
10
+ * qr-image="../../static/qr.png"
11
+ * primary-text="邀请您一起加入"
12
+ * secondary-text="长按二维码识别"
13
+ * />
14
+ *
15
+ * @example
16
+ * // 自定义样式
17
+ * <poster
18
+ * background-image="../../static/bg.png"
19
+ * qr-image="../../static/qr.png"
20
+ * primary-text="自定义标题"
21
+ * secondary-text="自定义副标题"
22
+ * canvas-background-color="#ff0000"
23
+ * primary-text-color="#333333"
24
+ * primary-text-size="32"
25
+ * />
26
+ *
27
+ * @example
28
+ * // 调用保存方法
29
+ * const poster = this.selectComponent('#poster');
30
+ * poster.saveImage();
31
+ *
32
+ * @events
33
+ * - drawcomplete: 绘制完成事件,返回 { tempImagePath }
34
+ * - savesuccess: 保存成功事件,返回 { tempImagePath }
35
+ * - saveerror: 保存失败事件,返回 { err, message }
36
+ * - error: 错误事件,返回 { err }
37
+ */
38
+ // components/poster/poster.js
39
+ Component({
40
+ /**
41
+ * 组件的属性列表
42
+ */
43
+ properties: {
44
+ // ========== 内容相关 ==========
45
+ // 背景图片
46
+ backgroundImage: {
47
+ type: String,
48
+ value: ''
49
+ },
50
+ // 二维码图片
51
+ qrImage: {
52
+ type: String,
53
+ value: ''
54
+ },
55
+ // 主文本
56
+ primaryText: {
57
+ type: String,
58
+ value: '邀请您一起加入POPO'
59
+ },
60
+ // 次文本
61
+ secondaryText: {
62
+ type: String,
63
+ value: '长按二维码识别'
64
+ },
65
+
66
+ // ========== Canvas相关 ==========
67
+ // Canvas背景色
68
+ canvasBackgroundColor: {
69
+ type: String,
70
+ value: '#7e57c2'
71
+ },
72
+ // Canvas缩放比例(用于隐藏canvas,不影响实际尺寸)
73
+ canvasZoom: {
74
+ type: Number,
75
+ value: 40
76
+ },
77
+ // 图片尺寸比例(宽:高),默认 750:1050
78
+ imageRatio: {
79
+ type: Object,
80
+ value: {
81
+ width: 750,
82
+ height: 1050
83
+ }
84
+ },
85
+ // 白色区域高度(rpx单位,会被转换为px)
86
+ whiteAreaHeight: {
87
+ type: Number,
88
+ value: 150
89
+ },
90
+
91
+ // ========== 颜色配置 ==========
92
+ // 白色区域背景色
93
+ whiteAreaBackgroundColor: {
94
+ type: String,
95
+ value: '#ffffff'
96
+ },
97
+ // 主文本颜色
98
+ primaryTextColor: {
99
+ type: String,
100
+ value: '#000000'
101
+ },
102
+ // 次文本颜色
103
+ secondaryTextColor: {
104
+ type: String,
105
+ value: '#9C9C9C'
106
+ },
107
+
108
+ // ========== 字体配置 ==========
109
+ // 主文本字体大小(rpx单位)
110
+ primaryTextSize: {
111
+ type: Number,
112
+ value: 28
113
+ },
114
+ // 次文本字体大小(rpx单位)
115
+ secondaryTextSize: {
116
+ type: Number,
117
+ value: 24
118
+ },
119
+ // 行间距比例(相对于次文本字体大小的倍数)
120
+ lineSpacingRatio: {
121
+ type: Number,
122
+ value: 0.3
123
+ },
124
+
125
+ // ========== 间距配置 ==========
126
+ // 左边距(rpx单位)
127
+ leftPadding: {
128
+ type: Number,
129
+ value: 30
130
+ },
131
+ // 右边距(rpx单位)
132
+ rightPadding: {
133
+ type: Number,
134
+ value: 30
135
+ },
136
+ // 二维码大小比例(相对于白色区域高度的比例,0-1之间)
137
+ qrSizeRatio: {
138
+ type: Number,
139
+ value: 0.8
140
+ },
141
+
142
+ // ========== 提示信息配置 ==========
143
+ // 图片生成中的提示
144
+ loadingText: {
145
+ type: String,
146
+ value: '图片生成中,请稍候'
147
+ },
148
+ // 图片加载失败提示
149
+ imageLoadErrorText: {
150
+ type: String,
151
+ value: '图片加载失败'
152
+ },
153
+ // 需要相册权限提示标题
154
+ permissionModalTitle: {
155
+ type: String,
156
+ value: '提示'
157
+ },
158
+ // 需要相册权限提示内容
159
+ permissionModalContent: {
160
+ type: String,
161
+ value: '需要您授权保存相册权限'
162
+ },
163
+ // 权限设置按钮文字
164
+ permissionModalConfirmText: {
165
+ type: String,
166
+ value: '去设置'
167
+ },
168
+ // 需要相册权限Toast提示
169
+ needPermissionText: {
170
+ type: String,
171
+ value: '需要相册权限'
172
+ },
173
+ // 保存成功提示
174
+ saveSuccessText: {
175
+ type: String,
176
+ value: '保存成功'
177
+ },
178
+ // 保存失败提示
179
+ saveErrorText: {
180
+ type: String,
181
+ value: '保存失败'
182
+ }
183
+ },
184
+
185
+ /**
186
+ * 组件的初始数据
187
+ */
188
+ data: {
189
+ photoWidth: 0, // canvas实际宽度(2倍图)
190
+ photoHeight: 0, // canvas实际高度(2倍图)
191
+ screenHeight: 0,
192
+ tempImagePath: '', // 保存临时图片路径
193
+ },
194
+
195
+ /**
196
+ * 组件生命周期
197
+ */
198
+ lifetimes: {
199
+ attached() {
200
+ // 组件实例进入页面节点树时执行
201
+ this.initCanvas();
202
+ }
203
+ },
204
+
205
+ /**
206
+ * 组件的方法列表
207
+ */
208
+ methods: {
209
+ /**
210
+ * 初始化Canvas
211
+ */
212
+ initCanvas() {
213
+ let that = this;
214
+ wx.getSystemInfo({
215
+ success: (res) => {
216
+ const { imageRatio, whiteAreaHeight } = that.properties;
217
+ // 使用2倍canvas提升清晰度
218
+ const canvasWidth = res.screenWidth * 2; // 2倍图宽度
219
+ // 根据配置的图片比例计算高度
220
+ const imageAreaHeight = canvasWidth * imageRatio.height / imageRatio.width;
221
+ // 白色区域高度转换为px(2倍图)
222
+ const whiteAreaHeightPx = whiteAreaHeight * canvasWidth / res.screenHeight * 2;
223
+ const canvasHeight = imageAreaHeight + whiteAreaHeightPx;
224
+
225
+ that.setData({
226
+ screenHeight: res.screenHeight,
227
+ photoWidth: canvasWidth, // canvas实际宽度(2倍图)
228
+ photoHeight: canvasHeight, // canvas实际高度(2倍图)
229
+ });
230
+ // 初始化完成后绘制
231
+ that.draw();
232
+ }
233
+ });
234
+ },
235
+
236
+ /**
237
+ * 绘制Canvas
238
+ */
239
+ draw() {
240
+ let that = this;
241
+ const {
242
+ backgroundImage,
243
+ qrImage,
244
+ primaryText,
245
+ secondaryText,
246
+ canvasBackgroundColor,
247
+ imageRatio,
248
+ whiteAreaHeight,
249
+ whiteAreaBackgroundColor,
250
+ primaryTextColor,
251
+ secondaryTextColor,
252
+ primaryTextSize,
253
+ secondaryTextSize,
254
+ lineSpacingRatio,
255
+ leftPadding,
256
+ rightPadding,
257
+ qrSizeRatio,
258
+ imageLoadErrorText
259
+ } = this.properties;
260
+
261
+ if (!backgroundImage) {
262
+ console.warn('背景图片未设置');
263
+ return;
264
+ }
265
+
266
+ // 获取图片信息
267
+ wx.getImageInfo({
268
+ src: backgroundImage,
269
+ success: (imageRes) => {
270
+ let ctx = wx.createCanvasContext('canvasPoster', that);
271
+
272
+ // 获取Canvas尺寸(2倍图)
273
+ let canvasWidth = that.data.photoWidth;
274
+ let canvasHeight = that.data.photoHeight;
275
+
276
+ // 绘制Canvas背景
277
+ ctx.setFillStyle(canvasBackgroundColor);
278
+ ctx.fillRect(0, 0, canvasWidth, canvasHeight);
279
+
280
+ // 计算图片区域高度
281
+ const imageAreaHeight = canvasWidth * imageRatio.height / imageRatio.width;
282
+ // 白色区域高度转换为px(2倍图)
283
+ const whiteAreaHeightPx = whiteAreaHeight * canvasWidth / this.data.screenHeight * 2;
284
+
285
+ // 绘制背景图片
286
+ ctx.drawImage(backgroundImage, 0, 0, canvasWidth, imageAreaHeight);
287
+
288
+ // 绘制白色区域
289
+ ctx.setFillStyle(whiteAreaBackgroundColor);
290
+ ctx.fillRect(0, imageAreaHeight, canvasWidth, whiteAreaHeightPx);
291
+
292
+ // 绘制文字
293
+ // 根据photoWidth(2倍图宽度)计算:rpx转px公式
294
+ // 原逻辑:28rpx -> 36 * canvasWidth / 750, 24rpx -> 32 * canvasWidth / 750
295
+ // 转换系数:主文本 36/28 ≈ 1.286, 次文本 32/24 ≈ 1.333
296
+ const primaryTextRatio = 36 / 28; // 保持与原逻辑一致的转换系数
297
+ const secondaryTextRatio = 32 / 24;
298
+ const fontSize1 = primaryTextSize * primaryTextRatio * canvasWidth / 750; // 主文本2倍图px值
299
+ const fontSize2 = secondaryTextSize * secondaryTextRatio * canvasWidth / 750; // 次文本2倍图px值
300
+ const leftPaddingPx = leftPadding * canvasWidth / 750; // 左边距2倍图px值
301
+
302
+ // 计算白色区域信息
303
+ const whiteAreaStartY = imageAreaHeight;
304
+ const whiteAreaCenterY = whiteAreaStartY + whiteAreaHeightPx / 2;
305
+
306
+ // 计算行间距
307
+ const lineSpacing = fontSize2 * lineSpacingRatio;
308
+ // 计算两行文字的总高度
309
+ const totalTextHeight = fontSize1 + lineSpacing + fontSize2;
310
+
311
+ // 计算第一行文字的y坐标(垂直居中)
312
+ // fillText的y坐标是基线位置,所以需要加上字体大小
313
+ const firstLineY = whiteAreaCenterY - totalTextHeight / 2 + fontSize1;
314
+ // 计算第二行文字的y坐标
315
+ const secondLineY = firstLineY + fontSize1 + lineSpacing;
316
+
317
+ // 绘制第一行文字(左对齐)
318
+ ctx.setFontSize(fontSize1);
319
+ ctx.setFillStyle(primaryTextColor);
320
+ ctx.setTextAlign('left');
321
+ ctx.fillText(primaryText, leftPaddingPx, firstLineY);
322
+
323
+ // 绘制第二行文字(左对齐)
324
+ ctx.setFontSize(fontSize2);
325
+ ctx.setFillStyle(secondaryTextColor);
326
+ ctx.setTextAlign('left');
327
+ ctx.fillText(secondaryText, leftPaddingPx, secondLineY);
328
+
329
+ // 绘制小程序码
330
+ if (qrImage) {
331
+ // 先获取二维码图片信息
332
+ wx.getImageInfo({
333
+ src: qrImage,
334
+ success: (qrRes) => {
335
+ // 计算二维码尺寸(正方形,根据配置的比例)
336
+ const qrSize = whiteAreaHeightPx * qrSizeRatio;
337
+ // 计算x坐标:距离右边
338
+ const rightPaddingPx = rightPadding * canvasWidth / 750; // 右边距2倍图px值
339
+ const qrX = canvasWidth - rightPaddingPx - qrSize;
340
+ // 计算y坐标:垂直居中
341
+ const qrY = whiteAreaCenterY - qrSize / 2;
342
+
343
+ // 绘制二维码(正方形)
344
+ ctx.drawImage(qrImage, qrX, qrY, qrSize, qrSize);
345
+
346
+ // 绘制所有内容(包括二维码)
347
+ that.drawCanvas(ctx, canvasWidth, canvasHeight);
348
+ },
349
+ fail: (err) => {
350
+ console.error('获取二维码图片失败:', err);
351
+ // 即使二维码加载失败,也继续绘制其他内容
352
+ that.drawCanvas(ctx, canvasWidth, canvasHeight);
353
+ }
354
+ });
355
+ } else {
356
+ // 没有二维码,直接绘制其他内容
357
+ that.drawCanvas(ctx, canvasWidth, canvasHeight);
358
+ }
359
+ },
360
+ fail: (err) => {
361
+ console.error('获取图片信息失败:', err);
362
+ wx.showToast({
363
+ title: imageLoadErrorText,
364
+ icon: 'none'
365
+ });
366
+ // 触发错误事件
367
+ that.triggerEvent('error', { err });
368
+ }
369
+ });
370
+ },
371
+
372
+ /**
373
+ * 绘制canvas并保存
374
+ */
375
+ drawCanvas(ctx, canvasWidth, canvasHeight) {
376
+ let that = this;
377
+ ctx.draw(true, () => {
378
+ //然后生成一个保存用的临时地址, 因为安卓机兼容问题, 所以方法要延迟.
379
+ setTimeout(() => {
380
+ wx.canvasToTempFilePath({
381
+ canvasId: 'canvasPoster',
382
+ x: 0,
383
+ y: 0,
384
+ width: canvasWidth,
385
+ height: canvasHeight,
386
+ destWidth: canvasWidth,
387
+ destHeight: canvasHeight,
388
+ success: res => {
389
+ let path = res.tempFilePath; //获取到了临时地址
390
+ // 保存临时路径,供按钮点击时使用
391
+ that.setData({
392
+ tempImagePath: path
393
+ });
394
+ // 触发绘制完成事件
395
+ that.triggerEvent('drawcomplete', { tempImagePath: path });
396
+ },
397
+ fail: (err) => {
398
+ console.error('生成临时图片失败:', err);
399
+ that.triggerEvent('error', { err });
400
+ }
401
+ }, that); //新版小程序不加this不会执行
402
+ }, 200);
403
+ });
404
+ },
405
+
406
+ /**
407
+ * 保存图片到相册
408
+ * 暴露给外部调用的方法
409
+ */
410
+ saveImage() {
411
+ const { loadingText, permissionModalTitle, permissionModalContent, permissionModalConfirmText, needPermissionText } = this.properties;
412
+
413
+ if (!this.data.tempImagePath) {
414
+ wx.showToast({
415
+ title: loadingText,
416
+ icon: 'none'
417
+ });
418
+ // 触发事件通知父组件
419
+ this.triggerEvent('saveerror', { message: loadingText });
420
+ return;
421
+ }
422
+
423
+ // 检查授权
424
+ wx.getSetting({
425
+ success: (res) => {
426
+ if (res.authSetting['scope.writePhotosAlbum']) {
427
+ // 已授权,直接保存
428
+ this.doSaveImage();
429
+ } else if (res.authSetting['scope.writePhotosAlbum'] === false) {
430
+ // 已拒绝授权,引导用户开启
431
+ wx.showModal({
432
+ title: permissionModalTitle,
433
+ content: permissionModalContent,
434
+ showCancel: true,
435
+ confirmText: permissionModalConfirmText,
436
+ success: (modalRes) => {
437
+ if (modalRes.confirm) {
438
+ wx.openSetting();
439
+ }
440
+ }
441
+ });
442
+ } else {
443
+ // 未授权,请求授权
444
+ wx.authorize({
445
+ scope: 'scope.writePhotosAlbum',
446
+ success: () => {
447
+ this.doSaveImage();
448
+ },
449
+ fail: () => {
450
+ wx.showToast({
451
+ title: needPermissionText,
452
+ icon: 'none'
453
+ });
454
+ this.triggerEvent('saveerror', { message: needPermissionText });
455
+ }
456
+ });
457
+ }
458
+ }
459
+ });
460
+ },
461
+
462
+ /**
463
+ * 执行保存图片
464
+ */
465
+ doSaveImage() {
466
+ const { saveSuccessText, saveErrorText } = this.properties;
467
+
468
+ wx.saveImageToPhotosAlbum({
469
+ filePath: this.data.tempImagePath,
470
+ success: () => {
471
+ wx.showToast({
472
+ title: saveSuccessText,
473
+ icon: 'success'
474
+ });
475
+ // 触发保存成功事件
476
+ this.triggerEvent('savesuccess', { tempImagePath: this.data.tempImagePath });
477
+ },
478
+ fail: (err) => {
479
+ wx.showToast({
480
+ title: saveErrorText,
481
+ icon: 'none'
482
+ });
483
+ console.error('保存图片失败:', err);
484
+ // 触发保存失败事件
485
+ this.triggerEvent('saveerror', { err });
486
+ }
487
+ });
488
+ }
489
+ }
490
+ });
package/poster.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "component": true,
3
+ "usingComponents": {}
4
+ }
package/poster.wxml ADDED
@@ -0,0 +1,16 @@
1
+ <!-- components/poster/poster.wxml -->
2
+ <view class="poster">
3
+ <canvas class="poster__canvas" canvas-id="canvasPoster" style="width:{{photoWidth}}px;height:{{photoHeight}}px;zoom:{{canvasZoom}}%"></canvas>
4
+ <view class="poster__image-container">
5
+ <image class="poster__image-container-image" src="{{backgroundImage}}"></image>
6
+ <view class="poster__white-area">
7
+ <view class="poster__white-area-left">
8
+ <view class="poster__text poster__text--primary">{{primaryText}}</view>
9
+ <view class="poster__text poster__text--secondary">{{secondaryText}}</view>
10
+ </view>
11
+ <view class="poster__white-area-right">
12
+ <image class="poster__qr-image" src="{{qrImage}}" />
13
+ </view>
14
+ </view>
15
+ </view>
16
+ </view>
package/poster.wxss ADDED
@@ -0,0 +1,77 @@
1
+ /* components/poster/poster.wxss - BEM风格 */
2
+
3
+ .poster {
4
+ display: flex;
5
+ flex-direction: column;
6
+ align-items: center;
7
+ padding: 20rpx;
8
+ position: relative;
9
+ }
10
+
11
+ /* Element: Canvas海报 */
12
+ .poster__canvas {
13
+ position: absolute;
14
+ left: 99999rpx;
15
+ top: 0rpx;
16
+ }
17
+
18
+ /* Element: 图片容器 */
19
+ .poster__image-container {
20
+ width: 600rpx;
21
+ height: 1025rpx;
22
+ margin: 0 auto;
23
+ margin-top: 50rpx;
24
+ box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
25
+ border-radius: 8rpx;
26
+ overflow: hidden;
27
+ }
28
+
29
+ /* Element: 主图片 */
30
+ .poster__image-container-image {
31
+ width: 100%;
32
+ height: 820rpx;
33
+ }
34
+
35
+ /* Element: 白色区域 */
36
+ .poster__white-area {
37
+ width: 600rpx;
38
+ height: 205rpx;
39
+ background-color: #fff;
40
+ display: flex;
41
+ justify-content: space-between;
42
+ align-items: center;
43
+ }
44
+
45
+ /* Element: 左侧文字区域 */
46
+ .poster__white-area-left {
47
+ display: flex;
48
+ flex-direction: column;
49
+ }
50
+
51
+ /* Element: 右侧二维码区域 */
52
+ .poster__white-area-right {
53
+ display: flex;
54
+ align-items: center;
55
+ margin-right: 30rpx;
56
+ }
57
+
58
+ /* Element: 文字 */
59
+ .poster__text {
60
+ padding-left: 30rpx;
61
+ }
62
+
63
+ .poster__text--primary {
64
+ font-size: 28rpx;
65
+ color: #000000;
66
+ }
67
+
68
+ .poster__text--secondary {
69
+ font-size: 24rpx;
70
+ color: #9c9c9c;
71
+ }
72
+
73
+ /* Element: 二维码图片 */
74
+ .poster__qr-image {
75
+ width: 120rpx;
76
+ height: 120rpx;
77
+ }