Browse Source

接口联调

htc 1 day ago
parent
commit
49702de1b5
8 changed files with 830 additions and 3084 deletions
  1. 6 1
      manifest.json
  2. 159 2785
      package-lock.json
  3. 1 0
      package.json
  4. 13 8
      pages/my.vue
  5. 22 11
      pagesMy/archivesDetail.vue
  6. 19 8
      pagesMy/practice.vue
  7. 605 270
      pagesMy/practiceRecord.vue
  8. 5 1
      pagesNonprofit/nonprofitDetail.vue

+ 6 - 1
manifest.json

@@ -54,7 +54,12 @@
         "setting" : {
             "urlCheck" : false,
             "minified" : true
-        },
+        },
+		"permission": {
+			"scope.writePhotosAlbum": {
+				"desc": "您的图片将保存到手机相册"
+			}
+		},
         "__usePrivacyCheck__" : true,
         "usingComponents" : true,
         "mergeVirtualHostAttributes" : true

File diff suppressed because it is too large
+ 159 - 2785
package-lock.json


+ 1 - 0
package.json

@@ -4,6 +4,7 @@
     "sass-loader": "^10.4.1"
   },
   "dependencies": {
+    "jspdf": "^3.0.4",
     "pinia": "^3.0.3",
     "uview-plus": "^3.5.41"
   }

+ 13 - 8
pages/my.vue

@@ -38,7 +38,7 @@
 			<view class="box-card adfacjb">
 				<view class="box-card-pre adffcac red" @click="handleTurnPage('/pagesMy/heartNumber')">
 					<image class="top" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/18/4e6e3db8-127f-433d-9644-66f1451a6d72.png"></image>
-					<view class="num">{{(heartNum||0).toFixed(2)}}</view>
+					<view class="num">{{(numInfo?.loveValue||0).toFixed(2)}}</view>
 					<view class="text adf">
 						<text>我的爱心值</text>
 						<image src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/18/35df3e58-7b8c-4d3c-8f09-41f252cb3805.png"></image>
@@ -46,7 +46,7 @@
 				</view>
 				<view class="box-card-pre adffcac purple" @click="handleTurnPage('/pagesMy/archives')">
 					<image class="top" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/18/dc26c6af-2617-47b7-ab38-9ccbf2016c5c.png"></image>
-					<view class="num">{{(fileNum||0).toFixed(2)}}</view>
+					<view class="num">{{(numInfo?.myArchives||0).toFixed(2)}}</view>
 					<view class="text adf">
 						<text>我的档案</text>
 						<image src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/18/51adb43e-acdd-475e-9e3d-64cbeedd4fe4.png"></image>
@@ -54,7 +54,7 @@
 				</view>
 				<view class="box-card-pre adffcac orange" @click="handleTurnPage('/pagesMy/volunteerHours')">
 					<image class="top" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/18/c5048d96-826e-4bbf-a133-dd2115162746.png"></image>
-					<view class="num">{{(hourNum||0).toFixed(2)}}</view>
+					<view class="num">{{(numInfo?.volunteerHours||0).toFixed(2)}}</view>
 					<view class="text adf">
 						<text>义工时长(h)</text>
 						<image src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/18/9bdc2845-e4bc-4215-82d0-9582066b710f.png"></image>
@@ -65,7 +65,7 @@
 		<view class="box" @click="handleTurnPage('/pagesMy/achievement')">
 			<view class="box-title">我的成就</view>
 			<view class="box-achievement adfacjb">
-				<view class="box-achievement-left">收获<span>{{achievement}}</span>项成就</view>
+				<view class="box-achievement-left">收获<span>{{numInfo?.myMedals||0}}</span>项成就</view>
 				<view class="box-achievement-right adfacjb">
 					<view class="imgs"></view>
 					<view class="jt">
@@ -106,10 +106,7 @@
 	const { isLogin } = useGlobalShare();
 	
 	const userInfo = ref(null)
-	const heartNum = ref(0)
-	const fileNum = ref(0)
-	const hourNum = ref(0)
-	const achievement = ref(0)
+	const numInfo = ref(null)
 	
 	const showLogin = () => {
 		if(userInfo.value) return
@@ -132,12 +129,20 @@
 		}
 	}
 	
