dedsudiyu 6 days ago
parent
commit
6163fc68bb
1 changed files with 969 additions and 2 deletions
  1. 969 2
      pages_safetyEducationExamination/views/exam/mockExamPlay.vue

+ 969 - 2
pages_safetyEducationExamination/views/exam/mockExamPlay.vue

@@ -1,8 +1,975 @@
+<!-- 模拟考试答题页 -->
 <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-correct">{{ correct }}</text>
+				<text class="stat-label">答对</text>
+			</view>
+			<view class="stat-divider"></view>
+			<view class="stat-item">
+				<text class="stat-num stat-wrong">{{ incorrect }}</text>
+				<text class="stat-label">答错</text>
+			</view>
+			<view class="stat-divider"></view>
+			<view class="stat-item">
+				<text class="stat-num">{{ (accuracy * 100).toFixed(0) }}%</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="getNumClass(gi, qi, q)"
+								@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>
+					<view class="q-header-right">
+						<view v-if="currentQuestion.difficulty" class="q-stars">
+							<text
+								v-for="s in 4"
+								:key="s"
+								:class="['star-icon', s <= currentQuestion.difficulty ? 'star-active' : 'star-empty']"
+							>★</text>
+						</view>
+						<view class="view-answer-btn" @click="viewAnswerShowType = !viewAnswerShowType">
+							{{ viewAnswerShowType ? '关闭答案' : '查看答案' }}
+						</view>
+					</view>
+				</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 v-if="viewAnswerShowType" class="answer-result-box">
+					<view class="answer-row">
+						<text class="answer-label">答案:</text>
+						<text class="answer-value">{{ arrayToString(currentQuestion.correctAnswerTags) }}</text>
+					</view>
+					<view class="analysis-title">试题解析</view>
+					<view class="analysis-content">
+						<rich-text v-if="currentQuestion.analysis" :nodes="currentQuestion.analysis"></rich-text>
+						<text v-else class="analysis-empty">无</text>
+					</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">
+					<text class="result-score-num">{{ resultData.score }}</text>
+					<text class="result-score-unit">分</text>
+				</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>
 
 <script>
+	import {
+		examElExamAttemptReviewSidebar,
+		examElExamAttemptReviewQuestion,
+		examUserExamGetExamSurplusTime,
+		examUserExamAnswerSave,
+		examUserExamSubmit,
+		examUserExamStart,
+	} from '@/pages_safetyEducationExamination/api/index.js'
+
+	export default {
+		data() {
+			return {
+				examId: '',
+				attemptId: '',
+				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: {},
+				viewAnswerShowType: false,
+			}
+		},
+		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)
+			},
+			// 答对:isCorrect === 1
+			correct() {
+				let count = 0
+				this.questionTypeGroups.forEach(g => {
+					;(g.questions || []).forEach(q => { if (q.isCorrect === 1) count++ })
+				})
+				return count
+			},
+			// 答错:isCorrect === 0
+			incorrect() {
+				let count = 0
+				this.questionTypeGroups.forEach(g => {
+					;(g.questions || []).forEach(q => { if (q.isCorrect === 0) count++ })
+				})
+				return count
+			},
+			// 正确率
+			accuracy() {
+				if (this.totalCount === 0) return 0
+				return parseFloat((this.correct / this.totalCount).toFixed(2))
+			},
+			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 || ''
+			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 response = await examElExamAttemptReviewSidebar({ attemptId: this.attemptId })
+					const data = response.data
+					this.examInfo = {
+						examTypeName: data.examTypeName || '',
+						examName: data.examName || '',
+						questionCount: data.questionCount || 0,
+						totalScore: data.totalScore || 0,
+						passScore: data.passScore || 0,
+					}
+					this.$set(this, 'questionTypeGroups', 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()
+					}
+				} catch (e) {
+					uni.showToast({ title: '加载失败', icon: 'none' })
+				} finally {
+					this.pageLoading = false
+				}
+			},
+			// 加载题目详情(examScene:2 模拟考试)
+			async loadQuestion(snapshotQuestionId) {
+				try {
+					const response = await examElExamAttemptReviewQuestion({
+						attemptId: this.attemptId,
+						snapshotQuestionId: snapshotQuestionId,
+						examScene: 2,
+					})
+					this.$set(this, 'currentQuestion', response.data)
+					this.$set(this, 'viewAnswerShowType', false)
+				} catch (e) {
+					uni.showToast({ title: '题目加载失败', icon: 'none' })
+				}
+			},
+			// 同步剩余时间并启动倒计时
+			async syncRemainingTime() {
+				try {
+					const response = await examUserExamGetExamSurplusTime({ attemptId: this.attemptId })
+					this.remainingSeconds = Number(response.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] : []
+				const qType = this.currentQuestion.questionType
+				if (qType == 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
+			},
+			// 保存当前答案,同时回写 isCorrect 到题号列表
+			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(response => {
+					// response.data 是 isCorrect 值(1正确 0错误)
+					const groups = this.questionTypeGroups
+					if (groups[this.groupIndex] && groups[this.groupIndex].questions[this.questionIndex]) {
+						this.$set(groups[this.groupIndex].questions[this.questionIndex], 'isCorrect', response.data)
+					}
+				}).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.isCorrect === 1) return 'correct'
+				if (q.isCorrect === 0) return 'wrong'
+				return ''
+			},
+			// 提交前确认
+			async requestSubmit(isAuto) {
+				if (this.isSubmitting || this.hasSubmitted) return
+				this.isSubmitting = true
+				await this.saveCurrentAnswer()
+				// 统计未答题数
+				let unanswered = 0
+				this.questionTypeGroups.forEach(g => {
+					;(g.questions || []).forEach(q => { if (q.isCorrect == null) unanswered++ })
+				})
+				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 response = await examUserExamSubmit({ attemptId: this.attemptId })
+					this.hasSubmitted = true
+					this.clearTimer()
+					this.buildResult(response.data)
+					this.showResult = true
+				} catch (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: '本次模拟考试成绩有效',
+						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 response = await examUserExamStart({ examId: this.examId, examScene: 2 })
+					const data = response.data
+					if (data && data.attemptId) {
+						this.attemptId = data.attemptId
+						this.showResult = false
+						this.hasSubmitted = false
+						this.isSubmitting = false
+						this.hasWarnedTime = false
+						this.viewAnswerShowType = 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: response.message || '重新考试失败', icon: 'none' })
+						uni.navigateBack()
+					}
+				} catch (e) {
+					uni.showToast({ title: '重新考试失败', icon: 'none' })
+				}
+			},
+			closeResult() {
+				this.showResult = false
+				uni.navigateBack()
+			},
+			// 答案标签转显示文字(Y→对 N→错)
+			arrayToString(list) {
+				if (!Array.isArray(list)) return ''
+				return list.map(item => {
+					if (item === 'Y') return '对'
+					if (item === 'N') return '错'
+					return item
+				}).join(',')
+			},
+		}
+	}
 </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-correct
