ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

uniapp答题小程序开发:题库、判分与错题本实践

uniapp答题小程序开发:题库、判分与错题本实践

简介:这套基于 uniapp 的考试答题小程序源码,面向希望学习跨端小程序开发、理解在线答题交互逻辑的初中级前端开发者与在校学生。项目围绕单选、多选、判断三种题型组织静态题库,题目与答案均预置在前端,并实现上一题、下一题切换,可用于参考页面跳转、状态维护、数据绑定和题型渲染等常见实现方式。zip 压缩包约1.47MB,文件总数与类型明细上游暂未提供,主体内容为 uniapp 工程源码,建议结合 HBuilderX 导入后阅读。已有2377人学习下载。由于项目未接入后台,也没有交卷评分与答题记录保存,它更适合作为课程设计、练习项目和二次开发底稿;读者可在此基础上补充登录注册、题库管理、答案校验和成绩展示等功能,以搭建更完整的在线考试应用。

1. 一个答题小程序从哪儿开始拆:先看清它服务的是谁

拿到这套 uniapp 开发的考试答题类小程序,我并没有急着跑编译,而是把页面从头到尾点了一遍。它干的事情其实很朴素:后台配一套题,学员在小程序里限时作答,交卷后立刻出分,做错的题自动归到错题本,过两天还能重刷一遍。界面谈不上花哨,真正值钱的地方在于「题库结构、答题状态、判分口径」这三件事能不能对齐。这类模板最适合两种人:一种是给培训机构、企业内训或者随堂测验做一个能直接上线的答题端,不想从零搭脚手架;另一种是手里已经攒了一份题库,缺一个能跑起来的前端壳子。后面按工程骨架、答题链路、判分与错题、踩坑排查、二次开发的顺序往下拆,代码块都能直接抄进项目里试。

2. 工程骨架与数据流:打开项目先看这四层目录

很多人拿到 uniapp 项目第一反应是直接点运行,结果编译报错、页面白屏,然后开始怀疑人生。我的习惯是先花十分钟把目录结构、pages.json 和请求层看一遍,因为小程序这端的问题,八成不是业务逻辑写错,而是路径、分包、环境变量这些工程层面的东西没配好。这一章先把骨架摸清,后面写业务才不会返工。

2.1 目录结构与页面注册

一份常见的答题类小程序目录大概长这样,我把它按职责切成四层:页面层、组件层、公共能力层、配置层。

exam-mini/ ├── pages/ │ ├── index/ # 首页:考试列表、入口按钮 │ ├── exam/ # 答题页,整个项目最重的页面 │ ├── result/ # 成绩单 │ ├── wrong/ # 错题本 │ └── mine/ # 个人中心、历史记录 ├── components/ │ ├── question-card/ # 按题型渲染题干和选项 │ ├── answer-sheet/ # 答题卡抽屉 │ └── count-down/ # 倒计时 ├── common/ │ ├── request.js # uni.request 的二次封装 │ ├── storage.js # 本地缓存读写 │ ├── grader.js # 判分逻辑 │ └── utils.js # 时间格式化、数组乱序之类 ├── store/ │ └── exam.js # 当前场次的答题状态 ├── static/ ├── pages.json ├── manifest.json └── uni.scss

页面注册全在pages.json里,答题页、成绩单这类非首页建议走分包,主包只留首页和个人中心。小程序有主包体积上限,题库如果带图片,主包很容易被撑爆。

