peixue-dev/peidu/uniapp/training-package/pages/training/exam.vue

721 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="exam-page">
<!-- 考试信息 -->
<view class="exam-header" v-if="!examStarted">
<view class="exam-title">{{ examInfo.examName }}</view>
<view class="exam-info">
<view class="info-item">
<text class="label">考试时长</text>
<text class="value">{{ examInfo.duration }}分钟</text>
</view>
<view class="info-item">
<text class="label">总分</text>
<text class="value">{{ examInfo.totalScore }}</text>
</view>
<view class="info-item">
<text class="label">及格分</text>
<text class="value">{{ examInfo.passScore }}</text>
</view>
<view class="info-item">
<text class="label">题目数量</text>
<text class="value">{{ examInfo.questionCount }}</text>
</view>
</view>
<view class="exam-description">{{ examInfo.description }}</view>
<button class="btn-start" @click="startExam">开始考试</button>
</view>
<!-- 考试进行中 -->
<view class="exam-content" v-if="examStarted && !examFinished">
<!-- 倒计时 -->
<view class="timer-bar">
<text class="timer-text">剩余时间:{{ formatTime(remainingTime) }}</text>
<text class="question-progress">{{ currentQuestionIndex + 1 }}/{{ questions.length }}</text>
</view>
<!-- 题目 -->
<view class="question-card">
<view class="question-header">
<text class="question-type">{{ getQuestionType(currentQuestion.questionType) }}</text>
<text class="question-score">{{ currentQuestion.score }}分</text>
</view>
<view class="question-content">{{ currentQuestion.questionContent }}</view>
<!-- 选项 -->
<view class="options">
<view
v-for="(option, index) in currentQuestion.options"
:key="index"
class="option-item"
:class="{ 'selected': isOptionSelected(option.key) }"
@click="selectOption(option.key)"
>
<view class="option-key">{{ option.key }}</view>
<view class="option-text">{{ option.value }}</view>
</view>
</view>
<!-- 填空题输入框 -->
<view v-if="currentQuestion.questionType === 'fill'" class="fill-input">
<textarea
v-model="answers[currentQuestion.id]"
placeholder="请输入答案"
class="textarea"
></textarea>
</view>
</view>
<!-- 导航按钮 -->
<view class="nav-buttons">
<button class="btn-prev" @click="prevQuestion" :disabled="currentQuestionIndex === 0">上一题</button>
<button class="btn-next" @click="nextQuestion" v-if="currentQuestionIndex < questions.length - 1">下一题</button>
<button class="btn-submit" @click="confirmSubmit" v-else>提交答卷</button>
</view>
<!-- 答题卡 -->
<view class="answer-sheet">
<view class="sheet-title">答题卡</view>
<view class="sheet-grid">
<view
v-for="(question, index) in questions"
:key="index"
class="sheet-item"
:class="{
'current': index === currentQuestionIndex,
'answered': answers[question.id]
}"
@click="jumpToQuestion(index)"
>
{{ index + 1 }}
</view>
</view>
</view>
</view>
<!-- 考试结果 -->
<view class="exam-result" v-if="examFinished">
<view class="result-icon">{{ examResult.isPassed ? '🎉' : '😢' }}</view>
<view class="result-title">{{ examResult.isPassed ? '恭喜通过考试!' : '很遗憾,未通过考试' }}</view>
<view class="result-score">
<text class="score-value">{{ examResult.totalScore }}</text>
<text class="score-label">分</text>
</view>
<view class="result-stats">
<view class="stat-item">
<text class="stat-value">{{ examResult.correctCount }}</text>
<text class="stat-label">答对</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ examResult.totalCount - examResult.correctCount }}</text>
<text class="stat-label">答错</text>
</view>
<view class="stat-item">
<text class="stat-value">{{ examResult.passScore }}</text>
<text class="stat-label">及格分</text>
</view>
</view>
<!-- 答题详情 -->
<view class="answer-details">
<view class="details-title">答题详情</view>
<view
v-for="(result, index) in examResult.results"
:key="index"
class="detail-item"
:class="{ 'correct': result.isCorrect, 'wrong': !result.isCorrect }"
>
<view class="detail-header">
<text class="detail-index">第{{ index + 1 }}题</text>
<text class="detail-score">{{ result.isCorrect ? '✓' : '✗' }} {{ result.score }}分</text>
</view>
<view class="detail-answer">
<text class="answer-label">你的答案:</text>
<text class="answer-value">{{ result.userAnswer || '未作答' }}</text>
</view>
<view class="detail-answer" v-if="!result.isCorrect">
<text class="answer-label">正确答案:</text>
<text class="answer-value correct-answer">{{ result.correctAnswer }}</text>
</view>
<view class="detail-explanation" v-if="result.explanation">
<text class="explanation-label">解析:</text>
<text class="explanation-text">{{ result.explanation }}</text>
</view>
</view>
</view>
<view class="result-buttons">
<button class="btn-back" @click="goBack">返回</button>
<button class="btn-retry" @click="retryExam" v-if="!examResult.isPassed">重新考试</button>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
examType: '',
level: '',
examInfo: {},
examStarted: false,
examFinished: false,
questions: [],
currentQuestionIndex: 0,
answers: {},
remainingTime: 0,
timer: null,
examResult: null
};
},
computed: {
currentQuestion() {
return this.questions[this.currentQuestionIndex] || {};
}
},
onLoad(options) {
this.examType = options.courseType;
this.level = options.level;
this.loadExamInfo();
},
onUnload() {
if (this.timer) {
clearInterval(this.timer);
}
},
methods: {
// 加载考试信息
async loadExamInfo() {
try {
const res = await this.$http.get('/api/training/exams', {
params: { examType: this.examType }
});
this.examInfo = res.data.find(exam => exam.level === this.level) || res.data[0];
} catch (error) {
uni.showToast({ title: '加载失败', icon: 'none' });
}
},
// 开始考试
async startExam() {
try {
const res = await this.$http.post('/api/training/start-exam', {
examId: this.examInfo.id
});
this.questions = res.data.questions.map(q => ({
...q,
options: JSON.parse(q.options)
}));
this.remainingTime = this.examInfo.duration * 60;
this.examStarted = true;
this.startTimer();
} catch (error) {
uni.showToast({ title: error.message || '开始考试失败', icon: 'none' });
}
},
// 开始计时
startTimer() {
this.timer = setInterval(() => {
this.remainingTime--;
if (this.remainingTime <= 0) {
this.submitExam();
}
}, 1000);
},
// 格式化时间
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}:${secs.toString().padStart(2, '0')}`;
},
// 获取题目类型
getQuestionType(type) {
const types = {
single: '单选题',
multiple: '多选题',
judge: '判断题',
fill: '填空题'
};
return types[type] || '';
},
// 选择选项
selectOption(key) {
const questionId = this.currentQuestion.id;
const questionType = this.currentQuestion.questionType;
if (questionType === 'multiple') {
// 多选题
let selected = this.answers[questionId] || '';
if (selected.includes(key)) {
selected = selected.replace(key, '');
} else {
selected += key;
}
this.$set(this.answers, questionId, selected.split('').sort().join(''));
} else {
// 单选题和判断题
this.$set(this.answers, questionId, key);
}
},
// 判断选项是否被选中
isOptionSelected(key) {
const answer = this.answers[this.currentQuestion.id] || '';
return answer.includes(key);
},
// 上一题
prevQuestion() {
if (this.currentQuestionIndex > 0) {
this.currentQuestionIndex--;
}
},
// 下一题
nextQuestion() {
if (this.currentQuestionIndex < this.questions.length - 1) {
this.currentQuestionIndex++;
}
},
// 跳转到指定题目
jumpToQuestion(index) {
this.currentQuestionIndex = index;
},
// 确认提交
confirmSubmit() {
const unanswered = this.questions.filter(q => !this.answers[q.id]).length;
if (unanswered > 0) {
uni.showModal({
title: '提示',
content: `还有${unanswered}题未作答,确定要提交吗?`,
success: (res) => {
if (res.confirm) {
this.submitExam();
}
}
});
} else {
this.submitExam();
}
},
// 提交考试
async submitExam() {
if (this.timer) {
clearInterval(this.timer);
}
try {
const res = await this.$http.post('/api/training/submit-exam', {
examId: this.examInfo.id,
answers: this.answers
});
this.examResult = res.data;
this.examFinished = true;
} catch (error) {
uni.showToast({ title: '提交失败', icon: 'none' });
}
},
// 重新考试
retryExam() {
this.examStarted = false;
this.examFinished = false;
this.currentQuestionIndex = 0;
this.answers = {};
this.examResult = null;
},
// 返回
goBack() {
uni.navigateBack();
}
}
};
</script>
<style lang="scss" scoped>
.exam-page {
min-height: 100vh;
background: #f5f5f5;
}
.exam-header {
background: #fff;
padding: 40rpx;
text-align: center;
.exam-title {
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.exam-info {
margin-bottom: 30rpx;
.info-item {
display: flex;
justify-content: center;
margin-bottom: 20rpx;
font-size: 28rpx;
.label {
color: #999;
}
.value {
color: #333;
font-weight: bold;
}
}
}
.exam-description {
font-size: 26rpx;
color: #666;
line-height: 1.6;
margin-bottom: 40rpx;
}
.btn-start {
width: 400rpx;
height: 88rpx;
background: linear-gradient(135deg, #7dd3c0, #5bc0ad);
color: #fff;
border-radius: 44rpx;
font-size: 32rpx;
border: none;
}
}
.exam-content {
padding: 20rpx;
}
.timer-bar {
display: flex;
justify-content: space-between;
align-items: center;
background: #fff;
padding: 20rpx 30rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
.timer-text {
font-size: 28rpx;
color: #ff4d4f;
font-weight: bold;
}
.question-progress {
font-size: 28rpx;
color: #999;
}
}
.question-card {
background: #fff;
padding: 30rpx;
border-radius: 12rpx;
margin-bottom: 20rpx;
.question-header {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
.question-type {
font-size: 24rpx;
color: #7dd3c0;
padding: 8rpx 16rpx;
background: rgba(125, 211, 192, 0.1);
border-radius: 8rpx;
}
.question-score {
font-size: 24rpx;
color: #ff4d4f;
}
}
.question-content {
font-size: 30rpx;
color: #333;
line-height: 1.6;
margin-bottom: 30rpx;
}
.options {
.option-item {
display: flex;
align-items: center;
padding: 20rpx;
background: #f5f5f5;
border-radius: 12rpx;
margin-bottom: 20rpx;
border: 2rpx solid transparent;
&.selected {
background: rgba(125, 211, 192, 0.1);
border-color: #7dd3c0;
}
.option-key {
width: 60rpx;
height: 60rpx;
line-height: 60rpx;
text-align: center;
background: #fff;
border-radius: 50%;
font-size: 28rpx;
color: #333;
margin-right: 20rpx;
}
.option-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
}
}
.fill-input {
.textarea {
width: 100%;
min-height: 200rpx;
padding: 20rpx;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 28rpx;
}
}
}
.nav-buttons {
display: flex;
gap: 20rpx;
margin-bottom: 20rpx;
button {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
border: none;
}
.btn-prev {
background: #f5f5f5;
color: #333;
&[disabled] {
opacity: 0.5;
}
}
.btn-next {
background: linear-gradient(135deg, #7dd3c0, #5bc0ad);
color: #fff;
}
.btn-submit {
background: linear-gradient(135deg, #ff6b6b, #ff4d4f);
color: #fff;
}
}
.answer-sheet {
background: #fff;
padding: 30rpx;
border-radius: 12rpx;
.sheet-title {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
}
.sheet-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 20rpx;
.sheet-item {
height: 80rpx;
line-height: 80rpx;
text-align: center;
background: #f5f5f5;
border-radius: 12rpx;
font-size: 28rpx;
color: #333;
&.current {
background: #7dd3c0;
color: #fff;
}
&.answered {
background: rgba(125, 211, 192, 0.3);
}
}
}
}
.exam-result {
background: #fff;
padding: 40rpx;
text-align: center;
.result-icon {
font-size: 120rpx;
margin-bottom: 20rpx;
}
.result-title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 30rpx;
}
.result-score {
margin-bottom: 40rpx;
.score-value {
font-size: 80rpx;
font-weight: bold;
color: #7dd3c0;
}
.score-label {
font-size: 32rpx;
color: #999;
margin-left: 10rpx;
}
}
.result-stats {
display: flex;
justify-content: space-around;
margin-bottom: 40rpx;
.stat-item {
text-align: center;
.stat-value {
display: block;
font-size: 48rpx;
font-weight: bold;
color: #333;
margin-bottom: 10rpx;
}
.stat-label {
font-size: 24rpx;
color: #999;
}
}
}
.answer-details {
text-align: left;
margin-top: 40rpx;
.details-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
}
.detail-item {
padding: 20rpx;
background: #f5f5f5;
border-radius: 12rpx;
margin-bottom: 20rpx;
border-left: 4rpx solid #7dd3c0;
&.wrong {
border-left-color: #ff4d4f;
}
.detail-header {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
.detail-index {
font-size: 28rpx;
color: #333;
font-weight: bold;
}
.detail-score {
font-size: 28rpx;
}
}
.detail-answer {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
.answer-label {
color: #999;
}
.correct-answer {
color: #52c41a;
}
}
.detail-explanation {
font-size: 24rpx;
color: #999;
line-height: 1.6;
margin-top: 15rpx;
padding-top: 15rpx;
border-top: 1rpx solid #e0e0e0;
.explanation-label {
font-weight: bold;
}
}
}
}
.result-buttons {
display: flex;
gap: 20rpx;
margin-top: 40rpx;
button {
flex: 1;
height: 88rpx;
border-radius: 44rpx;
font-size: 32rpx;
border: none;
}
.btn-back {
background: #f5f5f5;
color: #333;
}
.btn-retry {
background: linear-gradient(135deg, #7dd3c0, #5bc0ad);
color: #fff;
}
}
}
</style>