thinkphp vue 考试系统自动保存答题数据 防止页面刷新后无数据
在现代在线考试系统中,防止因页面刷新或意外关闭导致考生答题数据丢失是一个关键需求。本文将详细介绍如何基于ThinkPHP后端和Vue前端实现答题数据的自动保存功能,确保即使在页面刷新后,考生的答题进度也能完整恢复。
一、技术栈概述
- 后端框架:ThinkPHP 6.x
- 前端框架:Vue 3 + Element Plus
- 数据交互:Axios
- 本地存储:localStorage + sessionStorage
- 实时保存:防抖(debounce)技术
二、系统架构设计
1. 数据流设计
代码语言:javascript代码运行次数:0运行复制[Vue组件] → [本地存储] → [防抖处理] → [API请求] → [ThinkPHP控制器] → [数据库]
2. 关键点
- 双保险策略:本地存储 + 服务器存储
- 定时保存与事件触发保存结合
- 页面加载时数据恢复机制
三、前端实现细节
1. 答题数据模型设计
代码语言:javascript代码运行次数:0运行复制// 在Vue中设计答题数据模型
const answerModel = {
exam_id: '', // 考试ID
user_id: '', // 用户ID
start_time: '', // 开始时间
answers: { // 答案集合
// 题目ID: 答案
'question_1': 'A',
'question_2': ['A', 'C'], // 多选题
'question_3': '这是文本答案',
// ...
},
last_save_time: '' // 最后保存时间
}
2. 自动保存核心代码
代码语言:javascript代码运行次数:0运行复制import { debounce } from 'lodash-es';
export default {
data() {
return {
answers: {},
examInfo: {},
timer: null,
isSaving: false
}
},
created() {
// 从本地存储加载已有答案
this.loadLocalAnswers();
// 设置自动保存定时器
this.timer = setInterval(this.autoSave, 30000); // 30秒自动保存一次
// 窗口关闭前保存
window.addEventListener('beforeunload', this.handleBeforeUnload);
},
methods: {
// 加载本地答案
loadLocalAnswers() {
const saved = localStorage.getItem(`exam_${this.examInfo.id}_answers`);
if (saved) {
try {
this.answers = JSON.parse(saved);
this.$message.success('已恢复上次答题进度');
} catch (e) {
console.error('解析本地答案失败', e);
}
}
},
// 答案变更处理(使用防抖)
handleAnswerChange: debounce(function(questionId, answer) {
this.answers[questionId] = answer;
this.saveToLocal();
this.remoteSave(); // 远程保存
}, 1500), // 1.5秒内多次变更只保存一次
// 保存到本地存储
saveToLocal() {
localStorage.setItem(
`exam_${this.examInfo.id}_answers`,
JSON.stringify(this.answers)
);
},
// 远程保存
async remoteSave() {
if (this.isSaving) return;
this.isSaving = true;
try {
const res = await this.$api.exam.saveAnswers({
exam_id: this.examInfo.id,
answers: this.answers
});
if (res.success) {
this.lastSaveTime = new Date();
// 成功后可考虑清除本地存储
// localStorage.removeItem(`exam_${this.examInfo.id}_answers`);
}
} catch (error) {
console.error('保存失败', error);
// 保存失败后,数据仍在本地,下次可继续尝试
} finally {
this.isSaving = false;
}
},
// 自动保存
autoSave() {
if (Object.keys(this.answers).length > 0) {
this.remoteSave();
}
},
// 页面关闭前处理
handleBeforeUnload(e) {
if (Object.keys(this.answers).length > 0) {
// 如果有未保存的答案,提示用户
e.preventDefault();
e.returnValue = '您有未保存的答题数据,确定要离开吗?';
this.remoteSave(); // 尝试最后一次保存
}
}
},
beforeUnmount() {
// 清理定时器和事件监听
clearInterval(this.timer);
window.removeEventListener('beforeunload', this.handleBeforeUnload);
}
}
3. 页面恢复逻辑
在考试页面组件加载时:
代码语言:javascript代码运行次数:0运行复制async mounted() {
// 检查是否有本地保存的答案
const localAnswers = this.loadLocalAnswers();
// 同时从服务器获取已保存答案
try {
const res = await this.$api.exam.getSavedAnswers(this.examId);
if (res.data && res.data.answers) {
// 合并答案(以服务器为准)
this.answers = {
...localAnswers,
...res.data.answers
};
this.saveToLocal(); // 更新本地存储
}
} catch (error) {
console.error('获取保存答案失败', error);
// 继续使用本地答案
}
}
四、后端实现细节(ThinkPHP)
1. 数据库设计
代码语言:javascript代码运行次数:0运行复制CREATE TABLE `exam_answers` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`exam_id` bigint(20) NOT NULL COMMENT '考试ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`question_id` bigint(20) NOT NULL COMMENT '题目ID',
`answer` text COMMENT '答案',
`is_correct` tinyint(1) DEFAULT NULL COMMENT '是否正确',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_question` (`user_id`,`exam_id`,`question_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试答案表';
2. 控制器实现
代码语言:javascript代码运行次数:0运行复制<?php
namespace app\controller;
use think\facade\Db;
use think\facade\Request;
class ExamAnswer extends Base
{
/**
* 保存答案
*/
public function save()
{
$userId = $this->getUserId(); // 获取当前用户ID
$examId = Request::param('exam_id');
$answers = Request::param('answers/a'); // 获取数组参数
if (empty($examId) || empty($answers)) {
return json(['success' => false, 'msg' => '参数错误']);
}
try {
Db::startTrans();
foreach ($answers as $questionId => $answer) {
$data = [
'exam_id' => $examId,
'user_id' => $userId,
'question_id' => $questionId,
'answer' => is_array($answer) ? json_encode($answer) : $answer,
'update_time' => date('Y-m-d H:i:s')
];
// 使用insert...on duplicate key update实现存在更新,不存在插入
Db::name('exam_answers')
->insert($data, true)
->onDuplicate([
'answer' => $data['answer'],
'update_time' => $data['update_time']
])
->execute();
}
Db::commit();
return json(['success' => true]);
} catch (\Exception $e) {
Db::rollback();
return json(['success' => false, 'msg' => '保存失败: '.$e->getMessage()]);
}
}
/**
* 获取已保存答案
*/
public function getSavedAnswers($examId)
{
$userId = $this->getUserId();
$answers = Db::name('exam_answers')
->where('exam_id', $examId)
->where('user_id', $userId)
->column('answer', 'question_id');
// 处理JSON格式的多选答案
$result = [];
foreach ($answers as $questionId => $answer) {
$result[$questionId] = json_decode($answer, true) ?? $answer;
}
return json(['success' => true, 'data' => ['answers' => $result]]);
}
}
3. 路由配置
代码语言:javascript代码运行次数:0运行复制// config/route.php
use think\facade\Route;
Route::group('exam', function() {
Route::post('save', 'ExamAnswer/save');
Route::get('answers/:examId', 'ExamAnswer/getSavedAnswers');
});
五、优化与扩展
1. 性能优化
- 批量保存:前端收集一定量变更后批量提交
- 差异保存:只提交变更的答案,而非全部
- 压缩数据:对大文本答案进行压缩
2. 可靠性增强
- 保存重试机制:当网络失败时自动重试
- 冲突解决:检测并处理多设备同时编辑冲突
- 保存状态提示:在界面显示保存状态
3. 高级功能
- 答题历史追溯:保存每次修改的历史记录
- 断点续考:即使更换设备也能恢复考试
- 离线模式:在网络不稳定时仍能继续答题
六、总结
通过结合Vue的前端数据管理和ThinkPHP的后端数据持久化,我们实现了一个可靠的考试答题自动保存系统。关键点在于:
- 利用本地存储作为第一道防线,防止网络问题导致数据丢失
- 采用防抖技术优化频繁保存的性能问题
- 实现前后端协同的数据恢复机制
- 提供用户友好的保存状态反馈
这种方案不仅适用于考试系统,也可应用于各种需要防止数据丢失的表单场景,具有很好的通用性和扩展性。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2025-04-22,如有侵权请联系 cloudcommunity@tencent 删除数据系统thinkphp存储后端