{ "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "考试中心" } }, { "path": "pages/mine/mine", "style": { "navigationBarTitleText": "我的" } } ], "subPackages": [ { "root": "pages/exam", "pages": [ { "path": "index", "style": { "navigationBarTitleText": "答题", "disableScroll": false } } ] }, { "root": "pages/result", "pages": [{ "path": "index", "style": { "navigationBarTitleText": "成绩单" } }] } ], "preloadRule": { "pages/index/index": { "network": "all", "packages": ["pages/exam"] } } }

subPackages的root是分包根目录,下面的path是相对路径,拼接起来就是访问地址。preloadRule是我比较喜欢加的一项,首页加载完就预下载答题分包,用户点「开始考试」时基本无感,比进去再等一坨 JS 下载体验好很多。注意分包目录下不能再出现主包已有的同名静态资源,重复打包会让体积统计看起来很玄学。

2.2 题库数据模型:先把字段定死,再动手写页面

答题类项目最常见的返工,是题型字段设计得不统一。单选题答案存字符串,多选题存数组,判断题存布尔值,等到判分的时候写三个 if,后面加一个填空题就得推倒重来。我的做法是一开始就统一成数组,单选、多选、判断全都用数组承载答案。

// common/questionModel.js // 约定:answer 永远是数组,单选是长度 1 的数组 export const TYPE = { SINGLE: 'single', MULTIPLE: 'multiple', JUDGE: 'judge' } // 一道题的标准结构 export function createQuestion(raw) { return { id: raw.id, // 全局唯一,判分和错题本都靠它 type: raw.type, // single / multiple / judge stem: raw.stem, // 题干,允许保留下发的富文本 options: raw.options || [], // [{ key: 'A', text: '选项内容' }] answer: Array.isArray(raw.answer) ? raw.answer : [raw.answer], score: Number(raw.score) || 5, // 单题分值,判分时累加 analysis: raw.analysis || '', // 解析,答完给用户看 tags: raw.tags || [] // 知识点标签,错题本用来分类 } }

id必须全局唯一,因为错题本、答题状态、成绩明细都拿它当 key,用数组下标当 key 是血泪教训——题目一乱序,全串了。score做一次Number()兜底,后台从 Excel 导入题库时经常把分值导成字符串。tags是我额外加的一层,错题本按知识点归类,比单纯堆题目列表对用户有用得多。

多选题的开关用type控制,不要在前端再存一份「最多选几个」的配置,那个应该跟题目一起下发,叫maxSelect之类,避免前端硬编码。

2.3 请求层封装:把 loading 和错误提示收成一处

小程序的uni.request是回调式的,业务里到处写success/fail会很难维护。我一般封一层 Promise,统一处理 loading、token 和业务码。

// common/request.js const BASE_URL = process.env.NODE_ENV === 'development' ? 'http://127.0.0.1:8080/api' : 'https://your-domain.com/api' export function request({ url, method = 'GET', data = {}, loading = true, auth = true }) { return new Promise((resolve, reject) => { if (loading) uni.showLoading({ title: '加载中', mask: true }) const header = { 'content-type': 'application/json' } if (auth) { const token = uni.getStorageSync('token') if (token) header.Authorization = `Bearer ${token}` } uni.request({ url: BASE_URL + url, method, data, header, timeout: 15000, success: (res) => { // 业务码 0 视为成功,其余统一提示 if (res.statusCode === 200 && res.data && res.data.code === 0) { resolve(res.data.data) return } if (res.statusCode === 401) { uni.removeStorageSync('token') uni.reLaunch({ url: '/pages/index/index' }) reject(res.data) return } uni.showToast({ title: (res.data && res.data.msg) || '请求失败', icon: 'none' }) reject(res.data) }, fail: (err) => { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) }, complete: () => { if (loading) uni.hideLoading() } }) }) }

loading参数留给那些需要静默请求的场景,比如后台心跳、答题过程中的自动保存,如果每次都弹「加载中」,用户答到一半会被打断。auth参数则是为公开接口准备的,比如题库列表在未登录时也能看。BASE_URL用process.env.NODE_ENV区分环境,HBuilderX 和 CLI 两种创建方式都支持,真机调试记得把本地地址换成局域网 IP,127.0.0.1在手机上指向的是手机自己,这个坑几乎每个新手都要踩一次。

从架构上说,这四层的关系是:页面只负责交互和展示,组件负责题型复用,common提供无状态能力,store管当前场次的答题上下文。边界清不清楚,直接决定了后面加一种新题型要改几个文件。

3. 答题主链路:一次作答从进入到返回到底发生了什么

答题页是整个项目的心脏,它的状态比一般页面复杂得多:当前第几题、每道题选了什么、还剩多少时间、答题卡哪些题做过了、什么时候该自动交卷。这一章按「进入页面 → 渲染题目 → 切题 → 交卷」这条线往下走,每一步都落代码。

3.1 题型渲染:一个组件吃掉三种题型

题干和选项的渲染我全部收在question-card组件里,页面只传当前题目和已选答案,选中事件往上抛。这样做的好处是加题型只改组件,页面逻辑不动。

<!-- components/question-card/question-card.vue --> <template> <view class="card"> <view class="stem"> <text class="type-tag">{{ typeLabel }}</text> <rich-text :nodes="question.stem" /> </view> <view v-for="opt in question.options" :key="opt.key" class="option" :class="{ active: isChecked(opt.key) }" @tap="onSelect(opt.key)" > <text class="key">{{ opt.key }}</text> <text class="text">{{ opt.text }}</text> </view> </view> </template> <script> export default { name: 'question-card', props: { question: { type: Object, required: true }, picked: { type: Array, default: () => [] } // 当前题已选中的 key 数组 }, computed: { typeLabel() { return { single: '单选', multiple: '多选', judge: '判断' }[this.question.type] || '单选' } }, methods: { isChecked(key) { return this.picked.indexOf(key) > -1 }, onSelect(key) { if (this.question.type === 'multiple') { // 多选:切换选中状态,由父级维护数组 this.$emit('change', key) } else { // 单选和判断:直接覆盖 this.$emit('change', key, true) } } } } </script>

picked是当前题已选数组,父组件维护,子组件只读不写,这是单向数据流。onSelect的第二个参数replace用来告诉父级「这是覆盖还是切换」,多选走切换,单选走覆盖。判断题本质上就是两个选项的单选,options下发[{key:'T',text:'正确'},{key:'F',text:'错误'}]即可,没必要单独写一套组件。

rich-text是为了兼容题干里带下划线、加粗甚至图片的情况,后台编辑器出来的 HTML 直接塞进去。注意rich-text不支持事件绑定,题干里的链接点了不会跳,要跳转得自己解析出来用别的组件渲染。

3.2 倒计时与自动交卷:别用 setInterval 自增

倒计时最容易被写成「每秒count--」,然后在onHide里忘了清定时器,切后台回来时间对不上。正确姿势是记一个截止时间戳,每次 tick 都用「截止时间减去当前时间」算剩余,定时器只负责触发计算。

// pages/exam/index.vue 片段 export default { data() { return { endTime: 0, // 交卷截止时间戳(毫秒) restSeconds: 0, // 展示用的剩余秒数 timer: null, submitting: false // 防止重复交卷 } }, onLoad(options) { // duration 单位分钟,由上一页通过 url 传入 const minutes = Number(options.duration) || 30 this.endTime = Date.now() + minutes * 60 * 1000 this.tick() this.timer = setInterval(this.tick, 1000) }, onHide() { // 切后台不销毁定时器,但回到前台要立即校正 this.syncRest() }, onShow() { this.syncRest() }, onUnload() { clearInterval(this.timer) this.timer = null }, methods: { syncRest() { this.restSeconds = Math.max(0, Math.floor((this.endTime - Date.now()) / 1000)) }, tick() { this.syncRest() if (this.restSeconds <= 0) { clearInterval(this.timer) this.timer = null this.submit(true) // true 表示被动交卷 } }, async submit(auto = false) { if (this.submitting) return this.submitting = true try { // 交卷逻辑见 3.3 和 4.1 } finally { // 失败要允许重试,所以这里要复位 setTimeout(() => { this.submitting = false }, 800) } } } }

关键点有三个。endTime一旦确定就不再变,切后台、锁屏、微信被系统回收再打开,都能靠它算回正确剩余时间。onHide不清定时器但要syncRest,因为小程序切后台一段时间后定时器会被节流甚至暂停,回来时必须用时间戳纠偏。submitting是防重复交卷的闸门,用户手快连点两次「交卷」,没有这个标记就会发两个请求,后台生成两份成绩,用户看到两条记录,很难解释。

auto参数用来区分主动交卷和超时被动交卷,成绩单上可以给个「超时自动提交」的提示,用户心里有数。

3.3 答题状态与答题卡:用对象存答案,别用数组下标

答题状态我放在页面的data里一个对象上,key 是题目 id,value 是选中的 key 数组。

// 答案结构:{ '1001': ['A'], '1002': ['A', 'C'] } data() { return { questions: [], // 当前场次题目 currentIndex: 0, // 当前第几题 answerMap: {}, // 题目 id -> 选中 key 数组 sheetVisible: false // 答题卡抽屉 } }, computed: { currentQuestion() { return this.questions[this.currentIndex] || {} }, currentPicked() { return this.answerMap[this.currentQuestion.id] || [] }, answeredCount() { return Object.keys(this.answerMap).filter(k => this.answerMap[k].length > 0).length } }, methods: { onAnswerChange(key, replace) { const id = this.currentQuestion.id const old = this.answerMap[id] || [] if (replace) { this.answerMap[id] = [key] } else { const idx = old.indexOf(key) if (idx > -1) old.splice(idx, 1) else old.push(key) // 注意:这里要重新赋值,否则 Vue 2 检测不到对象属性的深层变化 this.$set(this.answerMap, id, old.slice()) return } this.$set(this.answerMap, id, [key]) }, jumpTo(index) { this.currentIndex = index this.sheetVisible = false } }

answerMap用对象而不是数组,理由很实际:题目可能乱序、可能从中间抽题,数组下标和题目对不上号。用题目 id 当 key,无论顺序怎么变,答案永远跟着题走。

Vue 2 的响应式有个老问题,直接this.answerMap[id] = [...]给不存在的 key 赋值,视图不会更新。必须用this.$set,或者提前把所有题目的 id 初始化成空数组。我一般选后者,onLoad里遍历一遍题目,this.$set(this.answerMap, q.id, []),后面就随便赋值了,调试的时候看着也清楚。

答题卡组件接收questions和answerMap,渲染成一格一格的数字,已答的给个高亮,当前题给个描边,点了就jumpTo。这玩意儿看着简单,但用户考试时最依赖它,能不能一眼看出哪题没做,直接影响交卷前的焦虑程度。

4. 判分、成绩单与错题本:结果数据怎么算、怎么存、怎么再用一次

答完题只是开始,用户真正在意的是分数和错在哪。这一章讲判分放在哪一端、成绩单的统计口径怎么定、错题本用本地缓存还是走服务端,这三件事定错了,后面改起来牵一发动全身。

4.1 判分放前端还是后端:看你的题库敢不敢公开

这是架构上第一个要拍板的问题。如果题目和答案都随列表接口下发到前端,那判分放前端最省事,交卷瞬间出分,不用等网络。但代价是答案在包里,抓个包就能看到,防不住有心人。如果答案不下发,交卷时把answerMap提交给后端,由后端比对返回分数和解析,安全性高,但强依赖网络,弱网环境交卷转圈体验很差。

我的折中做法是:练习模式前端判分、答案随题下发;考试模式只下发题目不下发答案,交卷走服务端判分。两套逻辑共用同一个判分函数签名,切模式只改数据来源。

// common/grader.js /** * @param {Array} questions 题目数组,每项含 id / answer / score * @param {Object} answerMap 用户答案 { id: ['A'] } * @param {Object} options { partial: false } 多选是否给部分分 */ export function grade(questions, answerMap, options = {}) { const { partial = false } = options let score = 0 let rightCount = 0 const detail = [] questions.forEach((q) => { const userAns = (answerMap[q.id] || []).slice().sort() const stdAns = q.answer.slice().sort() const isRight = userAns.length === stdAns.length && userAns.every((v, i) => v === stdAns[i]) let got = 0 if (isRight) { got = q.score rightCount += 1 } else if (partial && q.type === 'multiple' && userAns.length > 0) { // 多选部分分:选对且没选错,按比例给分,向下取整 const allRight = userAns.every(k => stdAns.indexOf(k) > -1) if (allRight) got = Math.floor(q.score * userAns.length / stdAns.length) } score += got detail.push({ id: q.id, type: q.type, userAns, stdAns, isRight, got, analysis: q.analysis }) }) return { score, rightCount, wrongCount: questions.length - rightCount, total: questions.length, detail } }

排序后逐项比较是判分的核心,因为多选答案顺序不该影响结果,['A','C']和['C','A']必须算同一份答案。partial参数控制多选要不要给部分分,这个口径一定要跟产品确认,因为「少选得一半分」和「少选不得分」在培训类考试里是两种完全不同的评分规则,代码里留个开关,比事后改逻辑强。

detail里存了用户的答案和标准答案,成绩单页可以直接拿它渲染,错题本也能从里面筛isRight === false的记录,不用再请求一次。

4.2 成绩单页:统计口径比图表好看更重要

成绩单页最容易做偏,堆一堆环形图、进度条,用户真正想知道的其实就三件事:多少分、错了几道、错在哪。我的页面结构固定为「总分卡 + 三项统计 + 题目明细列表」,明细列表按题型或者按对错分组。

// pages/result/index.vue 片段 onLoad() { const raw = uni.getStorageSync('lastResult') if (!raw) { uni.showToast({ title: '暂无成绩数据', icon: 'none' }) return } this.result = raw this.byType = this.groupByType(raw.detail) }, methods: { groupByType(detail) { const map = {} detail.forEach((item) => { if (!map[item.type]) map[item.type] = { total: 0, right: 0 } map[item.type].total += 1 if (item.isRight) map[item.type].right += 1 }) return Object.keys(map).map((type) => ({ type, label: { single: '单选题', multiple: '多选题', judge: '判断题' }[type] || type, ...map[type], rate: map[type].total ? Math.round(map[type].right / map[type].total * 100) : 0 })) } }

groupByType返回的是按题型聚合的结果,渲染成三行「单选 8/10 正确率 80%」就够用了。rate记得做除零保护,total为 0 时会得到NaN,页面上显示一个NaN%非常刺眼。

成绩结果我通过uni.setStorageSync('lastResult', result)传递给成绩单页,而不是用 URL 传参,因为明细数据可能很大,URL 长度有限制,中文和特殊字符还要编码,很容易翻车。用缓存传递的代价是刷新页面会读到旧数据,所以进入成绩单页后可以考虑清掉,或者在首页有「查看上次成绩」的入口时保留。

4.3 错题本:本地缓存能撑多久,什么时候必须上服务端

错题本的存储方案要看用户规模和使用场景。小规模、单设备的场景,本地缓存完全够用;用户换手机还要能看到错题,就必须同步到服务端。

// common/storage.js const WRONG_KEY = 'wrong_book' export function getWrongBook() { return uni.getStorageSync(WRONG_KEY) || {} } // 合并一批错题,同一题多次做错只保留最新一次 export function mergeWrongBook(detail) { const book = getWrongBook() detail.filter(d => !d.isRight).forEach((d) => { book[d.id] = { id: d.id, type: d.type, userAns: d.userAns, stdAns: d.stdAns, analysis: d.analysis, wrongAt: Date.now() } }) // 简单容量控制:超过 500 条按时间淘汰最旧的 const keys = Object.keys(book) if (keys.length > 500) { keys.sort((a, b) => book[a].wrongAt - book[b].wrongAt) .slice(0, keys.length - 500) .forEach(k => delete book[k]) } uni.setStorageSync(WRONG_KEY, book) return book }

微信小程序单个 key 的存储上限是 1MB,总上限 10MB 左右,uni.setStorageSync超了会直接抛异常,而且这个异常在部分基础库版本里吞得很隐蔽,页面看着正常但数据就是没存进去。所以一定要做容量控制,我给的 500 条只是经验值,题目带解析文本的话,实际能存的数量会少很多,最终要以真机测试为准。

同步到服务端的时机,我一般放在「交卷成功后」和「进入错题本页面时」两个点,交卷后静默批量上传,错题本页面下拉时拉取最新数据。不要每答错一道就发一次请求,考试过程中网络抖动一下就会攒一堆失败请求,重试逻辑写起来很烦。

5. 跑起来之后容易翻车的地方:五个高频问题排查

代码能编译不代表能上线,这一章列的五个问题,基本是我每次做答题类小程序都会遇到或者被问到的,按「现象 → 原因 → 解决」写清楚,遇到对得上的直接照着改。

5.1 真机上倒计时越走越慢,锁屏回来直接超时

现象是模拟器上计时正常,真机锁屏两分钟再打开,剩余时间没减多少,过一会儿突然跳到零并自动交卷。

原因是小程序切到后台后setInterval会被系统节流甚至暂停,靠累加计数必然失准。

解决办法就是 3.2 里的时间戳方案,endTime固定不变,每次onShow和onHide都调用一次syncRest重新计算。另外onUnload必须清定时器,否则页面销毁后定时器还在跑,控制台报「setData 在页面不存在时调用」的警告,严重的会导致内存泄漏。

5.2 交卷后成绩单空白,控制台提示找不到缓存

现象是答题页交卷成功,跳转成绩单页,页面上什么都没有。

原因通常是uni.setStorageSync写在了异步回调里,跳转却写在回调外面,页面已经跳过去了,数据还没写进去。或者存储的 key 拼错,大小写不一致。

解决是把跳转放进存储成功之后,或者干脆用uni.redirectTo在同一个then里顺序执行。另外成绩单页读取缓存时要做空判断,读不到就提示并给个返回入口,别让用户卡在白屏页上。

5.3 多选题切题再回来,之前选的没了

现象是选了 A、C,切到下一题再切回来,选项变回未选中。

原因是answerMap里的值被直接修改但没有触发响应式更新,Vue 2 检测不到新加的 key。另一个可能是切题时用了currentIndex重置了picked。

解决办法是统一用this.$set,或者在onLoad里把所有题目 id 预初始化成空数组,让 key 提前存在。如果你用的是 Vue 3 的reactive,这个问题会少一些,但answerMap直接赋新对象还是推荐Object.assign合并,避免引用被替换后其他组件手里的旧引用失效。

5.4 题库图片不显示,开发者工具正常真机 404

现象是题干里的图片在开发者工具能看到,真机上是裂图或者占位。

原因多半是图片域名没有配置到小程序的合法域名里,工具里勾了「不校验合法域名」所以看着正常,真机走的是严格校验。

解决办法是登录小程序后台,在开发设置里把图片所在的域名加到 downloadFile 合法域名,或者把图片资源放到自己的 CDN 上,用 https 访问。另外rich-text里的图片宽度不会自适应,小屏上容易溢出,给img标签加个max-width:100%的样式字符串再塞进去。

5.5 交卷按钮连点两次,后台多出一条成绩记录

现象是用户交卷后网络慢,以为没反应又点了一次,结果后台出现两条记录。

原因是按钮没有做防重复提交,前端发了两个请求。

解决办法是 3.2 里的submitting标记,请求发出前置 true,成功跳转后页面销毁自然失效,失败则在finally里延时复位。同时后端也要做幂等,用「用户 id + 场次 id」做唯一约束,前端防抖只能挡住手快的,挡不住网络重试和恶意请求。这一层双保险,缺一个都不行。

6. 二次开发:把通用答题模板改成你自己的业务

模板能不能用得顺手,关键看你改的是皮还是骨。改 UI 最省事,改题型和判分口径才是真正要动结构的地方,这里给两条改造路线和一套验证方法。

第一种是只换题库和界面。后端接口保持{ code, data, msg }不变,把pages/index的列表换成你自己的考试分类,question-card的样式按品牌色调整,uni.scss里改主色变量就够了。这类改造基本不动逻辑,一天能出效果。

第二种是加题型。比如加填空题,改动集中在三处:questionModel.js的TYPE加一个枚举,question-card里加一个v-if分支渲染textarea,grader.js里加字符串比对(要不要忽略空格、大小写要跟产品确认)。改完在detail里多存一个userAns的原始文本,成绩单页才能显示用户填了什么。加题型最怕的是在页面里到处写if (type === 'xxx'),所以一开始把题型分支收在组件和判分函数里,后面扩起来才不疼。

改造目标主要文件风险点
换品牌色/文案uni.scss、各页面模板无
接自己的题库接口common/request.js、pages/index字段结构对齐、分页
新增题型questionModel.js、question-card、grader.js判分口径、答题状态结构
错题同步服务端common/storage.js、交卷逻辑幂等、批量接口、冲突合并

验证方法我一般分三步走。先在开发者工具里用「清缓存 → 登录 → 答题 → 交卷 → 看成绩 → 进错题本」完整走一遍,这是最短的闭环。再用真机预览走一遍同样的流程,中间穿插一次锁屏和一次切后台,专门验证倒计时和缓存。最后用工具的网络面板把网速调成慢速,重复点交卷按钮,确认不会重复提交。三步走完,基本能把 5.3 和 5.5 那类问题挡在上线前。

从那以后我每接一个答题类需求,都先把题库字段和判分口径写成一张表,跟业务方逐字对完再动 UI,因为这两样东西一旦上线再改,历史成绩和错题数据就得跟着迁移,那个后悔药很难吃。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表