feat(diy-recommend-code): 微信小程序分享海报自动生成(仅推荐码组件)
之前微信小程序点击"立即分享"会触发 onShareAppMessage, 默认 imageUrl 用 diyData.shareImageUrl(为空时微信截全页)。 问题是分享图片是整张页面,而不是推荐码组件本身。 新增 generateSharePoster():用 uni.canvas 绘制一张仅含推荐码组件内容 的 375x480 海报: - 白底 + 顶部标题 - 米黄背景区 + 推荐码大字 - 二维码(如有 qrcodeRenderUrl) - 底部提示文字 并修改 onShareAppMessage: - 小程序环境(!isH5Platform):返回 Promise,等待 canvas 生成完成后 用临时文件路径作 imageUrl - 失败回退到 diyData.shareImageUrl - H5 环境:保持原逻辑(不走 canvas) 新增辅助方法: - _exportCanvas():canvas 转临时文件路径(支持 type=2d 和旧版 API) - _roundRect():绘制圆角矩形路径(canvas 2d 不直接支持 roundRect 时的兜底) - _downloadImage():网络图片下载到临时路径(uni.downloadFile Promise 化) 模板: - 在 root view 内加一个隐藏 canvas (canvas-id="sharePosterCanvas"), 仅小程序环境渲染,固定在视口外 left:-9999px top:-9999px
This commit is contained in:
@@ -327,6 +327,9 @@
|
|||||||
</view>
|
</view>
|
||||||
<!-- 登录组件占位(ns-login 全局 mixin 已注册) -->
|
<!-- 登录组件占位(ns-login 全局 mixin 已注册) -->
|
||||||
<ns-login ref="nsLoginBox" :popup="true" @getUserInfo="onLoginSuccess"></ns-login>
|
<ns-login ref="nsLoginBox" :popup="true" @getUserInfo="onLoginSuccess"></ns-login>
|
||||||
|
<!-- ============ 分享海报 canvas(隐藏,仅小程序分享时生成推荐码组件的截图) ============ -->
|
||||||
|
<canvas v-if="!isH5Platform" canvas-id="sharePosterCanvas" id="sharePosterCanvas"
|
||||||
|
style="position:fixed;left:-9999px;top:-9999px;width:375px;height:480px;z-index:-1;"></canvas>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -732,12 +735,197 @@ export default {
|
|||||||
onShareAppMessage() {
|
onShareAppMessage() {
|
||||||
const v = this.nc;
|
const v = this.nc;
|
||||||
const title = v.shareTitle || v.title || '邀您入驻低空经济产业平台';
|
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 {
|
return {
|
||||||
title,
|
title,
|
||||||
path: this.extractMiniProgramPath(),
|
path: this.extractMiniProgramPath(),
|
||||||
imageUrl: v.shareImageUrl || ''
|
imageUrl: v.shareImageUrl || ''
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 生成推荐码组件的分享海报
|
||||||
|
* 画布内容:白底 + 标题 + 推荐码大字 + 二维码(如果有) + 简短提示
|
||||||
|
* 返回 Promise<tempFilePath>
|
||||||
|
*/
|
||||||
|
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() {
|
onShareTimeline() {
|
||||||
const v = this.nc;
|
const v = this.nc;
|
||||||
// 从H5分享链接中提取完整查询参数,与分享给好友保持一致
|
// 从H5分享链接中提取完整查询参数,与分享给好友保持一致
|
||||||
|
|||||||
Reference in New Issue
Block a user