+		color #52c41a
+
+	.stat-wrong
+		color #ff4d4f
+
+	.stat-label
+		font-size 22rpx
+		color #999
+		margin-top 6rpx
+
+	.q-nav-panel
+		position fixed
+		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.correct
+		background #f6ffed
+		color #52c41a
+		border-color #b7eb8f
+
+	.q-num.wrong
+		background #fff2f0
+		color #ff4d4f
+		border-color #ffa39e
+
+	.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
+		flex 1
+		flex-wrap wrap
+
+	.q-header-right
+		display flex
+		align-items center
+		flex-shrink 0
+		margin-left 12rpx
+
+	.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-stars
+		display flex
+		align-items center
+		margin-right 16rpx
+
+	.star-icon
+		font-size 28rpx
+		font-style normal
+
+	.star-active
+		color #fadb14
+
+	.star-empty
+		color #d9d9d9
+
+	.view-answer-btn
+		font-size 22rpx
+		padding 6rpx 18rpx
+		background #0183fa
+		color #fff
+		border-radius 8rpx
+		white-space nowrap
+
+	.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
+
+	.answer-result-box
+		background #f5f5f5
+		border-radius 12rpx
+		padding 24rpx 28rpx
+		margin-top 20rpx
+
+	.answer-row
+		display flex
+		align-items center
+		margin-bottom 16rpx
+
+	.answer-label
+		font-size 26rpx
+		font-weight 600
+		color #333
+
+	.answer-value
+		font-size 26rpx
+		color #1677ff
+		font-weight 700
+
+	.analysis-title
+		font-size 26rpx
+		font-weight 600
+		color #333
+		margin-bottom 12rpx
+
+	.analysis-content
+		font-size 24rpx
+		color #666
+		line-height 1.7
+		padding 0 8rpx
+
+	.analysis-empty
+		font-size 24rpx
+		color #aaa
+
+	.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
+		display flex
+		align-items baseline
+		margin-bottom 16rpx
+
+	.result-score-num
+		font-size 80rpx
+		font-weight 700
+		color #F59A23
+		line-height 1
+
+	.result-score-unit
+		font-size 30rpx
+		color #F59A23
+		margin-left 8rpx
+
+	.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>