简介这份资源是基于 uniapp 框架开发的考试答题类小程序源码面向希望学习跨平台应用开发、了解前端考试逻辑的开发者与在校学生。项目实现了单选、多选、判断三种题型的答题界面并支持上一题、下一题自由切换题目与答案均以静态数据形式预设在前端未接入后台交互因此没有交卷评分与记录保存功能更适合作为学习参考而非实际考试平台。压缩包为 zip 格式大小约 1.47MB文件总数与类型明细上游未提供可省略该维度。目前已有 2372 人学习下载热度较高。通过阅读源码读者可以掌握 uniapp 的组件化开发方式、页面跳转、状态管理与数据绑定等核心用法理解前端如何维护题目索引与交互逻辑并以此为基础拓展用户登录、后台题库管理、分数计算、答案校验与防作弊等模块为构建更复杂的在线教育应用积累实践经验。1. 从一份 uniapp 答题小程序源码说起它到底能跑通哪些考试场景前阵子帮一个做职业培训的朋友看项目他手里有一套 uniapp 开发的考试答题类小程序源码想快速搭一个内部考核工具。我拆包跑了一遍发现这套东西的定位很清晰它不是通用低代码平台而是一套把「题库管理 → 组卷 → 答题 → 判分 → 错题回顾」这条链路写死的完整前端方案。适合谁一是手里有题库、想两周内上线一个刷题或考核小程序的团队二是想学 uniapp 跨端开发、拿一个真实业务闭环练手的前端。它解决的核心问题不是「怎么做一个答题界面」而是「答题状态机怎么在跨端环境下不翻车」。很多人第一次写答题逻辑都会卡在倒计时切后台、选项回显、交卷时机这几个点上这套源码的价值就在于把这些边界都处理过了。下面我按「能跑起来 → 看懂结构 → 改得动 → 避得开坑」的顺序拆一遍。2. 环境搭建与首次运行从 HBuilderX 到真机预览的完整链路2.1 工具链选型为什么是 HBuilderX 而不是纯 CLIuniapp 项目有两种起手方式HBuilderX 可视化创建或者 vue-cli 命令行创建。这套答题源码是基于 HBuilderX 工程结构组织的根目录下有manifest.json、pages.json、uni.scss这几个标志性文件。如果你用纯 CLI 去套manifest.json里的 appid 和平台配置会对不上编译到微信小程序时容易报「未找到 appid」的错。常见做法是直接用 HBuilderX 打开项目根目录而不是打开里面的某个子文件夹。我一般会先确认三件事pages.json里pages数组第一项是不是答题首页、manifest.json里mp-weixin节点的 appid 是否留空待填、uni.scss里有没有引用不存在的变量文件。这三点确认完再点运行。// manifest.json 关键片段微信小程序端 mp-weixin: { appid: , // 留空运行时由开发者工具填入自己的测试号 setting: { urlCheck: false, // 本地调试关掉域名校验否则请求题库接口会被拦 es6: true, minified: true }, usingComponents: true }这里urlCheck: false是本地联调的关键线上发布前必须改回true并在小程序后台配置合法域名。es6转 es5 建议开着答题逻辑里用了不少async/await老版本基础库不转译会白屏。2.2 依赖安装与运行命令如果项目根目录有package.json说明它用了 npm 管理部分依赖常见的是uview-ui或uni-ui。这时候不能只靠 HBuilderX 内置编译要先装依赖。# 进入项目根目录 cd exam-uniapp # 安装依赖建议用 npm 而非 yarnuniapp 对 lock 文件更敏感 npm install # 如果用了 uni-ui需要按官方方式引入 easycom 规则 # 检查 pages.json 是否已配置 easycom 自动引入装完依赖后HBuilderX 菜单里选「运行 → 运行到小程序模拟器 → 微信开发者工具」。第一次运行会自动拉起微信开发者工具如果拉不起来多半是开发者工具「设置 → 安全设置」里的服务端口没开。这个坑我踩过不止一次现象是 HBuilderX 控制台一直停在「正在编译」其实编译早完成了只是没法唤起工具。2.3 首次运行必须验证的三个动作跑起来之后别急着看界面好不好看先验证答题主链路是否通。我一般按这个顺序点进入首页看题目和选项是否从本地 mock 数据或接口正常渲染随便选一个选项看是否高亮、是否记录到答题状态点「下一题」再点「上一题」看之前选的答案有没有回显。这三步能过说明答题状态管理的基本盘是稳的。如果第二步选项点了没反应八成是click绑在了错误的层级或者选项数据里的id字段和v-for的:key对不上。这套源码里选项点击是走tap而不是click在小程序端tap的响应更可靠改成click在部分安卓机型上会有 300ms 延迟。3. 答题核心逻辑拆解状态机、倒计时与判分是怎么串起来的3.1 答题状态的数据结构设计这套源码没有用 Vuex 或 Pinia 做全局状态而是把答题状态收敛在答题页的data里。这个选择在小程序场景下是合理的因为答题页是单页闭环跨页共享的需求很弱。核心数据结构大致是这样// 答题页 data 中的核心状态 data() { return { questionList: [], // 当前试卷题目数组 currentIndex: 0, // 当前题号索引 userAnswers: {}, // { 题目id: 选项id } 记录用户作答 examStatus: answering, // answering | submitted | timeout remainSeconds: 0, // 剩余秒数 timer: null // setInterval 句柄用于清理 } }userAnswers用对象而不是数组是为了避免题目乱序或跳题时索引错位。这个细节很关键如果试卷支持随机抽题用数组下标存答案一旦题目顺序变了答案就全串了。用题目 id 做 key无论顺序怎么变都能对上。examStatus这个字段是整套逻辑的开关。交卷后把它置为submitted所有选项的点击事件里都要先判断这个状态否则用户交卷后还能改答案判分就乱了。3.2 倒计时的实现与后台切换处理倒计时是答题类小程序最容易翻车的地方。这套源码用的是setInterval每秒减一但加了一个关键处理在onHide和onShow里记录时间戳回到前台时用时间差校正剩余秒数。onShow() { if (this.examStatus ! answering) return // 从后台回到前台用时间戳校正倒计时 if (this.hideTimestamp) { const elapsed Math.floor((Date.now() - this.hideTimestamp) / 1000) this.remainSeconds Math.max(0, this.remainSeconds - elapsed) this.hideTimestamp null } this.startTimer() }, onHide() { // 记录切后台的时间点并清掉定时器防止后台空跑 this.hideTimestamp Date.now() this.clearTimer() }逻辑说明onHide时清掉setInterval避免小程序在后台被挂起后定时器行为不一致onShow时用Date.now()的差值补算真实流逝时间。这样即使用户切出去两分钟再回来倒计时也是准的。参数上remainSeconds是唯一的时间源不要另外维护一个endTime再反算两套时间源一定会打架。startTimer里每次 tick 都要判断remainSeconds 0到点自动交卷并且把examStatus置为timeout交卷原因和手动交卷区分开方便后续统计。3.3 判分逻辑与错题收集判分没有放在前端做而是把userAnswers提交给接口由后端返回得分和错题列表。这个设计是对的前端判分容易被改包作弊。但源码里保留了一个本地判分的降级分支用于接口不通时给用户一个即时反馈。// 提交答案 async submitExam() { if (this.examStatus ! answering) return this.examStatus submitted this.clearTimer() const payload { paperId: this.paperId, answers: this.userAnswers, submitType: this.remainSeconds 0 ? timeout : manual } try { const res await uni.request({ url: /api/exam/submit, method: POST, data: payload }) this.result res.data } catch (e) { // 降级本地比对答案仅用于展示不作为最终成绩 this.result this.localJudge() } }参数说明submitType区分超时和手动后端可以据此做不同的成绩标记。localJudge只做展示不要写回成绩否则用户刷新页面可能看到两个不同的分数。错题收集依赖userAnswers和题目正确答案的比对把错题 id 存到一个数组里跳转到错题回顾页时按 id 拉取题目详情。这里注意错题回顾页不要重新请求整张试卷只请求错题 id 对应的题目否则数据量白白翻几倍。4. 页面结构与题库对接改题库、换皮肤、加题型的实操路径4.1 题库数据格式与接口约定这套源码默认走的是「接口拉题」模式接口返回的题目结构决定了你改题库的难易度。常见字段约定如下字段类型说明idstring题目唯一标识判分和错题收集都依赖它typenumber1 单选2 多选3 判断stemstring题干支持富文本时用 rich-text 渲染optionsarray[{ id, content }]判断题也走这个结构answerarray正确答案的选项 id 数组多选时长度大于 1analysisstring解析答完题后展示如果你的题库是 Excel 或 CSV需要先转成这个结构。我一般写个 Node 脚本批量转注意answer一定要转成数组哪怕单选也包一层否则判分时includes会报错。4.2 新增题型的改动点源码默认支持单选、多选、判断。如果要加填空或简答改动集中在三处一是题目渲染模板里加v-ifquestion.type 4的分支二是userAnswers的存储结构要能容纳字符串而不只是选项 id三是判分接口要能处理非客观题。前两处是前端改第三处必须后端配合前端判分对主观题没有意义。!-- 题目渲染模板片段 -- view v-ifquestion.type 1 || question.type 3 view v-foropt in question.options :keyopt.id :class{ active: userAnswers[question.id] opt.id } tapselectOption(question.id, opt.id) {{ opt.content }} /view /view view v-else-ifquestion.type 2 !-- 多选userAnswers[question.id] 是数组 -- /view注意多选和单选的userAnswers值类型不同单选是字符串多选是数组。在selectOption里要根据question.type分流不要试图用一套逻辑兼容否则回显和判分都会出问题。4.3 换皮肤与主题变量uniapp 换皮肤最省事的方式是改uni.scss里的变量而不是全局搜替换颜色值。源码里主色、背景色、文字色都抽成了变量改这几个就能换整体风格。// uni.scss 主题变量 $exam-primary: #2b7fff; // 主色按钮和选中态 $exam-bg: #f5f6fa; // 页面背景 $exam-text: #1f2329; // 主文字 $exam-success: #00b42a; // 正确态 $exam-error: #f53f3f; // 错误态改完变量后要重新编译HBuilderX 有时会缓存 scss 编译结果改完没生效就点「重新编译」。这个玄学问题我遇到过几次清缓存比反复改代码管用。5. 避坑与排查答题小程序最容易翻车的五个点5.1 选项点击无响应或延迟现象用户点选项界面没反应或者要等半秒才高亮。原因click在小程序端有合成延迟且如果绑在了view的父级而选项是子级事件冒泡可能被拦截。解决统一用tap并确保绑定在选项本身而不是外层容器。如果用了自定义组件检查组件是否透传了tap事件。5.2 倒计时切后台后不准现象用户切出去接个电话回来倒计时少了好几秒甚至直接归零。原因setInterval在小程序后台会被节流或暂停单纯依赖它累加必然不准。解决按 3.2 的方式用onHide记录时间戳、onShow补算差值定时器只负责前台刷新显示。5.3 交卷后还能改答案现象点了交卷结果页都出来了返回答题页还能点选项。原因examStatus没有在所有交互入口做拦截或者页面栈里还留着旧的答题页实例。解决所有选项点击、上一题下一题的事件开头都加if (this.examStatus ! answering) return交卷后用uni.redirectTo而不是navigateTo把答题页从栈里替换掉。5.4 题目乱序后答案错位现象开了随机抽题用户答完发现答案对不上题。原因userAnswers用数组下标存答案题目顺序一变就串了。解决一律用题目 id 做 key渲染和判分都通过 id 查找不要依赖索引。5.5 真机上富文本题干显示异常现象开发者工具里题干正常真机上富文本标签直接露出来了。原因rich-text组件对 HTML 字符串的支持在各端不一致小程序端不支持部分标签和样式。解决题干尽量用纯文本加换行必须富文本时先在服务端把 HTML 转成rich-text支持的节点数组不要直接塞 HTML 字符串。6. 进阶技巧把答题数据用起来与本地调试的后悔药跑通之后这套源码真正的价值在于它留下的数据钩子。userAnswers和result这两个对象里藏着每个用户的作答轨迹稍微加工就能做错题本、能力雷达图、高频错题统计。我一般会在交卷成功后把result.wrongList单独存一份到本地缓存键名带上试卷 id这样用户下次进来能直接看历史错题不用重新请求接口。// 交卷成功后缓存错题用于离线错题本 const cacheKey wrong_${this.paperId} const history uni.getStorageSync(cacheKey) || [] const merged [...new Set([...history, ...this.result.wrongList])] uni.setStorageSync(cacheKey, merged)这里用Set去重避免同一道题反复错时缓存里堆一堆重复 id。缓存要设上限比如只留最近 200 条否则小程序存储空间会被撑爆。另一个实用技巧是本地调试的后悔药答题过程中如果改乱了状态不用重启小程序在控制台手动执行getCurrentPages()拿到当前页实例直接改它的data就能复位。我调试判分逻辑时经常这么干比反复杀进程重进快得多。// 在微信开发者工具控制台执行复位当前答题页状态 const page getCurrentPages().slice(-1)[0] page.setData({ currentIndex: 0, userAnswers: {}, examStatus: answering })注意setData在小程序里是异步的连续改多个字段建议合并成一次调用减少通信开销。这套源码里答题页的setData调用比较克制没有出现每选一个选项就全量刷新的情况这也是它跑起来比较跟手的原因之一。从那以后我每次拿到类似的答题类项目都会先把倒计时和交卷状态这两条链路单独拎出来压一遍再去看界面。因为界面问题一眼能看出来状态问题往往要用户答到一半才暴露那时候修的成本就高了。希望这套拆解能帮到你少走点我当年踩过的弯路。本文还有配套的精品资源点击获取