用 Uni-app 开发 C3 继续教育题库系统:静态资源导入、响应式交互与考试逻辑实现全解析
基于 Uni-app 开发的 C3 继续教育题库系统是一个功能完整的移动端学习应用,专注于提供高质量的在线练习和模拟考试体验。系统采用现代化的技术栈,具备良好的用户体验和稳定的性能表现。
一、核心功能模块
- 题库练习系统
1)三种题型支持:单选题(400道)、多选题(200道)、判断题(400道)
2)实时答题反馈,正确答案绿色标识,错误答案红色标识
3)智能题型切换,一键切换不同类型题目
4)自动分页浏览,支持上一题 / 下一题快速导航
5)单选题和判断题答对自动跳题功能 - 模拟考试系统
1)智能组卷算法:随机抽取 40 道单选题 + 20 道多选题 + 40 道判断题
2)严格考试计时:90 分钟倒计时,时间到自动交卷
3)实时答题卡:直观显示已答 / 未答题目状态
4)智能评分系统:支持多选题少选部分得分(0.5 分)
5)详细成绩分析:按题型统计得分,错题归类展示 - 数据管理系统
1)静态 JSON 文件优化加载,提升应用启动速度
2)全局数据共享,实现页面间数据无缝传递
3)本地数据缓存,减少网络请求
4)数据验证机制,确保题目数据完整性
二、技术实现亮点
1、前端技术栈
1)Uni-app 框架:实现一套代码多端运行
2)Vue.js 响应式数据绑定:确保界面与数据实时同步
3)Uni-ui 组件库:提供丰富的 UI 组件支持
4)原生 CSS3 动画:提升用户交互体验
2、核心技术特性
1)模块化架构设计,代码组织清晰
2)响应式布局适配,支持各种屏幕尺寸
3)智能状态管理,优化应用性能
4)完善的错误处理机制
5)优雅的加载状态和错误提示
3、用户体验优化
1)流畅的页面切换动画
2)直观的答题状态标识
3)智能交卷提醒,未答题目提示
4)详细的成绩分析报告
5)友好的操作反馈机制
三、系统优势
1、跨平台兼容性:基于 Uni-app 开发,可同时运行于 iOS、Android、H5 等多个平台
2、性能优化:静态资源本地加载,无需网络请求,响应速度快
3、用户体验:现代化 UI 设计,流畅的交互动画,直观的操作流程
4、功能完整:从练习到考试,从答题到成绩分析,覆盖完整学习周期
5、易于维护:模块化代码结构,清晰的逻辑分层,便于后续功能扩展
代码如下:
主页面
<template>
<view class="container">
<!-- 添加加载状态提示 -->
<view v-if="loading" class="loading-container">
<activity-indicator size="large" color="#007AFF" />
<text class="loading-text">正在加载题目数据...</text>
</view>
<!-- 主按钮区域 -->
<view v-else class="button-container">
<button
class="practice-btn"
:disabled="!dataReady"
@tap="goPractice"
>
<text class="btn-text">进入题库练习</text>
<text v-if="!dataReady" class="btn-tip">(题目加载中)</text>
</button>
<button
class="mock-btn"
:disabled="!dataReady"
@tap="startMockExam"
>
<text class="btn-text">开始模拟考试</text>
<text v-if="!dataReady" class="btn-tip">(题目加载中)</text>
</button>
</view>
<!-- 错误提示 -->
<view v-if="error" class="error-container">
<text class="error-text">题目加载失败,请检查数据文件</text>
<button class="retry-btn" @tap="loadAllQuestions">
<text>重新加载</text>
</button>
</view>
</view>
</template>
<script>
// 方案一:使用import静态导入(推荐)
import singleData from '@/static/data/single.json'
import multiData from '@/static/data/multi.json'
import judgeData from '@/static/data/judge.json'
export default {
data() {
return {
singleQuestions: [],
multiQuestions: [],
judgeQuestions: [],
loading: true,
error: false
}
},
computed: {
dataReady() {
return (
this.singleQuestions.length > 0 &&
this.multiQuestions.length > 0 &&
this.judgeQuestions.length > 0
)
}
},
onLoad() {
this.loadAllQuestions()
},
methods: {
// 修改后的加载方法
loadAllQuestions() {
this.loading = true
this.error = false
try {
// 直接使用导入的数据
this.singleQuestions = singleData
this.multiQuestions = multiData
this.judgeQuestions = judgeData
console.log('题目加载完成', {
single: this.singleQuestions.length,
multi: this.multiQuestions.length,
judge: this.judgeQuestions.length
})
// 存储到全局
getApp().globalData.questions = {
single: this.singleQuestions,
multi: this.multiQuestions,
judge: this.judgeQuestions
}
} catch (e) {
console.error('加载题目失败:', e)
this.error = true
} finally {
this.loading = false
}
},
goPractice() {
if (!this.dataReady) {
uni.showToast({
title: '题目尚未加载完成',
icon: 'none'
})
return
}
uni.navigateTo({
url: '/pages/practice/practice'
})
},
startMockExam() {
if (!this.dataReady) {
uni.showToast({
title: '题目尚未加载完成',
icon: 'none'
})
return
}
// 随机组卷(30单选+20多选+10判断)
const examQuestions = [
...this.getRandomQuestions(this.singleQuestions, 30),
...this.getRandomQuestions(this.multiQuestions, 20),
...this.getRandomQuestions(this.judgeQuestions, 10)
]
// 存储到全局
getApp().globalData.examQuestions = this.shuffleArray(examQuestions)
uni.navigateTo({
url: '/pages/mock/mock'
})
},
// 随机选题
getRandomQuestions(questions, count) {
const shuffled = [...questions].sort(() => Math.random() - 0.5)
return shuffled.slice(0, count)
},
// 打乱数组顺序
shuffleArray(array) {
const result = [...array]
for (let i = result.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[result[i], result[j]] = [result[j], result[i]]
}
return result
}
}
}
</script>
<style>
.container {
padding: 40rpx;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
background-color: #f8f8f8;
}
.loading-container {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.loading-text {
margin-top: 30rpx;
font-size: 28rpx;
color: #666;
}
.button-container {
width: 100%;
display: flex;
flex-direction: column;
gap: 40rpx;
}
button {
height: 120rpx;
border-radius: 16rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-size: 32rpx;
color: white;
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
}
button[disabled] {
opacity: 0.6;
}
.practice-btn {
background: linear-gradient(135deg, #007AFF, #00a8ff);
}
.mock-btn {
background: linear-gradient(135deg, #FF9500, #FFB700);
}
.btn-text {
font-weight: bold;
font-size: 36rpx;
}
.btn-tip {
font-size: 24rpx;
opacity: 0.8;
margin-top: 8rpx;
}
.error-container {
margin-top: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.error-text {
color: #FF4D4F;
font-size: 28rpx;
margin-bottom: 30rpx;
text-align: center;
}
.retry-btn {
padding: 20rpx 40rpx;
background-color: #007AFF;
color: white;
border-radius: 10rpx;
}
</style>
练习题页面
<template>
<view class="container">
<!-- 顶部题目类型标识 -->
<view class="question-type">
<text>
{{currentType === 'single' ? '单选题' : currentType === 'multi' ? '多选题' : '判断题'}}
</text>
</view>
<!-- 题型切换按钮 -->
<view class="type-switch">
<button
class="type-btn"
:class="{active: currentType === 'single'}"
@click="switchType('single')"
>
单选题
</button>
<button
class="type-btn"
:class="{active: currentType === 'multi'}"
@click="switchType('multi')"
>
多选题
</button>
<button
class="type-btn"
:class="{active: currentType === 'judge'}"
@click="switchType('judge')"
>
判断题
</button>
</view>
<!-- 当前题目 -->
<view v-if="currentQuestion" class="question-box">
<text class="q-title">{{currentPage}}. {{currentQuestion.question}}</text>
<!-- 多选题提示 -->
<view v-if="currentType === 'multi'" class="multi-tip">
<text>(多选题,请选择所有正确答案)</text>
</view>
<view class="options-container">
<view
v-for="(opt, i) in currentQuestion.options"
:key="i"
class="option"
:style="{ backgroundColor: getOptionColor(currentQuestion, i) }"
@click="choose(currentQuestion, i)"
>
<!-- 多选题显示正确对号和错误叉号 -->
<view v-if="currentType === 'multi' && currentQuestion.selected.includes(i)" class="checkmark">
{{ currentQuestion.answer.includes(i) ? '✓' : '✗' }}
</view>
<!-- 单选题显示选择对错标记 -->
<view v-if="currentType === 'single' && currentQuestion.selected !== -1 && currentQuestion.selected === i" class="checkmark">
{{ currentQuestion.answer.includes(i) ? '✓' : '✗' }}
</view>
<text class="option-text">{{getOptionPrefix(i)}} {{opt}}</text>
</view>
</view>
</view>
<!-- 分页控制 -->
<view class="pagination">
<button @click="prevPage" :disabled="currentPage === 1">上一题</button>
<text>{{currentPage}}/{{questions.length}}</text>
<button @click="nextPage" :disabled="currentPage === questions.length">下一题</button>
</view>
</view>
</template>
<script>
// 直接导入 JSON 数据,打包后可用
import singleData from '@/static/data/single.json';
import multiData from '@/static/data/multi.json';
import judgeData from '@/static/data/judge.json';
export default {
data() {
return {
questions: [],
currentPage: 1,
autoNextTimer: null,
currentType: 'single',
loading: false
}
},
computed: {
currentQuestion() {
return this.questions[this.currentPage - 1];
}
},
onLoad() {
this.loadQuestions('single');
},
methods: {
loadQuestions(type) {
this.loading = true;
this.currentType = type;
this.currentPage = 1;
let data = [];
switch(type) {
case 'single': data = singleData; break;
case 'multi': data = multiData; break;
case 'judge': data = judgeData; break;
}
// 格式化题目
this.questions = data.map(q => {
if (type === 'judge') {
return {
...q,
options: ['正确','错误'],
selected: -1,
answer: Array.isArray(q.answer) ? q.answer : [q.answer]
};
} else if (type === 'multi') {
return {...q, selected: []};
} else { // 单选
return {...q, selected: -1, answer: Array.isArray(q.answer) ? q.answer : [q.answer]};
}
});
this.loading = false;
},
getOptionPrefix(i) {
if (this.currentType === 'judge') return '';
return String.fromCharCode(65 + i) + '.';
},
switchType(type) {
if (this.currentType !== type) this.loadQuestions(type);
},
choose(q, i) {
if (this.currentType === 'multi') {
const idx = q.selected.indexOf(i);
if (idx === -1) q.selected.push(i);
else q.selected.splice(idx,1);
} else {
if (q.selected !== -1) return;
q.selected = i;
// 单选题:选择正确自动跳下一题
if (this.currentType === 'single' && q.answer.includes(i)) {
this.setAutoNext();
}
// 判断题:0表示正确选项,自动跳下一题
if (this.currentType === 'judge') {
if (q.answer.includes(i) && i===0) this.setAutoNext();
}
}
},
getOptionColor(q,i) {
if (this.currentType === 'multi') {
if (q.selected.includes(i)) return q.answer.includes(i) ? '#4CAF50' : '#F44336';
return '#f5f5f5';
} else if (this.currentType === 'single') {
if (q.selected === -1) return '#f5f5f5';
if (q.answer.includes(i)) return '#4CAF50';
if (q.selected === i && !q.answer.includes(i)) return '#F44336';
return '#f5f5f5';
} else { // 判断题
if (q.selected === -1) return '#f5f5f5';
if (q.answer.includes(i)) return '#4CAF50';
if (q.selected===i && !q.answer.includes(i)) return '#F44336';
return '#f5f5f5';
}
},
prevPage() {
this.clearAutoNext();
if (this.currentPage > 1) this.currentPage--;
},
nextPage() {
this.clearAutoNext();
if (this.currentPage < this.questions.length) this.currentPage++;
},
setAutoNext() {
this.clearAutoNext();
this.autoNextTimer = setTimeout(()=>{
if(this.currentPage < this.questions.length) this.currentPage++;
},1000);
},
clearAutoNext() {
if(this.autoNextTimer) {
clearTimeout(this.autoNextTimer);
this.autoNextTimer=null;
}
}
},
beforeDestroy() {
this.clearAutoNext();
}
}
</script>
<style>
.container {padding:20rpx; display:flex; flex-direction:column; height:100vh; color:#000;}
.question-type {padding:10rpx 20rpx; background-color:#2196F3; color:#fff; border-radius:20rpx; align-self:flex-start; margin-bottom:20rpx; font-size:28rpx; font-weight:bold;}
.type-switch {display:flex; justify-content:space-between; margin-bottom:20rpx;}
.type-btn {flex:1; margin:0 10rpx; padding:15rpx; font-size:28rpx; border-radius:8rpx; background-color:#f0f0f0; color:#000;}
.type-btn.active {background-color:#2196F3; color:#fff;}
.question-box {flex:1; margin:20rpx 0; padding:30rpx; border:1px solid #eee; border-radius:15rpx; background-color:#fff; display:flex; flex-direction:column;}
.multi-tip {padding:10rpx 0; color:#666; font-size:26rpx; margin-bottom:20rpx;}
.q-title {font-weight:bold; margin-bottom:20rpx; font-size:36rpx; line-height:1.6; color:#000;}
.options-container {flex:1; display:flex; flex-direction:column; justify-content:space-around;}
.option {width:100%; padding:30rpx; margin:15rpx 0; border-radius:12rpx; color:#000; box-sizing:border-box; font-size:32rpx; transition:all 0.3s; display:flex; align-items:center; min-height:120rpx; position:relative;}
.checkmark {position:absolute; left:20rpx; color:#fff; font-weight:bold; font-size:28rpx;}
.option-text {width:100%; word-break:break-word; margin-left:40rpx;}
.option:active {opacity:0.8; transform:scale(0.98);}
.pagination {display:flex; justify-content:space-between; align-items:center; padding:20rpx 0; margin:20rpx 0;}
button {padding:15rpx 40rpx; font-size:32rpx; border-radius:8rpx; background-color:#f0f0f0; color:#000;}
button:disabled {opacity:0.5;}
text {color:#000 !important;}
</style>
模拟考试页面
<template>
<view class="exam-container">
<!-- 考试进行中视图 -->
<view v-if="!examFinished">
<!-- 考试头部信息 -->
<view class="exam-header">
<text class="exam-title">模拟考试</text>
<view class="exam-info">
<text class="question-type">{{getQuestionType(currentQuestion.type)}}</text>
<text>题号: {{currentIndex + 1}}/{{questions.length}}</text>
<text class="timer" :class="{ 'warning': remainingTime < 600 }">时间: {{formatTime(remainingTime)}}</text>
</view>
</view>
<!-- 考试题目 -->
<view class="question-container">
<view class="question-box">
<!-- 修复:确保题目为空时不报错 -->
<text class="q-title" v-if="currentQuestion.id">{{currentQuestion.id}}. {{currentQuestion.question}}</text>
<text class="q-title" v-else>加载题目中...</text>
<!-- 选项容器:修复选中背景不显示问题 -->
<view class="options-container" v-if="currentQuestion.options">
<view
v-for="(opt, i) in currentQuestion.options"
:key="i"
class="option"
:class="{
'selected': isOptionSelected(i), // 核心:确保选中状态生效
'correct': showAnswer && currentQuestion.answer.includes(i),
'wrong': showAnswer && isOptionSelected(i) && !currentQuestion.answer.includes(i)
}"
@click="handleSelectOption(i)"
:disabled="examFinished"
>
<text class="option-text">{{getOptionPrefix(i)}}. {{opt}}</text>
</view>
</view>
<view v-else class="loading-options">选项加载中...</view>
</view>
</view>
<!-- 考试控制按钮 -->
<view class="exam-controls">
<button
class="control-btn prev-btn"
:disabled="currentIndex === 0 || examFinished"
@click="prevQuestion"
>
上一题
</button>
<button
class="control-btn next-btn"
:disabled="currentIndex === questions.length - 1 || examFinished"
@click="nextQuestion"
>
下一题
</button>
<button
class="control-btn submit-btn"
:disabled="examFinished"
@click="confirmSubmit"
>
提交试卷
</button>
</view>
<!-- 答题卡:显示已答/未答状态 -->
<view class="answer-sheet">
<text class="sheet-title">答题卡 ({{answeredCount}}/{{questions.length}})</text>
<view class="sheet-questions">
<view
v-for="(q, index) in questions"
:key="q.id"
class="sheet-item"
:class="{
'answered': isQuestionAnswered(q),
'unanswered': !isQuestionAnswered(q),
'current': index === currentIndex
}"
@click="jumpToQuestion(index)"
:disabled="examFinished"
>
{{index + 1}}
</view>
</view>
</view>
</view>
<!-- 考试结果详情视图 -->
<view v-if="examFinished && showResultDetail">
<view class="result-header">
<text class="result-title">考试结果</text>
<view class="result-summary">
<text class="score">得分: {{score.toFixed(1)}}/100.0</text>
<text class="evaluation">{{evaluation}}</text>
</view>
<button class="back-btn" @click="exitExam">返回</button>
</view>
<view class="result-detail">
<view class="filter-controls">
<button
class="filter-btn"
:class="{ 'active': filterType === 'all' }"
@click="filterType = 'all'"
>
全部题目
</button>
<button
class="filter-btn"
:class="{ 'active': filterType === 'correct' }"
@click="filterType = 'correct'"
>
做对 ({{correctCount}})
</button>
<button
class="filter-btn"
:class="{ 'active': filterType === 'wrong' }"
@click="filterType = 'wrong'"
>
做错/少选 ({{wrongOrPartialCount}})
</button>
<button
class="filter-btn"
:class="{ 'active': filterType === 'unanswered' }"
@click="filterType = 'unanswered'"
>
未答 ({{unansweredCount}})
</button>
</view>
<view class="questions-detail">
<view
v-for="(q, index) in filteredQuestions"
:key="q.id"
class="question-detail-item"
:class="{
'correct': isQuestionCorrect(q),
'wrong': isQuestionWrong(q),
'partial': isQuestionPartial(q),
'unanswered': !isQuestionAnswered(q)
}"
>
<view class="question-header">
<text class="question-num">{{index + 1}}. {{getQuestionType(q.type)}}</text>
<text class="question-status">
{{!isQuestionAnswered(q) ? '未答' : (isQuestionCorrect(q) ? '全对' : (isQuestionPartial(q) ? '少选' : '错误'))}}
</text>
</view>
<view class="question-content">{{q.question}}</view>
<!-- 显示每题得分(修复:少选显示0.5分) -->
<view class="question-score" v-if="isQuestionAnswered(q)">
本题得分: {{getQuestionScore(q)}}/{{getQuestionFullScore(q)}}
</view>
<view class="question-score" v-else>
本题得分: 0/{{getQuestionFullScore(q)}}(未答)
</view>
<view class="options-detail">
<view
v-for="(opt, i) in q.options"
:key="i"
class="detail-option"
:class="{
'user-selected': isUserSelected(q, i),
'correct-answer': q.answer.includes(i),
'wrong-selected': isUserSelected(q, i) && !q.answer.includes(i)
}"
>
<text class="option-prefix">{{getOptionPrefix(i)}}. </text>
<text class="option-content">{{opt}}</text>
<text v-if="q.answer.includes(i)" class="correct-mark">✓ 正确答案</text>
<text v-if="isUserSelected(q, i) && !q.answer.includes(i)" class="wrong-mark">✗ 你的答案</text>
<text v-if="isUserSelected(q, i) && q.answer.includes(i)" class="your-answer-mark">✓ 你的答案</text>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 提交确认弹窗 -->
<uni-popup ref="popup" type="dialog" :mask="true">
<uni-popup-dialog
type="info"
title="确认提交"
content="确定要提交试卷吗?提交后无法修改答案!"
@confirm="submitExam"
@close="closePopup"
></uni-popup-dialog>
</uni-popup>
<!-- 考试结果弹窗 -->
<uni-popup ref="resultPopup" type="dialog" :mask="true">
<uni-popup-dialog
type="info"
title="考试完成!"
:content="`本次考试得分:${score.toFixed(1)}/100.0\n${evaluation}\n\n是否查看详细答题情况?`"
confirmText="查看详情"
cancelText="直接返回"
@confirm="handleShowDetail"
@cancel="exitExam"
></uni-popup-dialog>
</uni-popup>
</view>
</template>
<script>
import singleData from '@/static/data/single.json';
import multiData from '@/static/data/multi.json';
import judgeData from '@/static/data/judge.json';
import uniPopup from '@dcloudio/uni-ui/lib/uni-popup/uni-popup.vue';
import uniPopupDialog from '@dcloudio/uni-ui/lib/uni-popup-dialog/uni-popup-dialog.vue';
export default {
components: { uniPopup, uniPopupDialog },
data() {
return {
questions: [],
currentIndex: 0,
remainingTime: 5400,
timer: null,
showAnswer: false,
examFinished: false,
showResultDetail: false,
score: 0,
evaluation: '',
filterType: 'all'
}
},
computed: {
currentQuestion() {
// 修复:确保currentIndex合法时才返回题目,避免空对象
if (this.questions.length && this.currentIndex >= 0 && this.currentIndex < this.questions.length) {
return this.questions[this.currentIndex];
}
return { id: '', question: '', options: [], answer: [], type: '' };
},
answeredCount() {
return this.questions.filter(q => this.isQuestionAnswered(q)).length;
},
unansweredCount() {
return this.questions.filter(q => !this.isQuestionAnswered(q)).length;
},
correctCount() {
return this.questions.filter(q => this.isQuestionAnswered(q) && this.isQuestionCorrect(q)).length;
},
wrongOrPartialCount() {
return this.questions.filter(q =>
this.isQuestionAnswered(q) && (!this.isQuestionCorrect(q) || this.isQuestionPartial(q))
).length;
},
filteredQuestions() {
switch(this.filterType) {
case 'all':
return this.questions;
case 'correct':
return this.questions.filter(q => this.isQuestionAnswered(q) && this.isQuestionCorrect(q));
case 'wrong':
return this.questions.filter(q =>
this.isQuestionAnswered(q) && (!this.isQuestionCorrect(q) || this.isQuestionPartial(q))
);
case 'unanswered':
return this.questions.filter(q => !this.isQuestionAnswered(q));
default:
return this.questions;
}
}
},
onLoad() {
this.initExam();
this.startTimer();
},
onUnload() {
this.stopTimer();
},
methods: {
initExam() {
try {
// 修复:确保JSON数据格式正确,避免空数据
const validSingle = singleData.filter(q => q.id && q.question && q.options && q.answer);
const validMulti = multiData.filter(q => q.id && q.question && q.options && q.answer);
const validJudge = judgeData.filter(q => q.id && q.question && q.options && q.answer);
const singleQuestions = this.getRandomQuestions(validSingle, 40).map(q => ({
...q,
type: 'single',
selected: -1 // 单选初始未选:-1
}));
const multiQuestions = this.getRandomQuestions(validMulti, 20).map(q => ({
...q,
type: 'multi',
selected: [] // 多选初始未选:空数组
}));
const judgeQuestions = this.getRandomQuestions(validJudge, 40).map(q => ({
...q,
type: 'judge',
selected: -1 // 判断初始未选:-1
}));
this.questions = [...singleQuestions, ...multiQuestions, ...judgeQuestions];
} catch (error) {
console.error('初始化考试失败:', error);
uni.showToast({ title: '加载题目失败', icon: 'none' });
setTimeout(() => { uni.navigateBack(); }, 1500);
}
},
getRandomQuestions(questions, count) {
const shuffled = [...questions].sort(() => Math.random() - 0.5);
return shuffled.slice(0, count);
},
startTimer() {
this.timer = setInterval(() => {
if (this.remainingTime > 0) {
this.remainingTime--;
} else {
this.stopTimer();
this.autoSubmitExam();
}
}, 1000);
},
stopTimer() {
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2,'0')}:${secs.toString().padStart(2,'0')}`;
},
getQuestionType(type) {
const types = { single:'单选题(1分)', multi:'多选题(2分)', judge:'判断题(0.5分)' };
return types[type] || '未知题型';
},
getOptionPrefix(index) {
return String.fromCharCode(65 + index);
},
// 修复:选中状态判断,确保单选/多选都能正确识别
isOptionSelected(index) {
const question = this.currentQuestion;
if (!question || question.selected === undefined) return false;
if (question.type === 'multi') {
// 多选:判断索引是否在selected数组中
return Array.isArray(question.selected) && question.selected.includes(index);
} else {
// 单选/判断:判断selected是否等于索引(排除-1未选状态)
return question.selected !== -1 && question.selected === index;
}
},
isQuestionAnswered(question) {
if (!question || question.selected === undefined) return false;
if (question.type === 'multi') {
return Array.isArray(question.selected) && question.selected.length > 0;
} else {
return question.selected !== -1;
}
},
isQuestionCorrect(question) {
if (!this.isQuestionAnswered(question)) return false;
if (question.type === 'multi') {
return question.selected.length === question.answer.length &&
question.selected.every(opt => question.answer.includes(opt));
} else {
return question.answer.includes(question.selected);
}
},
isQuestionPartial(question) {
if (question.type !== 'multi' || !this.isQuestionAnswered(question)) return false;
// 少选条件:无错选 + 未选全
const noWrongSelection = question.selected.every(opt => question.answer.includes(opt));
const notFullSelection = question.selected.length < question.answer.length;
return noWrongSelection && notFullSelection;
},
isQuestionWrong(question) {
if (!this.isQuestionAnswered(question)) return false;
if (question.type === 'multi') {
// 多选错误:有错选
return question.selected.some(opt => !question.answer.includes(opt));
} else {
// 单选/判断错误:选中≠正确答案
return !question.answer.includes(question.selected);
}
},
// 核心修改:多选题少选得分从1分改为0.5分
getQuestionScore(question) {
if (!this.isQuestionAnswered(question)) return 0;
switch(question.type) {
case 'single':
return this.isQuestionCorrect(question) ? 1 : 0;
case 'multi':
if (this.isQuestionCorrect(question)) return 2; // 全对:2分
if (this.isQuestionPartial(question)) return 0.5; // 少选:0.5分(原1分)
return 0; // 错选:0分
case 'judge':
return this.isQuestionCorrect(question) ? 0.5 : 0;
default:
return 0;
}
},
getQuestionFullScore(question) {
switch(question.type) {
case 'single': return 1;
case 'multi': return 2;
case 'judge': return 0.5;
default: return 0;
}
},
isUserSelected(question, index) {
if (!question || question.selected === undefined) return false;
if (question.type === 'multi') {
return Array.isArray(question.selected) && question.selected.includes(index);
} else {
return question.selected !== -1 && question.selected === index;
}
},
// 修复:选项选择逻辑,确保响应式更新生效
handleSelectOption(index) {
if (this.examFinished) return;
const question = this.currentQuestion;
if (!question) return;
// 用$set确保Vue能监听到selected的变化(响应式核心)
if (question.type === 'multi') {
const selected = [...(question.selected || [])];
const idx = selected.indexOf(index);
if (idx === -1) {
selected.push(index);
} else {
selected.splice(idx, 1);
}
this.$set(this.questions[this.currentIndex], 'selected', selected);
} else {
this.$set(this.questions[this.currentIndex], 'selected', index);
}
},
prevQuestion() {
if (this.currentIndex > 0 && !this.examFinished) {
this.currentIndex--;
}
},
nextQuestion() {
if (this.currentIndex < this.questions.length - 1 && !this.examFinished) {
this.currentIndex++;
}
},
jumpToQuestion(index) {
if (!this.examFinished && index >= 0 && index < this.questions.length) {
this.currentIndex = index;
}
},
confirmSubmit() {
if (this.examFinished) return;
if (this.unansweredCount > 0) {
uni.showModal({
title: '提示',
content: `仍有${this.unansweredCount}道题未答,确定要提交吗?`,
success: (res) => {
if (res.confirm) {
this.$refs.popup.open();
}
}
});
} else {
this.$refs.popup.open();
}
},
closePopup() {
this.$refs.popup.close();
},
autoSubmitExam() {
uni.showModal({
title: '考试时间到',
content: '考试时间已结束,系统将自动提交您的答案',
showCancel: false,
success: () => {
this.submitExam();
}
});
},
submitExam() {
this.stopTimer();
this.examFinished = true;
this.score = this.calculateScore();
this.evaluation = this.getEvaluation(this.score, 100);
this.$refs.popup.close();
setTimeout(() => {
if (this.$refs.resultPopup) {
this.$refs.resultPopup.open();
} else {
uni.showToast({ title: '分数已计算完成', icon: 'none' });
this.showResultDetail = true;
}
}, 100);
},
calculateScore() {
let total = 0;
this.questions.forEach(q => {
total += this.getQuestionScore(q);
});
return total;
},
getEvaluation(score, total) {
const percent = (score / total) * 100;
if (percent < 60) return '评价: 继续努力!';
if (percent < 90) return '评价: 还不错!';
if (percent < 100) return '评价: 你真棒!';
return '评价: 完美!全部答对!';
},
handleShowDetail() {
this.showResultDetail = true;
this.$refs.resultPopup.close();
},
exitExam() {
if (this.$refs.resultPopup) {
this.$refs.resultPopup.close();
}
uni.navigateBack();
}
}
}
</script>
<style>
/* 基础样式保持不变,强化选中状态样式 */
.exam-container {
padding: 20rpx;
display: flex;
flex-direction: column;
height: 100vh;
background-color: #f5f5f5;
}
/* 修复:确保选中选项背景色明显生效 */
.option {
padding: 25rpx;
border-radius: 12rpx;
background-color: #f5f5f5;
transition: all 0.3s ease; /* 增加过渡效果,选中更流畅 */
border: 2rpx solid transparent; /* 透明边框,选中时突出 */
}
/* 选中状态:蓝色背景+白色文字+边框强化 */
.option.selected {
background-color: #2196F3 !important; /* !important确保优先级,避免被覆盖 */
color: white !important;
border-color: #1976D2;
box-shadow: 0 4rpx 8rpx rgba(33, 150, 243, 0.2); /* 增加阴影,更醒目 */
}
/* 加载状态样式 */
.loading-options {
font-size: 28rpx;
color: #666;
padding: 30rpx;
text-align: center;
}
/* 答题卡样式 */
.sheet-item.unanswered {
background-color: #e0e0e0;
color: #666;
}
.sheet-item.answered {
background-color: #2196F3;
color: white;
}
/* 结果页样式 */
.detail-option.user-selected {
background-color: #e3f2fd;
border-left: 4rpx solid #2196F3;
}
.detail-option.correct-answer {
background-color: #e8f5e9;
border-left: 4rpx solid #4CAF50;
}
.detail-option.wrong-selected {
background-color: #ffebee;
border-left: 4rpx solid #F44336;
}
.your-answer-mark {
color: #2196F3;
font-size: 24rpx;
margin-left: 10rpx;
font-weight: bold;
}
.question-detail-item.unanswered {
border-left-color: #9e9e9e;
background-color: #f5f5f5;
}
.question-detail-item.unanswered .question-status {
background-color: #e0e0e0;
color: #666;
}
/* 其他原有样式保持不变 */
.exam-header {
padding: 20rpx;
background-color: #2196F3;
color: white;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.exam-title {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.exam-info {
display: flex;
justify-content: space-between;
font-size: 28rpx;
}
.question-type {
font-weight: bold;
color: #FFEB3B;
}
.timer {
color: #FFEB3B;
font-weight: bold;
}
.timer.warning {
color: #FF5252;
animation: flash 1s infinite alternate;
}
@keyframes flash {
from { opacity: 1; }
to { opacity: 0.7; }
}
.question-container {
flex: 1;
margin-bottom: 20rpx;
overflow-y: auto;
}
.question-box {
background-color: #fff;
border-radius: 15rpx;
padding: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
}
.q-title {
font-size: 32rpx;
line-height: 1.6;
margin-bottom: 30rpx;
color: #333;
display: block;
}
.options-container {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.option.correct {
background-color: #4CAF50;
color: white;
}
.option.wrong {
background-color: #F44336;
color: white;
}
.exam-controls {
display: flex;
justify-content: space-between;
margin-bottom: 20rpx;
}
.control-btn {
flex: 1;
margin: 0 10rpx;
padding: 20rpx;
font-size: 28rpx;
border-radius: 8rpx;
color: white;
border: none;
}
.prev-btn {
background-color: #607D8B;
}
.next-btn {
background-color: #2196F3;
}
.submit-btn {
background-color: #FF5722;
}
.answer-sheet {
background-color: #fff;
border-radius: 15rpx;
padding: 20rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
margin-bottom: 20rpx;
}
.sheet-title {
font-size: 30rpx;
font-weight: bold;
margin-bottom: 15rpx;
display: block;
}
.sheet-questions {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
max-height: 200rpx;
overflow-y: auto;
padding-bottom: 10rpx;
}
.sheet-item {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
}
.sheet-item.current {
border: 2rpx solid #FF5722;
box-sizing: border-box;
}
.result-header {
background-color: #2196F3;
color: white;
padding: 20rpx;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.result-title {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.result-summary {
display: flex;
justify-content: space-between;
font-size: 28rpx;
margin-bottom: 15rpx;
}
.score {
color: #FFEB3B;
font-weight: bold;
}
.evaluation {
font-style: italic;
}
.back-btn {
background-color: #FF5722;
color: white;
border: none;
padding: 15rpx 30rpx;
border-radius: 8rpx;
font-size: 28rpx;
}
.result-detail {
flex: 1;
overflow-y: auto;
}
.filter-controls {
display: flex;
justify-content: space-around;
margin-bottom: 20rpx;
flex-wrap: wrap;
gap: 10rpx;
}
.filter-btn {
background-color: #e0e0e0;
border: none;
padding: 15rpx 20rpx;
border-radius: 8rpx;
font-size: 26rpx;
}
.filter-btn.active {
background-color: #2196F3;
color: white;
}
.questions-detail {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.question-detail-item {
background-color: #fff;
border-radius: 15rpx;
padding: 25rpx;
box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.1);
border-left: 8rpx solid;
}
.question-detail-item.correct {
border-left-color: #4CAF50;
}
.question-detail-item.wrong {
border-left-color: #F44336;
}
.question-detail-item.partial {
border-left-color: #FFC107;
}
.question-header {
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
font-size: 28rpx;
}
.question-num {
font-weight: bold;
color: #333;
}
.question-status {
padding: 5rpx 15rpx;
border-radius: 20rpx;
font-size: 24rpx;
}
.question-detail-item.correct .question-status {
background-color: #e8f5e9;
color: #2e7d32;
}
.question-detail-item.wrong .question-status {
background-color: #ffebee;
color: #c62828;
}
.question-detail-item.partial .question-status {
background-color: #fff8e1;
color: #ff8f00;
}
.question-content {
font-size: 28rpx;
margin-bottom: 15rpx;
line-height: 1.6;
}
.question-score {
font-size: 24rpx;
color: #666;
margin-bottom: 15rpx;
font-style: italic;
}
.options-detail {
display: flex;
flex-direction: column;
gap: 15rpx;
}
.detail-option {
padding: 15rpx;
border-radius: 8rpx;
font-size: 26rpx;
display: flex;
flex-wrap: wrap;
align-items: center;
}
.option-prefix {
font-weight: bold;
margin-right: 10rpx;
}
.option-content {
flex: 1;
}
.correct-mark {
color: #4CAF50;
font-size: 24rpx;
margin-left: 10rpx;
font-weight: bold;
}
.wrong-mark {
color: #F44336;
font-size: 24rpx;
margin-left: 10rpx;
font-weight: bold;
}
</style>
单选题JSON数据库
题库内容涉及敏感词无法放置格式如下
[
{
"id": 1,
"type": "single",
"question": "",
"options": [
"规章制度",
"自治条例",
"国家法规政策",
"地方性法规"
],
"answer": [
2
]
},
{
"id": 2,
"type": "single",
"question": "",
"options": [
"规范",
"平稳",
"有序",
"安全"
],
"answer": [
3
]
}
]
多选题JSON数据库
题库内容涉及敏感词无法放置格式如下
[
{
"id": 401,
"type": "multi",
"question": "《安全生产管理知识》指导用书把施工过程中的安全生产管理知识分别设置为( )。",
"options": [
"管理",
"实操",
"法规",
"法律",
"政策"
],
"answer": [0,2,4]
},
{
"id": 402,
"type": "multi",
"question": "安全生产管理一般包括( )。",
"options": [
"",
"安全生产管理的理论学说",
"安全生产管理的经济学说",
"安全生产管理的实践和经验",
""
],
"answer": [0,1,3,4]
}
]
判断题JSON数据库
题库内容涉及敏感词无法放置格式如下
[
{
"id": 601,
"type": "judge",
"question": "安全生产管理一般包括四个部分,再往外延伸还会涉及安全生产管理的甲方服务等。",
"options": ["正确", "错误"],
"answer": [1]
},
{
"id": 602,
"type": "judge",
"question": "",
"options": ["正确", "错误"],
"answer": [1]
}
]

浙公网安备 33010602011771号