dedsudiyu 1 week ago
parent
commit
109596940d

+ 8 - 1
.claude/claude.md

@@ -44,4 +44,11 @@ id="screen-approval" 的内容重构至E:\git\2021项目\v3-小程序\pages_safe
 
 
 1. 重构E:\git\2021项目\v3-小程序\.claude\mobile_exam.html文件内 ID为screen-mock-detail的内容 至E:\git\2021项目\v3-小程序\pages_safetyEducationExamination\views\exam\mockExamInfo.vue内 要求符合uni-app 微信小程序 并且还原完整样式与原型不要擅自更改样式与原型
 1. 重构E:\git\2021项目\v3-小程序\.claude\mobile_exam.html文件内 ID为screen-mock-detail的内容 至E:\git\2021项目\v3-小程序\pages_safetyEducationExamination\views\exam\mockExamInfo.vue内 要求符合uni-app 微信小程序 并且还原完整样式与原型不要擅自更改样式与原型
 页面数据将会从上级列表页面的exam/user/exam/mockList接口中取值并且传入本页面 写好接参
 页面数据将会从上级列表页面的exam/user/exam/mockList接口中取值并且传入本页面 写好接参
-同时写好在pages_safetyEducationExamination/views/exam/index页面模拟考试状态时点击列表内容跳转到此页面的方法与传参 
+同时写好在pages_safetyEducationExamination/views/exam/index页面模拟考试状态时点击列表内容跳转到此页面的方法与传参 
+
+
+
+1. 重构E:\git\2021项目\v3-小程序\.claude\mobile_exam.html文件内 ID为screen-exam-doing的内容 至E:\git\2021项目\v3-小程序\pages_safetyEducationExamination\views\exam\onlineExamPlay.vue内 要求符合uni-app 微信小程序 并且还原完整样式与原型不要擅自更改样式与原型
+2. 重构完成后 阅读E:\git\2026项目\safetyEducationExamination\src\views\safetyEducationExaminationNew\featurePage\startTheExam\index.vue页面
+这个页面是PC端的考试页面 重构后的页面可以完全使用该页面的业务逻辑与操作方式 和接口数据 该页面是已完成的完整页面
+但是需要注意该页面是PC的 你需要注意重构后的页面是要求符合uni-app 微信小程序

+ 40 - 0
pages_safetyEducationExamination/api/index.js

@@ -93,6 +93,46 @@ export const examUserExamStart = (data) => {
 		data: { ...data }
 		data: { ...data }
 	})
 	})
 };
 };