+	const getUserNums = () => {
+		proxy.$api.get(`/wx/userWelfareData/${JSON.parse(uni.getStorageSync('userInfo')).id}`).then(({data:res})=>{
+			if(res.code!==0) return proxy.$showToast(res.msg)
+			numInfo.value = res.data;
+		})
+	}
+	
 	watch(()=>userStore.token,newVal=>{
 		getUserInfo()
 	})
 	
 	onMounted(()=>{
 		getUserInfo()
+		getUserNums()
 	})
 </script>
 

+ 22 - 11
pagesMy/archivesDetail.vue

@@ -10,28 +10,26 @@
 					</div>
 					<image class="logo" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/28/59a63fde-bbd8-4419-a65a-b714790f6b0a.png"></image>
 					<image class="heart" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/28/234c696f-a538-45c3-ab11-c0946305f2c2.png"></image>
-					<div class="level">Lv.{{4}}</div>
+					<div class="level">Lv.{{info?.userLevel||1}}</div>
 					<div class="name-tip">昵称/NAME</div>
-					<div class="name">{{'周晓瑾'}}</div>
+					<div class="name">{{info?.memberName||''}}</div>
 					<div class="kh-tip">家庭公益口号</div>
-					<div class="kh">{{'公益献爱心,真情暖人心'}}</div>
-					<div class="memo">亲爱的,从{{'2025年6月1日'}}至{{'2025年7月30日'}},您的每一分善意都让世界更温暖!</div>
+					<div class="kh">{{info?.welfareSlogan||''}}</div>
+					<div class="memo">亲爱的,从{{info?.signupStartTime||''}}至{{info?.activityEndTime||''}},您的每一分善意都让世界更温暖!</div>
 					<div class="gx-tip">爱心贡献/CONTRIBUTION</div>
-					<div class="num" style="margin-top: 28rpx;">捐赠爱心值<text>{{5000}}</text></div>
-					<div class="num">获得义工时长<text>{{2}}</text>小时</div>
+					<div class="num" style="margin-top: 28rpx;">捐赠爱心值<text>{{info?.valueLimit||0}}</text></div>
+					<div class="num">获得义工时长<text>{{info?.serviceHours||0}}</text>小时</div>
 				</div>
 			</div>
 		</div>
 		<div class="box box2">
 			<image class="box-title" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/28/6cbc5700-77f4-4ae8-b4eb-28c4e7748029.png"></image>
-			<div class="content">
-				{{'参加这次沙漠沙沙树种植活动,我深刻体会到环保行动的意义。在炎热的沙漠中,亲手栽下一棵棵沙沙树,不仅为荒漠增添了一抹绿色,更让我感受到保护生态环境的紧迫性。团队协作让效率倍增,大家的热情也让我深受鼓舞。这次活动让我明白,小小的行动也能为地球带来改变。未来,我会继续参与环保事业,号召更多人加入,共同守护我们的家园。'}}
-			</div>
+			<div class="content">{{info?.experience||''}}</div>
 		</div>
 		<div class="box box2">
 			<image class="box-title" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/28/56ffd9a5-b42a-4eb5-879d-b38a2c05bdae.png"></image>
 			<div class="content">
-				<image class="img" src="https://transcend.ringzle.com/xiaozhi-app/profile/2025/09/28/e1d9a1e2-d0fd-4503-bc06-8d8509efde01.png" mode="widthFix"></image>
+				<image class="img" v-for="(item,index) in imgs" :key="index" :src="item" mode="widthFix"></image>
 			</div>
 		</div>
 		<div class="back" @tap="goBack">返回</div>
@@ -40,11 +38,24 @@
 
 <script setup name="">
 	import CusHeader from '@/components/CusHeader/index.vue'
-	import { ref } from 'vue'
+	import { onLoad } from '@dcloudio/uni-app'
+	import { ref, getCurrentInstance } from 'vue'
+	const { proxy } = getCurrentInstance()
+	
+	const info = ref(null)
+	const imgs = ref([])
 	
 	const goBack = () => {
 		uni.navigateBack();
 	}
