|
|
@@ -1,276 +1,611 @@
|
|
|
-<template>
|
|
|
- <view class="common_page adffc" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
|
|
|
- <cus-header title="申领社会实践记录" bgColor="#FFFFFF"></cus-header>
|
|
|
- <div class="prove adffcac">
|
|
|
- <image class="prove-logo" mode="widthFix" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/965dc74b-fc45-4409-aa09-56877e75d2bc.png"></image>
|
|
|
- <image class="prove-title" mode="widthFix" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/7c341572-58b7-4afb-801f-93bf9890fa76.png"></image>
|
|
|
- <image class="prove-line" mode="widthFix" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/c21e680c-e83a-4f54-948c-19c49141ae99.png"></image>
|
|
|
- <div class="prove-no">证书编号:{{'650102000105375'}}</div>
|
|
|
- <div class="prove-info">
|
|
|
- <div class="prove-info-pre adf">
|
|
|
- <div class="prove-info-pre-left">
|
|
|
- <div class="prove-info-pre-left-text">义工服务姓名:</div>
|
|
|
- <div class="prove-info-pre-left-tip">Volunteer service Name</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre-right">{{'龙傲天'}}</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre adf">
|
|
|
- <div class="prove-info-pre-left">
|
|
|
- <div class="prove-info-pre-left-text space">所属学校:</div>
|
|
|
- <div class="prove-info-pre-left-tip">Affiliated school</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre-right">{{'深圳实验中学'}}</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre adf">
|
|
|
- <div class="prove-info-pre-left">
|
|
|
- <div class="prove-info-pre-left-text space">证件类型:</div>
|
|
|
- <div class="prove-info-pre-left-tip">Type of ID</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre-right">{{'居民身份证'}}</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre adf">
|
|
|
- <div class="prove-info-pre-left">
|
|
|
- <div class="prove-info-pre-left-text space">证件号码:</div>
|
|
|
- <div class="prove-info-pre-left-tip">IdCard No</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre-right">{{'342221********9876'}}</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre adf">
|
|
|
- <div class="prove-info-pre-left">
|
|
|
- <div class="prove-info-pre-left-text">义工服务时长:</div>
|
|
|
- <div class="prove-info-pre-left-tip">Volunteer service Time</div>
|
|
|
- </div>
|
|
|
- <div class="prove-info-pre-right">{{80}}小时</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="prove-memo">
|
|
|
- 您累计参与了 <span>{{5}}</span> 场活动,捐赠了 <span>{{2000}}</span> 爱心值。向您践行志愿精神,为社会进步奉献力量致以最崇高的敬意。
|
|
|
- </div>
|
|
|
- <div class="prove-memo" style="margin-top: 30rpx;">特发此证!</div>
|
|
|
- <div class="prove-bottom">
|
|
|
- <div class="prove-bottom-pre">证明单位:善行少年服务基金会</div>
|
|
|
- <div class="prove-bottom-pre">发证日期:{{'2025年06月30日'}}</div>
|
|
|
- </div>
|
|
|
- <image class="prove-seal" mode="widthFix" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/8ad6323d-f6a0-4057-8063-7eec97ec93f4.png"></image>
|
|
|
- </div>
|
|
|
- <div class="list">
|
|
|
- <div class="list-box" v-for="(item,index) in list" :key="index">
|
|
|
- <div class="title">{{'感恩有你 温暖前行'}}</div>
|
|
|
- <div class="content adf">
|
|
|
- <div class="left">
|
|
|
- <image :src="'https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/11/d3c53597-a848-4a33-8deb-ab256f028baa.png'"></image>
|
|
|
- </div>
|
|
|
- <div class="right">
|
|
|
+<template>
|
|
|
+ <!-- 页面主体内容 -->
|
|
|
+ <view class="common_page adffc" :style="{'min-height':h+'px', 'padding-top':mt+'px'}">
|
|
|
+ <cus-header title="申领社会实践记录" bgColor="#FFFFFF"></cus-header>
|
|
|
+ <!-- 证书部分 -->
|
|
|
+ <div class="prove adffcac">
|
|
|
+ <image class="prove-logo" mode="widthFix" :src="certificateData.logoUrl"></image>
|
|
|
+ <image class="prove-title" mode="widthFix" :src="certificateData.titleUrl"></image>
|
|
|
+ <image class="prove-line" mode="widthFix" :src="certificateData.lineUrl"></image>
|
|
|
+ <div class="prove-no">证书编号:{{ certificateData.certificateNumber }}</div>
|
|
|
+ <div class="prove-info">
|
|
|
+ <div class="prove-info-pre adf">
|
|
|
+ <div class="prove-info-pre-left">
|
|
|
+ <div class="prove-info-pre-left-text">义工服务姓名:</div>
|
|
|
+ <div class="prove-info-pre-left-tip">Volunteer service Name</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre-right">{{ certificateData.memberName }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre adf">
|
|
|
+ <div class="prove-info-pre-left">
|
|
|
+ <div class="prove-info-pre-left-text space">所属学校:</div>
|
|
|
+ <div class="prove-info-pre-left-tip">Affiliated school</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre-right">{{ certificateData.currentSchool }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre adf">
|
|
|
+ <div class="prove-info-pre-left">
|
|
|
+ <div class="prove-info-pre-left-text space">证件类型:</div>
|
|
|
+ <div class="prove-info-pre-left-tip">Type of ID</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre-right">{{ certificateData.idType }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre adf">
|
|
|
+ <div class="prove-info-pre-left">
|
|
|
+ <div class="prove-info-pre-left-text space">证件号码:</div>
|
|
|
+ <div class="prove-info-pre-left-tip">IdCard No</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre-right">{{ certificateData.idCard }}</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre adf">
|
|
|
+ <div class="prove-info-pre-left">
|
|
|
+ <div class="prove-info-pre-left-text">义工服务时长:</div>
|
|
|
+ <div class="prove-info-pre-left-tip">Volunteer service Time</div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-info-pre-right">{{ certificateData.volunteerHours }}小时</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="prove-memo">
|
|
|
+ 您累计参与了 <span>{{ certificateData.welfareCount }}</span> 场活动,捐赠了 <span>{{ certificateData.loveValue }}</span> 爱心值。向您践行志愿精神,为社会进步奉献力量致以最崇高的敬意。
|
|
|
+ </div>
|
|
|
+ <div class="prove-memo" style="margin-top: 30rpx;">特发此证!</div>
|
|
|
+ <div class="prove-bottom">
|
|
|
+ <div class="prove-bottom-pre">证明单位:{{ certificateData.issuer }}</div>
|
|
|
+ <div class="prove-bottom-pre">发证日期:{{ certificateData.createDate }}</div>
|
|
|
+ </div>
|
|
|
+ <image class="prove-seal" mode="widthFix" :src="certificateData.sealUrl"></image>
|
|
|
+ </div>
|
|
|
+ <!-- 列表部分 -->
|
|
|
+ <div class="list">
|
|
|
+ <div class="list-box" v-for="(item,index) in activityList" :key="index">
|
|
|
+ <div class="title">{{ item.activityName||'' }}</div>
|
|
|
+ <div class="content adf">
|
|
|
+ <div class="right">
|
|
|
+ <div class="right-pre adf">
|
|
|
+ <div class="tip">活动时间:</div>
|
|
|
+ <div class="text">{{ item.activityStartTime||'' }}</div>
|
|
|
+ </div>
|
|
|
<div class="right-pre adf">
|
|
|
- <div class="tip">活动时间:</div>
|
|
|
- <div class="text">{{'2025-06-01 15:00'}}</div>
|
|
|
- </div>
|
|
|
- <div class="right-pre adf">
|
|
|
- <div class="tip">爱心值贡献:</div>
|
|
|
- <div class="text">{{'200/每捐赠100爱心值可支持10本图书'}}</div>
|
|
|
- </div>
|
|
|
- <div class="right-pre adf">
|
|
|
- <div class="tip">义工时长:</div>
|
|
|
- <div class="text">{{'3小时'}}</div>
|
|
|
- </div>
|
|
|
- <div class="right-pre adf">
|
|
|
- <div class="tip">公益合作:</div>
|
|
|
- <div class="text">{{'深圳善行少年基金会'}}</div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- </div>
|
|
|
- <div class="btn">下载</div>
|
|
|
- </view>
|
|
|
-</template>
|
|
|
-
|
|
|
-<script setup name="">
|
|
|
+ <template v-if="item.activityLimit==2">
|
|
|
+ <div class="tip">专享券贡献:</div>
|
|
|
+ <div class="text">{{ item.valueLimit||0 }}张</div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="tip">爱心值贡献:</div>
|
|
|
+ <div class="text">{{ item.valueLimit||0 }}</div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <div class="right-pre adf">
|
|
|
+ <div class="tip">义工时长:</div>
|
|
|
+ <div class="text">{{ item.serviceHours||0 }}小时</div>
|
|
|
+ </div>
|
|
|
+ <div class="right-pre adf">
|
|
|
+ <div class="tip">公益合作:</div>
|
|
|
+ <div class="text">{{ item.channelName||'' }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 下载按钮 -->
|
|
|
+ <div class="btn" @click="handleDownload">
|
|
|
+ {{ isLoading ? '生成中...' : '下载' }}
|
|
|
+ </div>
|
|
|
+ </view>
|
|
|
+ <canvas canvas-id="pdf-canvas" :style="'width:' + canvasWidth + 'px; height:' + canvasHeight + 'px; position: fixed; left: -9999px; top: -9999px;'"></canvas>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
import CusHeader from '@/components/CusHeader/index.vue'
|
|
|
- import { ref } from 'vue'
|
|
|
+ import { onLoad } from '@dcloudio/uni-app'
|
|
|
+ import { ref, computed, getCurrentInstance } from 'vue'
|
|
|
+ const { proxy } = getCurrentInstance()
|
|
|
+
|
|
|
+ // 页面数据,实际项目中这些数据应该是动态获取的
|
|
|
+ const certificateData = ref({
|
|
|
+ logoUrl: 'https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/965dc74b-fc45-4409-aa09-56877e75d2bc.png',
|
|
|
+ titleUrl: 'https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/7c341572-58b7-4afb-801f-93bf9890fa76.png',
|
|
|
+ lineUrl: 'https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/c21e680c-e83a-4f54-948c-19c49141ae99.png',
|
|
|
+ backgroundUrl: 'https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/57b2f49c-beac-49e7-8840-777ec860ae59.png',
|
|
|
+ sealUrl: 'https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/8ad6323d-f6a0-4057-8063-7eec97ec93f4.png',
|
|
|
+ certificateNumber: '',
|
|
|
+ memberName: '',
|
|
|
+ currentSchool: '',
|
|
|
+ idType: '居民身份证',
|
|
|
+ idCard: '',
|
|
|
+ volunteerHours: 0,
|
|
|
+ welfareCount: 0,
|
|
|
+ loveValue: 0,
|
|
|
+ issuer: '善行少年服务基金会',
|
|
|
+ createDate: ''
|
|
|
+ });
|
|
|
+
|
|
|
+ const activityList = ref([]);
|
|
|
+
|
|
|
+ const isLoading = ref(false);
|
|
|
+ const canvasWidth = ref(0);
|
|
|
+ const canvasHeight = ref(0);
|
|
|
+
|
|
|
+ // rpx转px的工具函数
|
|
|
+ const rpxToPx = (rpx) => {
|
|
|
+ const screenWidth = uni.getSystemInfoSync().windowWidth;
|
|
|
+ return (screenWidth / 750) * rpx;
|
|
|
+ };
|
|
|
+
|
|
|
+ // 文本换行绘制函数
|
|
|
+ const drawWrappedText = (ctx, text, x, y, maxWidth, lineHeight) => {
|
|
|
+ const words = text.split('');
|
|
|
+ let line = '';
|
|
|
+ let currentY = y;
|
|
|
+
|
|
|
+ for (let n = 0; n < words.length; n++) {
|
|
|
+ const testLine = line + words[n];
|
|
|
+ const metrics = ctx.measureText(testLine);
|
|
|
+ const testWidth = metrics.width;
|
|
|
+ if (testWidth > maxWidth && n > 0) {
|
|
|
+ ctx.fillText(line, x, currentY);
|
|
|
+ line = words[n];
|
|
|
+ currentY += lineHeight;
|
|
|
+ } else {
|
|
|
+ line = testLine;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ ctx.fillText(line, x, currentY);
|
|
|
+ return currentY - y + lineHeight; // 返回这段文本所占的总高度
|
|
|
+ };
|
|
|
+
|
|
|
+ // 获取网络图片信息,返回一个Promise
|
|
|
+ const getImageInfo = (url) => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ uni.getImageInfo({
|
|
|
+ src: url,
|
|
|
+ success: (res) => resolve(res),
|
|
|
+ fail: (err) => reject(err),
|
|
|
+ });
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ // 主下载处理函数
|
|
|
+ // 主下载处理函数
|
|
|
+ const handleDownload = async () => {
|
|
|
+ if (isLoading.value) return;
|
|
|
+ isLoading.value = true;
|
|
|
+ uni.showLoading({ title: '正在生成图片...', mask: true });
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 1. 动态计算Canvas尺寸
|
|
|
+ const proveHeight = rpxToPx(945);
|
|
|
+ const pagePaddingTop = rpxToPx(20); // prove-margin-top
|
|
|
+ const pagePaddingBottom = rpxToPx(184); // 页面底部到按钮的距离
|
|
|
+
|
|
|
+ // 估算一个列表项的高度: 上下padding(36*2) + 标题(32) + 标题margin(30) + 4行内容(4*50) + 列表项间距(20)
|
|
|
+ // 这里的 4*50 是一个估算值,基于你的CSS
|
|
|
+ const singleListHeight = rpxToPx(36 * 2 + 32 + 30 + (20 + 24) * 4 + 20); // 估算每行高度(margin-top + font-size)
|
|
|
+
|
|
|
+ canvasWidth.value = uni.getSystemInfoSync().windowWidth;
|
|
|
+ // 总高度 = 证书高度 + 列表总高度 + 页面顶部边距 + 底部空白区域
|
|
|
+ canvasHeight.value = proveHeight + (singleListHeight * activityList.value.length) + pagePaddingTop + pagePaddingBottom;
|
|
|
+
|
|
|
+ // 2. 预加载所有图片
|
|
|
+ const imagesToLoad = [
|
|
|
+ certificateData.value.backgroundUrl,
|
|
|
+ certificateData.value.logoUrl,
|
|
|
+ certificateData.value.titleUrl,
|
|
|
+ certificateData.value.lineUrl,
|
|
|
+ certificateData.value.sealUrl
|
|
|
+ ];
|
|
|
+ const imageInfos = await Promise.all(imagesToLoad.map(url => getImageInfo(url)));
|
|
|
+
|
|
|
+ const [
|
|
|
+ bgImg, logoImg, titleImg, lineImg, sealImg
|
|
|
+ ] = imageInfos.map(info => info.path);
|
|
|
+
|
|
|
+ // 3. 开始绘制
|
|
|
+ const ctx = uni.createCanvasContext('pdf-canvas', proxy); // 传入 proxy
|
|
|
+
|
|
|
+ // 绘制白色背景
|
|
|
+ ctx.setFillStyle('#F5F5F5');
|
|
|
+ ctx.fillRect(0, 0, canvasWidth.value, canvasHeight.value);
|
|
|
+
|
|
|
+ let currentY = rpxToPx(20); // 从顶部20rpx的margin开始
|
|
|
+
|
|
|
+ // ---- 3.1 绘制证书 (这部分逻辑基本正确,无需大改) ----
|
|
|
+ const proveX = 0;
|
|
|
+ const proveWidth = canvasWidth.value;
|
|
|
+
|
|
|
+ // 绘制证书背景
|
|
|
+ ctx.drawImage(bgImg, proveX, currentY, proveWidth, proveHeight);
|
|
|
+
|
|
|
+ // 绘制Logo
|
|
|
+ ctx.drawImage(logoImg, rpxToPx(72), currentY + rpxToPx(58), rpxToPx(133), rpxToPx(40));
|
|
|
+ // 绘制标题
|
|
|
+ ctx.drawImage(titleImg, (proveWidth - rpxToPx(363)) / 2, currentY + rpxToPx(101), rpxToPx(363), rpxToPx(40));
|
|
|
+ // 绘制线条
|
|
|
+ ctx.drawImage(lineImg, (proveWidth - rpxToPx(400)) / 2, currentY + rpxToPx(101 + 40 + 7), rpxToPx(400), rpxToPx(5));
|
|
|
+
|
|
|
+ // 绘制证书编号
|
|
|
+ ctx.setFontSize(rpxToPx(20));
|
|
|
+ ctx.setFillStyle('#9F793F');
|
|
|
+ ctx.setTextAlign('center');
|
|
|
+ ctx.fillText(`证书编号:${certificateData.value.certificateNumber}`, proveWidth / 2, currentY + rpxToPx(190));
|
|
|
+
|
|
|
+ // 绘制个人信息
|
|
|
+ ctx.setTextAlign('left');
|
|
|
+ const infoStartX = (proveWidth - rpxToPx(135 + 188)) / 2;
|
|
|
+ const infoStartY = currentY + rpxToPx(244);
|
|
|
+ const infoItems = [
|
|
|
+ { label: '义工服务姓名:', value: certificateData.value?.memberName||'', tip: 'Volunteer service Name' },
|
|
|
+ { label: '所属学校:', value: certificateData.value?.currentSchool||'', tip: 'Affiliated school' },
|
|
|
+ { label: '证件类型:', value: certificateData.value?.idType, tip: 'Type of ID' },
|
|
|
+ { label: '证件号码:', value: certificateData.value?.idCard||'', tip: 'IdCard No' },
|
|
|
+ { label: '义工服务时长:', value: `${certificateData.value?.volunteerHours||0}小时`, tip: 'Volunteer service Time' }
|
|
|
+ ];
|
|
|
+ infoItems.forEach((item, index) => {
|
|
|
+ const itemY = infoStartY + index * rpxToPx(40);
|
|
|
+ ctx.setFillStyle('#252525');
|
|
|
+ ctx.setFontSize(rpxToPx(17));
|
|
|
+ ctx.fillText(item.label, infoStartX, itemY);
|
|
|
+ ctx.setFontSize(rpxToPx(10));
|
|
|
+ ctx.fillText(item.tip, infoStartX, itemY + rpxToPx(13));
|
|
|
+
|
|
|
+ const valueX = infoStartX + rpxToPx(135);
|
|
|
+ ctx.setFontSize(rpxToPx(17));
|
|
|
+ ctx.fillText(item.value, valueX, itemY);
|
|
|
+
|
|
|
+ ctx.setStrokeStyle('#DDCEAF');
|
|
|
+ ctx.setLineWidth(rpxToPx(1)); // 1px的线在高清屏下更清晰
|
|
|
+ ctx.beginPath();
|
|
|
+ ctx.moveTo(valueX, itemY + rpxToPx(8));
|
|
|
+ ctx.lineTo(valueX + rpxToPx(188), itemY + rpxToPx(8));
|
|
|
+ ctx.stroke();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 绘制memo
|
|
|
+ ctx.setFontSize(rpxToPx(20));
|
|
|
+ ctx.setFillStyle('#252525');
|
|
|
+ // Canvas无法识别<span>,需要分段绘制或在JS中处理颜色
|
|
|
+ const memoText1 = `您累计参与了 `;
|
|
|
+ const memoText2 = `${certificateData.value.welfareCount}`;
|
|
|
+ const memoText3 = ` 场活动,捐赠了 `;
|
|
|
+ const memoText4 = `${certificateData.value.loveValue}`;
|
|
|
+ const memoText5 = ` 爱心值。向您践行志愿精神,为社会进步奉献力量致以最崇高的敬意。`;
|
|
|
+ const memoY = currentY + rpxToPx(500);
|
|
|
+ const memoX = rpxToPx(80);
|
|
|
+
|
|
|
+ ctx.fillText(memoText1, memoX, memoY);
|
|
|
+ let currentX = memoX + ctx.measureText(memoText1).width;
|
|
|
+ ctx.setFillStyle('#C9A771'); // 设置高亮颜色
|
|
|
+ ctx.fillText(memoText2, currentX, memoY);
|
|
|
+ currentX += ctx.measureText(memoText2).width;
|
|
|
+ ctx.setFillStyle('#252525'); // 恢复默认颜色
|
|
|
+ ctx.fillText(memoText3, currentX, memoY);
|
|
|
+ currentX += ctx.measureText(memoText3).width;
|
|
|
+ ctx.setFillStyle('#C9A771'); // 设置高亮颜色
|
|
|
+ ctx.fillText(memoText4, currentX, memoY);
|
|
|
+ currentX += ctx.measureText(memoText4).width;
|
|
|
+ ctx.setFillStyle('#252525'); // 恢复默认颜色
|
|
|
+ // 后续文本太长,需要换行处理,这里简化为直接绘制,实际可能需要drawWrappedText
|
|
|
+ drawWrappedText(ctx, memoText5, currentX, memoY, proveWidth - rpxToPx(160) - (currentX-memoX), rpxToPx(31));
|
|
|
+
|
|
|
+ ctx.fillText('特发此证!', rpxToPx(80), currentY + rpxToPx(600));
|
|
|
+
|
|
|
+ // 绘制证明单位和日期
|
|
|
+ ctx.setFontSize(rpxToPx(17));
|
|
|
+ ctx.fillText(`证明单位:${certificateData.value.issuer}`, rpxToPx(80), currentY + rpxToPx(700));
|
|
|
+ ctx.fillText(`发证日期:${certificateData.value.createDate}`, rpxToPx(80), currentY + rpxToPx(730));
|
|
|
+
|
|
|
+ // 绘制印章
|
|
|
+ ctx.drawImage(sealImg, proveWidth - rpxToPx(73 + 131), currentY + proveHeight - rpxToPx(69 + 131), rpxToPx(131), rpxToPx(131));
|
|
|
+
|
|
|
+ currentY += proveHeight; // 更新Y坐标到证书底部
|
|
|
+
|
|
|
+ // ---- 3.2 绘制活动列表 ----
|
|
|
+ for (const item of activityList.value) {
|
|
|
+ currentY += rpxToPx(20); // 列表项间距
|
|
|
+
|
|
|
+ const boxX = rpxToPx(24);
|
|
|
+ const boxWidth = canvasWidth.value - rpxToPx(48);
|
|
|
+
|
|
|
+ let itemContentHeight = 0; // 动态计算每个item的高度
|
|
|
+
|
|
|
+ // 预计算高度
|
|
|
+ const titleHeight = rpxToPx(32);
|
|
|
+ const contentPaddingTop = rpxToPx(36);
|
|
|
+ const titleMarginBottom = rpxToPx(30);
|
|
|
+ const contentPaddingBottom = rpxToPx(36);
|
|
|
+ const rightItemLineHeight = rpxToPx(50); // 估算行高(包含margin)
|
|
|
+ itemContentHeight = contentPaddingTop + titleHeight + titleMarginBottom + (rightItemLineHeight * 4) + contentPaddingBottom;
|
|
|
+
|
|
|
+ // 绘制列表项背景
|
|
|
+ ctx.setFillStyle('#FFFFFF');
|
|
|
+ ctx.setShadow(0, rpxToPx(5), rpxToPx(10), 'rgba(0,0,0,0.05)'); // 可选:添加阴影使其更逼真
|
|
|
+ ctx.fillRect(boxX, currentY, boxWidth, itemContentHeight);
|
|
|
+ ctx.setShadow(0, 0, 0, 'rgba(0,0,0,0)'); // 清除阴影
|
|
|
+
|
|
|
+ const contentPaddingX = rpxToPx(20);
|
|
|
+ let itemInnerY = currentY + contentPaddingTop;
|
|
|
+
|
|
|
+ // 绘制标题
|
|
|
+ ctx.setFontSize(rpxToPx(32));
|
|
|
+ ctx.setFillStyle('#252525');
|
|
|
+ ctx.font = `bold ${rpxToPx(32)}px sans-serif`;
|
|
|
+ ctx.fillText(item.activityName || '', boxX + contentPaddingX, itemInnerY + rpxToPx(16)); // Y微调
|
|
|
+
|
|
|
+ itemInnerY += titleHeight + titleMarginBottom;
|
|
|
+
|
|
|
+ // 绘制右侧文字
|
|
|
+ const textStartX = boxX + contentPaddingX;
|
|
|
+ ctx.setFontSize(rpxToPx(24));
|
|
|
+ ctx.font = `normal ${rpxToPx(24)}px sans-serif`;
|
|
|
+
|
|
|
+ const rightItems = [
|
|
|
+ { tip: '活动时间:', text: item.activityStartTime || '' },
|
|
|
+ { tip: `${item.activityLimit == 2 ? '专享券贡献:' : '爱心值贡献:'}`, text: `${item.valueLimit || 0}${item.activityLimit == 2 ? '张' : ''}` },
|
|
|
+ { tip: '义工时长:', text: `${item.serviceHours || 0}小时` },
|
|
|
+ { tip: '公益合作:', text: item.channelName || '' },
|
|
|
+ ];
|
|
|
+
|
|
|
+ rightItems.forEach((rightItem, idx) => {
|
|
|
+ const textY = itemInnerY + (idx * rightItemLineHeight);
|
|
|
+ ctx.setFillStyle('#676775');
|
|
|
+ ctx.fillText(rightItem.tip, textStartX, textY);
|
|
|
+
|
|
|
+ const tipWidth = ctx.measureText(rightItem.tip).width;
|
|
|
+ ctx.setFillStyle('#252525');
|
|
|
+ ctx.font = `bold ${rpxToPx(24)}px sans-serif`;
|
|
|
+ // 绘制文本,这里不使用换行函数,因为内容一般不长
|
|
|
+ ctx.fillText(rightItem.text, textStartX + tipWidth, textY);
|
|
|
+ ctx.font = `normal ${rpxToPx(24)}px sans-serif`; // 重置字体
|
|
|
+ });
|
|
|
+
|
|
|
+ currentY += itemContentHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 执行绘制并生成图片
|
|
|
+ ctx.draw(false, () => {
|
|
|
+ uni.canvasToTempFilePath({
|
|
|
+ canvasId: 'pdf-canvas',
|
|
|
+ destWidth: canvasWidth.value * 2, // 提高图片清晰度
|
|
|
+ destHeight: canvasHeight.value * 2,
|
|
|
+ fileType: 'png',
|
|
|
+ quality: 1,
|
|
|
+ success: (res) => {
|
|
|
+ // 5. 保存图片到相册
|
|
|
+ uni.saveImageToPhotosAlbum({
|
|
|
+ filePath: res.tempFilePath,
|
|
|
+ success: () => {
|
|
|
+ uni.showToast({ title: '已保存到相册', icon: 'success' });
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.log(err);
|
|
|
+ if (err.errMsg && err.errMsg.includes('auth deny')) {
|
|
|
+ uni.showModal({
|
|
|
+ title: '提示',
|
|
|
+ content: '需要您授权保存相册',
|
|
|
+ showCancel: false,
|
|
|
+ success: () => uni.openSetting()
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: '保存失败,请稍后重试', icon: 'none' });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+ },
|
|
|
+ fail: (err) => {
|
|
|
+ console.error('canvasToTempFilePath failed:', err);
|
|
|
+ uni.showToast({ title: '图片生成失败', icon: 'none' });
|
|
|
+ },
|
|
|
+ complete: () => {
|
|
|
+ uni.hideLoading();
|
|
|
+ isLoading.value = false;
|
|
|
+ }
|
|
|
+ }, proxy); // 传入 proxy
|
|
|
+ });
|
|
|
|
|
|
- const list = ref([1,1,1,1])
|
|
|
-</script>
|
|
|
-
|
|
|
-<style scoped lang="scss">
|
|
|
- .common_page{
|
|
|
- padding-bottom: 184rpx;
|
|
|
-
|
|
|
- .prove{
|
|
|
- margin-top: 20rpx;
|
|
|
- background: url('https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/57b2f49c-beac-49e7-8840-777ec860ae59.png') no-repeat;
|
|
|
- background-size: 100% 100%;
|
|
|
- height: 945rpx;
|
|
|
- position: relative;
|
|
|
- &-logo{
|
|
|
- width: 133rpx;
|
|
|
- position: absolute;
|
|
|
- top: 58rpx;
|
|
|
- left: 72rpx;
|
|
|
- }
|
|
|
- &-title{
|
|
|
- width: 363rpx;
|
|
|
- margin-top: 101rpx;
|
|
|
- }
|
|
|
- &-line{
|
|
|
- width: 400rpx;
|
|
|
- margin-top: 7rpx;
|
|
|
- }
|
|
|
- &-seal{
|
|
|
- width: 131rpx;
|
|
|
- border-radius: 50%;
|
|
|
- position: absolute;
|
|
|
- bottom: 69rpx;
|
|
|
- right: 73rpx;
|
|
|
- }
|
|
|
-
|
|
|
- &-no{
|
|
|
- font-family: SourceHanSerifSC, SourceHanSerifSC;
|
|
|
- font-weight: bold;
|
|
|
- font-size: 20rpx;
|
|
|
- color: #9F793F;
|
|
|
- line-height: 20rpx;
|
|
|
- letter-spacing: 1px;
|
|
|
- margin-top: 9rpx;
|
|
|
- }
|
|
|
-
|
|
|
- &-info{
|
|
|
- margin-top: 27rpx;
|
|
|
- overflow: auto;
|
|
|
- &-pre{
|
|
|
- margin-top: 22rpx;
|
|
|
- &-left{
|
|
|
- width: 135rpx;
|
|
|
- &-text{
|
|
|
- font-family: PingFangSC, PingFang SC;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 17rpx;
|
|
|
- color: #252525;
|
|
|
- line-height: 17rpx;
|
|
|
- letter-spacing: 1px;
|
|
|
- &.space{
|
|
|
- letter-spacing: 10rpx;
|
|
|
- }
|
|
|
- }
|
|
|
- &-tip{
|
|
|
- font-family: PingFangSC, PingFang SC;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 10rpx;
|
|
|
- color: #252525;
|
|
|
- line-height: 10rpx;
|
|
|
- margin-top: 3rpx;
|
|
|
- }
|
|
|
- }
|
|
|
- &-right{
|
|
|
- width: 188rpx;
|
|
|
- font-family: PingFangSC, PingFang SC;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 17rpx;
|
|
|
- color: #252525;
|
|
|
- line-height: 17rpx;
|
|
|
- padding-bottom: 8rpx;
|
|
|
- border-bottom: 2rpx solid #DDCEAF;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- &-memo{
|
|
|
- width: 100%;
|
|
|
- padding: 0 80rpx;
|
|
|
- box-sizing: border-box;
|
|
|
- margin-top: 79rpx;
|
|
|
- font-family: PingFangSC, PingFang SC;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 20rpx;
|
|
|
- color: #252525;
|
|
|
- line-height: 31rpx;
|
|
|
- label{
|
|
|
- color: #C9A771;
|
|
|
- margin: 0 5rpx;
|
|
|
- }
|
|
|
- }
|
|
|
- &-bottom{
|
|
|
- width: 100%;
|
|
|
- padding: 0 80rpx;
|
|
|
- box-sizing: border-box;
|
|
|
- margin-top: 89rpx;
|
|
|
- overflow: hidden;
|
|
|
- &-pre{
|
|
|
- margin-top: 16rpx;
|
|
|
- font-family: PingFangSC, PingFang SC;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 17rpx;
|
|
|
- color: #252525;
|
|
|
- line-height: 20rpx;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+ } catch (error) {
|
|
|
+ console.error('handleDownload error:', error);
|
|
|
+ uni.hideLoading();
|
|
|
+ isLoading.value = false;
|
|
|
+ uni.showToast({
|
|
|
+ title: '生成失败,请检查网络或资源链接', // 提示更具体
|
|
|
+ icon: 'none'
|
|
|
+ });
|
|
|
+ }
|
|
|
+ };
|
|
|
+ onLoad((options)=>{
|
|
|
+ proxy.$api.get(`/core/social/practice/record/${options?.id||''}`).then(({data:res})=>{
|
|
|
+ if(res.code!==0) return proxy.$showToast(res.msg)
|
|
|
+ certificateData.value = {...certificateData.value,...res.data};
|
|
|
+ certificateData.value.idCard = certificateData.value.idCard&&certificateData.value.idCard.replace(/^(\d{6})(\d{8})(\d{3}[\dX])$/i,'$1********$3');
|
|
|
+ activityList.value = res.data?.activityVos||[];
|
|
|
+ })
|
|
|
+ })
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="scss">
|
|
|
+ .common_page {
|
|
|
+ background-color: #F5F5F5;
|
|
|
+ padding: 0 0 184rpx;
|
|
|
+
|
|
|
+ .prove{
|
|
|
+ margin: 20rpx 24rpx 0; // 改为外边距,避免影响全屏截图
|
|
|
+ width: calc(100% - 40rpx);
|
|
|
+ background: url('https://transcend.ringzle.com/xiaozhi-app/profile/2025/11/20/57b2f49c-beac-49e7-8840-777ec860ae59.png') no-repeat;
|
|
|
+ background-size: 100% 100%;
|
|
|
+ height: 945rpx;
|
|
|
+ position: relative;
|
|
|
+ &-logo{
|
|
|
+ width: 133rpx;
|
|
|
+ position: absolute;
|
|
|
+ top: 58rpx;
|
|
|
+ left: 72rpx;
|
|
|
+ }
|
|
|
+ &-title{
|
|
|
+ width: 363rpx;
|
|
|
+ margin-top: 101rpx;
|
|
|
+ }
|
|
|
+ &-line{
|
|
|
+ width: 400rpx;
|
|
|
+ margin-top: 7rpx;
|
|
|
+ }
|
|
|
+ &-seal{
|
|
|
+ width: 131rpx;
|
|
|
+ border-radius: 50%;
|
|
|
+ position: absolute;
|
|
|
+ bottom: 69rpx;
|
|
|
+ right: 73rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ &-no{
|
|
|
+ font-family: SourceHanSerifSC, SourceHanSerifSC;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #9F793F;
|
|
|
+ line-height: 20rpx;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ margin-top: 9rpx;
|
|
|
+ }
|
|
|
+
|
|
|
+ &-info{
|
|
|
+ margin-top: 27rpx;
|
|
|
+ overflow: auto;
|
|
|
+ &-pre{
|
|
|
+ margin-top: 22rpx;
|
|
|
+ &-left{
|
|
|
+ width: 135rpx;
|
|
|
+ &-text{
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 17rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 17rpx;
|
|
|
+ letter-spacing: 1px;
|
|
|
+ &.space{
|
|
|
+ letter-spacing: 10rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &-tip{
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 10rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 10rpx;
|
|
|
+ margin-top: 3rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &-right{
|
|
|
+ width: 188rpx;
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 17rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 17rpx;
|
|
|
+ padding-bottom: 8rpx;
|
|
|
+ border-bottom: 2rpx solid #DDCEAF;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &-memo{
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 80rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ margin-top: 79rpx;
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 20rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 31rpx;
|
|
|
+ label{ // canvas无法直接渲染span,颜色在js中处理
|
|
|
+ color: #C9A771;
|
|
|
+ margin: 0 5rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ &-bottom{
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 80rpx;
|
|
|
+ box-sizing: border-box;
|
|
|
+ margin-top: 89rpx;
|
|
|
+ overflow: hidden;
|
|
|
+ &-pre{
|
|
|
+ margin-top: 16rpx;
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 17rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 20rpx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
.list{
|
|
|
- &-box{
|
|
|
- margin-top: 20rpx;
|
|
|
- padding: 36rpx 20rpx;
|
|
|
- background: #FFFFFF;
|
|
|
- .title{
|
|
|
- font-family: PingFang-SC, PingFang-SC;
|
|
|
- font-weight: bold;
|
|
|
- font-size: 32rpx;
|
|
|
- color: #252525;
|
|
|
- line-height: 32rpx;
|
|
|
- }
|
|
|
- .content{
|
|
|
- margin-top: 30rpx;
|
|
|
- .left{
|
|
|
- width: 182rpx;
|
|
|
- height: 240rpx;
|
|
|
- image{
|
|
|
- width: 100%;
|
|
|
- height: 100%;
|
|
|
- }
|
|
|
- }
|
|
|
+ margin: 0 24rpx;
|
|
|
+ &-box{
|
|
|
+ margin-top: 20rpx;
|
|
|
+ padding: 36rpx 20rpx;
|
|
|
+ background: #FFFFFF;
|
|
|
+ .title{
|
|
|
+ font-family: PingFang-SC, PingFang-SC;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #252525;
|
|
|
+ line-height: 32rpx;
|
|
|
+ }
|
|
|
+ .content{
|
|
|
+ margin-top: 30rpx;
|
|
|
+ // .left{
|
|
|
+ // width: 182rpx;
|
|
|
+ // height: 240rpx;
|
|
|
+ // image{
|
|
|
+ // width: 100%;
|
|
|
+ // height: 100%;
|
|
|
+ // }
|
|
|
+ // }
|
|
|
.right{
|
|
|
- width: calc(100% - 182rpx);
|
|
|
- padding-left: 20rpx;
|
|
|
- box-sizing: border-box;
|
|
|
- &-pre{
|
|
|
- margin-top: 20rpx;
|
|
|
- &:first-child{
|
|
|
- margin-top: 12rpx;
|
|
|
- }
|
|
|
- .tip{
|
|
|
- width: 150rpx;
|
|
|
- font-family: PingFangSC, PingFang SC;
|
|
|
- font-weight: 400;
|
|
|
- font-size: 24rpx;
|
|
|
- color: #676775;
|
|
|
- }
|
|
|
- .text{
|
|
|
- width: calc(100% - 150rpx);
|
|
|
- font-family: PingFang-SC, PingFang-SC;
|
|
|
- font-weight: bold;
|
|
|
- font-size: 24rpx;
|
|
|
- color: #252525;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- .btn{
|
|
|
- width: calc(100% - 210rpx);
|
|
|
- height: 90rpx;
|
|
|
- background: #B7F358;
|
|
|
- border-radius: 45rpx;
|
|
|
- font-family: PingFang-SC, PingFang-SC;
|
|
|
- font-weight: bold;
|
|
|
- font-size: 32rpx;
|
|
|
- color: #151B29;
|
|
|
- line-height: 90rpx;
|
|
|
- text-align: center;
|
|
|
- letter-spacing: 2rpx;
|
|
|
- position: fixed;
|
|
|
- left: 105rpx;
|
|
|
- bottom: 64rpx;
|
|
|
- }
|
|
|
- }
|
|
|
+ width: 100%;
|
|
|
+ // width: calc(100% - 182rpx);
|
|
|
+ // padding-left: 20rpx;
|
|
|
+ // box-sizing: border-box;
|
|
|
+ &-pre{
|
|
|
+ margin-top: 20rpx;
|
|
|
+ &:first-child{
|
|
|
+ margin-top: 12rpx;
|
|
|
+ }
|
|
|
+ .tip{
|
|
|
+ width: 150rpx;
|
|
|
+ font-family: PingFangSC, PingFang SC;
|
|
|
+ font-weight: 400;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #676775;
|
|
|
+ }
|
|
|
+ .text{
|
|
|
+ width: calc(100% - 150rpx);
|
|
|
+ font-family: PingFang-SC, PingFang-SC;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #252525;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn{
|
|
|
+ width: calc(100% - 210rpx);
|
|
|
+ height: 90rpx;
|
|
|
+ background: #B7F358;
|
|
|
+ border-radius: 45rpx;
|
|
|
+ font-family: PingFang-SC, PingFang-SC;
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 32rpx;
|
|
|
+ color: #151B29;
|
|
|
+ line-height: 90rpx;
|
|
|
+ text-align: center;
|
|
|
+ letter-spacing: 2rpx;
|
|
|
+ position: fixed;
|
|
|
+ left: 105rpx;
|
|
|
+ bottom: 64rpx;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+ }
|
|
|
</style>
|