dedsudiyu 1 day ago
parent
commit
176cec1e81

+ 16 - 0
pages_safetyEducationExamination/api/index.js

@@ -261,6 +261,22 @@ export const examElUserExamAttemptAttemptRecord = (data) => {
 		data: { ...data }
 	})
 };
+//考试记录详情-试卷详情
+export const examElExamUserResultMyScoreDetail = (data) => {
+	return apiResquest({
+		url: `/exam/elExamUserResult/myScoreDetail`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//考试记录详情-题目列表
+export const examElExamAttemptPaperQuestionPage = (data) => {
+	return apiResquest({
+		url: `/exam/elExamAttempt/paper-question-page`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
 //在线考试-详情
 export const examUserExamDetail = (data) => {
 	return apiResquest({

+ 33 - 49
pages_safetyEducationExamination/component/richTextPdf.vue

@@ -16,6 +16,10 @@
 			content: {
 				type: String,
 				default: ''
+			},
+			examId: {
+				type: [String, Number],
+				default: ''
 			}
 		},
 		data() {
@@ -46,7 +50,7 @@
 					// 由于小程序无法直接将 rich-text 渲染到 canvas,
 					// 这里采用保存为图片的简化方案
 
-					await this._saveAsImage();
+					this._saveAsImage();
 
 				} catch (error) {
 					console.error('PDF 生成失败:', error);
@@ -60,55 +64,35 @@
 				}
 			},
 
-			// 简化方案:保存为图片
-			async _saveAsImage() {
-				// 微信小程序中,rich-text 无法直接转为 canvas
-				// 需要使用以下方案之一:
-				// 1. 后端生成PDF接口
-				// 2. 使用 web-view 加载HTML后截图
-				// 3. 提示用户长按保存或分享
+			// 调用后端PDF下载接口
+			_saveAsImage() {
+				const { config } = require('@/api/request/config.js');
+				const token = uni.getStorageSync('token');
+				const url = config.base_url + '/exam/user/exam/commitment/download?examId=' + this.examId;
 
-				// uni.showModal({
-				// 	title: '提示',
-				// 	content: '微信小程序暂不支持直接生成PDF,建议在电脑端下载或长按保存内容',
-				// 	showCancel: false
-				// });
-
-				// 如果有后端PDF生成接口,可以这样调用:
-				const result = await this._callBackendPdfApi();
-				if (result.pdfUrl) {
-				  uni.downloadFile({
-				    url: result.pdfUrl,
-				    success: (res) => {
-				      uni.saveFile({
-				        tempFilePath: res.tempFilePath,
-				        success: () => {
-				          uni.showToast({ title: '保存成功' });
-				        }
-				      });
-				    }
-				  });
-				}
-			},
-
-			// 调用后端PDF生成接口(示例)
-			async _callBackendPdfApi() {
-				return new Promise((resolve, reject) => {
-					uni.request({
-						url: '/api/generate-pdf', // 替换为实际接口
-						method: 'POST',
-						data: {
-							htmlContent: this.content
-						},
-						success: (res) => {
-							if (res.data.code === 200) {
-								resolve(res.data.data);
-							} else {
-								reject(new Error(res.data.message));
-							}
-						},
-						fail: reject
-					});
+				uni.downloadFile({
+					url,
+					header: {
+						'Authorization': token
+					},
+					success: (res) => {
+						if (res.statusCode === 200) {
+							uni.saveFile({
+								tempFilePath: res.tempFilePath,
+								success: () => {
+									uni.showToast({ title: '下载成功', icon: 'success' });
+								},
+								fail: () => {
+									uni.showToast({ title: '保存失败,请重试', icon: 'none' });
+								}
+							});
+						} else {
+							uni.showToast({ title: 'PDF下载失败', icon: 'none' });
+						}
+					},
+					fail: () => {
+						uni.showToast({ title: 'PDF下载失败,请重试', icon: 'none' });
+					}
 				});
 			}
 		}

+ 2 - 1
pages_safetyEducationExamination/views/exam/incorrectQuestionsList.vue

@@ -26,7 +26,8 @@
 						<text class="tag text-xs" :class="item.errorCount >= 3 ? 'tag-red' : 'tag-orange'">答错{{ item.errorCount || 0 }}次</text>
 					</view>
 					<view class="q-content">
-						<text class="text-sm">{{ item.questionContent }}</text>
+						<!-- <text class="text-sm">{{ item.questionContent }}</text> -->
+						<rich-text class="text-sm" :nodes="item.questionContent || ''"></rich-text>
 					</view>
 					<view v-if="item.userAnswer || item.correctAnswer" class="answer-box">
 						<view v-if="item.userAnswer" class="mb4">

+ 14 - 5
pages_safetyEducationExamination/views/exam/index.vue

@@ -120,7 +120,7 @@
 					<picker mode="date" :value="endDate" @change="onEndDateChange" >
 						<view class="picker-input top-picker-input-box" :style="!endDate?'color:#999;font-size:22rpx;':''">{{ endDate?endDate:'请选择' }}</view>
 					</picker>
-					<view class="top-right-box-button" @click="resetScoreList()">重置</view>
+					<view class="top-right-box-button" @click="resetButton()">重置</view>
 				</view>
 				<!-- 考试类型标签 -->
 				<scroll-view scroll-x class="tag-scroll">
@@ -294,8 +294,16 @@
 			resetScoreList() {
 				this.scorePage = 1;
 				this.scoreNoMore = false;
-				this.startTime = '';
-				this.endTime = '';
+				this.$set(this, 'scoreList', []);
+				this.loadScoreList();
+			},
+			resetButton(){
+				this.scorePage = 1;
+				this.scoreNoMore = false;
+				this.startDate = '';
+				this.endDate = '';
+				this.resultIndex = 0;
+				this.activeTagIndex = 0;
 				this.$set(this, 'scoreList', []);
 				this.loadScoreList();
 			},
@@ -313,7 +321,7 @@
 						resultStatus: result.value || '',
 						startTime: this.startDate?this.startDate + 'T00:00:00':'',
 						endTime: this.endDate?this.endDate + 'T23:59:59':'',
-						examScene:null
+						examScene:1,
 					};
 					const { data } = await examElExamUserResultMyScoreListPage(obj);
 					if (data.code === 200) {
@@ -373,9 +381,10 @@
 				});
 			},
 			goToScoreList(item) {
+				console.log('item',item);
 				// 跳转答题详情,传递 attemptId / examId
 				uni.navigateTo({
-					url: `/pages_safetyEducationExamination/views/exam/scoreList?examId=${item.examId}`
+					url: `/pages_safetyEducationExamination/views/exam/scoresList?examId=${item.examId}`
 				});
 			},
 			goToCert(item) {

+ 4 - 2
pages_safetyEducationExamination/views/exam/onlineExamInfo.vue

@@ -58,14 +58,14 @@
 				</view>
 		</view>
 		<!-- 考试承诺书弹窗 -->
-		<uni-popup ref="preExamDialog" type="center" :mask-click="false">
+		<uni-popup ref="preExamDialog" type="center" :mask-click="true">
 			<view class="pre-exam-dialog">
 				<view class="dialog-header">
 					<text class="dialog-title">考试承诺书</text>
 				</view>
 				<view class="dialog-body">
 					<scroll-view scroll-y class="content-scroll">
-						<richTextPdf ref="richTextPdf" :content="richTextHtml" />
+						<richTextPdf ref="richTextPdf" :content="richTextHtml" :examId="richTextExamId" />
 					</scroll-view>
 				</view>
 				<view class="dialog-footer">
@@ -102,6 +102,7 @@
 				// 承诺书相关
 				preButtonType: false,
 				richTextHtml: '',
+				richTextExamId:'',
 				preData: null,
 				itemData:{},
 			}
@@ -164,6 +165,7 @@
 					openCommitmentDialog: (examItem) => {
 						this.preButtonType = examItem.commitmentPrintEnabled == 1;
 						this.richTextHtml = examItem.commitmentContent;
+						this.richTextExamId = examItem.examId;
 						this.preData = examItem;
 						this.$refs.preExamDialog.open();
 					},

+ 64 - 49
pages_safetyEducationExamination/views/exam/scoreDetail.vue

@@ -44,7 +44,7 @@
 							<view class="text-xs text-gray">错误</view>
 						</view>
 						<view class="stat-item">
-							<view class="stat-num text-blue">{{ detail.accuracy }}</view>
+							<view class="stat-num text-blue">{{ detail.accuracy }}%</view>
 							<view class="text-xs text-gray">正确率</view>
 						</view>
 					</view>
@@ -55,7 +55,7 @@
 			<view class="detail-panel">
 				<view class="detail-title">
 					答题详情
-					<text class="text-gray detail-title-sub">(共{{ detail.totalQuestionNum || 0 }}题)</text>
+					<text class="text-gray detail-title-sub">(共{{ total || 0 }}题)</text>
 				</view>
 
 				<view v-for="(item, index) in answerList" :key="index" class="question-card">
@@ -69,17 +69,23 @@
 							{{ isCorrect(item) ? '答对' : '答错' }}
 						</text>
 					</view>
-					<view class="q-content">{{ item.questionContent }}</view>
+					<view class="q-content">
+						<rich-text :nodes="item.questionContent || ''"></rich-text>
+					</view>
 					<view class="answer-box" :class="isCorrect(item) ? 'answer-box-correct' : 'answer-box-wrong'">
 						<view class="mb4">
 							<text class="text-xs text-gray">我的答案:</text>
-							<text class="text-xs fw500" :class="isCorrect(item) ? 'text-green' : 'text-red'">
-								{{ item.userAnswer }}{{ answerSuffix(item) }}
+							<text v-if="item.answerTags[0]" class="text-xs fw500" :class="isCorrect(item) ? 'text-green' : 'text-red'">
+								<!-- {{ item.answerTags }}{{ answerSuffix(item) }} -->
+								{{ item.answerTags }}
+							</text>
+							<text v-if="!item.answerTags[0]" class="text-xs fw500" :class="isCorrect(item) ? 'text-green' : 'text-red'">
+								未作答
 							</text>
 						</view>
 						<view>
 							<text class="text-xs text-gray">正确答案:</text>
-							<text class="text-xs text-green fw500">{{ item.correctAnswer }}</text>
+							<text class="text-xs text-green fw500">{{ item.correctAnswerTags }}</text>
 						</view>
 					</view>
 					<view v-if="item.analysis" class="analysis-box">解析:{{ item.analysis }}</view>
@@ -94,6 +100,13 @@
 </template>
 
 <script>
+	import {
+		parseTime
+	} from '@/component/public.js'
+	import {
+		examElExamUserResultMyScoreDetail,
+		examElExamAttemptPaperQuestionPage
+	} from '@/pages_safetyEducationExamination/api/index.js'
 	// 题型映射:1-单选题,2-多选题,3-判断题
 	const QUESTION_TYPE_MAP = {
 		1: '单选题',
@@ -180,27 +193,30 @@
 			// 上部分:考试概况(独立详情接口)
 			async loadExamDetail() {
 				try {
-					// TODO 后台接口未提供,先占位;接口就绪后放开以下代码
-					// const { data } = await examElExamAttemptAnswerDetail({ attemptId: this.attemptId });
-					// if (data.code === 200 && data.data) {
-					//     const d = data.data;
-					//     this.detail = {
-					//         examTypeName: d.examTypeName || '',
-					//         examName: d.examName || '',
-					//         isPass: d.examStatus === 1,
-					//         examTime: parseTime(d.startTime, '{y}-{m}-{d}'),
-					//         usedTime: d.myDuration || '-',
-					//         totalScore: d.totalScore || 0,
-					//         passScore: d.passScore || 0,
-					//         attemptCount: d.attemptCount || 0,
-					//         attemptLimit: d.attemptLimit || 0,
-					//         bestScore: d.bestScore || 0,
-					//         totalQuestionNum: d.totalQuestionNum || 0,
-					//         correctCount: d.correctCount || 0,
-					//         wrongCount: d.wrongCount || 0,
-					//         accuracy: d.accuracy || '-',
-					//     };
-					// }
+					let obj = {
+						examId:this.examId,
+						examScene:1,
+					}
+					const { data } = await examElExamUserResultMyScoreDetail(obj);
+					if (data.code === 200 && data.data) {
+					    const d = data.data;
+					    this.detail = {
+					        examTypeName: d.examTypeName || '',
+					        examName: d.examName || '',
+					        isPass: d.examStatus === 1,
+					        examTime: parseTime(d.startTime, '{y}-{m}-{d}'),
+					        usedTime: d.myDuration || '-',
+					        totalScore: d.totalScore || 0,
+					        passScore: d.passScore || 0,
+					        attemptCount: d.attemptCount || 0,
+					        attemptLimit: d.attemptLimit || 0,
+					        bestScore: d.bestScore || 0,
+					        totalQuestionNum: d.totalQuestionNum || 0,
+					        correctCount: d.correctCount || 0,
+					        wrongCount: d.wrongCount || 0,
+					        accuracy: d.accuracy || '-',
+					    };
+					}
 				} catch (e) {
 					console.error('获取考试详情失败', e);
 				}
@@ -216,23 +232,22 @@
 				if (this.listLoading || this.noMore) return;
 				this.listLoading = true;
 				try {
-					// TODO 后台接口未提供,先占位;接口就绪后放开以下代码
-					// const { data } = await examElExamAttemptAnswerAttemptAnswer({
-					//     page: this.page,
-					//     pageSize: this.pageSize,
-					//     attemptId: this.attemptId,
-					// });
-					// if (data.code === 200) {
-					//     const records = data.data.records || [];
-					//     const newList = this.page === 1 ? records : [...this.answerList, ...records];
-					//     this.$set(this, 'answerList', newList);
-					//     this.total = data.data.total;
-					//     if (newList.length >= this.total) {
-					//         this.noMore = true;
-					//     } else {
-					//         this.page++;
-					//     }
-					// }
+					const { data } = await examElExamAttemptPaperQuestionPage({
+					    page: this.page,
+					    pageSize: this.pageSize,
+					    attemptId: this.attemptId,
+					});
+					if (data.code === 200) {
+					    const records = data.data.records || [];
+					    const newList = this.page === 1 ? records : [...this.answerList, ...records];
+					    this.$set(this, 'answerList', newList);
+					    this.total = data.data.total;
+					    if (newList.length >= this.total) {
+					        this.noMore = true;
+					    } else {
+					        this.page++;
+					    }
+					}
 				} catch (e) {
 					console.error('获取答题详情失败', e);
 				} finally {
@@ -349,15 +364,15 @@
 		border-color #b7eb8f
 
 	.diff-normal
-		background #e6f4ff
-		color #1677ff
-		border-color #91caff
-
-	.diff-hard
 		background #fff7e6
 		color #fa8c16
 		border-color #ffd591
 
+	.diff-hard
+		background #fff2f0
+		color #f5222d
+		border-color #ffccc7
+
 	.diff-extreme
 		background #f9f0ff
 		color #722ed1

+ 58 - 11
pages_safetyEducationExamination/views/exam/scoresList.vue

@@ -5,9 +5,13 @@
 				v-for="(item, index) in scoreList"
 				:key="index"
 				class="score-card"
-				@click="goToScoreDetail(item)"
-			>
-				<view>123</view>
+				@click="goToScoreDetail(item)">
+				<view class="left-box">
+					<view>考试时间:{{item.startTime}}</view>
+					<view>得分:{{item.totalScore}}</view>
+					<view>及格分:{{item.passScore}}</view>
+				</view>
+				<view class="right-box" :class="item.passStatus ==0?'colorA':'colorB'">{{item.passStatus ==0?'未通过':'通过'}}</view>
 			</view>
 			<view v-if="!scoreList[0] && !scoreLoading" class="empty-tip">暂无考试记录</view>
 			<view v-if="scoreLoading" class="loading-tip">加载中...</view>
@@ -32,16 +36,20 @@
 				scorePageSize: 10,
 				scoreTotal: 0,
 				scoreNoMore: false,
+				examId:'',
 			}
 		},
 		created() {
 			
 		},
+		onLoad(options){
+			this.$set(this,'examId',options.examId);
+		},
 		onShow() {
-			
+			this.loadScoreList();
 		},
 		methods: {
-			goToScoreDetail(){
+			goToScoreDetail(item){
 				// 跳转答题详情,传递 attemptId / examId
 				uni.navigateTo({
 					url: `/pages_safetyEducationExamination/views/exam/scoreDetail?attemptId=${item.attemptId || ''}&examId=${item.examId || ''}`
@@ -59,7 +67,7 @@
 					const obj = {
 						page: this.scorePage,
 						pageSize: this.scorePageSize,
-						examId: '',
+						examId: this.examId,
 					};
 					const { data } = await examElUserExamAttemptAttemptRecord(obj);
 					if (data.code === 200) {
@@ -88,10 +96,49 @@
 
 <style lang="stylus" scoped>
 	.scoreList{
-		height 100%
-		display flex
-		flex-direction column
-		overflow hidden
-		background-color #f0f4fb
+		height: 100%;
+		display: flex;
+		flex-direction: column;
+		overflow: hidden;
+		background-color: #f0f4fb;
+		.scroll-content{
+			height:100%;
+		}
+		.score-card{
+			margin:28rpx 28rpx 0;
+			background-color: #fff;
+			border-radius:10rpx;
+			padding:20rpx;
+			box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
+			border: 1rpx solid #dde3ed;
+			display: flex;
+			font-size:28rpx;
+			.left-box{
+				flex:1;
+			}
+			.right-box{
+				line-height:110rpx;
+				margin-right:20rpx;
+			}
+			.colorA{
+				color:#f5222d;
+			}
+			.colorB{
+				color:#52c41a;
+			}
+		}
+		.empty-tip{
+			text-align: center;
+			line-height: 100rpx;
+			color: #aaa;
+			font-size: 24rpx;
+		}
+		
+		.loading-tip{
+			text-align: center;
+			line-height: 100rpx;
+			color: #aaa;
+			font-size: 22rpx;
+		}
 	}
 </style>

+ 4 - 2
pages_safetyEducationExamination/views/home/component/examTask.vue

@@ -23,14 +23,14 @@
 		</view>
 
 		<!-- 考试承诺书弹窗 -->
-		<uni-popup ref="preExamDialog" type="center" :mask-click="false">
+		<uni-popup ref="preExamDialog" type="center" :mask-click="true">
 			<view class="pre-exam-dialog">
 				<view class="dialog-header">
 					<text class="dialog-title">考试承诺书</text>
 				</view>
 				<view class="dialog-body">
 					<scroll-view scroll-y class="content-scroll">
-						<richTextPdf ref="richTextPdf" :content="richTextHtml" />
+						<richTextPdf ref="richTextPdf" :content="richTextHtml" :examId="richTextExamId" />
 					</scroll-view>
 				</view>
 				<view class="dialog-footer">
@@ -63,6 +63,7 @@
 				// 承诺书相关
 				preButtonType: false,
 				richTextHtml: '',
+				richTextExamId:'',
 				preData: null,
 				itemData:{},
 			}
@@ -84,6 +85,7 @@
 						openCommitmentDialog: (examItem) => {
 							this.preButtonType = examItem.commitmentPrintEnabled == 1;
 							this.richTextHtml = examItem.commitmentContent;
+							this.richTextExamId = examItem.examId;
 							this.preData = examItem;
 							this.$refs.preExamDialog.open();
 						},

+ 4 - 2
pages_safetyEducationExamination/views/home/component/specialExam.vue

@@ -44,14 +44,14 @@
 		</view>
 		
 		<!-- 考试承诺书弹窗 -->
-		<uni-popup ref="preExamDialog" type="center" :mask-click="false">
+		<uni-popup ref="preExamDialog" type="center" :mask-click="true">
 			<view class="pre-exam-dialog">
 				<view class="dialog-header">
 					<text class="dialog-title">考试承诺书</text>
 				</view>
 				<view class="dialog-body">
 					<scroll-view scroll-y class="content-scroll">
-						<richTextPdf ref="richTextPdf" :content="richTextHtml" />
+						<richTextPdf ref="richTextPdf" :content="richTextHtml" :examId="richTextExamId" />
 					</scroll-view>
 				</view>
 				<view class="dialog-footer">
@@ -84,6 +84,7 @@
 				// 承诺书相关
 				preButtonType: false,
 				richTextHtml: '',
+				richTextExamId:'',
 				preData: null,
 				itemData:{},
 			}
@@ -104,6 +105,7 @@
 					openCommitmentDialog: (examItem) => {
 						this.preButtonType = examItem.commitmentPrintEnabled == 1;
 						this.richTextHtml = examItem.commitmentContent;
+						this.richTextExamId = examItem.examId;
 						this.preData = examItem;
 						this.$refs.preExamDialog.open();
 					},