继续考试功能已实现

This commit is contained in:
chenqiang 2025-09-13 07:24:13 +08:00
parent 681e3fe273
commit 08bf170216
10 changed files with 1451 additions and 346 deletions

View File

@ -772,9 +772,10 @@ async function startPaper(paperId) {
/**
* 提交考试
* @param {number} paperId - 试卷ID
* @param {number} duration_seconds - 答题时长(秒)
* @returns {Promise<Object>} - 包含操作结果和试卷数据的对象
*/
async function submitPaper(paperId) {
async function submitPaper(paperId, duration_seconds = 0) {
try {
const userDb = await getDbConnection(getUserDbPath());
const currentTime = formatDateTime(new Date());
@ -783,9 +784,10 @@ async function submitPaper(paperId) {
`UPDATE examinee_papers
SET paper_status = 2,
paper_submit_time = ?,
paper_end_time = ?
paper_end_time = ?,
paper_duration_seconds = ?
WHERE id = ?`,
[currentTime, currentTime, paperId]
[currentTime, currentTime, duration_seconds, paperId]
);
// 查询更新后的试卷数据
@ -819,12 +821,23 @@ async function endPaper(paperId) {
const userDb = await getDbConnection(getUserDbPath());
const currentTime = formatDateTime(new Date());
// 先查询当前试卷的paper_minutes
const existingPaper = await userDb.getAsync(
`SELECT paper_minutes FROM examinee_papers WHERE id = ?`,
[paperId]
);
// 计算paper_duration_seconds (将分钟转换为秒)
const paperMinutes = existingPaper ? parseInt(existingPaper.paper_minutes) : 0;
const paperDurationSeconds = paperMinutes > 0 ? paperMinutes * 60 : 0;
await userDb.runAsync(
`UPDATE examinee_papers
SET paper_status = 2,
paper_end_time = ?
paper_end_time = ?,
paper_duration_seconds = ?
WHERE id = ?`,
[currentTime, paperId]
[currentTime, paperDurationSeconds, paperId]
);
// 查询更新后的试卷数据
@ -851,18 +864,20 @@ async function endPaper(paperId) {
/**
* 更新试卷最后操作时间
* @param {number} paperId - 试卷ID
* @param {number} duration_seconds - 答题时长(秒)
* @returns {Promise<Object>} - 包含操作结果和试卷数据的对象
*/
async function processPaper(paperId) {
async function processPaper(paperId, duration_seconds) {
try {
const userDb = await getDbConnection(getUserDbPath());
const currentTime = formatDateTime(new Date());
await userDb.runAsync(
`UPDATE examinee_papers
SET paper_last_time = ?
SET paper_last_time = ?,
paper_duration_seconds = ?
WHERE id = ?`,
[currentTime, paperId]
[currentTime, duration_seconds, paperId]
);
// 查询更新后的试卷数据
@ -1196,8 +1211,195 @@ async function getPaper(paperId) {
}
}
// 在文件末尾添加所有导出
module.exports = {
/**
* 根据考生ID获取历史考试记录
* @param {number} examineeId - 考生ID
* @returns {Promise<Object>} - 包含查询结果的对象
*/
async function getExamineePaper(examineeId) {
try {
// 1. 获取数据库连接
const systemDb = await getDbConnection(getSystemDbPath());
const userDb = await getDbConnection(getUserDbPath());
// 2. 查询考生信息
const examinee = await userDb.getAsync(
`SELECT * FROM examinee WHERE id = ?`,
[examineeId]
);
if (!examinee) {
console.log(`未找到ID为${examineeId}的考生`);
return {
success: false,
message: '未找到历史考试',
data: null
};
}
// 3. 查询考生的所有试卷记录
const examineePapers = await userDb.allAsync(
`SELECT * FROM examinee_papers WHERE examinee_id = ? ORDER BY created_at DESC`,
[examineeId]
);
if (!examineePapers || examineePapers.length === 0) {
console.log(`未找到ID为${examineeId}的考生的试卷记录`);
return {
success: false,
message: '未找到历史考试',
data: null
};
}
// 4. 为每个试卷获取详细信息
const papersWithDetails = [];
for (const paper of examineePapers) {
const paperId = paper.id;
// 4.1 查询试卷关联的题目
const paperQuestions = await userDb.allAsync(
`SELECT * FROM paper_questions WHERE paper_id = ?`,
[paperId]
);
// 4.2 为每个题目获取详细信息
const questionsWithDetails = [];
for (const question of paperQuestions) {
const questionId = question.id;
const questionType = question.question_type;
// 查询题型名称
const dictItem = await systemDb.getAsync(
`SELECT item_name FROM dict_items WHERE type_code = 'question_type' AND item_code = ?`,
[questionType]
);
const questionWithDetails = {
...question,
question_type_name: dictItem && dictItem.item_name ? dictItem.item_name : '',
images: [],
datasets: [],
choices: [],
blanks: []
};
// 查询题目图片
const images = await userDb.allAsync(
`SELECT * FROM question_images WHERE question_id = ?`,
[questionId]
);
questionWithDetails.images = images;
// 查询题目数据集
const datasets = await userDb.allAsync(
`SELECT * FROM question_datasets WHERE question_id = ?`,
[questionId]
);
// 解析dataset_data为数组
questionWithDetails.datasets = datasets.map(dataset => ({
...dataset,
dataset_data: JSON.parse(dataset.dataset_data || '[]')
}));
// 根据题型查询对应的答案表
if (questionType === 'choice') {
// 查询选择题
const choices = await userDb.allAsync(
`SELECT * FROM question_choices WHERE question_id = ?`,
[questionId]
);
// 解析数组列
questionWithDetails.choices = choices.map(choice => ({
...choice,
choice_options: JSON.parse(choice.choice_options || '[]'),
correct_answers: JSON.parse(choice.correct_answers || '[]'),
examinee_answers: JSON.parse(choice.examinee_answers || '[]')
}));
} else if (questionType === 'fill_blank') {
// 查询填空题
const blanks = await userDb.allAsync(
`SELECT * FROM question_fill_blanks WHERE question_id = ?`,
[questionId]
);
// 解析数组列
questionWithDetails.blanks = blanks.map(blank => ({
...blank,
correct_answers: JSON.parse(blank.correct_answers || '[]'),
examinee_answers: JSON.parse(blank.examinee_answers || '[]')
}));
}
questionsWithDetails.push(questionWithDetails);
}
// 4.3 构建完整的试卷对象
const fullPaper = {
...paper,
examinee: examinee,
questions: questionsWithDetails
};
papersWithDetails.push(fullPaper);
}
return {
success: true,
message: '找到历史考试',
data: papersWithDetails
};
} catch (error) {
console.error('获取考生历史试卷过程中发生错误:', error);
return {
success: false,
message: `获取历史考试失败: ${error.message}`,
data: null
};
}
}
// 更新导出语句,添加getExamineePaper方法
/**
* 继续考试
* @param {number} paperId - 试卷ID
* @param {number} usedMinutes - 已用时间(分钟)
* @returns {Promise<Object>} - 包含操作结果和试卷数据的对象
*/
async function continueExam(paperId) {
try {
const userDb = await getDbConnection(getUserDbPath());
const currentTime = new Date();
const formattedCurrentTime = formatDateTime(currentTime);
await userDb.runAsync(
`UPDATE examinee_papers
SET paper_status = 1,
paper_last_time = ?
WHERE id = ?`,
[formattedCurrentTime, paperId]
);
// 查询更新后的试卷数据
const paper = await userDb.getAsync(
`SELECT * FROM examinee_papers WHERE id = ?`,
[paperId]
);
return {
success: true,
message: '考试已成功继续',
data: paper
};
} catch (error) {
console.error('继续考试失败:', error);
return {
success: false,
message: `继续考试失败: ${error.message}`
};
}
}
exports = module.exports = {
formatDateTime,
batchInsert,
generateExamineePaper,
@ -1209,5 +1411,7 @@ module.exports = {
endPaper,
processPaper,
checkPaperAnswers,
getPaper
getPaper,
getExamineePaper,
continueExam
};

View File

@ -1,6 +1,7 @@
const path = require('path');
const fs = require('fs');
const { app } = require('electron');
// 添加缺少的path模块导入
const path = require('path');
// 判断是否为开发环境
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;

View File

@ -208,6 +208,7 @@ userSchema = {
paper_last_time TEXT,
paper_submit_time TEXT,
paper_end_time TEXT,
paper_duration_seconds INTEGER DEFAULT 0,
paper_status INTEGER NOT NULL DEFAULT 0,
paper_score_real REAL DEFAULT 0,
paper_score REAL DEFAULT 0,

View File

@ -1,5 +1,6 @@
const sqlite3 = require('sqlite3');
const { promisify } = require('util');
// 添加缺少的sqlite3模块导入
const sqlite3 = require('sqlite3');
sqlite3.verbose();

View File

@ -110,8 +110,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
ipcRenderer.invoke("examinee-update", { id, examineeData }),
examineeDelete: (id) => ipcRenderer.invoke("examinee-delete", id),
// 在考生考试服务相关接口部分添加examingCheckPaperAnswers
// 考生考试服务相关接口
// 在"考生考试服务相关接口"部分添加getPaper接口
examingGetPaper: ({ paperId }) =>
ipcRenderer.invoke("examing-get-paper", { paperId }),
// 在考生考试服务相关接口
examingGeneratePaper: ({ examineeId, examId }) =>
ipcRenderer.invoke("examing-generate-paper", { examineeId, examId }),
examingGetPaperStatus: ({ examineeId, examId }) =>
@ -125,21 +127,26 @@ contextBridge.exposeInMainWorld("electronAPI", {
tableName,
relatedId,
}),
// 修复:examingUpdateAnswer接口的参数结构
examingUpdateAnswer: ({ tableName, id, answers }) =>
ipcRenderer.invoke("examing-update-answer", { tableName, id, answers }),
// 在考生考试服务相关接口部分确保examingStartPaper正确暴露
examingStartPaper: ({ paperId }) =>
ipcRenderer.invoke("examing-start-paper", { paperId }),
examingSubmitPaper: ({ paperId }) =>
ipcRenderer.invoke("examing-submit-paper", { paperId }),
// 确保examingGetExamResult API正确暴露
examingSubmitPaper: ({ paperId, duration_seconds = 0 }) =>
ipcRenderer.invoke("examing-submit-paper", { paperId, duration_seconds }),
examingGetExamResult: ({ paperId }) =>
ipcRenderer.invoke("examing-get-exam-result", { paperId }),
examingCheckPaperAnswers: ({ paperId }) =>
ipcRenderer.invoke("examing-check-paper-answers", { paperId }),
examingProcessPaper: ({ paperId }) =>
ipcRenderer.invoke("examing-process-paper", { paperId }),
examingProcessPaper: ({ paperId, duration_seconds = 0 }) =>
ipcRenderer.invoke("examing-process-paper", { paperId, duration_seconds }),
examingGetExamineePaper: ({ examineeId }) =>
ipcRenderer.invoke("examing-get-examinee-paper", { examineeId }),
// 继续考试接口
examingContinuePaper: ({ paperId }) =>
ipcRenderer.invoke("examing-continue-paper", { paperId }),
// 新增:结束考试接口
examingEndPaper: ({ paperId }) =>
ipcRenderer.invoke("examing-end-paper", { paperId }),
// 文件服务相关接口
// 保留一个fileGeneratePaperPdf定义,移除重复的

View File

@ -1,4 +1,4 @@
// 保留第一个导入
// 首先在文件顶部的require语句中添加getExamineePaper
const {
generateExamineePaper,
loadPaperSerial,
@ -10,13 +10,12 @@ const {
processPaper,
checkPaperAnswers,
getPaper,
getExamineePaper,
continueExam, // 添加continueExam到导入列表中
} = require("../db/examing.js");
const { getDbConnection, closeAllConnections } = require("../db/index.js");
const { getUserDbPath } = require("../db/path.js");
// 删除下面这行重复的导入
// const { generateExamineePaper, loadPaperSerial, getQuestionByRelatedId, updateExamineeAnswer, startPaper, submitPaper, endPaper, processPaper, checkPaperAnswers, getPaper } = require('../db/examing.js');
/**
* 服务层:生成考生试卷
* @param {Object} examineeData - 考生数据
@ -244,19 +243,41 @@ async function startPaperService(paperId) {
}
}
/**
* 服务层:获取试卷详细信息
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含试卷详细信息的对象
*/
async function getPaperService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await getPaper(paperId);
return result;
} catch (error) {
console.error("服务层: 获取试卷详细信息失败", error);
return {
success: false,
message: `获取试卷详细信息失败: ${error.message}`,
};
}
}
/**
* 服务层:提交考试
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果的对象
*/
async function submitPaperService(paperId) {
async function submitPaperService(paperId, duration_seconds) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
// 1. 提交试卷
const submitResult = await submitPaper(paperId);
const submitResult = await submitPaper(paperId, duration_seconds);
if (!submitResult.success) {
throw new Error(submitResult.message);
@ -338,16 +359,20 @@ async function endPaperService(paperId) {
/**
* 服务层:处理试卷
* @param {number} paperId - 试卷ID
* @param {number} duration_seconds - 答题时长(秒)
* @returns {Promise<Object>} - 包含操作结果的对象
*/
async function processPaperService(paperId) {
async function processPaperService(paperId, duration_seconds) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await processPaper(paperId);
return result;
// 这样在继续考试时,就不会覆盖数据库中已有的paper_duration_seconds值
if (duration_seconds === undefined || duration_seconds === null) {
return await processPaper(paperId);
}
return await processPaper(paperId, duration_seconds);
} catch (error) {
console.error("服务层: 处理试卷失败", error);
return {
@ -380,10 +405,50 @@ async function checkPaperAnswersService(paperId) {
}
/**
* 初始化考试相关的IPC处理程序
* @param {import('electron').IpcMain} ipcMain - IPC主进程实例
* 服务层:根据考生ID获取试卷数据
* @param {number} examineeId - 考生ID
* @returns {Promise<Object>} - 包含试卷数据的对象
*/
// 在initExamingIpc函数中添加examing-get-exam-result处理器
async function getExamineePaperService(examineeId) {
try {
if (!examineeId || examineeId <= 0) {
throw new Error("考生ID必须为正数");
}
const result = await getExamineePaper(examineeId);
return result;
} catch (error) {
console.error("服务层: 获取考生试卷数据失败", error);
return {
success: false,
message: `获取考生试卷数据失败: ${error.message}`,
};
}
}
/**
* 服务层:继续未完成的考试
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果的对象
*/
async function continueExamService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await continueExam(paperId);
return result;
} catch (error) {
console.error("服务层: 继续考试失败", error);
return {
success: false,
message: `继续考试失败: ${error.message}`,
};
}
}
// 在initExamingIpc函数中添加continueExam的IPC处理程序(在其他处理器之后添加)
function initExamingIpc(ipcMain) {
// 生成考生试卷
ipcMain.handle(
@ -490,17 +555,21 @@ function initExamingIpc(ipcMain) {
});
// 提交考试
ipcMain.handle("examing-submit-paper", async (event, { paperId }) => {
try {
return await submitPaperService(paperId);
} catch (error) {
console.error("提交考试失败:", error);
return {
success: false,
message: `提交考试失败: ${error.message}`,
};
// 提交考试
ipcMain.handle(
"examing-submit-paper",
async (event, { paperId, duration_seconds }) => {
try {
return await submitPaperService(paperId, duration_seconds);
} catch (error) {
console.error("提交考试失败:", error);
return {
success: false,
message: `提交考试失败: ${error.message}`,
};
}
}
});
);
// 结束考试
ipcMain.handle("examing-end-paper", async (event, { paperId }) => {
@ -516,17 +585,20 @@ function initExamingIpc(ipcMain) {
});
// 处理试卷
ipcMain.handle("examing-process-paper", async (event, { paperId }) => {
try {
return await processPaperService(paperId);
} catch (error) {
console.error("处理试卷失败:", error);
return {
success: false,
message: `处理试卷失败: ${error.message}`,
};
ipcMain.handle(
"examing-process-paper",
async (event, { paperId, duration_seconds }) => {
try {
return await processPaperService(paperId, duration_seconds);
} catch (error) {
console.error("处理试卷失败:", error);
return {
success: false,
message: `处理试卷失败: ${error.message}`,
};
}
}
});
);
// 检查试卷答案并计算得分
ipcMain.handle("examing-check-paper-answers", async (event, { paperId }) => {
@ -553,6 +625,48 @@ function initExamingIpc(ipcMain) {
};
}
});
// 添加通过考生ID获取试卷数据的IPC处理器
ipcMain.handle(
"examing-get-examinee-paper",
async (event, { examineeId }) => {
try {
return await getExamineePaperService(examineeId);
} catch (error) {
console.error("获取考生试卷数据失败:", error);
return {
success: false,
message: `获取考生试卷数据失败: ${error.message}`,
};
}
}
);
// 添加继续考试的IPC处理器
ipcMain.handle("examing-continue-paper", async (event, { paperId }) => {
try {
return await continueExamService(paperId);
} catch (error) {
console.error("继续考试失败:", error);
return {
success: false,
message: `继续考试失败: ${error.message}`,
};
}
});
// 在initExamingIpc函数中添加getPaper的IPC处理器
ipcMain.handle("examing-get-paper", async (event, { paperId }) => {
try {
return await getPaperService(paperId);
} catch (error) {
console.error("获取试卷详细信息失败:", error);
return {
success: false,
message: `获取试卷详细信息失败: ${error.message}`,
};
}
});
}
// 导出使用CommonJS格式
@ -568,5 +682,9 @@ module.exports = {
endPaperService,
processPaperService,
checkPaperAnswersService,
getExamResultService,
getExamineePaperService,
continueExamService,
getPaperService,
initExamingIpc,
};

View File

@ -395,10 +395,17 @@ async function generatePaperPdf(jsonString) {
const endTime = paperData.paper_end_time;
// 截取考试日期 (假设格式为 'YYYY-MM-DD HH:mm:ss')
const examDate = startTime.split(' ')[0];
// 计算用时(分钟)
const start = new Date(startTime.replace(/-/g, '/'));
const end = new Date(endTime.replace(/-/g, '/'));
const durationMinutes = Math.round((end - start) / (1000 * 60));
// 计算用时(分钟)- 直接使用paper_duration_seconds字段
let durationMinutes = 0;
if (paperData.paper_duration_seconds && paperData.paper_duration_seconds > 0) {
// 直接从paper_duration_seconds字段获取并转换为分钟
durationMinutes = Math.round(paperData.paper_duration_seconds / 60);
} else {
// 备选方案:如果paper_duration_seconds不存在或无效,使用原来的计算方式
const start = new Date(startTime.replace(/-/g, '/'));
const end = new Date(endTime.replace(/-/g, '/'));
durationMinutes = Math.round((end - start) / (1000 * 60));
}
// 提取试卷信息
// 计算总题量(每个question下的choice题和fill_blank题的数量之和)

View File

@ -106,7 +106,8 @@ export default {
return {
size: 'default',
pdfPath: '',
isChecking: false
isChecking: false,
paperData: null // 新增:在组件内保存paper数据
}
},
// 在script部分确保正确的计算属性定义
@ -115,22 +116,9 @@ export default {
examinee () {
return this.$store.state.examinee || {}
},
// 从store中获取试卷信息
// 修改:不再从store中获取试卷信息,改为使用组件内数据
paper () {
// 如果store中没有数据,尝试从localStorage获取备用数据
if (!this.$store.state.paper || Object.keys(this.$store.state.paper).length === 0) {
const savedPaper = localStorage.getItem('lastExamPaper');
if (savedPaper) {
try {
// 解析并返回localStorage中的数据,但不直接修改store
return JSON.parse(savedPaper);
} catch (e) {
console.error('解析localStorage试卷数据失败:', e);
}
}
return {};
}
return this.$store.state.paper || {};
return this.paperData || {}
},
// 图标样式计算属性
iconStyle () {
@ -164,112 +152,101 @@ export default {
// 检查试卷数据
checkPaperData () {
// 1. 首先检查store中是否有完整数据
if (this.paper && this.paper.id) {
console.log('store中已有完整试卷数据');
// 检查是否需要判卷
if (!this.paper.is_checked) {
this.checkAnswers(this.paper.id);
}
return;
}
// 2. 从路由参数获取paperId
const paperIdFromRoute = this.$route.params.paperId;
// 1. 从路由参数获取paperId
const paperIdFromRoute = this.$route.params.paperId
if (!paperIdFromRoute) {
console.error('路由参数中没有找到paperId');
this.$message.error('无法获取试卷信息,请重新提交试卷');
return;
console.error('路由参数中没有找到paperId')
this.$message.error('无法获取试卷信息,请重新提交试卷')
return
}
// 3. 尝试从localStorage恢复数据
const savedPaperData = localStorage.getItem('lastExamPaper');
// 2. 尝试从localStorage恢复数据
const savedPaperData = localStorage.getItem('lastExamPaper')
if (savedPaperData) {
try {
const paperData = JSON.parse(savedPaperData);
const paperData = JSON.parse(savedPaperData)
// 验证恢复的数据是否匹配当前paperId
if (paperData.id && paperData.id.toString() === paperIdFromRoute.toString()) {
console.log('从localStorage恢复到store的试卷数据:', paperData);
// 保存到store,确保所有地方都能通过this.paper访问
this.$store.commit('setPaper', paperData);
console.log('从localStorage恢复的试卷数据:', paperData)
// 修改:保存到组件本地而不是store
this.paperData = paperData
// 检查是否需要判卷
if (!paperData.is_checked) {
this.checkAnswers(paperData.id);
this.checkAnswers(paperData.id)
}
return;
return
}
} catch (e) {
console.error('解析localStorage中的试卷数据失败:', e);
console.error('解析localStorage中的试卷数据失败:', e)
}
}
// 4. 如果以上都失败,调用API加载数据
console.log('调用API加载试卷数据,paperId:', paperIdFromRoute);
this.loadExamResult(paperIdFromRoute);
// 3. 调用API加载数据
console.log('调用API加载试卷数据,paperId:', paperIdFromRoute)
this.loadExamResult(paperIdFromRoute)
},
// 加载考试结果 - 确保数据正确保存到store
async loadExamResult (paperIdFromParams) {
try {
const paperId = paperIdFromParams || this.$route.params.paperId;
const paperId = paperIdFromParams || this.$route.params.paperId
if (!paperId) {
console.error('没有找到试卷ID');
this.$message.error('无法获取试卷信息,请重新提交试卷');
return;
console.error('没有找到试卷ID')
this.$message.error('无法获取试卷信息,请重新提交试卷')
return
}
// 检查API是否存在
if (!window.electronAPI || typeof window.electronAPI.examingGetExamResult !== 'function') {
console.error('examingGetExamResult API不存在');
this.$message.warning('获取考试结果功能暂不可用');
return;
console.error('examingGetExamResult API不存在')
this.$message.warning('获取考试结果功能暂不可用')
return
}
const result = await window.electronAPI.examingGetExamResult({ paperId });
const result = await window.electronAPI.examingGetExamResult({ paperId })
if (result && result.success && result.data) {
// 数据已经是对象,直接保存到store
this.$store.commit('setPaper', result.data);
// 修改:保存到组件本地而不是store
this.paperData = result.data
// 同时保存到localStorage作为备份
localStorage.setItem('lastExamPaper', JSON.stringify(result.data));
localStorage.setItem('lastExamPaper', JSON.stringify(result.data))
console.log('API获取并保存到store的试卷数据:', this.$store.state.paper);
console.log('API获取的试卷数据:', this.paperData)
// 检查是否需要判卷
if (!result.data.is_checked) {
this.checkAnswers(result.data.id || paperId);
this.checkAnswers(result.data.id || paperId)
}
} else {
console.error('获取考试结果失败:', result?.message || '未知错误');
this.$message.error('获取考试结果失败');
console.error('获取考试结果失败:', result?.message || '未知错误')
this.$message.error('获取考试结果失败')
}
} catch (error) {
console.error('加载考试结果失败:', error);
this.$message.error('加载考试结果失败');
console.error('加载考试结果失败:', error)
this.$message.error('加载考试结果失败')
}
},
// 判卷方法
// 判卷方法
async checkAnswers () {
try {
if (!this.paper || !this.paper.id) {
console.error('没有试卷ID,无法判卷');
this.$message.warning('缺少试卷信息,无法判卷');
return;
if (!this.paperData || !this.paperData.id) {
console.error('没有试卷ID,无法判卷')
this.$message.warning('缺少试卷信息,无法判卷')
return
}
this.isChecking = true
const paperId = this.paper.id
const paperId = this.paperData.id
// 检查API是否存在
if (!window.electronAPI || typeof window.electronAPI.examingCheckPaperAnswers !== 'function') {
console.error('examingCheckPaperAnswers API不存在');
this.$message.warning('判卷功能暂不可用');
this.isChecking = false;
return;
console.error('examingCheckPaperAnswers API不存在')
this.$message.warning('判卷功能暂不可用')
this.isChecking = false
return
}
const result = await window.electronAPI.examingCheckPaperAnswers({ paperId })
@ -280,26 +257,26 @@ export default {
// 新增:在判卷成功后生成PDF
try {
// 获取最新的试卷数据(包含判卷结果)
const paperDataStr = JSON.stringify(this.paper);
const paperDataStr = JSON.stringify(this.paperData)
// 生成PDF
const pdfResult = await window.electronAPI.fileGeneratePaperPdf(paperDataStr);
const pdfResult = await window.electronAPI.fileGeneratePaperPdf(paperDataStr)
if (pdfResult && pdfResult.filePath) {
this.pdfPath = pdfResult;
console.log('PDF生成成功,保存路径:', pdfResult.filePath);
this.pdfPath = pdfResult
console.log('PDF生成成功,保存路径:', pdfResult.filePath)
// this.$message.success('考试报告PDF已生成');
} else {
console.error('PDF生成失败:', pdfResult);
console.error('PDF生成失败:', pdfResult)
}
} catch (pdfError) {
console.error('PDF生成过程异常:', pdfError);
console.error('PDF生成过程异常:', pdfError)
}
} else {
console.error('判卷失败:', result?.message || '未知错误');
this.$message.error('判卷失败,请稍后重试');
console.error('判卷失败:', result?.message || '未知错误')
this.$message.error('判卷失败,请稍后重试')
}
} catch (error) {
console.error('判卷过程异常:', error);
this.$message.error('判卷过程中发生异常');
console.error('判卷过程异常:', error)
this.$message.error('判卷过程中发生异常')
} finally {
this.isChecking = false
}
@ -328,36 +305,54 @@ export default {
// 获取格式化的开始时间
getFormattedStartTime () {
if (!this.paper || !this.paper.paper_start_time) return '未知';
return this.formatDateTime(this.paper.paper_start_time);
if (!this.paper || !this.paper.paper_start_time) return '未知'
return this.formatDateTime(this.paper.paper_start_time)
},
// 获取格式化的结束时间
getFormattedEndTime () {
if (!this.paper || !this.paper.paper_end_time) return '未知';
return this.formatDateTime(this.paper.paper_end_time);
if (!this.paper || !this.paper.paper_end_time) return '未知'
return this.formatDateTime(this.paper.paper_end_time)
},
// 计算考试总时长
getDuration () {
if (!this.paper || !this.paper.paper_start_time || !this.paper.paper_end_time) {
return '未知';
// 优先检查paper_duration_seconds是否存在且有效
if (this.paper && this.paper.paper_duration_seconds !== undefined && this.paper.paper_duration_seconds !== null) {
const totalSeconds = this.paper.paper_duration_seconds
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
if (hours > 0) {
return `${hours}小时${minutes}分${seconds}秒`
} else if (minutes > 0) {
return `${minutes}分${seconds}秒`
} else {
return `${seconds}秒`
}
}
const startTime = new Date(this.paper.paper_start_time);
const endTime = new Date(this.paper.paper_end_time);
const durationMs = endTime - startTime;
// 如果paper_duration_seconds不存在或无效,则使用原来的时间差值计算作为备选
if (!this.paper || !this.paper.paper_start_time || !this.paper.paper_end_time) {
return '未知'
}
const hours = Math.floor(durationMs / (1000 * 60 * 60));
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((durationMs % (1000 * 60)) / 1000);
const startTime = new Date(this.paper.paper_start_time)
const endTime = new Date(this.paper.paper_end_time)
const durationMs = endTime - startTime
const hours = Math.floor(durationMs / (1000 * 60 * 60))
const minutes = Math.floor((durationMs % (1000 * 60 * 60)) / (1000 * 60))
const seconds = Math.floor((durationMs % (1000 * 60)) / 1000)
if (hours > 0) {
return `${hours}小时${minutes}分${seconds}秒`;
return `${hours}小时${minutes}分${seconds}秒`
} else if (minutes > 0) {
return `${minutes}分${seconds}秒`;
return `${minutes}分${seconds}秒`
} else {
return `${seconds}秒`;
return `${seconds}秒`
}
},

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@
<div class="exam-bottom-bar rounded shadow-sm mt-2">
<el-button type="primary" size="small" @click="prevQuestion" :disabled="currentQuestion === 1">上一题</el-button>
<el-button type="primary" size="small" @click="nextQuestion" :disabled="currentQuestion === questionList.length">下一题</el-button>
<el-button type="danger" size="small" @click="showExamPreview" :disabled="!canSubmit">交卷预览</el-button>
<el-button type="danger" size="small" @click="showExamPreview" :disabled="!canSubmitComputed">交卷预览</el-button>
</div>
</div>
@ -70,6 +70,8 @@ export default {
timer: null,
processTimer: null,
paperId: '',
action: '', // 新增:存储action参数
paperData: null, // 新增:在组件内保存paper数据
blankAnswerTip: '数字请保留2位小数,如"100.00"',
showPreviewModal: false, // 控制交卷预览模态框显示
previewQuestionData: [] // 存储预览用的所有题目数据
@ -80,9 +82,9 @@ export default {
examinee() {
return this.$store.state.examinee
},
// 从store中获取试卷信息
// 修改:不再从store中获取试卷信息,改为使用本地数据
paper() {
return this.$store.state.paper
return this.paperData
},
// 当前题目数据
currentQuestionData() {
@ -91,6 +93,34 @@ export default {
// 添加:已作答题目数量计算
answeredCount() {
return this.questionList.filter(item => item.answered === 1).length;
},
// 添加:使用computed计算交卷按钮可用性
canSubmitComputed() {
// 如果没有paper信息,默认不可用
if (!this.paper) {
return false;
}
// 如果没有设置最小时长,直接可用
if (!this.paper.paper_minutes_min) {
return true;
}
// 将countdown字符串转换为剩余秒数
const timeParts = this.countdown.split(':');
if (timeParts.length !== 3) {
return false;
}
const hours = parseInt(timeParts[0], 10);
const minutes = parseInt(timeParts[1], 10);
const seconds = parseInt(timeParts[2], 10);
const remainingSeconds = hours * 3600 + minutes * 60 + seconds;
const paperMinutesSeconds = this.paper.paper_minutes * 60;
const paperMinutesMinSeconds = this.paper.paper_minutes_min * 60;
// 根据新规则:倒计时的剩余总秒数 + paper_minutes_min分钟转为的秒数 <= paper_minutes分钟数转为的秒数
return remainingSeconds + paperMinutesMinSeconds <= paperMinutesSeconds;
}
},
created() {
@ -98,6 +128,10 @@ export default {
if (this.$route.params && this.$route.params.paperId) {
this.paperId = this.$route.params.paperId
}
// 获取action参数
if (this.$route.params && this.$route.params.action) {
this.action = this.$route.params.action
}
},
mounted() {
// 检查是否已登录
@ -106,10 +140,13 @@ export default {
return
}
// 优先从路由参数获取paperId
// 优先从路由参数获取paperId和action
if (!this.paperId && this.$route.params && this.$route.params.paperId) {
this.paperId = this.$route.params.paperId
}
if (!this.action && this.$route.params && this.$route.params.action) {
this.action = this.$route.params.action
}
// 如果路由参数中没有paperId,尝试从localStorage获取
if (!this.paperId) {
@ -138,37 +175,70 @@ export default {
clearInterval(this.processTimer)
this.processTimer = null
}
// 清除试卷信息
this.$store.commit('clearPaper')
// 移除:不再清除试卷信息
// this.$store.commit('clearPaper')
},
methods: {
// 初始化考试
async initializeExam() {
try {
console.log('初始化考试,paperId:', this.paperId)
console.log('初始化考试,paperId:', this.paperId, 'action:', this.action)
// 清除可能存在的旧试卷信息
this.$store.commit('clearPaper')
// 1. 从接口获取试卷信息
await this.loadPaperInfo(this.paperId)
// 2. 加载试题列表
await this.loadQuestionList(this.paperId)
// 3. 初始化计时器
if (this.paper && this.paper.paper_minutes) {
this.initializeTimer(this.paper.paper_minutes * 60)
// 根据action参数决定调用哪个接口
if (this.action === 'start') {
// 开始新考试
const startResult = await window.electronAPI.examingStartPaper({ paperId: this.paperId })
console.log('开始考试结果:', startResult);
if (startResult && startResult.success && startResult.data) {
this.paperData = startResult.data
} else {
throw new Error('开始考试失败')
}
} else if (this.action === 'continue') {
// 继续考试
const continueResult = await window.electronAPI.examingContinuePaper({ paperId: this.paperId })
console.log('继续考试结果:', continueResult);
if (continueResult && continueResult.success && continueResult.data) {
this.paperData = continueResult.data
} else {
throw new Error('继续考试失败')
}
} else {
this.initializeTimer(120 * 60) // 默认120分钟
// 默认情况,直接获取试卷信息
await this.loadPaperInfo(this.paperId)
}
// 4. 启动自动保存定时器
// 确保已获取到paperData
if (!this.paperData) {
throw new Error('未能获取到试卷数据,请重试')
}
// 加载试题列表
await this.loadQuestionList(this.paperId)
// 初始化计时器 - 严格基于数据库中的paper_duration_seconds
let totalSeconds = 0
if (this.paperData && this.paperData.paper_minutes) {
// 将试卷总时长转换为秒
const paperTotalSeconds = this.paperData.paper_minutes * 60
// 必须使用数据库中的paper_duration_seconds计算剩余时间
// 如果数据库中没有该值或为0,则使用完整时长
const dbDurationSeconds = this.paperData.paper_duration_seconds || 0
totalSeconds = Math.max(0, paperTotalSeconds - dbDurationSeconds)
console.log(`使用数据库中的paper_duration_seconds计算剩余时间 - 总时长: ${paperTotalSeconds}秒, 已用时: ${dbDurationSeconds}秒, 剩余时间: ${totalSeconds}秒`)
}
this.initializeTimer(totalSeconds)
// 启动自动保存定时器 - 确保不会将paper_duration_seconds改小
this.startProcessTimer(this.paperId)
this.isReady = true
this.canSubmit = this.paper && this.paper.paper_minutes_min ?
false : true // 根据最小时长决定初始提交状态
} catch (error) {
console.error('初始化考试失败:', error)
this.$message.error(`初始化考试失败: ${error.message || '未知错误'}`)
@ -197,11 +267,12 @@ export default {
this.$message.warning('考试时间剩余10分钟')
}
// 移除定时器中的canSubmit设置
// 检查是否达到最小交卷时间
if (this.paper && this.paper.paper_minutes_min &&
remainingSeconds <= totalSeconds - (this.paper.paper_minutes_min * 60)) {
this.canSubmit = true
}
// if (this.paper && this.paper.paper_minutes_min &&
// remainingSeconds + (this.paper.paper_minutes_min * 60) <= totalSeconds) {
// this.canSubmit = true
// }
}
}, 1000)
},
@ -222,75 +293,80 @@ export default {
this.processTimer = null
}
// 每15秒调用一次processPaper接口
// 定期调用processPaper接口
this.processTimer = setInterval(async () => {
try {
// 正确传递对象参数 { paperId: paperId }
const result = await window.electronAPI.examingProcessPaper({ paperId: paperId })
// 将返回的paper对象同步到store中
// 计算当前已用时间(秒)
let duration_seconds = 0
let shouldUpdateDuration = false
if (this.paperData && this.paperData.paper_minutes && this.countdown) {
// 将countdown字符串转换为剩余秒数
const timeParts = this.countdown.split(':')
if (timeParts.length === 3) {
const hours = parseInt(timeParts[0], 10)
const minutes = parseInt(timeParts[1], 10)
const seconds = parseInt(timeParts[2], 10)
const remainingSeconds = hours * 3600 + minutes * 60 + seconds
// 计算已用时:总时长秒数减去剩余秒数
duration_seconds = this.paperData.paper_minutes * 60 - remainingSeconds
duration_seconds = Math.max(0, duration_seconds)
// 关键逻辑:只有当计算出的duration_seconds大于数据库中的值时才更新
const dbDurationSeconds = this.paperData.paper_duration_seconds || 0
shouldUpdateDuration = duration_seconds > dbDurationSeconds
if (!shouldUpdateDuration) {
console.log(`跳过更新paper_duration_seconds: 计算值(${duration_seconds})不大于数据库值(${dbDurationSeconds})`)
}
}
}
// 构建参数 - 只有当需要更新duration_seconds时才传递该参数
const params = { paperId: paperId }
if (shouldUpdateDuration) {
params.duration_seconds = duration_seconds
}
const result = await window.electronAPI.examingProcessPaper(params)
// 修改:不再保存到store,而是保存到组件本地
if (result.success && result.data) {
this.$store.commit('setPaper', result.data)
// 更新本地数据时,确保保留较大的paper_duration_seconds值
const newData = result.data
const currentDuration = this.paperData.paper_duration_seconds || 0
const newDuration = newData.paper_duration_seconds || 0
// 如果新数据中的duration_seconds小于当前值,则保留当前值
if (newDuration < currentDuration) {
newData.paper_duration_seconds = currentDuration
}
this.paperData = newData
}
} catch (error) {
console.error('自动更新试卷状态失败:', error)
// 错误不中断定时器,继续尝试下一次
}
}, 15000) // 15秒 = 15000毫秒
}, 5000) // 5秒 = 5000毫秒
},
// 加载试卷信息的方法
// 加载试卷信息的方法 - 简化版,只获取数据不修改
async loadPaperInfo(paperId) {
try {
console.log('开始加载试卷信息,paperId:', paperId)
// 清除可能存在的旧试卷信息
this.$store.commit('clearPaper')
// 1. 先获取试卷的当前状态
// 只获取试卷信息
const result = await window.electronAPI.examingProcessPaper({ paperId: paperId })
console.log('loadPaperInfo结果:', result)
if (result && result.success && result.data) {
// 2. 检查试卷状态,如果是未开始(0),则调用startPaper开始考试
if (result.data.paper_status === 0) {
console.log('试卷状态为未开始,调用startPaper开始考试')
const startResult = await window.electronAPI.examingStartPaper({ paperId: paperId })
console.log('开始考试结果:', startResult);
if (startResult && startResult.success && startResult.data) {
// 使用开始考试后的数据
this.$store.commit('setPaper', startResult.data)
this.canSubmit = startResult.data.paper_minutes_min ? false : true
} else {
console.error('开始考试失败')
this.$store.commit('setPaper', result.data)
this.canSubmit = result.data.paper_minutes_min ? false : true
}
} else {
// 如果不是未开始状态,直接使用返回的数据
this.$store.commit('setPaper', result.data)
this.canSubmit = result.data.paper_minutes_min ? false : true
}
} else {
console.error('加载试卷信息失败: 返回数据无效')
// 如果无法获取试卷信息,创建一个默认的试卷对象以避免页面白屏
this.$store.commit('setPaper', {
id: paperId,
paper_score: 0,
paper_minutes: 0,
paper_minutes_min: 0
})
this.canSubmit = false
// 保存到组件本地
this.paperData = result.data
}
} catch (error) {
console.error('加载试卷信息失败:', error)
// 错误情况下也创建一个默认的试卷对象
this.$store.commit('setPaper', {
id: paperId,
paper_score: 0,
paper_minutes: 0,
paper_minutes_min: 0
})
this.canSubmit = false
this.$message.error(`加载试卷信息失败: ${error.message || '未知错误'}`)
}
},
@ -624,10 +700,10 @@ export default {
// 交卷方法 - 确保数据完全保存到store后再跳转
async submitExam() {
try {
if (!this.canSubmit) {
this.$message.warning('您的答案尚未保存,请等待保存完成后再提交');
return;
}
if (!this.canSubmitComputed) {
this.$message.warning('您的答案尚未保存,请等待保存完成后再提交');
return;
}
// 显示确认对话框
const confirmResult = await this.$confirm('确定要提交试卷吗?提交后将无法继续答题。', '确认提交', {
@ -639,8 +715,29 @@ export default {
// 保存当前答案
await this.saveCurrentAnswer();
// 调用提交试卷API
const result = await window.electronAPI.examingSubmitPaper({ paperId: this.paper.id });
// 修改:不再使用start_time和currentTime计算duration_seconds
// 改为使用paper_minutes与倒计时的差值计算
let durationSeconds = 0
if (this.paperData && this.paperData.paper_minutes && this.countdown) {
// 将countdown字符串转换为剩余秒数
const timeParts = this.countdown.split(':')
if (timeParts.length === 3) {
const hours = parseInt(timeParts[0], 10)
const minutes = parseInt(timeParts[1], 10)
const seconds = parseInt(timeParts[2], 10)
const remainingSeconds = hours * 3600 + minutes * 60 + seconds
// 计算已用时:总时长秒数减去剩余秒数
durationSeconds = this.paperData.paper_minutes * 60 - remainingSeconds
durationSeconds = Math.max(0, durationSeconds)
}
}
// 调用提交试卷API,传递答题时长
const result = await window.electronAPI.examingSubmitPaper({
paperId: this.paperData.id,
duration_seconds: durationSeconds
});
console.log('提交试卷API返回:', result);
if (result && result.success) {
@ -649,12 +746,12 @@ export default {
try {
// 尝试解析数据
const paperData = JSON.parse(paperDataStr);
// 1. 将试卷数据保存到store
this.$store.commit('setPaper', paperData);
// const paperData = JSON.parse(paperDataStr);
// 1. 移除:不再保存到store
// this.$store.commit('setPaper', paperData);
// 2. 同时保存到localStorage作为备用
localStorage.setItem('lastExamPaper', paperDataStr);
// 2. 同时保存到localStorage作为备用
localStorage.setItem('lastExamPaper', paperDataStr);
// 移除:PDF生成代码已移至EndView.vue
@ -664,7 +761,7 @@ export default {
setTimeout(() => {
this.$router.push({
name: 'End',
params: { paperId: this.paper.id }
params: { paperId: this.paperData.id }
});
}, 300); // 增加延迟时间到300ms
} catch (jsonError) {
@ -672,13 +769,14 @@ export default {
this.$message.error('处理试卷数据失败');
// 即使解析失败,也尝试保存数据
this.$store.commit('setPaper', { id: this.paper.id, rawData: result.data });
localStorage.setItem('lastExamPaper', paperDataStr);
// 移除:不再保存到store
// this.$store.commit('setPaper', { id: this.paperData.id, rawData: result.data });
localStorage.setItem('lastExamPaper', paperDataStr);
setTimeout(() => {
this.$router.push({
name: 'End',
params: { paperId: this.paper.id }
params: { paperId: this.paperData.id }
});
}, 300);
}
@ -702,13 +800,13 @@ export default {
await this.saveCurrentAnswer()
// 修复:使用与high_version一致的接口调用方式
const result = await window.electronAPI.endPaper(this.paper.id)
const result = await window.electronAPI.examingEndPaper(this.paper.id)
if (result && result.success) {
// 将返回的paper对象同步到store中
if (result.data) {
this.$store.commit('setPaper', result.data)
}
// if (result.data) {
// this.$store.commit('setPaper', result.data)
// }
// 清理定时器
if (this.processTimer) {
clearInterval(this.processTimer)