继续考试功能已实现

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,9 +555,12 @@ function initExamingIpc(ipcMain) {
});
// 提交考试
ipcMain.handle("examing-submit-paper", async (event, { paperId }) => {
// 提交考试
ipcMain.handle(
"examing-submit-paper",
async (event, { paperId, duration_seconds }) => {
try {
return await submitPaperService(paperId);
return await submitPaperService(paperId, duration_seconds);
} catch (error) {
console.error("提交考试失败:", error);
return {
@ -500,7 +568,8 @@ function initExamingIpc(ipcMain) {
message: `提交考试失败: ${error.message}`,
};
}
});
}
);
// 结束考试
ipcMain.handle("examing-end-paper", async (event, { paperId }) => {
@ -516,9 +585,11 @@ function initExamingIpc(ipcMain) {
});
// 处理试卷
ipcMain.handle("examing-process-paper", async (event, { paperId }) => {
ipcMain.handle(
"examing-process-paper",
async (event, { paperId, duration_seconds }) => {
try {
return await processPaperService(paperId);
return await processPaperService(paperId, duration_seconds);
} catch (error) {
console.error("处理试卷失败:", error);
return {
@ -526,7 +597,8 @@ function initExamingIpc(ipcMain) {
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];
// 计算用时(分钟)
// 计算用时(分钟)- 直接使用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, '/'));
const durationMinutes = Math.round((end - start) / (1000 * 60));
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 () {
// storelocalStorage
if (!this.$store.state.paper || Object.keys(this.$store.state.paper).length === 0) {
const savedPaper = localStorage.getItem('lastExamPaper');
if (savedPaper) {
try {
// localStoragestore
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);
// storethis.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 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);
const hours = Math.floor(totalSeconds / 3600)
const minutes = Math.floor((totalSeconds % 3600) / 60)
const seconds = totalSeconds % 60
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}`
}
}
// paper_duration_seconds使
if (!this.paper || !this.paper.paper_start_time || !this.paper.paper_end_time) {
return '未知'
}
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}`
} else if (minutes > 0) {
return `${minutes}${seconds}`
} else {
return `${seconds}`
}
},

View File

@ -4,7 +4,7 @@
<div class="content-wrapper">
<!-- 考试须知卡片 -->
<div class="exam-card rounded-lg shadow-lg p-4 border">
<div class="flex justify-between text-center">
<div class="justify-between text-center">
<h3 class="font-bold text-primary">考生信息</h3>
</div>
<!-- 考生信息部分 -->
@ -12,7 +12,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-user mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'user']" :style="iconStyle"/>
姓名
</div>
</template>
@ -21,7 +21,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-id-card mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'id-card']" :style="iconStyle" />
身份证号
</div>
</template>
@ -30,7 +30,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-ticket mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'ticket-alt']" :style="iconStyle" />
准考证号
</div>
</template>
@ -38,7 +38,7 @@
</el-descriptions-item>
</el-descriptions>
<el-divider />
<div class="flex justify-between text-center mt-2">
<div class="justify-between text-center mt-2">
<h3 class="font-bold text-primary">考试信息</h3>
</div>
<div class="space-y-4">
@ -46,7 +46,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-clock-o mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'clock']" :style="iconStyle"/>
考试时长
</div>
</template>
@ -55,7 +55,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-hourglass-half mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'hourglass-half']" :style="iconStyle"/>
最短考试时长
</div>
</template>
@ -64,7 +64,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-list-ol mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'list-ol']" :style="iconStyle"/>
考题数量
</div>
</template>
@ -73,7 +73,7 @@
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<i class="fa fa-list-ol mr-2" :style="iconStyle"></i>
<font-awesome-icon :icon="['fas', 'scroll']" :style="iconStyle"/>
考试总分
</div>
</template>
@ -81,8 +81,121 @@
</el-descriptions-item>
</el-descriptions>
</div>
<!-- 未完成试卷提示区域 -->
<div v-if="unfinishedPaper" class="warning-card mt-4 p-4 bg-yellow-50 border-l-4 border-yellow-500 rounded">
<div class="text-red-600 font-bold mb-2">你有未完成的考试</div>
<el-descriptions class="margin-top" :column="2" :size="size" border>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'calendar']" :style="iconStyle"/>
开始时间
</div>
</template>
{{ unfinishedPaper.paper_start_time }}
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'clock']" :style="iconStyle"/>
已用时长
</div>
</template>
{{ usedMinutes }}分钟
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'hourglass-half']" :style="iconStyle"/>
剩余时长
</div>
</template>
{{ remainingMinutes }}分钟
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'check-square']" :style="iconStyle"/>
已答题量
</div>
</template>
{{ answeredQuestionsCount }} (选择题{{ answeredChoicesCount }}填空题{{ answeredFillBlanksCount }})
</el-descriptions-item>
</el-descriptions>
<div class="mt-3 text-sm text-gray-700">
你可以继续进行未完成的考试或者重新开始重新开始后将清除所有历史考试记录
</div>
<div class="mt-4 flex space-x-2">
<el-button type="primary" @click="continueExam">继续考试</el-button>
<el-button type="warning" @click="restartExam">重新开始</el-button>
</div>
</div>
<!-- 已完成试卷提示区域 -->
<div v-else-if="completedPaper" class="completed-card mt-4 p-4 bg-green-50 border-l-4 border-green-500 rounded">
<div class="text-green-600 font-bold mb-2">你有已完成的考试</div>
<el-descriptions class="margin-top" :column="2" :size="size" border>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'calendar']" :style="iconStyle"/>
开始时间
</div>
</template>
{{ completedPaper.paper_start_time }}
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'calendar-check']" :style="iconStyle"/>
结束时间
</div>
</template>
{{ completedPaper.paper_end_time }}
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'clock']" :style="iconStyle"/>
考试用时
</div>
</template>
{{ completedExamDuration }}分钟
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'check-square']" :style="iconStyle"/>
已答题量
</div>
</template>
{{ completedAnsweredQuestionsCount }}
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'list-ol']" :style="iconStyle"/>
总题量
</div>
</template>
{{ completedTotalQuestions }}
</el-descriptions-item>
<el-descriptions-item>
<template slot="label">
<div class="cell-item">
<font-awesome-icon :icon="['fas', 'scroll']" :style="iconStyle"/>
试卷满分
</div>
</template>
{{ completedPaper.paper_score }}
</el-descriptions-item>
</el-descriptions>
<div class="mt-4 space-x-2 text-center">
<el-button type="success" @click="restartExam">重新考试</el-button>
</div>
</div>
<el-divider />
<div class="flex justify-between text-center mt-2">
<div class="justify-between text-center mt-2">
<h3 class="font-bold text-primary">考试须知</h3>
</div>
<div class="space-y-4">
@ -102,7 +215,8 @@
</div>
<div class="text-center mt-6">
<el-button type="info" @click="exitExam">返回</el-button>
<el-button type="primary" @click="startExam" :loading="isLoading">开始考试</el-button>
<el-button v-if="!unfinishedPaper && !completedPaper" type="primary" @click="startExam"
:loading="isLoading">开始考试</el-button>
</div>
</div>
</div>
@ -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
//
//
// questionsExamingPreview.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) {
// questionexaminee_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) {
// questionexaminee_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_status2
const statusNotTwo = paper.paper_status !== 2
// 2. duration_seconds0paper_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_status2
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 {
// ExamingpaperIdaction=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,7 +819,7 @@ export default {
//
// startExamstore
async startExam() {
async startExam () {
if (!this.lastExam) {
this.$message.warning('请先获取考试信息')
return
@ -326,10 +886,13 @@ export default {
type: 'success'
}
).then(() => {
// 使
// ExamingpaperIdaction=start
this.$router.push({
name: 'Examing',
params: { paperId: result.data.id }
params: {
paperId: result.data.id,
action: 'start'
}
})
})
} else {
@ -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;
}
.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;
}
</style>

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
// paperIdaction
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
}
// paperIdlocalStorage
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 {
this.initializeTimer(120 * 60) // 120
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 {
//
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
}
// 15processPaper
// processPaper
this.processTimer = setInterval(async () => {
try {
// { paperId: paperId }
const result = await window.electronAPI.examingProcessPaper({ paperId: paperId })
// paperstore
//
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. 0startPaper
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,7 +700,7 @@ export default {
// - store
async submitExam() {
try {
if (!this.canSubmit) {
if (!this.canSubmitComputed) {
this.$message.warning('您的答案尚未保存,请等待保存完成后再提交');
return;
}
@ -639,8 +715,29 @@ export default {
//
await this.saveCurrentAnswer();
// API
const result = await window.electronAPI.examingSubmitPaper({ paperId: this.paper.id });
// 使start_timecurrentTimeduration_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,9 +746,9 @@ 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);
@ -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 });
// 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) {
// paperstore
if (result.data) {
this.$store.commit('setPaper', result.data)
}
// if (result.data) {
// this.$store.commit('setPaper', result.data)
// }
//
if (this.processTimer) {
clearInterval(this.processTimer)