From 08bf170216c3923a666dad0f07cf67b63bdb7b9c Mon Sep 17 00:00:00 2001 From: chenqiang Date: Sat, 13 Sep 2025 07:24:13 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E8=80=83=E8=AF=95=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B7=B2=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/db/examing.js | 226 ++++++- background/db/path.js | 3 +- background/db/schema.js | 1 + background/db/utils.js | 3 +- background/preload.js | 25 +- background/service/examingService.js | 182 +++++- background/service/fileService.js | 15 +- src/views/user/EndView.vue | 185 +++--- src/views/user/ExamineeHomeView.vue | 877 +++++++++++++++++++++++---- src/views/user/ExamingView.vue | 280 ++++++--- 10 files changed, 1451 insertions(+), 346 deletions(-) diff --git a/background/db/examing.js b/background/db/examing.js index c6eb5e7..a4f21bc 100644 --- a/background/db/examing.js +++ b/background/db/examing.js @@ -772,9 +772,10 @@ async function startPaper(paperId) { /** * 提交考试 * @param {number} paperId - 试卷ID + * @param {number} duration_seconds - 答题时长(秒) * @returns {Promise} - 包含操作结果和试卷数据的对象 */ -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} - 包含操作结果和试卷数据的对象 */ -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} - 包含查询结果的对象 + */ +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} - 包含操作结果和试卷数据的对象 + */ +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 }; \ No newline at end of file diff --git a/background/db/path.js b/background/db/path.js index e287e59..d4b862c 100644 --- a/background/db/path.js +++ b/background/db/path.js @@ -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; diff --git a/background/db/schema.js b/background/db/schema.js index b02304d..ac00cba 100644 --- a/background/db/schema.js +++ b/background/db/schema.js @@ -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, diff --git a/background/db/utils.js b/background/db/utils.js index aa0f73a..20cba98 100644 --- a/background/db/utils.js +++ b/background/db/utils.js @@ -1,5 +1,6 @@ -const sqlite3 = require('sqlite3'); const { promisify } = require('util'); +// 添加缺少的sqlite3模块导入 +const sqlite3 = require('sqlite3'); sqlite3.verbose(); diff --git a/background/preload.js b/background/preload.js index 7084a39..b5b00bf 100644 --- a/background/preload.js +++ b/background/preload.js @@ -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定义,移除重复的 diff --git a/background/service/examingService.js b/background/service/examingService.js index 264d42f..e309958 100644 --- a/background/service/examingService.js +++ b/background/service/examingService.js @@ -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} - 包含试卷详细信息的对象 + */ +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} - 包含操作结果的对象 */ -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} - 包含操作结果的对象 */ -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} - 包含试卷数据的对象 */ -// 在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} - 包含操作结果的对象 + */ +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, }; diff --git a/background/service/fileService.js b/background/service/fileService.js index 6eb11fd..08581e3 100644 --- a/background/service/fileService.js +++ b/background/service/fileService.js @@ -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题的数量之和) diff --git a/src/views/user/EndView.vue b/src/views/user/EndView.vue index c7df41d..1aa48fe 100644 --- a/src/views/user/EndView.vue +++ b/src/views/user/EndView.vue @@ -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}秒` } }, diff --git a/src/views/user/ExamineeHomeView.vue b/src/views/user/ExamineeHomeView.vue index bfb6840..9bf7ac1 100644 --- a/src/views/user/ExamineeHomeView.vue +++ b/src/views/user/ExamineeHomeView.vue @@ -4,7 +4,7 @@
-
+

考生信息

@@ -12,7 +12,7 @@ @@ -21,7 +21,7 @@ @@ -30,7 +30,7 @@ @@ -38,7 +38,7 @@ -
+

考试信息

@@ -46,7 +46,7 @@ @@ -55,7 +55,7 @@ @@ -64,7 +64,7 @@ @@ -73,7 +73,7 @@ @@ -81,8 +81,121 @@
+ +
+
你有未完成的考试
+ + + + {{ unfinishedPaper.paper_start_time }} + + + + {{ usedMinutes }}分钟 + + + + {{ remainingMinutes }}分钟 + + + + {{ answeredQuestionsCount }}题 (选择题:{{ answeredChoicesCount }},填空题:{{ answeredFillBlanksCount }}) + + +
+ 你可以继续进行未完成的考试,或者重新开始,重新开始后将清除所有历史考试记录。 +
+
+ 继续考试 + 重新开始 +
+
+ + +
+
你有已完成的考试
+ + + + {{ completedPaper.paper_start_time }} + + + + {{ completedPaper.paper_end_time }} + + + + {{ completedExamDuration }}分钟 + + + + {{ completedAnsweredQuestionsCount }}题 + + + + {{ completedTotalQuestions }}题 + + + + {{ completedPaper.paper_score }}分 + + +
+ 重新考试 +
+
-
+

