1076 lines
34 KiB
Vue
1076 lines
34 KiB
Vue
<template>
|
||
<view class="exam-detail-container">
|
||
<!-- 顶部信息栏 -->
|
||
<view class="exam-header">
|
||
<view class="header-info">
|
||
<text class="exam-name">{{ examInfo.examName || '加载中...' }}</text>
|
||
<text class="exam-subject" v-if="examInfo.subjectName">{{ examInfo.subjectName }}</text>
|
||
</view>
|
||
<view class="timer-section">
|
||
<text class="timer-label">剩余时间</text>
|
||
<text class="timer-value" :class="{ 'timer-warning': remainingTime < 300 }">
|
||
{{ formatTime(remainingTime) }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 答题卡 -->
|
||
<view class="answer-sheet" v-if="showAnswerSheet">
|
||
<view class="sheet-header">
|
||
<text class="sheet-title">答题卡</text>
|
||
<text class="close-btn" @click="showAnswerSheet = false">×</text>
|
||
</view>
|
||
<view class="sheet-content">
|
||
<view
|
||
v-for="(question, index) in questions"
|
||
:key="question.id"
|
||
class="sheet-item"
|
||
:class="{
|
||
'answered': answers[question.id] !== undefined && answers[question.id] !== '',
|
||
'current': currentIndex === index
|
||
}"
|
||
@click="jumpToQuestion(index)"
|
||
>
|
||
{{ index + 1 }}
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 题目区域 -->
|
||
<scroll-view
|
||
class="questions-container"
|
||
scroll-y
|
||
:scroll-into-view="'question-' + currentIndex"
|
||
scroll-with-animation
|
||
>
|
||
<view
|
||
v-for="(question, index) in questions"
|
||
:key="question.id"
|
||
:id="'question-' + index"
|
||
class="question-item"
|
||
>
|
||
<view class="question-header">
|
||
<text class="question-number">第 {{ index + 1 }} 题</text>
|
||
<text class="question-score">{{ question.score }}分</text>
|
||
</view>
|
||
|
||
<view class="question-content">
|
||
<text class="question-text">
|
||
{{ question.questionContent }}
|
||
<text v-if="question.questionType === 'multiple'" class="multiple-tip">(可多选)</text>
|
||
</text>
|
||
</view>
|
||
|
||
<!-- 单选题 -->
|
||
<view v-if="question.questionType === 'single'" class="options-list">
|
||
<view
|
||
v-for="(option, optIndex) in parseOptions(question.options)"
|
||
:key="optIndex"
|
||
class="option-item"
|
||
:class="{ 'selected': answers[question.id] === option }"
|
||
@click="selectAnswer(question.id, option)"
|
||
>
|
||
<view class="option-radio">
|
||
<view class="radio-dot" v-if="answers[question.id] === option"></view>
|
||
</view>
|
||
<text class="option-label">{{ getOptionLabel(optIndex) }}</text>
|
||
<text class="option-text">{{ option }}</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 多选题 -->
|
||
<view v-if="question.questionType === 'multiple'">
|
||
<view class="multiple-hint">💡 提示:本题为多选题,可以选择多个答案</view>
|
||
<view class="options-list">
|
||
<view
|
||
v-for="(option, optIndex) in parseOptions(question.options)"
|
||
:key="optIndex"
|
||
class="option-item"
|
||
:class="{ 'selected': isOptionSelected(question.id, option) }"
|
||
@click="toggleMultipleAnswer(question.id, option)"
|
||
>
|
||
<view class="option-checkbox">
|
||
<text class="checkbox-icon" v-if="isOptionSelected(question.id, option)">✓</text>
|
||
</view>
|
||
<text class="option-label">{{ getOptionLabel(optIndex) }}</text>
|
||
<text class="option-text">{{ option }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 判断题 -->
|
||
<view v-if="question.questionType === 'judge'" class="options-list">
|
||
<view
|
||
class="option-item"
|
||
:class="{ 'selected': answers[question.id] === '正确' }"
|
||
@click="selectAnswer(question.id, '正确')"
|
||
>
|
||
<view class="option-radio">
|
||
<view class="radio-dot" v-if="answers[question.id] === '正确'"></view>
|
||
</view>
|
||
<text class="option-text">正确</text>
|
||
</view>
|
||
<view
|
||
class="option-item"
|
||
:class="{ 'selected': answers[question.id] === '错误' }"
|
||
@click="selectAnswer(question.id, '错误')"
|
||
>
|
||
<view class="option-radio">
|
||
<view class="radio-dot" v-if="answers[question.id] === '错误'"></view>
|
||
</view>
|
||
<text class="option-text">错误</text>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 填空题 -->
|
||
<view v-if="question.questionType === 'fill'" class="fill-answer">
|
||
<textarea
|
||
v-model="answers[question.id]"
|
||
placeholder="请输入答案"
|
||
class="fill-input"
|
||
@blur="saveAnswers"
|
||
></textarea>
|
||
</view>
|
||
|
||
<!-- 简答题 -->
|
||
<view v-if="question.questionType === 'essay'" class="essay-answer">
|
||
<textarea
|
||
v-model="answers[question.id]"
|
||
placeholder="请输入答案"
|
||
class="essay-input"
|
||
:auto-height="true"
|
||
@blur="saveAnswers"
|
||
></textarea>
|
||
</view>
|
||
</view>
|
||
</scroll-view>
|
||
|
||
<!-- 底部操作栏 -->
|
||
<view class="bottom-actions">
|
||
<button class="btn-sheet" @click="showAnswerSheet = !showAnswerSheet">答题卡</button>
|
||
<button
|
||
class="btn-prev"
|
||
:disabled="currentIndex === 0"
|
||
@click="prevQuestion"
|
||
>上一题</button>
|
||
<button
|
||
class="btn-next"
|
||
:disabled="currentIndex === questions.length - 1"
|
||
@click="nextQuestion"
|
||
>下一题</button>
|
||
<button class="btn-submit" @click="showSubmitModal = true">提交</button>
|
||
</view>
|
||
|
||
<!-- 提交确认弹窗 -->
|
||
<view class="submit-modal" v-if="showSubmitModal" @click.stop>
|
||
<view class="modal-mask" @click="showSubmitModal = false"></view>
|
||
<view class="modal-content">
|
||
<view class="modal-header">
|
||
<text class="modal-title">确认提交</text>
|
||
</view>
|
||
<view class="modal-body">
|
||
<text class="modal-text">确定要提交答案吗?提交后将无法修改。</text>
|
||
<view class="answer-stats">
|
||
<text class="stats-item">已答题:{{ answeredCount }}/{{ questions.length }}</text>
|
||
<text class="stats-item">未答题:{{ unansweredCount }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="modal-footer">
|
||
<button class="btn-cancel" @click="showSubmitModal = false">取消</button>
|
||
<button class="btn-confirm" @click="submitExam">确认提交</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 退出确认弹窗 -->
|
||
<view class="exit-modal" v-if="showExitModal" @click.stop>
|
||
<view class="modal-mask" @click="showExitModal = false"></view>
|
||
<view class="modal-content">
|
||
<view class="modal-header">
|
||
<text class="modal-title">退出考试</text>
|
||
</view>
|
||
<view class="modal-body">
|
||
<view class="answer-stats">
|
||
<text class="stats-item">已答题:{{ answeredCount }}/{{ questions.length }}</text>
|
||
<text class="stats-item">未答题:{{ unansweredCount }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="modal-footer">
|
||
<button class="btn-cancel" @click="showExitModal = false">取消</button>
|
||
<button class="btn-confirm-exit" @click="confirmExit">确定退出</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
</view>
|
||
</template>
|
||
|
||
<script>
|
||
import { getExamQuestions, submitExamAnswer } from '@/api/study/exam.js'
|
||
|
||
export default {
|
||
data() {
|
||
return {
|
||
examId: null,
|
||
examInfo: {},
|
||
questions: [],
|
||
answers: {},
|
||
currentIndex: 0,
|
||
loading: false,
|
||
showAnswerSheet: false,
|
||
showSubmitModal: false,
|
||
showExitModal: false,
|
||
isExiting: false, // 标记是否正在退出
|
||
// 倒计时相关
|
||
remainingTime: 0,
|
||
timer: null,
|
||
startTime: null,
|
||
duration: 0
|
||
}
|
||
},
|
||
computed: {
|
||
answeredCount() {
|
||
// 只计算当前考试题目的答案数量,避免包含旧题目的答案
|
||
return this.questions.filter(question => {
|
||
const answer = this.answers[question.id]
|
||
return answer !== undefined && answer !== '' && answer !== null
|
||
}).length
|
||
},
|
||
unansweredCount() {
|
||
return this.questions.length - this.answeredCount
|
||
}
|
||
},
|
||
onLoad(options) {
|
||
if (options.id) {
|
||
this.examId = options.id
|
||
this.loadExamData()
|
||
} else {
|
||
uni.showToast({
|
||
title: '缺少考试ID',
|
||
icon: 'none'
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 1500)
|
||
}
|
||
},
|
||
onBackPress() {
|
||
// 如果用户已确认退出,不拦截
|
||
if (this.isExiting) {
|
||
return false
|
||
}
|
||
// 拦截返回按钮,显示退出确认弹窗
|
||
if (this.answeredCount > 0) {
|
||
this.showExitModal = true
|
||
return true // 阻止默认返回行为
|
||
}
|
||
return false
|
||
},
|
||
onUnload() {
|
||
this.clearTimer()
|
||
// 保存答案到本地(退出时也会保存)
|
||
this.saveAnswers()
|
||
},
|
||
onHide() {
|
||
// 页面隐藏时保存答案
|
||
this.saveAnswers()
|
||
},
|
||
methods: {
|
||
async loadExamData() {
|
||
this.loading = true
|
||
try {
|
||
const response = await getExamQuestions(this.examId)
|
||
if (response.code === 200 && response.data) {
|
||
this.examInfo = response.data.exam || {}
|
||
this.questions = response.data.questions || []
|
||
this.duration = (this.examInfo.duration || 120) * 60 // 转换为秒
|
||
|
||
// 初始化倒计时
|
||
this.remainingTime = this.duration
|
||
this.startTime = Date.now()
|
||
this.startTimer()
|
||
|
||
// 加载本地保存的答案
|
||
this.loadAnswers()
|
||
} else {
|
||
throw new Error(response.msg || '加载失败')
|
||
}
|
||
} catch (error) {
|
||
console.error('加载考试数据失败', error)
|
||
const errorMsg = error.message || '加载失败'
|
||
|
||
// 如果是已做过的提示,显示模态框而不是toast
|
||
if (errorMsg.includes('已经完成过') || errorMsg.includes('不能重复作答')) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: errorMsg,
|
||
showCancel: false,
|
||
confirmText: '我知道了',
|
||
success: () => {
|
||
uni.navigateBack()
|
||
}
|
||
})
|
||
} else {
|
||
uni.showToast({
|
||
title: errorMsg,
|
||
icon: 'none',
|
||
duration: 2000
|
||
})
|
||
setTimeout(() => {
|
||
uni.navigateBack()
|
||
}, 2000)
|
||
}
|
||
} finally {
|
||
this.loading = false
|
||
}
|
||
},
|
||
|
||
startTimer() {
|
||
this.clearTimer()
|
||
this.timer = setInterval(() => {
|
||
const elapsed = Math.floor((Date.now() - this.startTime) / 1000)
|
||
this.remainingTime = Math.max(0, this.duration - elapsed)
|
||
|
||
if (this.remainingTime <= 0) {
|
||
this.clearTimer()
|
||
uni.showModal({
|
||
title: '时间到',
|
||
content: '考试时间已到,系统将自动提交答案。',
|
||
showCancel: false,
|
||
success: () => {
|
||
this.submitExam()
|
||
}
|
||
})
|
||
}
|
||
}, 1000)
|
||
},
|
||
|
||
clearTimer() {
|
||
if (this.timer) {
|
||
clearInterval(this.timer)
|
||
this.timer = null
|
||
}
|
||
},
|
||
|
||
formatTime(seconds) {
|
||
const hours = Math.floor(seconds / 3600)
|
||
const minutes = Math.floor((seconds % 3600) / 60)
|
||
const secs = seconds % 60
|
||
if (hours > 0) {
|
||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||
}
|
||
return `${minutes}:${secs.toString().padStart(2, '0')}`
|
||
},
|
||
|
||
parseOptions(optionsStr) {
|
||
if (!optionsStr) return []
|
||
try {
|
||
return JSON.parse(optionsStr)
|
||
} catch (e) {
|
||
// 如果不是JSON,尝试按逗号分割
|
||
return optionsStr.split(',').map(s => s.trim())
|
||
}
|
||
},
|
||
|
||
getOptionLabel(index) {
|
||
return String.fromCharCode(65 + index) + '.'
|
||
},
|
||
|
||
selectAnswer(questionId, answer) {
|
||
this.$set(this.answers, questionId, answer)
|
||
this.saveAnswers()
|
||
},
|
||
|
||
toggleMultipleAnswer(questionId, option) {
|
||
const currentAnswer = this.answers[questionId] || ''
|
||
const answerArray = currentAnswer ? currentAnswer.split(',') : []
|
||
const index = answerArray.indexOf(option)
|
||
|
||
if (index > -1) {
|
||
answerArray.splice(index, 1)
|
||
} else {
|
||
answerArray.push(option)
|
||
}
|
||
|
||
this.$set(this.answers, questionId, answerArray.join(','))
|
||
this.saveAnswers()
|
||
},
|
||
|
||
isOptionSelected(questionId, option) {
|
||
const answer = this.answers[questionId] || ''
|
||
return answer.split(',').includes(option)
|
||
},
|
||
|
||
prevQuestion() {
|
||
if (this.currentIndex > 0) {
|
||
this.currentIndex--
|
||
}
|
||
},
|
||
|
||
nextQuestion() {
|
||
if (this.currentIndex < this.questions.length - 1) {
|
||
this.currentIndex++
|
||
}
|
||
},
|
||
|
||
jumpToQuestion(index) {
|
||
this.currentIndex = index
|
||
this.showAnswerSheet = false
|
||
},
|
||
|
||
saveAnswers() {
|
||
// 保存答案到本地存储,包含用户ID以区分不同用户
|
||
const userInfo = uni.getStorageSync('userInfo') || {}
|
||
const userId = userInfo.userId || userInfo.id
|
||
|
||
const key = `exam_answers_${this.examId}`
|
||
uni.setStorageSync(key, {
|
||
userId: userId, // 保存用户ID
|
||
answers: this.answers,
|
||
timestamp: Date.now()
|
||
})
|
||
},
|
||
|
||
loadAnswers() {
|
||
// 从本地存储加载答案,但只加载当前用户的答案
|
||
const userInfo = uni.getStorageSync('userInfo') || {}
|
||
const currentUserId = userInfo.userId || userInfo.id
|
||
|
||
const key = `exam_answers_${this.examId}`
|
||
const saved = uni.getStorageSync(key)
|
||
|
||
// 验证用户ID,只有当前用户的答案才会加载
|
||
if (saved && saved.answers) {
|
||
if (saved.userId && saved.userId === currentUserId) {
|
||
// 用户ID匹配,加载答案
|
||
this.answers = { ...this.answers, ...saved.answers }
|
||
console.log('加载了当前用户的答案缓存')
|
||
} else {
|
||
// 用户ID不匹配,清除旧答案
|
||
console.log('检测到用户切换,清除旧答案缓存')
|
||
uni.removeStorageSync(key)
|
||
this.answers = {}
|
||
}
|
||
}
|
||
},
|
||
|
||
confirmExit() {
|
||
// 保存答案到本地
|
||
this.saveAnswers()
|
||
// 标记正在退出,防止 onBackPress 拦截
|
||
this.isExiting = true
|
||
this.showExitModal = false
|
||
this.clearTimer()
|
||
// 返回上一页
|
||
this.$nextTick(() => {
|
||
const pages = getCurrentPages()
|
||
console.log('当前页面栈:', pages.length)
|
||
// 如果页面栈大于1,说明有上一页,可以返回
|
||
if (pages.length > 1) {
|
||
uni.navigateBack({
|
||
delta: 1
|
||
})
|
||
} else {
|
||
// 如果没有上一页,跳转到考试列表页
|
||
uni.redirectTo({
|
||
url: '/pages/exam/list'
|
||
})
|
||
}
|
||
})
|
||
},
|
||
|
||
async submitExam() {
|
||
this.showSubmitModal = false
|
||
this.clearTimer()
|
||
|
||
// 检查是否有题目
|
||
if (!this.questions || this.questions.length === 0) {
|
||
uni.showToast({
|
||
title: '没有可提交的题目',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 构建答案数组
|
||
const answerList = this.questions.map(question => {
|
||
if (!question || !question.id) {
|
||
console.warn('题目数据不完整:', question)
|
||
return null
|
||
}
|
||
return {
|
||
questionId: question.id,
|
||
answer: this.answers[question.id] || ''
|
||
}
|
||
}).filter(item => item !== null) // 过滤掉无效项
|
||
|
||
// 检查答案列表是否为空
|
||
if (answerList.length === 0) {
|
||
uni.showToast({
|
||
title: '答案数据异常,请重试',
|
||
icon: 'none'
|
||
})
|
||
return
|
||
}
|
||
|
||
// 计算用时(秒)
|
||
const duration = Math.floor((Date.now() - this.startTime) / 1000)
|
||
|
||
// 调试信息
|
||
console.log('提交答案数据:', {
|
||
examId: this.examId,
|
||
answerCount: answerList.length,
|
||
questionCount: this.questions.length,
|
||
duration: duration,
|
||
answerList: answerList
|
||
})
|
||
|
||
uni.showLoading({
|
||
title: '提交中...',
|
||
mask: true
|
||
})
|
||
|
||
try {
|
||
const response = await submitExamAnswer(this.examId, answerList, duration)
|
||
if (response.code === 200) {
|
||
// 清除本地保存的答案
|
||
const key = `exam_answers_${this.examId}`
|
||
uni.removeStorageSync(key)
|
||
|
||
uni.hideLoading()
|
||
uni.showToast({
|
||
title: '提交成功',
|
||
icon: 'success'
|
||
})
|
||
|
||
// 跳转到结果页面
|
||
setTimeout(() => {
|
||
// 检查响应数据中是否有 id
|
||
const scoreId = (response.data && response.data.id) ? response.data.id : ''
|
||
uni.redirectTo({
|
||
url: `/pages/exam/result?examId=${this.examId}${scoreId ? `&scoreId=${scoreId}` : ''}`
|
||
})
|
||
}, 1500)
|
||
} else {
|
||
throw new Error(response.msg || '提交失败')
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading()
|
||
console.error('提交答案失败', error)
|
||
|
||
// 解析错误信息
|
||
let errorMessage = '网络错误,请重试'
|
||
if (error && error.message) {
|
||
errorMessage = error.message
|
||
// 如果是后端返回的数组越界错误,给出更友好的提示
|
||
if (errorMessage.includes('Index:') && errorMessage.includes('Size:')) {
|
||
errorMessage = '提交数据格式错误,请重试'
|
||
}
|
||
// 如果是已提交的错误,显示特殊提示
|
||
if (errorMessage.includes('已提交') || errorMessage.includes('重复提交') || errorMessage.includes('已经完成过')) {
|
||
uni.showModal({
|
||
title: '提示',
|
||
content: errorMessage,
|
||
showCancel: false,
|
||
confirmText: '返回',
|
||
success: () => {
|
||
uni.navigateBack()
|
||
}
|
||
})
|
||
return
|
||
}
|
||
} else if (error && error.errMsg) {
|
||
errorMessage = error.errMsg
|
||
}
|
||
|
||
uni.showModal({
|
||
title: '提交失败',
|
||
content: errorMessage,
|
||
showCancel: true,
|
||
confirmText: '重试',
|
||
cancelText: '取消',
|
||
success: (res) => {
|
||
if (res.confirm) {
|
||
this.submitExam()
|
||
} else {
|
||
// 重新启动计时器
|
||
this.startTimer()
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.exam-detail-container {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100vh;
|
||
background-color: #f5f7fa;
|
||
}
|
||
|
||
.exam-header {
|
||
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
|
||
padding: 30rpx;
|
||
padding-top: calc(var(--status-bar-height) + 30rpx);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 30rpx 40rpx;
|
||
padding-top: calc(var(--status-bar-height) + 30rpx);
|
||
}
|
||
|
||
.header-info {
|
||
flex: 1;
|
||
|
||
.exam-name {
|
||
display: block;
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
margin-bottom: 10rpx;
|
||
}
|
||
|
||
.exam-subject {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
}
|
||
}
|
||
|
||
.timer-section {
|
||
text-align: right;
|
||
|
||
.timer-label {
|
||
display: block;
|
||
font-size: 24rpx;
|
||
color: rgba(255, 255, 255, 0.8);
|
||
margin-bottom: 8rpx;
|
||
}
|
||
|
||
.timer-value {
|
||
display: block;
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #fff;
|
||
|
||
&.timer-warning {
|
||
color: #ffeb3b;
|
||
animation: blink 1s infinite;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes blink {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.answer-sheet {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
display: flex;
|
||
align-items: flex-end;
|
||
|
||
.sheet-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 30rpx;
|
||
background: #fff;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
|
||
.sheet-title {
|
||
font-size: 32rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.close-btn {
|
||
font-size: 48rpx;
|
||
color: #999;
|
||
line-height: 1;
|
||
}
|
||
}
|
||
|
||
.sheet-content {
|
||
background: #fff;
|
||
padding: 30rpx;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 20rpx;
|
||
|
||
.sheet-item {
|
||
width: 80rpx;
|
||
height: 80rpx;
|
||
line-height: 80rpx;
|
||
text-align: center;
|
||
border: 2rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
background: #fff;
|
||
|
||
&.answered {
|
||
background: rgb(55 140 224);
|
||
color: #fff;
|
||
border-color: rgb(55 140 224);
|
||
}
|
||
|
||
&.current {
|
||
border-color: rgb(55 140 224);
|
||
border-width: 4rpx;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.questions-container {
|
||
flex: 1;
|
||
padding: 20rpx;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.question-item {
|
||
background: #fff;
|
||
border-radius: 16rpx;
|
||
padding: 30rpx;
|
||
margin-bottom: 20rpx;
|
||
|
||
.question-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 20rpx;
|
||
padding-bottom: 20rpx;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
|
||
.question-number {
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
color: #333;
|
||
}
|
||
|
||
.question-score {
|
||
font-size: 24rpx;
|
||
color: rgb(55 140 224);
|
||
background: #e6f7ff;
|
||
padding: 4rpx 12rpx;
|
||
border-radius: 8rpx;
|
||
}
|
||
}
|
||
|
||
.question-content {
|
||
margin-bottom: 30rpx;
|
||
|
||
.question-text {
|
||
font-size: 30rpx;
|
||
line-height: 1.6;
|
||
color: #333;
|
||
|
||
.multiple-tip {
|
||
color: rgb(55 140 224);
|
||
font-size: 26rpx;
|
||
font-weight: 500;
|
||
margin-left: 8rpx;
|
||
}
|
||
}
|
||
}
|
||
|
||
.multiple-hint {
|
||
padding: 16rpx 20rpx;
|
||
margin-bottom: 20rpx;
|
||
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
|
||
border-left: 4rpx solid rgb(55 140 224);
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
color: rgb(55 140 224);
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
|
||
.options-list {
|
||
.option-item {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 20rpx;
|
||
margin-bottom: 16rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 12rpx;
|
||
background: #fff;
|
||
|
||
&.selected {
|
||
border-color: rgb(55 140 224);
|
||
background: #e6f7ff;
|
||
}
|
||
|
||
.option-radio {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 2rpx solid #ddd;
|
||
border-radius: 50%;
|
||
margin-right: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
.radio-dot {
|
||
width: 24rpx;
|
||
height: 24rpx;
|
||
background: rgb(55 140 224);
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
|
||
.option-checkbox {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
border: 2rpx solid #ddd;
|
||
border-radius: 8rpx;
|
||
margin-right: 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: #fff;
|
||
|
||
.checkbox-icon {
|
||
color: rgb(55 140 224);
|
||
font-size: 28rpx;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
|
||
.option-label {
|
||
font-size: 28rpx;
|
||
color: rgb(55 140 224);
|
||
font-weight: bold;
|
||
margin-right: 12rpx;
|
||
min-width: 60rpx;
|
||
}
|
||
|
||
.option-text {
|
||
flex: 1;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
}
|
||
}
|
||
}
|
||
|
||
.fill-answer, .essay-answer {
|
||
.fill-input, .essay-input {
|
||
width: 100%;
|
||
min-height: 120rpx;
|
||
padding: 20rpx;
|
||
border: 2rpx solid #e0e0e0;
|
||
border-radius: 12rpx;
|
||
font-size: 28rpx;
|
||
background: #fff;
|
||
}
|
||
|
||
.essay-input {
|
||
min-height: 200rpx;
|
||
}
|
||
}
|
||
|
||
.bottom-actions {
|
||
display: flex;
|
||
gap: 10rpx;
|
||
padding: 20rpx;
|
||
background: #fff;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
|
||
button {
|
||
flex: 1;
|
||
padding: 20rpx;
|
||
border-radius: 8rpx;
|
||
font-size: 28rpx;
|
||
border: none;
|
||
|
||
&.btn-sheet {
|
||
background: #f5f5f5;
|
||
color: #333;
|
||
}
|
||
|
||
&.btn-prev, &.btn-next {
|
||
background: #e6f7ff;
|
||
color: rgb(55 140 224);
|
||
|
||
&:disabled {
|
||
background: #f5f5f5;
|
||
color: #ccc;
|
||
}
|
||
}
|
||
|
||
&.btn-submit {
|
||
background: linear-gradient(135deg, rgb(55 140 224) 0%, rgb(45 120 200) 100%);
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
}
|
||
}
|
||
|
||
.submit-modal,
|
||
.exit-modal {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 2000;
|
||
|
||
.modal-mask {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(4rpx);
|
||
}
|
||
|
||
.modal-content {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
width: 600rpx;
|
||
max-width: 90%;
|
||
background: #fff;
|
||
border-radius: 24rpx;
|
||
overflow: hidden;
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.15);
|
||
|
||
@media (min-width: 768px) {
|
||
width: 500px;
|
||
border-radius: 20px;
|
||
}
|
||
|
||
.modal-header {
|
||
padding: 40rpx 30rpx 30rpx;
|
||
text-align: center;
|
||
border-bottom: 1rpx solid #f0f0f0;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 36px 30px 24px;
|
||
}
|
||
|
||
.modal-title {
|
||
font-size: 36rpx;
|
||
font-weight: bold;
|
||
color: #1a1a1a;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 32px;
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 40rpx 30rpx;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 36px 30px;
|
||
}
|
||
|
||
.modal-text {
|
||
display: block;
|
||
font-size: 30rpx;
|
||
color: #333;
|
||
margin-bottom: 24rpx;
|
||
text-align: center;
|
||
line-height: 1.6;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 28px;
|
||
margin-bottom: 20px;
|
||
}
|
||
}
|
||
|
||
.answer-stats {
|
||
background: #f8f9fa;
|
||
border-radius: 12rpx;
|
||
padding: 24rpx;
|
||
display: flex;
|
||
justify-content: space-around;
|
||
|
||
@media (min-width: 768px) {
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
}
|
||
|
||
.stats-item {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.5;
|
||
|
||
@media (min-width: 768px) {
|
||
font-size: 26px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.modal-footer {
|
||
display: flex;
|
||
border-top: 1rpx solid #f0f0f0;
|
||
|
||
button {
|
||
flex: 1;
|
||
padding: 28rpx;
|
||
border: none;
|
||
font-size: 30rpx;
|
||
font-weight: 500;
|
||
background: transparent;
|
||
transition: background-color 0.2s;
|
||
|
||
@media (min-width: 768px) {
|
||
padding: 24px;
|
||
font-size: 28px;
|
||
}
|
||
|
||
&.btn-cancel {
|
||
color: #666;
|
||
border-right: 1rpx solid #f0f0f0;
|
||
|
||
&:active {
|
||
background: #f5f5f5;
|
||
}
|
||
}
|
||
|
||
&.btn-confirm {
|
||
color: rgb(55 140 224);
|
||
font-weight: 600;
|
||
|
||
&:active {
|
||
background: rgba(55, 140, 224, 0.1);
|
||
}
|
||
}
|
||
|
||
&.btn-confirm-exit {
|
||
color: #ff4d4f;
|
||
font-weight: 600;
|
||
|
||
&:active {
|
||
background: rgba(255, 77, 79, 0.1);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|