+//查询考试题号导航
+export const examElExamAttemptReviewSidebar = (data) => {
+	return apiResquest({
+		url: `/exam/elExamAttempt/review-sidebar`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//查询考试题目详情
+export const examElExamAttemptReviewQuestion = (data) => {
+	return apiResquest({
+		url: `/exam/elExamAttempt/review-question`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//查询考试剩余时间
+export const examUserExamGetExamSurplusTime = (data) => {
+	return apiResquest({
+		url: `/exam/user/exam/getExamSurplusTime`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//保存考试答案
+export const examUserExamAnswerSave = (data) => {
+	return apiResquest({
+		url: `/exam/user/exam/answer/save`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
+//提交考试
+export const examUserExamSubmit = (data) => {
+	return apiResquest({
+		url: `/exam/user/exam/submit`,
+		method: 'POST',
+		data: { ...data }
+	})
+};
 //类型选项卡数据
 //类型选项卡数据
 export const examElDisciplineTypeList = (data) => {
 export const examElDisciplineTypeList = (data) => {
 	return apiResquest({
 	return apiResquest({

+ 6 - 6
pages_safetyEducationExamination/views/exam/mockExamInfo.vue

@@ -26,16 +26,16 @@
 						<view class="text-orange fw500">{{ examData.passScore }} 分</view>
 						<view class="text-orange fw500">{{ examData.passScore }} 分</view>
 					</view>
 					</view>
 				</view>
 				</view>
-				<view class="score-summary">
+				<view class="score-summary" v-for="(item,index) in examData.mockHistoryScores" :key="index">
 					<view class="text-sm fw500 mb8">历次模拟成绩</view>
 					<view class="text-sm fw500 mb8">历次模拟成绩</view>
 					<view class="row-between mb4">
 					<view class="row-between mb4">
-						<text class="text-sm text-gray">第 {{ examData.attemptCount || 0 }} 次 {{ examData.attemptCount}}</text>
-						<text class="text-sm fw500" :class="examData.isPassed ? 'text-green' : 'text-red'">
-							最高 {{ examData.maxScore || 0 }} 分({{ examData.isPassed ? '已通过' : '未通过' }})
+						<text class="text-sm text-gray">第 {{ index+1 }} 次 {{ item.submitTime}}</text>
+						<text class="text-sm fw500" :class="item.isPassed ? 'text-green' : 'text-red'">
+							最高 {{ item.score || 0 }} 分({{ item.isPassed ? '已通过' : '未通过' }})
 						</text>
 						</text>
 					</view>
 					</view>
-					<view v-if="!examData.isPassed" class="text-xs text-gray">
-						还需提高 {{ scoreGap }} 分达到及格线
+					<view v-if="!item.isPassed" class="text-xs text-gray">
+						还需提高 {{ item.passScoreGap }} 分达到及格线
 					</view>
 					</view>
 				</view>
 				</view>
 				<view class="notice">模拟考试需达到及格分数({{ examData.passScore }}分)方可参加正式在线考试。</view>
 				<view class="notice">模拟考试需达到及格分数({{ examData.passScore }}分)方可参加正式在线考试。</view>

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

@@ -64,6 +64,9 @@
 	import {
 	import {
 		parseTime
 		parseTime
 	} from '@/component/public.js'
 	} from '@/component/public.js'
+	import {
+		examUserExamStart
+	} from '@/pages_safetyEducationExamination/api/index.js'
 
 
 	export default {
 	export default {
 		data() {
 		data() {
@@ -123,8 +126,40 @@
 					icon: 'none'
 					icon: 'none'
 				});
 				});
 			},
 			},
-			startExam() {
-				
+			async startExam() {
+				if (!this.examData.available) {
+					uni.showToast({
+						title: this.examData.unavailableDesc || '暂不可考试',
+						icon: 'none'
+					});
+					return;
+				}
+
+				let attemptId = this.examData.attemptId;
+				if (!attemptId) {
+					try {
+						const { data } = await examUserExamStart({
+							examId: this.examData.examId,
+							examScene: 1
+						});
+						if (data.code !== 200 || !data.data || !data.data.attemptId) {
+							uni.showToast({
+								title: data.message || '开始考试失败',
+								icon: 'none'
+							});
+							return;
+						}
+						attemptId = data.data.attemptId;
+					} catch (e) {
+						console.error('开始考试失败', e);
+						uni.showToast({ title: '开始考试失败', icon: 'none' });
+						return;
+					}
+				}
+
+				uni.navigateTo({
+					url: `/pages_safetyEducationExamination/views/exam/onlineExamPlay?examId=${encodeURIComponent(this.examData.examId)}&attemptId=${encodeURIComponent(attemptId)}&examKind=${encodeURIComponent(this.examData.examKind || '')}`
+				});
 			},
 			},
 		},
 		},
 	}
 	}

+ 841 - 2
pages_safetyEducationExamination/views/exam/onlineExamPlay.vue

@@ -1,8 +1,847 @@
+<!-- 在线考试答题页 -->
 <template>
 <template>
+	<view class="exam-screen">
+		<!-- 顶部栏 -->
+		<view class="topbar">
+			<view class="topbar-left" @click="requestSubmit(false)">‹ 交卷</view>
+			<view class="topbar-title">{{ examInfo.examName || '考试中' }}</view>
+			<view class="topbar-right" @click="toggleQuestionNav">题号</view>
+		</view>
+
+		<!-- 剩余时间条 -->
+		<view class="time-bar">
+			<view class="time-bar-left">
+				<text class="time-label">剩余时间</text>
+				<text class="time-value">{{ formattedTime }}</text>
+			</view>
+			<text class="time-tip">请合理安排答题时间</text>
+		</view>
+
+		<!-- 答题统计 -->
+		<view class="stat-row">
+			<view class="stat-item">
+				<text class="stat-num stat-answered">{{ answeredCount }}</text>
+				<text class="stat-label">已答</text>
+			</view>
+			<view class="stat-divider"></view>
+			<view class="stat-item">
+				<text class="stat-num">{{ unansweredCount }}</text>
+				<text class="stat-label">未答</text>
+			</view>
+			<view class="stat-divider"></view>
+			<view class="stat-item">
+				<text class="stat-num">{{ totalCount }}</text>
+				<text class="stat-label">总题</text>
+			</view>
+		</view>
+
+		<!-- 题号导航面板 -->
+		<view v-if="showQuestionNav" class="q-nav-panel">
+			<view class="q-nav-scroll-wrapper">
+				<view class="q-nav-header">
+					<text class="q-nav-title">题目列表</text>
+					<text class="q-nav-close" @click="toggleQuestionNav">✕</text>
+				</view>
+				<scroll-view scroll-y class="q-nav-scroll">
+					<view v-for="(group, gi) in questionTypeGroups" :key="gi" class="q-group">
+						<view class="q-group-title">{{ group.questionTypeName }}({{ group.questionCount }}题 {{ group.totalScore }}分)</view>
+						<view class="q-num-row">
+							<view
+								v-for="(q, qi) in group.questions"
+								:key="qi"
+								class="q-num"
+								:class="gi === groupIndex && qi === questionIndex ? 'current' : (q.answered === 1 ? 'answered' : '')"
+								@click="selectQuestion(gi, qi)"
+							>{{ q.questionNo }}</view>
+						</view>
+					</view>
+				</scroll-view>
+			</view>
+		</view>
+
+		<!-- 题目内容 -->
+		<scroll-view scroll-y class="q-scroll-content">
+			<view v-if="currentQuestion.snapshotQuestionId" class="q-card">
+				<view class="q-header">
+					<view class="q-header-left">
+						<text class="q-type-badge">{{ currentQuestion.questionTypeName }}</text>
+						<text class="q-num-text">第 {{ currentQuestion.questionNo }} 题</text>
+						<text class="q-score-text">({{ currentQuestion.questionScore }}分)</text>
+					</view>
+					<text v-if="currentQuestion.difficulty" class="q-difficulty">{{ getDifficultyText(currentQuestion.difficulty) }}</text>
+				</view>
+				<view class="q-content">
+					<rich-text :nodes="currentQuestion.questionContent || ''"></rich-text>
+				</view>
+				<view class="q-options">
+					<view
+						v-for="(opt, oi) in (currentQuestion.options || [])"
+						:key="oi"
+						class="q-option"
+						:class="{ selected: isSelected(opt.optionLabel) }"
+						@click="selectAnswer(opt)"
+					>
+						<view class="q-option-label">{{ opt.optionLabel }}</view>
+						<view class="q-option-content">
+							<rich-text :nodes="opt.optionContent || opt.optionLabel"></rich-text>
+						</view>
+					</view>
+				</view>
+			</view>
+			<view v-else-if="pageLoading" class="empty-tip">加载中...</view>
+			<view v-else class="empty-tip">暂无题目</view>
+			<view class="scroll-bottom-space"></view>
+		</scroll-view>
+
+		<!-- 上一题/下一题 -->
+		<view class="q-nav">
+			<view class="q-nav-btn" :class="{ disabled: isFirstQuestion }" @click="prevQuestion">上一题</view>
+			<view class="q-nav-btn q-nav-btn-next" :class="{ disabled: isLastQuestion }" @click="nextQuestion">下一题</view>
+		</view>
+
+		<!-- 底部提交 -->
+		<view class="exam-bottom">
+			<view class="btn-primary" @click="requestSubmit(false)">提交试卷</view>
+		</view>
+
+		<!-- 交卷结果遮罩 -->
+		<view v-if="showResult" class="result-shade">
+			<view class="result-card">
+				<view class="result-title">{{ resultData.title }}</view>
+				<view class="result-score">{{ resultData.score }} 分</view>
+				<view class="result-desc">{{ resultData.desc }}</view>
+				<view class="result-btns">
+					<view v-if="resultData.canRetake" class="result-btn result-btn-outline" @click="retakeExam">重新考试</view>
+					<view class="result-btn result-btn-primary" @click="closeResult">{{ resultData.canRetake ? '返回' : '确认' }}</view>
+				</view>
+			</view>
+		</view>
+	</view>
 </template>
 </template>
 
 
 <script>
 <script>
+	import {
+		examElExamAttemptReviewSidebar,
+		examElExamAttemptReviewQuestion,
+		examUserExamGetExamSurplusTime,
+		examUserExamAnswerSave,
+		examUserExamSubmit,
+		examUserExamStart,
+	} from '@/pages_safetyEducationExamination/api/index.js'
+
+	export default {
+		data() {
+			return {
+				examId: '',
+				attemptId: '',
+				examKind: '',
+				examInfo: {},
+				questionTypeGroups: [],
+				currentQuestion: {},
+				groupIndex: 0,
+				questionIndex: 0,
+				remainingSeconds: 0,
+				timer: null,
+				hasWarnedTime: false,
+				showQuestionNav: false,
+				pageLoading: false,
+				isSaving: false,
+				isSubmitting: false,
+				hasSubmitted: false,
+				showResult: false,
+				resultData: {},
+			}
+		},
+		computed: {
+			isFirstQuestion() {
+				return this.groupIndex === 0 && this.questionIndex === 0;
+			},
+			isLastQuestion() {
+				const lastGi = this.questionTypeGroups.length - 1;
+				if (lastGi < 0) return true;
+				const lastGroup = this.questionTypeGroups[lastGi];
+				return this.groupIndex === lastGi && this.questionIndex === (lastGroup.questions.length - 1);
+			},
+			totalCount() {
+				return this.questionTypeGroups.reduce((s, g) => s + (g.questions ? g.questions.length : 0), 0);
+			},
+			answeredCount() {
+				return this.questionTypeGroups.reduce((s, g) => {
+					return s + (g.questions || []).filter(q => q.answered === 1).length;
+				}, 0);
+			},
+			unansweredCount() {
+				return this.totalCount - this.answeredCount;
+			},
+			formattedTime() {
+				const s = Math.max(0, Math.floor(this.remainingSeconds));
+				const m = Math.floor(s / 60);
+				const sec = s % 60;
+				return m + '分' + (sec < 10 ? '0' : '') + sec + '秒';
+			},
+		},
+		onLoad(options) {
+			this.examId = options.examId || '';
+			this.attemptId = options.attemptId || '';
+			this.examKind = options.examKind || '';
+			if (!this.attemptId) {
+				uni.showToast({ title: '考试信息无效', icon: 'none' });
+				setTimeout(() => uni.navigateBack(), 1500);
+				return;
+			}
+			this.pageLoading = true;
+			this.loadSidebar();
+		},
+		onShow() {
+			if (this.hasSubmitted || !this.attemptId) return;
+			if (this.questionTypeGroups.length > 0) {
+				this.syncRemainingTime();
+			}
+		},
+		onHide() {
+			this.clearTimer();
+			this.saveCurrentAnswer();
+		},
+		onUnload() {
+			this.clearTimer();
+			this.saveCurrentAnswer();
+		},
+		methods: {
+			async loadSidebar() {
+				try {
+					const { data } = await examElExamAttemptReviewSidebar({
+						attemptId: this.attemptId,
+						examKind: this.examKind,
+					});
+					if (data.code === 200 && data.data) {
+						this.examInfo = {
+							examTypeName: data.data.examTypeName || '',
+							examName: data.data.examName || '',
+							questionCount: data.data.questionCount || 0,
+							totalScore: data.data.totalScore || 0,
+							passScore: data.data.passScore || 0,
+						};
+						this.$set(this, 'questionTypeGroups', data.data.questionTypeGroups || []);
+						this.groupIndex = 0;
+						this.questionIndex = 0;
+						const firstGroup = this.questionTypeGroups[0];
+						if (firstGroup && firstGroup.questions && firstGroup.questions.length > 0) {
+							await this.loadQuestion(firstGroup.questions[0].snapshotQuestionId);
+							this.syncRemainingTime();
+						}
+					} else {
+						uni.showToast({ title: data.message || '加载失败', icon: 'none' });
+					}
+				} catch (e) {
+					console.error('加载题号导航失败', e);
+				} finally {
+					this.pageLoading = false;
+				}
+			},
+			async loadQuestion(snapshotQuestionId) {
+				try {
+					const { data } = await examElExamAttemptReviewQuestion({
+						attemptId: this.attemptId,
+						snapshotQuestionId: snapshotQuestionId,
+						examScene: 1,
+					});
+					if (data.code === 200 && data.data) {
+						this.$set(this, 'currentQuestion', data.data);
+					}
+				} catch (e) {
+					console.error('加载题目失败', e);
+				}
+			},
+			async syncRemainingTime() {
+				try {
+					const { data } = await examUserExamGetExamSurplusTime({ attemptId: this.attemptId });
+					if (data.code === 200) {
+						this.remainingSeconds = Number(data.data) || 0;
+						this.clearTimer();
+						this.startTimer();
+					}
+				} catch (e) {
+					console.error('获取剩余时间失败', e);
+				}
+			},
+			startTimer() {
+				if (this.timer) return;
+				this.timer = setInterval(() => {
+					if (this.remainingSeconds <= 0) {
+						this.clearTimer();
+						this.autoSubmit();
+						return;
+					}
+					this.remainingSeconds--;
+					if (this.remainingSeconds === 60 && !this.hasWarnedTime) {
+						this.hasWarnedTime = true;
+						uni.showToast({ title: '请注意剩余时间', icon: 'none', duration: 2000 });
+					}
+				}, 1000);
+			},
+			clearTimer() {
+				if (this.timer) {
+					clearInterval(this.timer);
+					this.timer = null;
+				}
+			},
+			selectAnswer(opt) {
+				if (!this.currentQuestion.snapshotQuestionId) return;
+				let tags = Array.isArray(this.currentQuestion.answerTags) ? [...this.currentQuestion.answerTags] : [];
+				if (this.currentQuestion.questionType === 2) {
+					const idx = tags.indexOf(opt.optionLabel);
+					if (idx === -1) {
+						tags.push(opt.optionLabel);
+					} else {
+						tags.splice(idx, 1);
+					}
+				} else {
+					tags = [opt.optionLabel];
+				}
+				this.$set(this.currentQuestion, 'answerTags', tags);
+			},
+			isSelected(label) {
+				const tags = this.currentQuestion.answerTags;
+				return Array.isArray(tags) && tags.indexOf(label) !== -1;
+			},
+			saveCurrentAnswer() {
+				const q = this.currentQuestion;
+				if (!q || !q.snapshotQuestionId) return Promise.resolve();
+				const tags = Array.isArray(q.answerTags) ? q.answerTags.filter(Boolean) : [];
+				if (tags.length === 0) return Promise.resolve();
+				return examUserExamAnswerSave({
+					attemptId: this.attemptId,
+					snapshotQuestionId: q.snapshotQuestionId,
+					userAnswer: tags.join(','),
+				}).then(res => {
+					if (res.data && res.data.code === 200) {
+						const groups = this.questionTypeGroups;
+						if (groups[this.groupIndex] && groups[this.groupIndex].questions[this.questionIndex]) {
+							this.$set(groups[this.groupIndex].questions[this.questionIndex], 'answered', 1);
+						}
+					}
+				}).catch(e => {
+					console.error('保存答案失败', e);
+				});
+			},
+			async changeQuestion(targetGi, targetQi) {
+				if (this.isSaving) return;
+				if (targetGi === this.groupIndex && targetQi === this.questionIndex) return;
+				const group = this.questionTypeGroups[targetGi];
+				if (!group || !group.questions || !group.questions[targetQi]) return;
+				this.isSaving = true;
+				await this.saveCurrentAnswer();
+				this.isSaving = false;
+				this.groupIndex = targetGi;
+				this.questionIndex = targetQi;
+				await this.loadQuestion(group.questions[targetQi].snapshotQuestionId);
+			},
+			selectQuestion(gi, qi) {
+				this.changeQuestion(gi, qi);
+				this.showQuestionNav = false;
+			},
+			prevQuestion() {
+				if (this.isFirstQuestion || this.isSaving) return;
+				if (this.questionIndex > 0) {
+					this.changeQuestion(this.groupIndex, this.questionIndex - 1);
+				} else {
+					const prevGi = this.groupIndex - 1;
+					const prevGroup = this.questionTypeGroups[prevGi];
+					if (prevGroup) {
+						this.changeQuestion(prevGi, prevGroup.questions.length - 1);
+					}
+				}
+			},
+			nextQuestion() {
+				if (this.isLastQuestion || this.isSaving) return;
+				const currentGroup = this.questionTypeGroups[this.groupIndex];
+				if (this.questionIndex < currentGroup.questions.length - 1) {
+					this.changeQuestion(this.groupIndex, this.questionIndex + 1);
+				} else {
+					this.changeQuestion(this.groupIndex + 1, 0);
+				}
+			},
+			toggleQuestionNav() {
+				this.showQuestionNav = !this.showQuestionNav;
+			},
+			getNumClass(gi, qi, q) {
+				if (gi === this.groupIndex && qi === this.questionIndex) return 'current';
+				if (q.answered === 1) return 'answered';
+				return '';
+			},
+			getDifficultyText(d) {
+				return { 1: '简单', 2: '一般', 3: '较难', 4: '很难' }[d] || '';
+			},
+			async requestSubmit(isAuto) {
+				if (this.isSubmitting || this.hasSubmitted) return;
+				this.isSubmitting = true;
+				await this.saveCurrentAnswer();
+				const unanswered = this.unansweredCount;
+				if (isAuto) {
+					await this.doSubmit();
+					this.isSubmitting = false;
+					return;
+				}
+				const content = unanswered > 0 ? `还有${unanswered}题未答,确认要交卷吗?` : '确认要交卷吗?';
+				uni.showModal({
+					title: '提示',
+					content: content,
+					success: async (res) => {
+						if (res.confirm) {
+							await this.doSubmit();
+						}
+						this.isSubmitting = false;
+					},
+					fail: () => { this.isSubmitting = false; }
+				});
+			},
+			async autoSubmit() {
+				if (this.isSubmitting || this.hasSubmitted) return;
+				this.isSubmitting = true;
+				await this.saveCurrentAnswer();
+				uni.showToast({ title: '考试时间结束,正在提交…', icon: 'none' });
+				await this.doSubmit();
+				this.isSubmitting = false;
+			},
+			async doSubmit() {
+				try {
+					const { data } = await examUserExamSubmit({ attemptId: this.attemptId });
+					if (data.code === 200 && data.data) {
+						this.hasSubmitted = true;
+						this.clearTimer();
+						this.buildResult(data.data);
+						this.showResult = true;
+					} else {
+						uni.showToast({ title: data.message || '交卷失败', icon: 'none' });
+					}
+				} catch (e) {
+					console.error('交卷失败', e);
+					uni.showToast({ title: '交卷失败', icon: 'none' });
+				}
+			},
+			buildResult(resp) {
+				const score = resp.totalScore || 0;
+				const pass = score >= (this.examInfo.passScore || 0);
+				if (pass) {
+					this.resultData = {
+						title: '考试通过',
+						score: score,
+						desc: resp.needPrompt === 1 ? '恭喜!本次成绩为最佳记录' : '考试已通过',
+						canRetake: false,
+					};
+				} else if (resp.canContinueExam === 1) {
+					this.resultData = {
+						title: '考试未通过',
+						score: score,
+						desc: `未达及格线 ${this.examInfo.passScore} 分,可重新考试`,
+						canRetake: true,
+					};
+				} else {
+					this.resultData = {
+						title: '考试未通过',
+						score: score,
+						desc: '未达及格线,暂无剩余考试机会',
+						canRetake: false,
+					};
+				}
+			},
+			async retakeExam() {
+				try {
+					const { data } = await examUserExamStart({ examId: this.examId, examScene: 1 });
+					if (data.code === 200 && data.data && data.data.attemptId) {
+						this.attemptId = data.data.attemptId;
+						this.showResult = false;
+						this.hasSubmitted = false;
+						this.isSubmitting = false;
+						this.hasWarnedTime = false;
+						this.$set(this, 'questionTypeGroups', []);
+						this.$set(this, 'currentQuestion', {});
+						this.groupIndex = 0;
+						this.questionIndex = 0;
+						this.remainingSeconds = 0;
+						this.pageLoading = true;
+						this.loadSidebar();
+					} else {
+						uni.showToast({ title: data.message || '重新考试失败', icon: 'none' });
+					}
+				} catch (e) {
+					console.error('重新考试失败', e);
+					uni.showToast({ title: '重新考试失败', icon: 'none' });
+				}
+			},
+			closeResult() {
+				this.showResult = false;
+				uni.navigateBack();
+			},
+		}
+	}
 </script>
 </script>
 
 
-<style>
-</style>
+<style lang="stylus" scoped>
+	.exam-screen
+		height 100%
+		display flex
+		flex-direction column
+		background #f5f7fa
+		position relative
+
+	.topbar
+		display flex
+		align-items center
+		justify-content space-between
+		height 88rpx
+		padding 0 28rpx
+		background #fff
+		border-bottom 1rpx solid #e8edf3
+		flex-shrink 0
+
+	.topbar-left
+		font-size 28rpx
+		color #1677ff
+		min-width 80rpx
+
+	.topbar-title
+		font-size 28rpx
+		font-weight 600
+		color #222
+		flex 1
+		text-align center
+
+	.topbar-right
+		font-size 28rpx
+		color #1677ff
+		min-width 80rpx
+		text-align right
+
+	.time-bar
+		display flex
+		align-items center
+		justify-content space-between
+		padding 20rpx 28rpx
+		background linear-gradient(90deg, #1677ff 0%, #0958d9 100%)
+		flex-shrink 0
+
+	.time-bar-left
+		display flex
+		align-items baseline
+
+	.time-label
+		font-size 24rpx
+		color rgba(255,255,255,0.85)
+		margin-right 16rpx
+
+	.time-value
+		font-size 44rpx
+		font-weight 700
+		color #fff
+
+	.time-tip
+		font-size 22rpx
+		color rgba(255,255,255,0.75)
+
+	.stat-row
+		display flex
+		align-items center
+		background #fff
+		padding 20rpx 0
+		flex-shrink 0
+		border-bottom 1rpx solid #e8edf3
+
+	.stat-item
+		flex 1
+		display flex
+		flex-direction column
+		align-items center
+
+	.stat-divider
+		width 1rpx
+		height 40rpx
+		background #eee
+
+	.stat-num
+		font-size 36rpx
+		font-weight 700
+		color #333
+		line-height 1
+
+	.stat-answered
+		color #1677ff
+
+	.stat-label
+		font-size 22rpx
+		color #999
+		margin-top 6rpx
+
+	.q-nav-panel
+		position absolute
+		top 0
+		left 0
+		right 0
+		bottom 0
+		background rgba(0,0,0,0.45)
+		z-index 100
+		display flex
+		flex-direction column
+		justify-content flex-end
+
+	.q-nav-scroll-wrapper
+		background #fff
+		border-radius 32rpx 32rpx 0 0
+		max-height 70vh
+		display flex
+		flex-direction column
+
+	.q-nav-header
+		display flex
+		align-items center
+		justify-content space-between
+		padding 28rpx 28rpx 16rpx
+		flex-shrink 0
+
+	.q-nav-title
+		font-size 30rpx
+		font-weight 600
+		color #222
+
+	.q-nav-close
+		font-size 32rpx
+		color #999
+		padding 10rpx
+
+	.q-nav-scroll
+		flex 1
+		overflow hidden
+		padding-bottom 40rpx
+
+	.q-group
+		padding 0 28rpx 16rpx
+
+	.q-group-title
+		font-size 24rpx
+		color #666
+		line-height 60rpx
+
+	.q-num-row
+		display flex
+		flex-wrap wrap
+
+	.q-num
+		width 72rpx
+		height 72rpx
+		line-height 72rpx
+		text-align center
+		font-size 24rpx
+		border-radius 12rpx
+		margin 8rpx
+		background #f4f6fa
+		color #333
+		border 1rpx solid transparent
+
+	.q-num.answered
+		background #e6f4ff
+		color #1677ff
+		border-color #91caff
+
+	.q-num.current
+		background #1677ff
+		color #fff
+
+	.q-scroll-content
+		flex 1
+		overflow hidden
+
+	.q-card
+		margin 24rpx
+		padding 28rpx
+		background #fff
+		border-radius 20rpx
+		border 1rpx solid #dde3ed
+
+	.q-header
+		display flex
+		align-items center
+		justify-content space-between
+		margin-bottom 20rpx
+
+	.q-header-left
+		display flex
+		align-items center
+
+	.q-type-badge
+		font-size 20rpx
+		padding 4rpx 14rpx
+		background #e6f4ff
+		color #1677ff
+		border 1rpx solid #91caff
+		border-radius 8rpx
+		margin-right 12rpx
+
+	.q-num-text
+		font-size 26rpx
+		color #333
+
+	.q-score-text
+		font-size 22rpx
+		color #999
+
+	.q-difficulty
+		font-size 22rpx
+		color #fa8c16
+		background #fff7e6
+		padding 4rpx 12rpx
+		border-radius 8rpx
+
+	.q-content
+		font-size 28rpx
+		line-height 1.8
+		color #222
+		margin-bottom 24rpx
+
+	.q-options
+		display flex
+		flex-direction column
+
+	.q-option
+		display flex
+		align-items center
+		padding 22rpx 20rpx
+		margin-bottom 16rpx
+		border 2rpx solid #dde3ed
+		border-radius 16rpx
+		background #fff
+
+	.q-option.selected
+		border-color #1677ff
+		background #e6f4ff
+
+	.q-option-label
+		width 48rpx
+		height 48rpx
+		line-height 48rpx
+		text-align center
+		border-radius 50%
+		background #f4f6fa
+		color #333
+		font-size 24rpx
+		font-weight 600
+		margin-right 20rpx
+		flex-shrink 0
+
+	.q-option.selected .q-option-label
+		background #1677ff
+		color #fff
+
+	.q-option-content
+		flex 1
+		font-size 26rpx
+		color #333
+		line-height 1.6
+
+	.scroll-bottom-space
+		height 200rpx
+
+	.q-nav
+		display flex
+		background #fff
+		border-top 1rpx solid #e8edf3
+		flex-shrink 0
+
+	.q-nav-btn
+		flex 1
+		height 88rpx
+		line-height 88rpx
+		text-align center
+		font-size 28rpx
+		color #1677ff
+
+	.q-nav-btn.q-nav-btn-next
+		border-left 1rpx solid #e8edf3
+
+	.q-nav-btn.disabled
+		color #ccc
+
+	.exam-bottom
+		padding 20rpx 24rpx
+		padding-bottom calc(20rpx + env(safe-area-inset-bottom))
+		background #fff
+		border-top 1rpx solid #e8edf3
+		flex-shrink 0
+
+	.btn-primary
+		background #1677ff
+		color #fff
+		border-radius 16rpx
+		font-size 30rpx
+		font-weight 600
+		height 88rpx
+		line-height 88rpx
+		text-align center
+
+	.result-shade
+		position fixed
+		top 0
+		left 0
+		right 0
+		bottom 0
+		background rgba(0,0,0,0.55)
+		z-index 200
+		display flex
+		align-items center
+		justify-content center
+
+	.result-card
+		width 580rpx
+		background #fff
+		border-radius 24rpx
+		padding 48rpx 40rpx 36rpx
+		display flex
+		flex-direction column
+		align-items center
+
+	.result-title
+		font-size 32rpx
+		font-weight 700
+		color #222
+		margin-bottom 20rpx
+
+	.result-score
+		font-size 72rpx
+		font-weight 700
+		color #1677ff
+		line-height 1
+		margin-bottom 16rpx
+
+	.result-desc
+		font-size 26rpx
+		color #666
+		text-align center
+		margin-bottom 36rpx
+		line-height 1.6
+
+	.result-btns
+		display flex
+		width 100%
+
+	.result-btn
+		flex 1
+		height 80rpx
+		line-height 80rpx
+		text-align center
+		border-radius 14rpx
+		font-size 28rpx
+
+	.result-btn-outline
+		border 1rpx solid #1677ff
+		color #1677ff
+		margin-right 20rpx
+
+	.result-btn-primary
+		background #1677ff
+		color #fff
+
+	.empty-tip
+		text-align center
+		padding 80rpx 0
+		color #aaa
+		font-size 26rpx
+</style>