diff --git a/components-diy/diy-recommend-code.vue b/components-diy/diy-recommend-code.vue index 8024c65..94b1016 100644 --- a/components-diy/diy-recommend-code.vue +++ b/components-diy/diy-recommend-code.vue @@ -327,6 +327,9 @@ + + @@ -732,12 +735,197 @@ export default { onShareAppMessage() { const v = this.nc; const title = v.shareTitle || v.title || '邀您入驻低空经济产业平台'; + // 小程序环境下,生成「仅推荐码组件」的截图海报作为分享图 + // H5 平台不进入 canvas 路径,直接用 diyData.shareImageUrl + if (!this.isH5Platform) { + return new Promise((resolve) => { + // 先生成海报;失败或非小程序时回退到默认 imageUrl + this.generateSharePoster() + .then((posterPath) => { + resolve({ + title, + path: this.extractMiniProgramPath(), + imageUrl: posterPath || (v.shareImageUrl || '') + }); + }) + .catch(() => { + resolve({ + title, + path: this.extractMiniProgramPath(), + imageUrl: v.shareImageUrl || '' + }); + }); + }); + } return { title, path: this.extractMiniProgramPath(), imageUrl: v.shareImageUrl || '' }; }, + /** + * 生成推荐码组件的分享海报 + * 画布内容:白底 + 标题 + 推荐码大字 + 二维码(如果有) + 简短提示 + * 返回 Promise + */ + generateSharePoster() { + const v = this.nc; + const code = this.code || v.placeholderCode || 'ABCD1234'; + const title = v.title || '我的推荐码'; + const W = 375; + const H = 480; + return new Promise((resolve, reject) => { + try { + const ctx = uni.createCanvasContext('sharePosterCanvas', this); + if (!ctx) return reject(new Error('createCanvasContext 失败')); + // 1. 白底 + ctx.setFillStyle('#FFFFFF'); + ctx.fillRect(0, 0, W, H); + // 2. 顶部标题 + ctx.setFontSize(18); + ctx.setFillStyle('#1A1A1A'); + ctx.setTextAlign('left'); + ctx.fillText(title, 24, 44); + // 3. 提示文字 + ctx.setFontSize(12); + ctx.setFillStyle('#999999'); + ctx.fillText('分享给好友,邀请入驻获得奖励', 24, 70); + // 4. 米黄背景区 + ctx.setFillStyle('#FFF8F0'); + const blockY = 90, blockH = 120, blockX = 24, blockW = W - 48; + this._roundRect(ctx, blockX, blockY, blockW, blockH, 12); + ctx.fill(); + // 5. 推荐码(居中) + ctx.setFontSize(36); + ctx.setFillStyle(v.codeColor || '#DC143C'); + ctx.setTextAlign('center'); + ctx.setFontWeight('bold'); + ctx.fillText(code, W / 2, blockY + blockH / 2 + 12); + // 6. 二维码(如果有 url) + const qr = this.qrcodeRenderUrl; + if (qr) { + // 先下载到本地临时路径 + this._downloadImage(qr).then((localPath) => { + // 二维码绘制在中部下方,正方形 180x180 + const qrSize = 180; + const qrX = (W - qrSize) / 2; + const qrY = blockY + blockH + 24; + // 白色边框区 + ctx.setFillStyle('#FFFFFF'); + this._roundRect(ctx, qrX - 6, qrY - 6, qrSize + 12, qrSize + 12, 12); + ctx.fill(); + ctx.drawImage(localPath, qrX, qrY, qrSize, qrSize); + // 7. 底部提示 + ctx.setFontSize(11); + ctx.setFillStyle('#999999'); + ctx.setTextAlign('center'); + ctx.fillText(v.posterQrTip || '扫码进入并填写入驻信息', W / 2, qrY + qrSize + 24); + this._exportCanvas(W, H).then(resolve).catch(reject); + }).catch(() => { + // 二维码加载失败,直接导出无二维码版 + this._exportCanvas(W, H).then(resolve).catch(reject); + }); + } else { + this._exportCanvas(W, H).then(resolve).catch(reject); + } + } catch (e) { + reject(e); + } + }); + }, + /** + * canvas 转图片(uni.canvasToTempFilePath Promise 化) + */ + _exportCanvas(W, H) { + return new Promise((resolve, reject) => { + // 微信小程序: type=2d canvas,需要传入 canvas 实例 + // 旧版非 type=2d: 用 canvasId 字符串 + // 这里两种方式都尝试 + const query = uni.createSelectorQuery().in(this); + query.select('#sharePosterCanvas') + .fields({ node: true, size: true }) + .exec((res) => { + if (res && res[0] && res[0].node) { + // 新版 type=2d canvas + uni.canvasToTempFilePath({ + canvas: res[0].node, + width: W, + height: H, + destWidth: W * 2, + destHeight: H * 2, + fileType: 'png', + quality: 1, + success: (r) => resolve(r.tempFilePath), + fail: (err) => { + // 退化到旧 API + uni.canvasToTempFilePath({ + canvasId: 'sharePosterCanvas', + width: W, + height: H, + destWidth: W * 2, + destHeight: H * 2, + fileType: 'png', + quality: 1, + success: (r2) => resolve(r2.tempFilePath), + fail: (err2) => reject(err2 || err) + }, this); + } + }, this); + } else { + // 旧版 canvas:ctx.draw(false, ...) 后导出 + const ctx = uni.createCanvasContext('sharePosterCanvas', this); + if (ctx && ctx.draw) { + ctx.draw(false, () => { + uni.canvasToTempFilePath({ + canvasId: 'sharePosterCanvas', + width: W, + height: H, + destWidth: W * 2, + destHeight: H * 2, + fileType: 'png', + quality: 1, + success: (r) => resolve(r.tempFilePath), + fail: (err) => reject(err) + }, this); + }); + } else { + reject(new Error('canvas 初始化失败')); + } + } + }); + }); + }, + /** + * 圆角矩形路径(canvas API) + */ + _roundRect(ctx, x, y, w, h, r) { + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + ctx.closePath(); + }, + /** + * 下载图片到临时路径(Promise 化) + * 支持本地路径直接返回 + */ + _downloadImage(url) { + return new Promise((resolve, reject) => { + if (!url) return reject(new Error('url 为空')); + // 已是临时/本地路径 + if (!/^https?:\/\//i.test(url)) return resolve(url); + uni.downloadFile({ + url, + success: (res) => { + if (res.statusCode === 200 && res.tempFilePath) resolve(res.tempFilePath); + else reject(new Error('下载失败:' + res.statusCode)); + }, + fail: reject + }); + }); + }, onShareTimeline() { const v = this.nc; // 从H5分享链接中提取完整查询参数,与分享给好友保持一致