简介:这是一份基于uniapp框架开发的考试答题类小程序源码,面向希望学习跨平台应用开发、了解在线答题交互逻辑的初学者与进阶开发者。项目实现了单选、多选、判断三种题型的答题界面,并支持上一题、下一题自由切换,题目与答案均以静态数据形式预设在前端,未接入后台交互,因此更适合作为学习参考而非实际考试平台。压缩包为zip格式,大小约1.47MB,文件总数暂未提供明细,从项目性质推断应包含Vue页面组件、静态资源与配置文件等。目前已有2372人学习下载,热度较高。通过阅读源码,读者可以掌握uniapp的组件化开发方式、页面跳转、状态管理与数据绑定技巧,理解前端如何维护题目索引并驱动界面更新,同时也能清晰认识到从静态答题到完整在线考试系统所需补充的登录注册、后台题库管理、答案校验、评分展示与防作弊等拓展方向,为后续构建更复杂的在线教育应用打下实践基础。
1. 从一份能直接跑的 uniapp 考试答题小程序说起
前阵子帮一个做职业培训的朋友看项目,他手里有一套 uniapp 开发的考试答题类小程序源码,想改成内部员工考核工具。我拿到手第一反应是:这类项目看着简单,真拆起来坑不少——题库怎么存、倒计时怎么防作弊、交卷后怎么算分、错题本怎么关联用户,每一块都能让新手卡半天。这套源码把考试答题的完整链路都跑通了:题库加载、单选多选判断、倒计时交卷、自动评分、错题回顾、成绩记录,一套流程下来没有断点。它适合两类人:一是想快速搭一个答题类小程序的开发者,二是拿它当 uniapp 实战练手项目、想搞懂跨端页面通信和数据流的人。技术栈就是 uniapp 全家桶,Vue 语法写页面,一套代码编译到微信小程序和 H5,后端接口用常见的 RESTful 风格对接。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这套源码拆开讲。
2. 项目结构与技术选型:uniapp 跨端答题的骨架长什么样
2.1 目录结构逐个说清
拿到源码先别急着跑,把目录扫一遍心里有底。这套项目的目录是 uniapp 标准结构,我按实际文件职责标注:
├── pages/ # 页面目录,每个考试环节一个页面 │ ├── index/ # 首页:考试列表、开始考试入口 │ ├── exam/ # 答题页:核心页面,题目渲染+倒计时 │ ├── result/ # 成绩页:得分、正确率、错题入口 │ └── wrong/ # 错题本:按科目/时间筛选错题 ├── components/ # 公共组件 │ ├── question-card.vue # 题目卡片:题干+选项+选中态 │ └── count-down.vue # 倒计时组件:独立计时逻辑 ├── static/ # 静态资源:图标、占位图 ├── utils/ # 工具函数 │ ├── request.js # 请求封装:统一 baseURL、拦截器 │ └── storage.js # 本地缓存:token、临时答题记录 ├── store/ # 状态管理(Vuex) │ └── exam.js # 考试状态:当前题目索引、答案数组 ├── App.vue # 应用入口:全局生命周期 ├── main.js # 挂载 Vue 实例、注册 store ├── manifest.json # 跨端配置:appid、权限、编译目标 └── pages.json # 路由与导航栏配置这里有个新手容易懵的点:pages.json和manifest.json是两个完全不同的东西。前者管页面路由、导航栏样式、tabBar,后者管编译目标、小程序 appid、权限声明。改页面跳转去pages.json,改编译平台去manifest.json,别搞混。
2.2 技术选型为什么是 uniapp 而不是原生小程序
这套源码选 uniapp 的核心原因就一个:跨端。同一套 Vue 代码,npm run dev:mp-weixin编译成微信小程序,npm run dev:h5编译成网页,改一处两端都生效。对于考试答题这种交互不复杂、但需要多端分发的场景,uniapp 的性价比很高。
具体到答题场景,uniapp 的几个能力被用上了:
- 条件编译:用
#ifdef MP-WEIXIN处理小程序特有的 API,比如获取用户信息、分享转发,H5 端走另一套逻辑。 - 生命周期:
onLoad拿路由参数(比如考试 ID),onUnload清理倒计时定时器,防止内存泄漏。 - 组件通信:题目卡片用 props 接收题目数据,用
$emit把选中答案抛给父页面。
状态管理用的是 Vuex,因为答题过程中「当前第几题、已选答案、剩余时间」这些状态要在多个页面间共享。如果只用组件 props 传,交卷页拿不到答题页的数据,会很痛苦。
2.3 数据流:一道题从加载到提交走了哪些路
把数据流理清楚,后面改代码才不会迷路。这套项目的答题数据流是这样的:
- 进入答题页
onLoad,从路由参数拿到examId。 - 调
utils/request.js封装的接口,拉取该场考试的题目列表。 - 题目数据存进 Vuex 的
exam模块,同时初始化一个空答案数组。 - 用户点选项,触发
question-card的$emit,父页面更新 Vuex 里对应索引的答案。 - 倒计时组件每秒 tick,剩余时间同步到 Vuex。
- 时间到或用户点交卷,把答案数组 POST 给评分接口。
- 评分结果存本地缓存,跳转成绩页展示。
提示:答案数组的索引要和题目数组的索引严格对应,这是后面算分不出错的前提。我见过有人用题目 ID 当 key 存答案,结果题目顺序一变就对不上,血泪经验。
3. 核心模块拆解:题目渲染、倒计时与自动评分怎么实现
3.1 题目卡片组件:单选多选判断的统一渲染
答题页最核心的是题目渲染。这套源码没有给每种题型写一个组件,而是用一个question-card.vue统一处理,靠type字段区分。核心逻辑如下:
<template> <view class="question-card"> <!-- 题干:支持富文本,用 rich-text 渲染 --> <rich-text :nodes="question.title"></rich-text> <!-- 选项列表:单选/判断用 radio 样式,多选用 checkbox 样式 --> <view v-for="(opt, idx) in question.options" :key="idx" class="option" :class="{ active: isSelected(opt.value) }" @click="handleSelect(opt.value)" > <text class="option-label">{{ opt.label }}</text> <text class="option-text">{{ opt.text }}</text> </view> </view> </template> <script> export default { name: 'QuestionCard', props: { // 题目对象:包含 type/title/options 等字段 question: { type: Object, required: true }, // 当前已选答案:单选是字符串,多选是数组 selected: { type: [String, Array], default: '' } }, methods: { isSelected(value) { // 多选时 selected 是数组,用 includes 判断 if (Array.isArray(this.selected)) { return this.selected.includes(value) } return this.selected === value }, handleSelect(value) { let next if (this.question.type === 'multiple') { // 多选:已选则取消,未选则追加 const arr = Array.isArray(this.selected) ? [...this.selected] : [] const pos = arr.indexOf(value) pos > -1 ? arr.splice(pos, 1) : arr.push(value) next = arr } else { // 单选/判断:直接覆盖 next = value } // 把新答案抛给父页面,由父页面写入 Vuex this.$emit('change', next) } } } </script>逻辑说明:组件本身不存答案,只负责「展示 + 抛出选择事件」,答案的归属权在父页面和 Vuex。这样做的好处是组件可复用、状态单一来源。参数上,question.type决定交互模式,selected的类型跟着变——单选传字符串,多选传数组,isSelected里做了兼容判断。
3.2 倒计时组件:防作弊与页面隐藏的处理
考试场景的倒计时不能只用setInterval减一,因为小程序切后台、页面隐藏时定时器会被节流甚至暂停,回来时间就不准了。这套源码用的是「时间戳差值法」:
// components/count-down.vue 核心逻辑 export default { data() { return { remain: 0, // 剩余秒数,用于展示 endTime: 0 // 考试结束的绝对时间戳 } }, methods: { start(durationMinutes) { // 用当前时间 + 时长算出结束时间戳,而不是单纯倒减 this.endTime = Date.now() + durationMinutes * 60 * 1000 this.tick() this.timer = setInterval(this.tick, 1000) }, tick() { // 每次都用结束时间戳减当前时间,切后台回来也能自动校正 const left = Math.max(0, Math.floor((this.endTime - Date.now()) / 1000)) this.remain = left if (left <= 0) { clearInterval(this.timer) this.$emit('timeup') // 通知父页面自动交卷 } } }, beforeDestroy() { // 页面销毁必须清定时器,否则内存泄漏 clearInterval(this.timer) } }逻辑说明:关键在endTime这个绝对时间戳。不管定时器被节流成什么样,每次 tick 都用「结束时间 - 当前时间」重算剩余秒数,切后台再回来数字是对的。参数durationMinutes从考试配置里读,一般 30 到 120 分钟。timeup事件触发后,父页面调交卷接口,不给用户留继续答题的窗口。
注意:
beforeDestroy里清定时器这一步千万别省。我踩过坑,页面返回后定时器还在跑,控制台一直报错,查了半天才发现是这里漏了。
3.3 自动评分:前端算分还是后端算分
这套源码的评分放在后端,前端只负责提交答案。这是对的——前端算分容易被改,考试场景必须后端说了算。前端提交的数据结构长这样:
// 交卷时提交的 payload const payload = { examId: this.examId, answers: this.answerList.map((ans, idx) => ({ questionId: this.questionList[idx].id, // 题目 ID,后端据此判分 answer: ans // 用户答案,单选字符串/多选数组 })), submitTime: Date.now() // 提交时间,后端可校验是否超时 }后端返回的评分结果包含总分、每题对错、正确答案。前端拿到后存进本地缓存,成绩页和错题本都从这里读。参数上,questionId是判分的唯一依据,所以题目接口返回时必须带 ID,不能只返回题干和选项。
3.4 错题本:本地缓存还是服务端存储
错题本有两种做法:本地缓存(uni.setStorageSync)和服务端存储。这套源码用的是服务端存储,因为考试记录要跨设备同步。错题本页面调接口拉取当前用户的错题列表,按科目和时间筛选。如果只是个人练手项目,用本地缓存也够,utils/storage.js里已经封装好了读写方法,改起来很快。
4. 跑起来:从环境准备到真机预览的完整步骤
4.1 环境准备与依赖安装
跑 uniapp 项目,环境这块新手最容易卡。按顺序来:
# 1. 装 Node.js,建议 16.x 或 18.x,太新的版本 HBuilderX 可能不认 node -v # 2. 全局装 vue-cli(如果是 cli 方式创建的项目) npm install -g @vue/cli # 3. 进项目目录,装依赖 cd your-project npm install # 4. 如果依赖装得慢,换淘宝镜像 npm config set registry https://registry.npmmirror.com依赖装完后,项目根目录会有node_modules。如果npm install报错,八成是 Node 版本不对或者网络问题,先换镜像再试。
4.2 编译到微信小程序
这是最常用的运行方式:
# 编译到微信小程序,生成 dist/dev/mp-weixin 目录 npm run dev:mp-weixin编译成功后,打开微信开发者工具,导入dist/dev/mp-weixin这个目录(注意不是项目根目录)。导入后如果报「appid 不合法」,去manifest.json里把mp-weixin.appid改成你自己的测试号,或者用测试号模式。
提示:微信开发者工具里要勾选「不校验合法域名」,否则本地接口请求会被拦。上线前记得在微信后台配好合法域名。
4.3 编译到 H5 快速验证
不想开微信开发者工具的话,直接跑 H5 最快:
# 编译到 H5,启动本地服务 npm run dev:h5浏览器打开http://localhost:8080就能看到页面。H5 端调试方便,改代码热更新快,适合先验证逻辑再上小程序。
4.4 接口对接:把 mock 数据换成真实后端
源码里utils/request.js的baseURL默认指向一个示例地址,跑之前要改成你自己的后端:
// utils/request.js const BASE_URL = 'https://your-api-domain.com/api' // 改成你的接口地址 function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' // 带上登录态 }, success: (res) => { if (res.statusCode === 200) { resolve(res.data) } else { // 401 统一跳登录 if (res.statusCode === 401) { uni.navigateTo({ url: '/pages/login/login' }) } reject(res) } }, fail: reject }) }) }参数说明:BASE_URL是接口前缀,Authorization从本地缓存读 token。如果后端还没搭好,可以先用 mock 数据把前端流程跑通,request.js里加个开关判断走 mock 还是走真实接口。
4.5 真机预览与常见编译报错
H5 跑通后,想在手机上看效果,用 HBuilderX 的「运行到手机或模拟器」。真机预览常见报错:
- 白屏:多半是路由配置问题,检查
pages.json里页面路径有没有写错。 - 接口 404:
baseURL没配对,或者后端没开跨域。 - 样式错乱:uniapp 的
rpx单位在小程序和 H5 上表现略有差异,用upx2px或统一用rpx能缓解。
5. 避坑与排查:答题小程序最容易翻车的几个地方
5.1 倒计时切后台不准
现象:用户切到微信聊天再回来,倒计时少了好几秒甚至直接归零。原因:setInterval在小程序后台被节流,单纯减一的逻辑会丢时间。解决:用第 3.2 节的绝对时间戳法,每次 tick 重算剩余时间,不依赖定时器累加。
5.2 多选题答案类型不统一
现象:单选提交是字符串,多选提交是数组,后端判分时类型对不上,全判错。原因:前端没有统一答案格式,单选多选混着传。解决:在提交前统一转换,单选也包成数组,或者后端按题型分别处理。我一般会在utils里写个normalizeAnswer函数,提交前过一遍。
5.3 页面返回后定时器还在跑
现象:用户答完题返回首页,控制台持续报错,页面卡顿。原因:setInterval没在beforeDestroy或onUnload里清理。解决:组件销毁和页面卸载两个钩子都加上clearInterval,双保险。
5.4 题目顺序每次刷新都变
现象:用户答到一半刷新页面,题目顺序乱了,答案对不上。原因:后端每次返回题目顺序随机,前端没做顺序锁定。解决:题目接口返回时带一个固定的sort字段,前端按sort排序后再渲染;或者把题目列表缓存到本地,刷新时优先读缓存。
5.5 小程序包体积超限
现象:编译到微信小程序提示「包体积超过 2M」。原因:static目录塞了太多大图,或者引入了没用的第三方库。解决:图片走 CDN,static只放小图标;用微信开发者工具的「代码依赖分析」找出大文件,该删的删。
6. 二次开发:把通用答题改成你自己的考核工具
拿到这套源码,多数人不是原样用,而是要改成自己的业务。我按改造优先级说几个方向。
改题型:现在支持单选、多选、判断。要加填空或简答,在question-card.vue里加一个type === 'fill'的分支,渲染input或textarea,答案提交格式改成字符串。后端判分逻辑也要跟着加,填空题一般用关键词匹配或人工阅卷。
改评分规则:现在是一题一分。要改成多选漏选得一半分、错选不得分,改后端判分接口就行,前端不用动。如果非要在前端算,在utils里加个calcScore函数,但我不建议——考试场景前端算分等于把答案暴露给用户。
加防作弊:可以监听页面隐藏事件,用户切后台超过 N 次自动交卷。uniapp 里用onHide生命周期配合计数器实现:
// 答题页加防作弊 onHide() { this.leaveCount++ if (this.leaveCount >= 3) { uni.showToast({ title: '切屏过多,自动交卷', icon: 'none' }) this.submitExam() // 调交卷接口 } }接自己的题库:把题目接口的返回格式对齐源码里的字段(id、type、title、options、answer),前端就不用改。如果字段名不一样,在request.js后面加一层数据转换,别去动页面代码。
验证改造是否成功:改完先跑 H5,用浏览器的 Vue Devtools 看 Vuex 里的答案数组对不对;再跑小程序,重点测切后台、交卷、错题本三个流程。我一般会准备一套 10 题的测试数据,覆盖所有题型,每次改完都跑一遍。
从那以后我每次改答题类项目,都强制先把「倒计时、答案格式、定时器清理」这三件事过一遍,因为翻车基本都翻在这。希望帮到你。
本文还有配套的精品资源,点击获取