This commit is contained in:
2026-03-06 16:54:32 +08:00
parent be96b28828
commit 47594ed095
46 changed files with 3745 additions and 462 deletions

View File

@ -70,9 +70,16 @@
</view>
</view>
</view>
<canvas
id="myCanvas"
type="2d"
:style="{ width: canvasWidth + 'px', height: canvasHeight + 'px' }"
style="position: fixed; left: -9999px; top: -9999px;"
></canvas>
<!-- 隐藏的 wxml-to-canvas 组件用于生成证书图片 -->
<wxml-to-canvas class="widget" :width="canvasWidth" :height="canvasHeight" style="position: fixed; left: -9999px; top: -9999px;"></wxml-to-canvas>
<!-- <wxml-to-canvas class="widget" :width="canvasWidth" :height="canvasHeight" style="position: fixed; left: -9999px; top: -9999px;"></wxml-to-canvas> -->
</view>
</template>
@ -107,7 +114,7 @@ export default {
},
onReady() {
// 获取 wxml-to-canvas 组件实例
this.widget = this.$refs.widget || this.selectComponent('.widget');
// this.widget = this.$refs.widget || this.selectComponent('.widget');
},
methods: {
getCertificates(data) {
@ -125,6 +132,27 @@ export default {
closeModal() {
this.showModal = false;
},
wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const words = text.split('');
let line = '';
let currentY = y;
for (let i = 0; i < words.length; i++) {
const testLine = line + words[i];
const metrics = ctx.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && i > 0) {
ctx.fillText(line, x, currentY);
line = words[i];
currentY += lineHeight;
} else {
line = testLine;
}
}
ctx.fillText(line, x, currentY);
},
onScroll(e) {
// 获取滚动位置单位px
const scrollLeft = e.detail.scrollLeft;
@ -141,138 +169,172 @@ export default {
this.currentPage = newPage;
}
},
async saveCurrentImage() {
// 保存当前图片 - 使用 wxml-to-canvas 生成
const currentIndex = this.currentPage - 1;
const currentItem = this.certificateList[currentIndex];
if (!currentItem) {
uni.showToast({
title: '证书信息加载中,请稍候',
icon: 'none'
});
return;
}
// 存储当前证书信息到 data供模板使用
this.currentCertificateItem = currentItem;
// 获取组件实例 - 多种方式尝试
if (!this.widget) {
this.widget = this.$refs.widget;
}
if (!this.widget) {
this.widget = this.selectComponent('.widget');
}
if (!this.widget) {
// 延迟重试一次
await new Promise(resolve => setTimeout(resolve, 100));
this.widget = this.$refs.widget || this.selectComponent('.widget');
}
if (!this.widget) {
uni.hideLoading();
uni.showToast({
title: '组件初始化失败,请重试',
icon: 'none',
duration: 3000
});
console.error('wxml-to-canvas 组件获取失败');
return;
}
// 检查组件方法是否可用
if (typeof this.widget.renderToCanvas !== 'function') {
uni.hideLoading();
uni.showToast({
title: '组件方法不可用,请重试',
icon: 'none',
duration: 3000
});
console.error('renderToCanvas 方法不存在');
return;
}
uni.showLoading({
title: '生成图片中...',
mask: true
});
try {
// 固定画布尺寸
const canvasWidth = 317;
const canvasHeight = 224;
// 更新 canvas 尺寸
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
await this.$nextTick();
// 生成证书的 wxml 和 style使用固定尺寸
const { wxml, style } = this.generateCertificateTemplate();
// 渲染到 canvas
const container = await this.widget.renderToCanvas({ wxml, style });
// 转换为图片
const res = await this.widget.canvasToTempFilePath({
fileType: 'png',
quality: 1,
width: this.canvasWidth,
height: this.canvasHeight
});
if (!res || !res.tempFilePath) {
throw new Error('生成图片路径失败');
async loadImage(src) {
console.log('正在加载图片:', src);
return new Promise((resolve, reject) => {
tt.downloadFile({
url: src,
success: (res) => {
if (res.statusCode === 200) {
console.log('图片加载成功:', res.tempFilePath);
resolve(res.tempFilePath);
} else {
console.error('图片加载失败,状态码:', res.statusCode);
reject(new Error(`图片加载失败,状态码: ${res.statusCode}`));
}
// 保存到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
uni.hideLoading();
const errMsg = err?.errMsg || '未知错误';
uni.showToast({
title: '保存失败,请检查相册权限',
icon: 'none',
duration: 3000
});
console.error('保存图片失败:', errMsg, err);
},
fail: (err) => {
console.error('图片加载失败:', err.errMsg);
reject(new Error(`图片加载失败: ${err.errMsg}`));
}
});
});
},
async saveCurrentImage() {
const currentIndex = this.currentPage - 1;
const currentItem = this.certificateList[currentIndex];
if (!currentItem) {
uni.showToast({
title: '证书信息加载中,请稍候',
icon: 'none'
});
return;
}
this.currentCertificateItem = currentItem;
uni.showLoading({
title: '生成图片中...',
mask: true
});
try {
// 获取 Canvas 节点
const query = tt.createSelectorQuery();
const canvasNode = await new Promise((resolve, reject) => {
query
.select('#myCanvas')
.fields({ node: true, size: true })
.exec((res) => {
if (res[0]) {
resolve(res[0].node);
} else {
reject(new Error('Canvas 节点获取失败'));
}
});
} catch (error) {
});
// 获取 2D 上下文
const ctx = canvasNode.getContext('2d');
const dpr = tt.getSystemInfoSync().pixelRatio;
canvasNode.width = this.canvasWidth * dpr;
canvasNode.height = this.canvasHeight * dpr;
ctx.scale(dpr, dpr);
// 清空画布
ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight);
// 加载并绘制背景图
const backgroundImgPath = await this.loadImage(`${this.imgPrefix}certificateGround.png`);
const backgroundImg = canvasNode.createImage();
backgroundImg.src = backgroundImgPath;
await new Promise((resolve) => {
backgroundImg.onload = () => {
ctx.drawImage(backgroundImg, 0, 0, this.canvasWidth, this.canvasHeight);
resolve();
};
});
// 设置字体样式
ctx.font = '20px sans-serif';
ctx.fillStyle = '#FF19A0';
ctx.textAlign = 'center';
// 绘制证书标题
const title = currentItem.title || '为爱续航证书';
ctx.fillText(title, this.canvasWidth / 2, 50);
// 绘制英文标题
ctx.font = '12px sans-serif';
const titleEn = currentItem.title_en || 'Certificate of Love Endurance';
ctx.fillText(titleEn, this.canvasWidth / 2, 70);
// 绘制用户名
ctx.font = '16px sans-serif';
const userName = this.userName || '周佳佳';
ctx.fillText(userName, this.canvasWidth / 2, 100);
// 绘制描述文本
ctx.font = '12px sans-serif';
ctx.fillStyle = '#333333';
ctx.textAlign = 'left';
const description = `您累计捐赠${currentItem.source_value || 0}克粮,为毛孩子奉献爱心,点燃希望,感谢您的捐赠让世界变得更温暖。`;
this.wrapText(ctx, description, 20, 130, this.canvasWidth - 40, 16);
// 绘制结尾文字
ctx.fillText('特发此证,以表谢忱!', 20, 200);
// 绘制证书编号
ctx.font = '10px sans-serif';
ctx.fillStyle = '#FF19A0';
ctx.textAlign = 'right';
const certificateNo = `证书编号: ${currentItem.certificate_no || currentItem.id || '234546678896666788'}`;
ctx.fillText(certificateNo, this.canvasWidth - 20, this.canvasHeight - 20);
// 绘制证书图标
const badgeImgPath = await this.loadImage(currentItem.certificate_url);
const badgeImg = canvasNode.createImage();
badgeImg.src = badgeImgPath;
await new Promise((resolve) => {
badgeImg.onload = () => {
ctx.drawImage(badgeImg, this.canvasWidth - 60, this.canvasHeight - 80, 40, 40);
resolve();
};
});
// 导出为 base64 数据
const dataURL = canvasNode.toDataURL('image/png');
const base64Data = dataURL.replace(/^data:image\/\w+;base64,/, '');
// 将 base64 数据写入临时文件
const tempFilePath = `${tt.env.USER_DATA_PATH}/temp_certificate.png`;
const fs = tt.getFileSystemManager();
fs.writeFileSync(tempFilePath, base64Data, 'base64');
// 保存到相册
uni.saveImageToPhotosAlbum({
filePath: tempFilePath,
success: () => {
uni.hideLoading();
const errorMsg = error?.message || error?.toString() || '未知错误';
const errorInfo = `生成图片失败: ${errorMsg}`;
console.log(errorInfo, '--=')
uni.showToast({
title: errorInfo.length > 20 ? '生成图片失败,请重试' : errorInfo,
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
uni.hideLoading();
const errMsg = err?.errMsg || '未知错误';
uni.showToast({
title: '保存失败,请检查相册权限',
icon: 'none',
duration: 3000
});
// 使用多种方式记录错误,确保线上环境能捕获
console.error('生成证书图片失败:', error);
console.error('错误详情:', {
message: error?.message,
stack: error?.stack,
error: error
});
// 如果 console 不可用,尝试通过其他方式记录
if (typeof uni.reportError === 'function') {
uni.reportError({
error: errorInfo,
errorInfo: JSON.stringify(error)
});
}
console.error('保存图片失败:', errMsg, err);
}
},
});
} catch (error) {
uni.hideLoading();
const errorMsg = error?.message || error?.toString() || '未知错误';
uni.showToast({
title: errorMsg.length > 20 ? '生成图片失败,请重试' : errorMsg,
icon: 'none',
duration: 3000
});
console.error('生成证书图片失败:', error);
}
},
generateCertificateTemplate() {
// 使用存储的证书信息
const item = this.currentCertificateItem;