Files
wagoo-douy3/src/pageHome/welfare/certificate-list.vue
2026-03-06 16:54:32 +08:00

836 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view>
<view class="certificate-list-container" :class="{ 'empty-container': !certificateList || certificateList.length === 0 }">
<template v-if="certificateList && certificateList.length > 0">
<view v-for="(item, index) in certificateList" :key="item.id || index" class="certificate-item" @click="openModal(item, index)">
<image :src="item.certificate_url" mode="aspectFit" class="certificate-image" />
</view>
</template>
<view v-else class="certificate-placeholder">
<image :src="`${imgPrefix}certificatePlaceholder.png`" mode="aspectFit" class="placeholder-image" />
<text class="placeholder-text">暂无内容</text>
</view>
</view>
<!-- 遮罩层 -->
<view v-if="showModal" class="modal-overlay" @click="closeModal">
<view class="modal-content" @click.stop>
<scroll-view class="scroll-container" scroll-x @scroll="onScroll">
<view class="scroll-content">
<view
v-for="(item, index) in certificateList"
:key="item.id || index"
class="certificate-modal-item"
:class="{ 'first-item': index === 0, 'not-last-item': index !== certificateList.length - 1 }"
:style="{ backgroundImage: `url(${imgPrefix}certificateGround.png)` }"
>
<!-- 主要内容容器 - 居中 -->
<view class="certificate-main-content">
<!-- 证书标题 -->
<view class="certificate-title-section">
<text class="certificate-title">{{ item.title || '为爱续航证书' }}</text>
<text class="certificate-title-en">{{ item.title_en || 'Certificate of Love Endurance' }}</text>
<image class="certificate-line" :src="`${imgPrefix}certificateLine.png`" mode="aspectFit" />
</view>
<!-- 用户名称 -->
<view class="user-name-section">
<text class="user-name">{{ userName || '周佳佳' }}</text>
<text class="user-title">先生/女士</text>
</view>
<!-- 证书描述 -->
<view class="certificate-description description-text">{{ `您累计捐赠${item.source_value}克粮,为毛孩子奉献爱心,点燃希望,感谢您的捐赠让世界变得更温暖。` }}</view>
<!-- 证书结尾 -->
<view class="certificate-ending ending-text">特发此证,以表谢忱!</view>
</view>
<!-- 证书图标和编号 - 右下角 -->
<view class="certificate-footer-info">
<view class="certificate-badge">
<image :src="item.certificate_url" mode="aspectFit" class="badge-image" />
</view>
<text class="certificate-number">证书编号: {{ item.certificate_no || item.id || '234546678896666788' }}</text>
</view>
</view>
<!-- 右边距占位元素 -->
<view class="right-spacer"></view>
</view>
</scroll-view>
<!-- 计数器 -->
<view class="certificate-counter">
<text class="counter-text">{{ currentPage }}/{{ certificateList.length }}</text>
</view>
<!-- 操作按钮 -->
<view class="certificate-actions">
<button class="action-btn" @click.stop="saveCurrentImage">保存当前图片</button>
<!-- <button class="action-btn" @click.stop="shareCertificate">分享</button> -->
</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> -->
</view>
</template>
<script>
import { getCertificates as getCertificatesApi } from '@/api/user';
import { imgPrefix } from '@/utils/common';
export default {
name: 'CertificateList',
data() {
return {
imgPrefix,
certificateList: [],
showModal: false,
currentItem: null,
currentIndex: 0,
currentPage: 1,
canvasWidth: 317,
canvasHeight: 224,
currentCertificateItem: null // 存储当前要生成图片的证书信息
};
},
computed: {
userName() {
// 从 Vuex store 中获取用户信息
const userInfo = this.$store.state?.user?.userInfo || {};
return userInfo.username;
}
},
onShow() {
this.getCertificates();
},
onReady() {
// 获取 wxml-to-canvas 组件实例
// this.widget = this.$refs.widget || this.selectComponent('.widget');
},
methods: {
getCertificates(data) {
getCertificatesApi(data).then((res) => {
const list = res?.data;
this.certificateList = list;
});
},
openModal(item, index) {
this.currentItem = item;
this.currentIndex = index;
this.currentPage = index + 1;
this.showModal = true;
},
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;
// 获取系统信息,计算 rpx 到 px 的转换比例
const systemInfo = uni.getSystemInfoSync();
const rpxToPx = systemInfo.windowWidth / 750;
// 每个证书项的宽度634rpx加上间距20rpx
const itemWidth = (634 + 20) * rpxToPx;
// 计算当前显示的索引(四舍五入)
const index = Math.round(scrollLeft / itemWidth);
// 更新当前页码从1开始最大不超过列表长度
const newPage = Math.min(Math.max(index + 1, 1), this.certificateList.length);
if (newPage !== this.currentPage) {
this.currentPage = newPage;
}
},
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}`));
}
},
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 节点获取失败'));
}
});
});
// 获取 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();
uni.showToast({
title: '保存成功',
icon: 'success'
});
},
fail: (err) => {
uni.hideLoading();
const errMsg = err?.errMsg || '未知错误';
uni.showToast({
title: '保存失败,请检查相册权限',
icon: 'none',
duration: 3000
});
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;
if (!item) {
throw new Error('证书信息不存在');
}
// 固定容器尺寸
const width = 317;
const height = 224;
// 固定内容宽度和位置(根据设计图调整)
const mainContentWidth = 250;
const mainContentLeft = (width - mainContentWidth) / 2;
// 确保数据正确转义
const title = (item.title || '为爱续航证书').replace(/"/g, '&quot;');
const titleEn = (item.title_en || 'Certificate of Love Endurance').replace(/"/g, '&quot;');
const rawUserName = this.userName || '周佳佳';
const sanitizedUserName = rawUserName.replace(/"/g, '&quot;');
const description = `您累计捐赠${item.source_value || 0}克粮,为毛孩子奉献爱心,点燃希望,感谢您的捐赠让世界变得更温暖。`.replace(/"/g, '&quot;');
const certificateNo = `证书编号: ${item.certificate_no || item.id || '234546678896666788'}`.replace(/"/g, '&quot;');
const nameLength = rawUserName.length;
const estimatedCharWidth = 18;
const minNameWidth = estimatedCharWidth * 2;
const maxNameWidth = mainContentWidth - 60;
const nameWrapperWidth = Math.min(maxNameWidth, Math.max(estimatedCharWidth * nameLength, minNameWidth));
const wxml = `
<view class="container">
<image class="bg-image" src="${this.imgPrefix}certificateGround.png"></image>
<view class="main-content">
<view class="title-section">
<text class="title">${title}</text>
<text class="title-en">${titleEn}</text>
<image class="line-image" src="${this.imgPrefix}certificateLine.png"></image>
</view>
<view class="user-section">
<view class="name-wrapper">
<text class="user-name">${sanitizedUserName}</text>
<view class="underline"></view>
</view>
<text class="user-title">先生/女士</text>
</view>
<view class="description">
<text class="description-text">${description}</text>
</view>
<view class="ending">
<text class="ending-text">特发此证,以表谢忱!</text>
</view>
</view>
<view class="footer-info">
<view class="badge">
<image class="badge-image" src="${item.certificate_url || ''}"></image>
</view>
<text class="certificate-number">${certificateNo}</text>
</view>
</view>
`;
const style = {
container: {
width: width,
height: height,
position: 'relative',
backgroundColor: '#ffffff',
left: 0,
top: 0,
overflow: 'hidden'
},
bgImage: {
width: width,
height: height,
position: 'absolute',
left: 0,
top: 0,
zIndex: 0
},
mainContent: {
width: mainContentWidth,
height: height - 100,
position: 'absolute',
left: mainContentLeft,
top: 38,
flexDirection: 'column',
alignItems: 'center'
},
titleSection: {
width: mainContentWidth,
flexDirection: 'column',
alignItems: 'center',
marginBottom: 7
},
title: {
width: mainContentWidth,
height: 25,
fontSize: 20,
color: '#FF19A0',
textAlign: 'center',
verticalAlign: 'middle',
lineHeight: 20,
marginBottom: 1
},
titleEn: {
width: mainContentWidth,
height: 15,
fontSize: 8,
color: '#FF19A0',
textAlign: 'center',
verticalAlign: 'middle',
lineHeight: 8,
marginBottom: 4
},
lineImage: {
width: mainContentWidth,
height: 2
},
userSection: {
width: mainContentWidth,
height: 15,
flexDirection: 'row',
alignItems: 'baseline',
marginBottom: 4
},
nameWrapper: {
width: nameWrapperWidth,
height: 15,
flexDirection: 'column',
marginRight: 2,
position: 'relative'
},
userName: {
width: nameWrapperWidth,
height: 18,
fontSize: 14,
color: '#FF19A0',
verticalAlign: 'top',
lineHeight: 14
},
underline: {
width: nameWrapperWidth,
height: 1,
backgroundColor: '#FF19A0',
position: 'absolute',
left: 0,
top: 15
},
userTitle: {
width: 50,
height: 15,
fontSize: 11,
color: '#333333',
verticalAlign: 'top',
lineHeight: 11
},
description: {
width: mainContentWidth,
height: 75,
marginBottom: 4
},
descriptionText: {
width: mainContentWidth,
height: 75,
fontSize: 11,
color: '#333333',
textAlign: 'left',
verticalAlign: 'top',
lineHeight: 16.5
},
ending: {
width: mainContentWidth,
height: 11
},
endingText: {
width: mainContentWidth,
height: 15,
fontSize: 11,
color: '#333333',
textAlign: 'left',
verticalAlign: 'top',
lineHeight: 11
},
footerInfo: {
width: 100,
height: 60,
position: 'absolute',
right: 17,
bottom: 17,
flexDirection: 'column',
alignItems: 'flex-end'
},
badge: {
width: 40,
height: 40,
marginBottom: 10
},
badgeImage: {
width: 40,
height: 40
},
certificateNumber: {
width: 100,
height: 10,
fontSize: 7,
color: '#FF19A0',
textAlign: 'right',
verticalAlign: 'top',
lineHeight: 7
}
};
return { wxml, style };
},
shareCertificate() {
// 分享证书
const currentIndex = this.currentPage - 1;
const currentItem = this.certificateList[currentIndex];
if (!currentItem) {
uni.showToast({
title: '证书信息加载中,请稍候',
icon: 'none'
});
return;
}
// 使用微信小程序的分享功能
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
});
// 或者显示分享提示
uni.showToast({
title: '请点击右上角分享',
icon: 'none',
duration: 2000
});
}
}
};
</script>
<style lang="scss" scoped>
.certificate-list-container {
margin: 20rpx;
background-color: #fff;
padding: 20rpx;
border-radius: 16rpx;
display: flex;
flex-wrap: wrap;
&.empty-container {
min-height: calc(100vh - 80rpx);
height: 100%;
}
}
.certificate-item {
flex: 0 0 calc(100% / 3);
display: flex;
justify-content: center;
}
.certificate-image {
width: 200rpx;
height: 200rpx;
}
.certificate-placeholder {
width: 100%;
min-height: 400rpx;
background-color: #fff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
}
.placeholder-image {
width: 213.15px;
height: 160px;
margin-bottom: 20rpx;
}
.placeholder-text {
font-size: 28rpx;
color: #999;
}
// 遮罩层样式
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: rgba(0, 0, 0, 0.5);
z-index: 999;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.modal-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.scroll-content{
display: flex;
}
.certificate-modal-item.not-last-item {
margin-right: 20rpx;
}
.right-spacer {
flex-shrink: 0;
width: 40rpx;
height: 1rpx;
}
.certificate-counter {
// position: absolute;
// bottom: 40rpx;
// left: 50%;
// transform: translateX(-50%);
// z-index: 1000;
margin-top: 20rpx;
}
.counter-text {
font-size: 24rpx;
color: #fff;
}
.certificate-actions {
display: flex;
margin-top: 40rpx;
justify-content: center;
}
.action-btn {
width: 152.5px;
height: 40px;
font-size: 16px;
font-weight: 500;
border: none;
border-radius: 4rpx;
background: #FF19A0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
border-radius: 400rpx;
padding: 0;
}
.action-btn:first-child {
margin-right: 24rpx;
}
.action-btn::after {
border: none;
}
.action-btn:active {
opacity: 0.8;
}
.certificate-modal-item {
width: calc(100vw - 80rpx);
height: 448rpx;
background-size: 100% 100%;
background-position: center;
background-repeat: no-repeat;
flex-shrink: 0;
position: relative;
box-sizing: border-box;
}
.certificate-modal-item.first-item {
margin-left: 40rpx;
}
.certificate-main-content {
width: 502rpx;
position: absolute;
top: 76rpx;
left: 50%;
transform: translate(-50%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.certificate-title-section {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 14rpx;
width: 100%;
}
.certificate-title {
font-size: 40rpx;
font-weight: bold;
color: #FF19A0;
margin-bottom: 2rpx;
text-align: center;
}
.certificate-title-en {
font-size: 16rpx;
color: #FF19A0;
text-align: center;
margin-bottom: 8rpx;
}
.certificate-line {
width: 100%;
height: 4rpx;
}
.user-name-section {
display: flex;
align-items: baseline;
margin-bottom: 8rpx;
width: 100%;
}
.user-name {
font-size: 28rpx;
color: #FF19A0;
margin-right: 4rpx;
text-decoration: underline;
}
.user-title {
font-size: 22rpx;
}
.certificate-description {
width: 100%;
box-sizing: border-box;
margin-bottom: 8rpx;
font-size: 22rpx;
color: #333;
text-align: left;
}
.certificate-ending {
width: 100%;
font-size: 22rpx;
color: #333;
}
.certificate-footer-info {
position: absolute;
bottom: 34rpx;
right: 34rpx;
display: flex;
flex-direction: column;
align-items: flex-end;
}
.certificate-badge {
display: flex;
flex-direction: column;
align-items: center;
}
.badge-image {
width: 80rpx;
height: 80rpx;
margin-bottom: 36rpx;
margin-right: 36rpx;
}
.certificate-number {
font-size: 14rpx;
color: #FF19A0;
text-align: right;
}
.modal-certificate-image {
width: 100%;
height: auto;
max-width: 600rpx;
}
</style>