+	
+	onLoad(options=>{
+		proxy.$api.get(`/core/activity/signup/getMemberProfile/${options?.activityId}/${options?.memberId}`).then(({data:res})=>{
+			if(res.code!==0) return proxy.$showToast(res.msg)
+			info.value = res.data;
+			imgs.value = res.data.activityFile&&res.data.activityFile.split(';');
+		})
+	})
 </script>
 
 <style scoped lang="scss">

+ 19 - 8
pagesMy/practice.vue

@@ -32,9 +32,9 @@
 				<up-list @scrolltolower="scrolltolower" style="height: 100%;">
 					<up-list-item v-for="(item, index) in yslList" :key="index">
 						<div class="ysl-box">
-							<div class="ysl-box-title">{{'感恩有你 温暖前行'}}</div>
-							<div class="ysl-box-tip">申领时间:{{'2025-06-01 ~ 2025-07-01'}}</div>
-							<div class="ysl-box-tip">申 领 人:{{'张琳琳'}}</div>
+							<div class="ysl-box-title">证书编号:{{item?.certificateNumber||''}}</div>
+							<div class="ysl-box-tip">申领时间:{{item?.createDate||''}}</div>
+							<div class="ysl-box-tip">申 领 人:{{item?.memberName||''}}</div>
 							<div class="ysl-box-btn" @tap="handleDetail(item)">查看</div>
 						</div>
 					</up-list-item>
@@ -85,7 +85,7 @@
 		queryParams.value.page = 1;
 		isOver.value = false;
 		list.value = [];
-		yslList.value = [1];
+		yslList.value = [];
 		if(!queryParams.value.memberId) queryParams.value.memberId = memberList.value[0]?.id;
 		if(tidx.value===1){
 			getKslList()
@@ -96,7 +96,7 @@
 	
 	const handleDetail = item => {
 		uni.navigateTo({
-			url:'/pagesMy/practiceRecord'
+			url:'/pagesMy/practiceRecord?id='+item.id
 		})
 	}
 	
@@ -123,9 +123,9 @@
 	const getYslList = () => {
 		proxy.$api.get('/core/social/practice/record/claimedPage',queryParams.value).then(({data:res})=>{
 			if(res.code!==0) return proxy.$showToast(res.msg)
-			// yslList.value = [...yslList.value,...res.data.list]
-			// queryParams.value.page++;
-			// if(res.data.list.length===0) isOver.value = true
+			yslList.value = [...yslList.value,...res.data.list]
+			queryParams.value.page++;
+			if(res.data.list.length===0) isOver.value = true
 		})
 	}
 	
@@ -173,7 +173,18 @@
 	}
 	
 	const handleApply = () => {
+		let activityIds = list.value.filter(l=>l.check).map(l=>l.activityId);
+		if(activityIds.length===0) return proxy.$showToast('请至少选择一条记录')
 		
+		proxy.$api.post('/core/social/practice/record',{
+			activityIds,
+			memberId:queryParams.value.memberId
+		}).then(({data:res})=>{
+			if(res.code!==0) return proxy.$showToast(res.msg)
+			tidx.value = 2;
+			getDataByTab()
+			proxy.$showToast('申领成功')
+		})
 	}
 	
 	onMounted(()=>{

+ 605 - 270
pagesMy/practiceRecord.vue

@@ -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>

+ 5 - 1
pagesNonprofit/nonprofitDetail.vue

@@ -191,7 +191,9 @@
 				success: (res) => {
 					if(res.errMsg=='scanCode:ok'){
 						let { id } = JSON.parse(res.result)
+						console.log(id,'id');
 						proxy.$api.post('/core/activity/signup/signinBySingle',{
+							activityId:id??'',
 							memberSignupId:memberSignupId.value,
 							userId:JSON.parse(uni.getStorageSync('userInfo')).id
 						}).then(({data:res})=>{
@@ -216,7 +218,9 @@
 		})
 	}
 	const handleReview = () => {
-		
+		uni.navigateTo({
+			url:'/pagesMy/archivesDetail?activityId='+info.value?.activityId+'&memberId='+info.value?.memberId
+		})
 	}
 	
 	const getUserLoveTicket = () => {