考试须知

@@ -102,7 +215,8 @@
返回 - 开始考试 + 开始考试
@@ -118,7 +232,7 @@ export default { ElDivider: require('element-ui').Divider, ElButton: require('element-ui').Button }, - data() { + data () { return { isLoading: false, size: 'default', @@ -126,17 +240,19 @@ export default { examLoading: true, examNotices: [], totalQuestions: 0, - totalScore: 0 + totalScore: 0, + unfinishedPaper: null, // 未完成的试卷数据 + completedPaper: null // 已完成的试卷数据 } }, computed: { - examinee() { + examinee () { return this.$store.state.examinee }, - isLoggedIn() { + isLoggedIn () { return this.$store.state.isLoggedIn }, - iconStyle() { + iconStyle () { const marginMap = { large: '8px', default: '6px', @@ -145,22 +261,228 @@ export default { return { marginRight: marginMap[this.size] || marginMap.default } + }, + // 已用时长(分钟) + usedMinutes() { + if (!this.unfinishedPaper) return 0 + + // 优先使用paper_duration_seconds + if (this.unfinishedPaper.paper_duration_seconds !== undefined) { + return Math.ceil(this.unfinishedPaper.paper_duration_seconds / 60) + } + + // 保留原有逻辑作为后备 + if (this.unfinishedPaper.paper_start_time && this.unfinishedPaper.paper_last_time) { + const startTime = new Date(this.unfinishedPaper.paper_start_time) + const lastTime = new Date(this.unfinishedPaper.paper_last_time) + return Math.floor((lastTime - startTime) / (1000 * 60)) + } + + return 0 + }, + // 剩余时长(分钟) + remainingMinutes() { + if (!this.unfinishedPaper || !this.unfinishedPaper.paper_minutes) return 0 + + // 使用paper_duration_seconds计算剩余时间 + if (this.unfinishedPaper.paper_duration_seconds !== undefined) { + const totalMinutes = this.unfinishedPaper.paper_minutes + const usedMinutes = Math.ceil(this.unfinishedPaper.paper_duration_seconds / 60) + return Math.max(0, totalMinutes - usedMinutes) + } + + // 保留原有逻辑作为后备 + if (this.unfinishedPaper.paper_start_time && this.unfinishedPaper.paper_last_time) { + const totalMinutes = this.unfinishedPaper.paper_minutes + const usedMinutes = this.usedMinutes + return Math.max(0, totalMinutes - usedMinutes) + } + + return this.unfinishedPaper.paper_minutes + }, + // 格式化的已用时长 + formattedUsedTime() { + const minutes = this.usedMinutes + const hours = Math.floor(minutes / 60) + const mins = minutes % 60 + return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}` + }, + // 格式化的剩余时长 + formattedRemainingTime() { + const minutes = this.remainingMinutes + const hours = Math.floor(minutes / 60) + const mins = minutes % 60 + return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}` + }, + // 计算已答题数量 + answeredQuestionsCount () { + if (!this.unfinishedPaper || !this.unfinishedPaper.questions) { + return 0 + } + const count = this.answeredChoicesCount + this.answeredFillBlanksCount + console.log('计算已答题数量:', { + total: count, + choices: this.answeredChoicesCount, + fillBlanks: this.answeredFillBlanksCount + }) + return count + }, + // 已答选择题数量 + answeredChoicesCount () { + if (!this.unfinishedPaper || !this.unfinishedPaper.questions) { + return 0 + } + return this.unfinishedPaper.questions.filter(q => { + if (q.question_type !== 'choice') return false + + // 检查选择题的答案是否有效 + // 注意:这里的逻辑需要根据实际数据结构调整 + // 如果questions中的数据结构与ExamingPreview.vue中的不同,可能需要调整 + if (q.choices && q.choices.length > 0) { + // 处理choices数组结构 + return q.choices.some(choice => { + // 对于每个选项,检查examinee_answers是否有效 + return this.getValidAnswerCount(choice.examinee_answers) > 0 + }) + } else if (q.examinee_answers) { + // 处理直接在question对象中的examinee_answers + return this.getValidAnswerCount(q.examinee_answers) > 0 + } + return false + }).length + }, + // 已答填空题数量 + answeredFillBlanksCount () { + if (!this.unfinishedPaper || !this.unfinishedPaper.questions) { + return 0 + } + return this.unfinishedPaper.questions.filter(q => { + if (q.question_type !== 'fill_blank') return false + + // 检查填空题的答案是否有效 + // 注意:这里的逻辑需要根据实际数据结构调整 + if (q.blanks && q.blanks.length > 0) { + // 处理blanks数组结构 + return q.blanks.some(blank => { + return this.getValidAnswerCount(blank.examinee_answers) > 0 + }) + } else if (q.examinee_answers) { + // 处理直接在question对象中的examinee_answers + return this.getValidAnswerCount(q.examinee_answers) > 0 + } + return false + }).length + }, + // 已完成试卷的考试用时(分钟) + completedExamDuration () { + if (!this.completedPaper) return 0 + + // 优先使用paper_duration_seconds + if (this.completedPaper.paper_duration_seconds !== undefined) { + return Math.ceil(this.completedPaper.paper_duration_seconds / 60) + } + + // 保留原有逻辑作为后备 + if (this.completedPaper.paper_start_time && this.completedPaper.paper_end_time) { + const startTime = new Date(this.completedPaper.paper_start_time) + const endTime = new Date(this.completedPaper.paper_end_time) + // 计算分钟数差值 + return Math.ceil((endTime - startTime) / (1000 * 60)) + } + + return 0 + }, + // 已完成试卷的已答题量 + completedAnsweredQuestionsCount () { + if (!this.completedPaper || !this.completedPaper.questions) { + return 0 + } + return this.completedPaper.questions.filter(q => { + // 判断问题是否已回答,逻辑与未完成试卷相同 + if (q.choices && q.choices.length > 0) { + // 处理选择题 + return q.choices.some(choice => { + return this.getValidAnswerCount(choice.examinee_answers) > 0 + }) + } else if (q.blanks && q.blanks.length > 0) { + // 处理填空题 + return q.blanks.some(blank => { + return this.getValidAnswerCount(blank.examinee_answers) > 0 + }) + } else if (q.examinee_answers) { + // 直接处理examinee_answers + return this.getValidAnswerCount(q.examinee_answers) > 0 + } + return false + }).length + }, + // 已完成试卷的总题量 + completedTotalQuestions () { + if (!this.completedPaper || !this.completedPaper.questions) { + return 0 + } + + // 计算总题量:选择题数量 + 填空题数量 + let totalQuestionsCount = 0 + + this.completedPaper.questions.forEach(q => { + // 计算选择题数量 + if ((q.question_type === 'choice' || q.question_type === 'multiple_choice') && q.choices && Array.isArray(q.choices)) { + totalQuestionsCount += q.choices.length + } + // 计算填空题数量 + else if (q.question_type === 'fill_blank' && q.blanks && Array.isArray(q.blanks)) { + totalQuestionsCount += q.blanks.length + } + }) + + return totalQuestionsCount + }, + // 已完成试卷的已答选择题数量 + completedAnsweredChoicesCount () { + if (!this.completedPaper || !this.completedPaper.questions) { + return 0 + } + return this.completedPaper.questions.filter(q => { + if (q.question_type !== 'choice' && q.question_type !== 'multiple_choice') return false + if (q.choices && q.choices.length > 0) { + return q.choices.some(choice => { + return this.getValidAnswerCount(choice.examinee_answers) > 0 + }) + } + return false + }).length + }, + // 已完成试卷的已答填空题数量 + completedAnsweredFillBlanksCount () { + if (!this.completedPaper || !this.completedPaper.questions) { + return 0 + } + return this.completedPaper.questions.filter(q => { + if (q.question_type !== 'fill_blank') return false + if (q.blanks && q.blanks.length > 0) { + return q.blanks.some(blank => { + return this.getValidAnswerCount(blank.examinee_answers) > 0 + }) + } + return false + }).length } }, watch: { - lastExam(newValue) { + lastExam (newValue) { if (newValue) { this.examLoading = false } }, - isLoggedIn(newVal) { + isLoggedIn (newVal) { if (!newVal) { // 如果未登录,重定向到欢迎页 this.$router.push('/') } } }, - mounted() { + mounted () { // 检查是否已登录 if (!this.isLoggedIn) { this.$router.push('/') @@ -169,17 +491,255 @@ export default { this.fetchLastExam() this.getQuestionsCountAndScore() + this.checkUnfinishedPaper() // 检查是否有未完成的试卷 }, methods: { + // 计算有效答案数量(与ExamingPreview.vue保持一致) + getValidAnswerCount (answer) { + if (!answer) return 0 + + // 处理字符串类型 + if (typeof answer === 'string') { + const trimmed = answer.trim() + // 空字符串、只包含空格的字符串、空数组字符串都视为0个有效答案 + if (trimmed === '' || trimmed === '[]') { + return 0 + } + + // 检查是否是JSON数组格式 + if (trimmed.startsWith('[') && trimmed.endsWith(']')) { + try { + const parsed = JSON.parse(trimmed) + if (Array.isArray(parsed)) { + // 计算非空且清除空格后不为空的元素数量 + return parsed.filter(item => { + const itemStr = String(item || '') + return itemStr.trim() !== '' + }).length + } + } catch (e) { + // 解析失败,视为普通字符串 + return trimmed !== '' ? 1 : 0 + } + } + + // 普通字符串 + return trimmed !== '' ? 1 : 0 + } + + // 处理数组类型 + else if (Array.isArray(answer)) { + // 计算非空且清除空格后不为空的元素数量 + return answer.filter(item => { + const itemStr = String(item || '') + return itemStr.trim() !== '' + }).length + } + + // 其他类型 + return 0 + }, + + // 检查是否有未完成的试卷... + async checkUnfinishedPaper () { + if (!this.examinee || !this.examinee.id) { + console.warn('未获取到考生信息,无法检查未完成试卷') + return + } + + console.log('开始检查未完成试卷:', { + examineeId: this.examinee.id, + examineeName: this.examinee.examinee_name + }) + + try { + // 调用接口获取考生试卷数据 + const result = await window.electronAPI.examingGetExamineePaper({ + examineeId: this.examinee.id + }) + + console.log('检查未完成试卷API响应结果:', result) + + if (result && result.success && result.data) { + console.log('检查未完成试卷成功,开始应用自定义判断逻辑') + + // 根据用户指定的逻辑判断是否有未完成的考试 + let unfinishedPaper = null + let completedPaper = null + + // 如果data是数组,遍历查找未完成和已完成的试卷 + if (Array.isArray(result.data)) { + unfinishedPaper = result.data.find(paper => this.isPaperUnfinished(paper)) + completedPaper = result.data.find(paper => this.isPaperCompleted(paper)) + } + // 如果data是单个对象,直接检查是否未完成或已完成 + else if (typeof result.data === 'object') { + if (this.isPaperUnfinished(result.data)) { + unfinishedPaper = result.data + } else if (this.isPaperCompleted(result.data)) { + completedPaper = result.data + } + } + + if (unfinishedPaper) { + // 存在未完成的试卷 + this.unfinishedPaper = unfinishedPaper + console.log('发现未完成的试卷,详细信息:', { + paperId: this.unfinishedPaper.id, + paperStatus: this.unfinishedPaper.paper_status, + startTime: this.unfinishedPaper.paper_start_time, + lastTime: this.unfinishedPaper.paper_last_time, + submitTime: this.unfinishedPaper.paper_submit_time, + endTime: this.unfinishedPaper.paper_end_time, + paperMinutes: this.unfinishedPaper.paper_minutes, + questionsCount: this.unfinishedPaper.questions ? this.unfinishedPaper.questions.length : 0, + answeredQuestionsCount: this.answeredQuestionsCount, + totalScore: this.unfinishedPaper.total_score + }) + } else { + console.log('没有发现未完成的试卷') + } + + if (completedPaper) { + // 存在已完成的试卷 + this.completedPaper = completedPaper + console.log('发现已完成的试卷,详细信息:', { + paperId: this.completedPaper.id, + paperStatus: this.completedPaper.paper_status, + startTime: this.completedPaper.paper_start_time, + endTime: this.completedPaper.paper_end_time, + paperMinutes: this.completedPaper.paper_minutes + }) + } else { + console.log('没有发现已完成的试卷') + } + } else { + console.warn('检查未完成试卷失败:', result ? result.message : '未知错误') + } + } catch (error) { + console.error('检查未完成试卷异常:', error) + } + }, + + // 判断试卷是否未完成的辅助方法 + isPaperUnfinished (paper) { + if (!paper) return false + + // 新的判定规则: + // 1. paper_status值不是2 + const statusNotTwo = paper.paper_status !== 2 + + // 2. duration_seconds大于0且小于paper_minutes换成的秒数 + let durationValid = false + if (paper.paper_duration_seconds !== undefined && paper.paper_minutes) { + const totalSeconds = paper.paper_minutes * 60 + durationValid = paper.paper_duration_seconds >= 0 && paper.paper_duration_seconds < totalSeconds + } + + // 3. paper_end_time为空 + const noEndTime = !paper.paper_end_time + + // 记录判断过程 + console.log('判断试卷是否未完成:', { + paperId: paper.id, + statusNotTwo, + durationValid, + noEndTime, + isUnfinished: statusNotTwo && durationValid && noEndTime + }) + + // 同时满足以上所有条件才是未完成的考试 + return statusNotTwo && durationValid && noEndTime + }, + + // 判断试卷是否已完成的辅助方法 + isPaperCompleted (paper) { + if (!paper) return false + + // 条件1: paper_status值为2 + const statusIsTwo = paper.paper_status === 2 + + // 条件2: paper_end_time有值 + const hasEndTime = !!paper.paper_end_time + + // 记录判断过程 + console.log('判断试卷是否已完成:', { + paperId: paper.id, + statusIsTwo, + hasEndTime, + isCompleted: statusIsTwo && hasEndTime + }) + + // 同时满足以上所有条件才是已完成的考试 + return statusIsTwo && hasEndTime + }, + + // 继续考试 + continueExam () { + if (this.unfinishedPaper && this.unfinishedPaper.id) { + this.$confirm( + '确定要继续上次的考试吗?', + '继续考试确认', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ).then(async () => { + try { + // 直接跳转到Examing页面,传递paperId和action=continue + this.$router.push({ + name: 'Examing', + params: { + paperId: this.unfinishedPaper.id, + action: 'continue' + } + }) + } catch (error) { + console.error('继续考试失败:', error) + this.$message.error(`继续考试失败: ${error.message || '未知错误'}`) + } + }).catch(() => { + // 用户取消继续考试 + console.log('用户取消继续考试') + }) + } + }, + + // 重新开始考试 + async restartExam () { + this.$confirm( + '确定要重新开始考试吗?这将清除所有历史考试记录。', + '重新开始确认', + { + confirmButtonText: '确定', + cancelButtonText: '取消', + type: 'warning' + } + ).then(async () => { + try { + // 清除未完成的试卷状态,然后调用开始考试方法 + this.unfinishedPaper = null + await this.startExam() + } catch (error) { + console.error('重新开始考试失败:', error) + this.$message.error('重新开始考试失败') + } + }).catch(() => { + // 用户取消操作 + console.log('用户取消重新开始') + }) + }, + // 所有方法保持不变 // 格式化身份证号(第11-15位替换为*) - formatIdCard(idCard) { + formatIdCard (idCard) { if (!idCard || idCard.length !== 18) return idCard return idCard.substring(0, 9) + '*****' + idCard.substring(14) }, // 格式化考试时长(分钟转小时) - formatExamDuration(minutes) { + formatExamDuration (minutes) { if (!minutes) return '未知' const hours = Math.floor(minutes / 60) const mins = minutes % 60 @@ -187,7 +747,7 @@ export default { }, // 得到考题数量和总分 - async getQuestionsCountAndScore() { + async getQuestionsCountAndScore () { try { // 注意:这里需要确认接口是否存在 // 如果不存在,可能需要创建或修改现有接口 @@ -201,7 +761,7 @@ export default { }, // 获取最新考试信息 - async fetchLastExam() { + async fetchLastExam () { this.examLoading = true try { // 调用Electron API获取最新考试信息 @@ -235,7 +795,7 @@ export default { }, // 退出考试 - exitExam() { + exitExam () { this.$confirm( '确定要退出考试吗?', '退出确认', @@ -247,8 +807,8 @@ export default { ).then(() => { // 完全清除store中的数据 this.$store.commit('clearUser') // 修改为已定义的mutation - // 同时清除试卷信息 - this.$store.commit('clearPaper') + // 移除:不再清除试卷信息 + // this.$store.commit('clearPaper') // 重定向到欢迎页 this.$router.push('/') }).catch(() => { @@ -259,89 +819,92 @@ export default { // 开始考试方法 // 修改startExam方法,移除保存完整试卷信息到store的代码 - async startExam() { - if (!this.lastExam) { - this.$message.warning('请先获取考试信息') - return - } + async startExam () { + if (!this.lastExam) { + this.$message.warning('请先获取考试信息') + return + } - if (!this.examinee || !this.examinee.id || !this.examinee.examinee_name) { - this.$message.warning('未获取到完整的考生信息') - return - } + if (!this.examinee || !this.examinee.id || !this.examinee.examinee_name) { + this.$message.warning('未获取到完整的考生信息') + return + } - this.isLoading = true + this.isLoading = true - try { - console.log('开始生成试卷...', { examinee: this.examinee, exam: this.lastExam }) + try { + console.log('开始生成试卷...', { examinee: this.examinee, exam: this.lastExam }) - // 创建可序列化的完整考生数据对象 - const examineeData = { - id: this.examinee.id, - examinee_name: this.examinee.examinee_name || '', - examinee_id_card: this.examinee.examinee_id_card || '', - examinee_admission_ticket: this.examinee.examinee_admission_ticket || '', - examinee_gender: this.examinee.examinee_gender || '', - examinee_unit: this.examinee.examinee_unit || '', - written_exam_room: this.examinee.written_exam_room || '', - written_exam_seat: this.examinee.written_exam_seat || '', - computer_exam_room: this.examinee.computer_exam_room || '', - computer_exam_seat: this.examinee.computer_exam_seat || '' - } + // 创建可序列化的完整考生数据对象 + const examineeData = { + id: this.examinee.id, + examinee_name: this.examinee.examinee_name || '', + examinee_id_card: this.examinee.examinee_id_card || '', + examinee_admission_ticket: this.examinee.examinee_admission_ticket || '', + examinee_gender: this.examinee.examinee_gender || '', + examinee_unit: this.examinee.examinee_unit || '', + written_exam_room: this.examinee.written_exam_room || '', + written_exam_seat: this.examinee.written_exam_seat || '', + computer_exam_room: this.examinee.computer_exam_room || '', + computer_exam_seat: this.examinee.computer_exam_seat || '' + } - // 使用JSON序列化/反序列化确保对象可克隆 - const examData = JSON.parse(JSON.stringify({ - id: this.lastExam.id, - exam_name: this.lastExam.exam_name || '', - exam_description: this.lastExam.exam_description || '', - exam_minutes: this.lastExam.exam_minutes || 0, - exam_minutes_min: this.lastExam.exam_minutes_min || 0, - exam_notice: this.lastExam.exam_notice || [] - })) + // 使用JSON序列化/反序列化确保对象可克隆 + const examData = JSON.parse(JSON.stringify({ + id: this.lastExam.id, + exam_name: this.lastExam.exam_name || '', + exam_description: this.lastExam.exam_description || '', + exam_minutes: this.lastExam.exam_minutes || 0, + exam_minutes_min: this.lastExam.exam_minutes_min || 0, + exam_notice: this.lastExam.exam_notice || [] + })) - // 直接调用ipcRenderer接口,跳过preload.js中定义的不匹配方法 - const result = await window.electronAPI.ipcRenderer.invoke('examing-generate-paper', { - examineeData: examineeData, - examData: examData - }) + // 直接调用ipcRenderer接口,跳过preload.js中定义的不匹配方法 + const result = await window.electronAPI.ipcRenderer.invoke('examing-generate-paper', { + examineeData: examineeData, + examData: examData + }) - if (result && result.success) { - console.log('生成试卷成功:', result) + if (result && result.success) { + console.log('生成试卷成功:', result) - // 移除:不再保存完整试卷信息到store - // this.$store.commit('setPaper', result.data) + // 移除:不再保存完整试卷信息到store + // this.$store.commit('setPaper', result.data) - // 仅保存试卷ID到localStorage(作为备用机制) - try { - localStorage.setItem('currentPaperId', result.data.id) - } catch (error) { - console.warn('保存试卷ID到localStorage失败:', error) - } + // 仅保存试卷ID到localStorage(作为备用机制) + try { + localStorage.setItem('currentPaperId', result.data.id) + } catch (error) { + console.warn('保存试卷ID到localStorage失败:', error) + } - this.$alert( - '已完成组卷,点击"进入考试"开始答题', - '组卷完成', - { - confirmButtonText: '进入考试', - type: 'success' - } - ).then(() => { - // 使用正确的路由名称进行跳转 - this.$router.push({ - name: 'Examing', - params: { paperId: result.data.id } - }) - }) - } else { - console.error('生成试卷失败:', result) - this.$message.error(`生成试卷失败: ${result.message || '未知错误'}`) - } - } catch (error) { - console.error('生成试卷异常:', error) - this.$message.error(`无法生成试卷: ${error.message || '未知错误'}`) - } finally { - this.isLoading = false - } + this.$alert( + '已完成组卷,点击"进入考试"开始答题', + '组卷完成', + { + confirmButtonText: '进入考试', + type: 'success' + } + ).then(() => { + // 跳转到Examing页面,传递paperId和action=start + this.$router.push({ + name: 'Examing', + params: { + paperId: result.data.id, + action: 'start' + } + }) + }) + } else { + console.error('生成试卷失败:', result) + this.$message.error(`生成试卷失败: ${result.message || '未知错误'}`) + } + } catch (error) { + console.error('生成试卷异常:', error) + this.$message.error(`无法生成试卷: ${error.message || '未知错误'}`) + } finally { + this.isLoading = false + } } } } @@ -369,11 +932,17 @@ export default { .content-wrapper { flex: 1; width: 100%; - height: 100vh; + /* 移除固定的height: 100vh */ + /* height: 100vh; */ display: flex; - align-items: center; + align-items: flex-start; + /* 修改为顶部对齐而非居中 */ + /* align-items: center; */ justify-content: center; box-sizing: border-box; + /* 添加padding-top来避开Header */ + padding-top: 20px; + padding-bottom: 20px; } /* 考试卡片样式 - 响应式设计 */ @@ -384,6 +953,27 @@ export default { display: flex; flex-direction: column; box-sizing: border-box; + /* 确保卡片在小屏幕上不会太宽 */ + max-width: 900px; +} + +/* 未完成试卷提示卡片样式 */ +.warning-card { + animation: pulse 2s infinite; +} + +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0.4); + } + + 70% { + box-shadow: 0 0 0 10px rgba(245, 158, 11, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(245, 158, 11, 0); + } } /* 适配中小屏幕 */ @@ -415,4 +1005,87 @@ export default { .margin-top { margin-top: 20px !important; } - \ No newline at end of file + +.bg-yellow-50 { + background-color: #fffbeb; +} + +.border-l-4 { + border-left-width: 4px; +} + +.border-yellow-500 { + border-color: #f59e0b; +} + +.text-red-600 { + color: #dc2626; +} + +.text-sm { + font-size: 14px; +} + +.space-y-2> :not([hidden])~ :not([hidden]) { + margin-top: 8px; +} + +.space-y-4> :not([hidden])~ :not([hidden]) { + margin-top: 16px; +} + +.flex { + display: flex; +} + +.space-x-2> :not([hidden])~ :not([hidden]) { + margin-left: 8px; +} + +.mt-3 { + margin-top: 12px; +} + +.mt-4 { + margin-top: 16px; +} + +.text-gray-700 { + color: #374151; +} + +.text-gray-500 { + color: #6b7280; +} + +/* 已完成试卷提示卡片样式 */ +.completed-card { + animation: successPulse 2s infinite; +} + +@keyframes successPulse { + 0% { + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0.4); + } + + 70% { + box-shadow: 0 0 0 10px rgba(34, 197, 94, 0); + } + + 100% { + box-shadow: 0 0 0 0 rgba(34, 197, 94, 0); + } +} + +.bg-green-50 { + background-color: #f0fdf4; +} + +.border-green-500 { + border-color: #22c55e; +} + +.text-green-600 { + color: #15803d; +} + diff --git a/src/views/user/ExamingView.vue b/src/views/user/ExamingView.vue index 0fcc15f..abcdcce 100644 --- a/src/views/user/ExamingView.vue +++ b/src/views/user/ExamingView.vue @@ -31,7 +31,7 @@
上一题 下一题 - 交卷预览 + 交卷预览
@@ -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)