完成了考试全流程,包括答题、交卷、抢卷、判分、生成pdf

This commit is contained in:
chenqiang 2025-08-11 22:45:16 +08:00
parent 1da167638a
commit e2b5844fec
29 changed files with 4392 additions and 203 deletions

2
.gitignore vendored
View File

@ -29,3 +29,5 @@ dist-electron
*.sw? *.sw?
*.tsbuildinfo *.tsbuildinfo
*.pdf

File diff suppressed because it is too large Load Diff

View File

@ -203,7 +203,7 @@ export const userSchema = {
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
examinee_id INTEGER NOT NULL, examinee_id INTEGER NOT NULL,
paper_minutes INTEGER NOT NULL DEFAULT 0, paper_minutes INTEGER NOT NULL DEFAULT 0,
paper_minuts_min INTEGER NOT NULL DEFAULT 0, paper_minutes_min INTEGER NOT NULL DEFAULT 0,
paper_start_time TEXT, paper_start_time TEXT,
paper_last_time TEXT, paper_last_time TEXT,
paper_submit_time TEXT, paper_submit_time TEXT,

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
electron/font/simsun.ttc Normal file

Binary file not shown.

BIN
electron/font/simsun.ttf Normal file

Binary file not shown.

View File

@ -31,6 +31,10 @@ import {
initExamingIpc initExamingIpc
} from "./service/examingService.js"; } from "./service/examingService.js";
import {
initFileIpc
} from "./service/fileService.js";
// 定义 __dirname 和 __filename // 定义 __dirname 和 __filename
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -163,6 +167,8 @@ function setupIpcMain() {
initExamineeIpc(ipcMain); initExamineeIpc(ipcMain);
initExamingIpc(ipcMain); initExamingIpc(ipcMain);
initFileIpc(ipcMain);
} }
// 确保在 app.whenReady() 中调用 setupIpcMain() // 确保在 app.whenReady() 中调用 setupIpcMain()

View File

@ -1,6 +1,7 @@
const { contextBridge, ipcRenderer } = require('electron'); const { contextBridge, ipcRenderer } = require('electron');
// 暴露API给渲染进程 // 暴露API给渲染进程
// 在contextBridge.exposeInMainWorld中添加以下方法
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
// 数据库相关 // 数据库相关
checkDatabaseInitialized: () => ipcRenderer.invoke('check-database-initialized'), checkDatabaseInitialized: () => ipcRenderer.invoke('check-database-initialized'),
@ -64,9 +65,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
updateExaminee: (id, examineeData) => ipcRenderer.invoke('examinee-update', { id, examineeData }), updateExaminee: (id, examineeData) => ipcRenderer.invoke('examinee-update', { id, examineeData }),
deleteExaminee: (id) => ipcRenderer.invoke('examinee-delete', id), deleteExaminee: (id) => ipcRenderer.invoke('examinee-delete', id),
userLogin: (idCard, admissionTicket) => ipcRenderer.invoke('user-login', { idCard, admissionTicket }), userLogin: (idCard, admissionTicket) => ipcRenderer.invoke('user-login', { idCard, admissionTicket }),
// 考试进行相关API // 考试进行相关API
generateExamineePaper: (examineeData, examData) => ipcRenderer.invoke('examing-generate-paper', { examineeData, examData }), generateExamineePaper: (examineeData, examData) => ipcRenderer.invoke('examing-generate-paper', { examineeData, examData }),
getExamineePaperStatus: (examineeId) => ipcRenderer.invoke('examing-get-paper-status', examineeId), getExamineePaperStatus: (examineeId) => ipcRenderer.invoke('examing-get-paper-status', examineeId),
updatePaperStatus: (paperId, statusData) => ipcRenderer.invoke('examing-update-paper-status', { paperId, statusData }), updatePaperStatus: (paperId, statusData) => ipcRenderer.invoke('examing-update-paper-status', { paperId, statusData }),
loadPaperSerial: (paperId) => ipcRenderer.invoke('examing-load-paper-serial', paperId),
getQuestionByRelatedId: (tableName, id) => ipcRenderer.invoke('examing-get-question-by-related-id', { tableName, id }),
updateExamineeAnswer: (tableName, id, answers) => ipcRenderer.invoke('examing-update-answer', { tableName, id, answers }),
startPaper: (paperId) => ipcRenderer.invoke('examing-start-paper', paperId),
submitPaper: (paperId) => ipcRenderer.invoke('examing-submit-paper', paperId),
endPaper: (paperId) => ipcRenderer.invoke('examing-end-paper', paperId),
processPaper: (paperId) => ipcRenderer.invoke('examing-process-paper', paperId),
checkPaperAnswers: (paperId) => ipcRenderer.invoke('examing-check-paper-answers', paperId), // 添加暴露接口
// 文件相关API
generatePdf: (pdfData, fileName) => ipcRenderer.invoke('file-generate-pdf', pdfData, fileName),
fileTest: () => ipcRenderer.invoke('file-test'),
generatePaperPdf: (jsonString) => ipcRenderer.invoke('file-generate-paper-pdf', jsonString),
}); });

View File

@ -1,7 +1,17 @@
// 导入数据库操作函数 // 导入数据库操作函数
import { generateExamineePaper } from '../db/examing.js'; import {
import { getDbConnection, closeAllConnections } from '../db/index.js'; generateExamineePaper,
import { getUserDbPath } from '../db/path.js'; loadPaperSerial,
getQuestionByRelatedId,
updateExamineeAnswer,
startPaper,
submitPaper,
endPaper,
processPaper,
checkPaperAnswers, // 添加导入
} from "../db/examing.js";
import { getDbConnection, closeAllConnections } from "../db/index.js";
import { getUserDbPath } from "../db/path.js";
/** /**
* 服务层生成考生试卷 * 服务层生成考生试卷
@ -14,24 +24,27 @@ export async function generateExamineePaperService(examineeData, examData) {
try { try {
// 数据验证 // 数据验证
if (!examineeData || !examineeData.id || !examineeData.examinee_name) { if (!examineeData || !examineeData.id || !examineeData.examinee_name) {
throw new Error('考生数据不完整必须包含ID和姓名'); throw new Error("考生数据不完整必须包含ID和姓名");
} }
if (!examData || !examData.exam_minutes || examData.exam_minutes <= 0) { if (!examData || !examData.exam_minutes || examData.exam_minutes <= 0) {
throw new Error('考试时长必须为正数'); throw new Error("考试时长必须为正数");
} }
if (examData.exam_minutes_min === undefined || examData.exam_minutes_min < 0) { if (
throw new Error('最短考试时长必须为非负数'); examData.exam_minutes_min === undefined ||
examData.exam_minutes_min < 0
) {
throw new Error("最短考试时长必须为非负数");
} }
const result = await generateExamineePaper(examineeData, examData); const result = await generateExamineePaper(examineeData, examData);
return result; return result;
} catch (error) { } catch (error) {
console.error('服务层: 生成考生试卷失败', error); console.error("服务层: 生成考生试卷失败", error);
return { return {
success: false, success: false,
message: `生成试卷失败: ${error.message}` message: `生成试卷失败: ${error.message}`,
}; };
} }
} }
@ -44,18 +57,18 @@ export async function generateExamineePaperService(examineeData, examData) {
export async function getExamineePaperStatusService(examineeId) { export async function getExamineePaperStatusService(examineeId) {
try { try {
if (!examineeId || examineeId <= 0) { if (!examineeId || examineeId <= 0) {
throw new Error('考生ID必须为正数'); throw new Error("考生ID必须为正数");
} }
const userDb = await getDbConnection(getUserDbPath()); const userDb = await getDbConnection(getUserDbPath());
const paperStatus = await userDb.getAsync( const paperStatus = await userDb.getAsync(
'SELECT * FROM examinee_papers WHERE examinee_id = ?', "SELECT * FROM examinee_papers WHERE examinee_id = ?",
[examineeId] [examineeId]
); );
return paperStatus; return paperStatus;
} catch (error) { } catch (error) {
console.error('服务层: 获取考生试卷状态失败', error); console.error("服务层: 获取考生试卷状态失败", error);
throw error; throw error;
} }
} }
@ -69,7 +82,7 @@ export async function getExamineePaperStatusService(examineeId) {
export async function updatePaperStatusService(paperId, statusData) { export async function updatePaperStatusService(paperId, statusData) {
try { try {
if (!paperId || paperId <= 0) { if (!paperId || paperId <= 0) {
throw new Error('试卷ID必须为正数'); throw new Error("试卷ID必须为正数");
} }
const userDb = await getDbConnection(getUserDbPath()); const userDb = await getDbConnection(getUserDbPath());
@ -79,32 +92,32 @@ export async function updatePaperStatusService(paperId, statusData) {
const values = []; const values = [];
if (statusData.paper_start_time !== undefined) { if (statusData.paper_start_time !== undefined) {
fields.push('paper_start_time = ?'); fields.push("paper_start_time = ?");
values.push(statusData.paper_start_time); values.push(statusData.paper_start_time);
} }
if (statusData.paper_last_time !== undefined) { if (statusData.paper_last_time !== undefined) {
fields.push('paper_last_time = ?'); fields.push("paper_last_time = ?");
values.push(statusData.paper_last_time); values.push(statusData.paper_last_time);
} }
if (statusData.paper_submit_time !== undefined) { if (statusData.paper_submit_time !== undefined) {
fields.push('paper_submit_time = ?'); fields.push("paper_submit_time = ?");
values.push(statusData.paper_submit_time); values.push(statusData.paper_submit_time);
} }
if (statusData.paper_end_time !== undefined) { if (statusData.paper_end_time !== undefined) {
fields.push('paper_end_time = ?'); fields.push("paper_end_time = ?");
values.push(statusData.paper_end_time); values.push(statusData.paper_end_time);
} }
if (statusData.paper_status !== undefined) { if (statusData.paper_status !== undefined) {
fields.push('paper_status = ?'); fields.push("paper_status = ?");
values.push(statusData.paper_status); values.push(statusData.paper_status);
} }
if (statusData.paper_score_real !== undefined) { if (statusData.paper_score_real !== undefined) {
fields.push('paper_score_real = ?'); fields.push("paper_score_real = ?");
values.push(statusData.paper_score_real); values.push(statusData.paper_score_real);
} }
@ -115,51 +128,365 @@ export async function updatePaperStatusService(paperId, statusData) {
// 添加WHERE条件的值 // 添加WHERE条件的值
values.push(paperId); values.push(paperId);
const sql = `UPDATE examinee_papers SET ${fields.join(', ')} WHERE id = ?`; const sql = `UPDATE examinee_papers SET ${fields.join(", ")} WHERE id = ?`;
await userDb.runAsync(sql, values); await userDb.runAsync(sql, values);
return true; return true;
} catch (error) { } catch (error) {
console.error('服务层: 更新试卷状态失败', error); console.error("服务层: 更新试卷状态失败", error);
throw error; throw error;
} }
} }
/**
* 服务层加载试卷试题序列
* @param {number} paperId - 试卷ID
* @returns {Promise<Array>} - 包含试题序列的数组
*/
export async function loadPaperSerialService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await loadPaperSerial(paperId);
return {
success: true,
data: result,
};
} catch (error) {
console.error("服务层: 加载试卷试题序列失败", error);
return {
success: false,
message: `加载试卷试题序列失败: ${error.message}`,
};
}
}
/**
* 服务层根据表名和ID获取完整的试题数据
* @param {string} tableName - 表名 (question_choices question_fill_blanks)
* @param {number} id - 记录ID
* @returns {Promise<Object>} - 包含试题数据的对象
*/
export async function getQuestionByRelatedIdService(tableName, id) {
try {
// if (!tableName || !['question_choices', 'question_fill_blanks'].includes(tableName)) {
// throw new Error('无效的表名,只能是 question_choices 或 question_fill_blanks');
// }
// if (!id || id <= 0) {
// throw new Error('记录ID必须为正数');
// }
const result = await getQuestionByRelatedId(tableName, id);
return {
success: true,
data: result,
};
} catch (error) {
console.error("服务层: 获取试题数据失败", error);
return {
success: false,
message: `获取试题数据失败: ${error.message}`,
};
}
}
/**
* 服务层更新考生答案
* @param {string} tableName - 表名 (question_choices question_fill_blanks)
* @param {number} id - 记录ID
* @param {Array|string} answers - 考生答案
* @returns {Promise<Object>} - 包含更新结果的对象
*/
export async function updateExamineeAnswerService(tableName, id, answers) {
try {
if (!["question_choices", "question_fill_blanks"].includes(tableName)) {
throw new Error(
"无效的表名,只能是 question_choices 或 question_fill_blanks"
);
}
if (!id || id <= 0) {
throw new Error("记录ID必须为正数");
}
const result = await updateExamineeAnswer(tableName, id, answers);
return {
success: true,
message: "答案更新成功",
};
} catch (error) {
console.error("服务层: 更新考生答案失败", error);
return {
success: false,
message: `更新答案失败: ${error.message}`,
};
}
}
/**
* 服务层开始考试
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果的对象
*/
export async function startPaperService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await startPaper(paperId);
return result;
} catch (error) {
console.error("服务层: 开始考试失败", error);
return {
success: false,
message: `开始考试失败: ${error.message}`,
};
}
}
/**
* 服务层提交考试
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果的对象
*/
export async function submitPaperService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await submitPaper(paperId);
return result;
} catch (error) {
console.error("服务层: 提交考试失败", error);
return {
success: false,
message: `提交考试失败: ${error.message}`,
};
}
}
/**
* 服务层结束考试
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果的对象
*/
export async function endPaperService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await endPaper(paperId);
return result;
} catch (error) {
console.error("服务层: 结束考试失败", error);
return {
success: false,
message: `结束考试失败: ${error.message}`,
};
}
}
/**
* 服务层处理试卷
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果的对象
*/
export async function processPaperService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await processPaper(paperId);
return result;
} catch (error) {
console.error("服务层: 处理试卷失败", error);
return {
success: false,
message: `处理试卷失败: ${error.message}`,
};
}
}
/**
* 服务层检查试卷答案并计算得分
* @param {number} paperId - 试卷ID
* @returns {Promise<Object>} - 包含操作结果和试卷数据的对象
*/
export async function checkPaperAnswersService(paperId) {
try {
if (!paperId || paperId <= 0) {
throw new Error("试卷ID必须为正数");
}
const result = await checkPaperAnswers(paperId);
return result;
} catch (error) {
console.error("服务层: 检查试卷答案失败", error);
return {
success: false,
message: `检查试卷答案失败: ${error.message}`,
};
}
}
/** /**
* 初始化考试相关的IPC处理程序 * 初始化考试相关的IPC处理程序
* @param {import('electron').IpcMain} ipcMain - IPC主进程实例 * @param {import('electron').IpcMain} ipcMain - IPC主进程实例
*/ */
export function initExamingIpc(ipcMain) { export function initExamingIpc(ipcMain) {
// 生成考生试卷 // 生成考生试卷
ipcMain.handle('examing-generate-paper', async (event, { examineeData, examData }) => { ipcMain.handle(
try { "examing-generate-paper",
return await generateExamineePaperService(examineeData, examData); async (event, { examineeData, examData }) => {
} catch (error) { try {
console.error('生成考生试卷失败:', error); return await generateExamineePaperService(examineeData, examData);
return { } catch (error) {
success: false, console.error("生成考生试卷失败:", error);
message: `生成试卷失败: ${error.message}` return {
}; success: false,
message: `生成试卷失败: ${error.message}`,
};
}
} }
}); );
// 获取考生试卷状态 // 获取考生试卷状态
ipcMain.handle('examing-get-paper-status', async (event, examineeId) => { ipcMain.handle("examing-get-paper-status", async (event, examineeId) => {
try { try {
return await getExamineePaperStatusService(examineeId); return await getExamineePaperStatusService(examineeId);
} catch (error) { } catch (error) {
console.error('获取考生试卷状态失败:', error); console.error("获取考生试卷状态失败:", error);
return null; return null;
} }
}); });
// 更新试卷状态 // 更新试卷状态
ipcMain.handle('examing-update-paper-status', async (event, { paperId, statusData }) => { ipcMain.handle(
"examing-update-paper-status",
async (event, { paperId, statusData }) => {
try {
return await updatePaperStatusService(paperId, statusData);
} catch (error) {
console.error("更新试卷状态失败:", error);
return false;
}
}
);
// 加载试卷试题序列
ipcMain.handle("examing-load-paper-serial", async (event, paperId) => {
try { try {
return await updatePaperStatusService(paperId, statusData); return await loadPaperSerialService(paperId);
} catch (error) { } catch (error) {
console.error('更新试卷状态失败:', error); console.error("加载试卷试题序列失败:", error);
return false; return {
success: false,
message: `加载试卷试题序列失败: ${error.message}`,
};
} }
}); });
}
// 根据表名和ID获取完整的试题数据
ipcMain.handle(
"examing-get-question-by-related-id",
async (event, { tableName, id }) => {
try {
return await getQuestionByRelatedIdService(tableName, id);
} catch (error) {
console.error("获取试题数据失败:", error);
return {
success: false,
message: `获取试题数据失败: ${error.message}`,
};
}
}
);
// 更新考生答案
ipcMain.handle(
"examing-update-answer",
async (event, { tableName, id, answers }) => {
try {
return await updateExamineeAnswerService(tableName, id, answers);
} catch (error) {
console.error("更新考生答案失败:", error);
return {
success: false,
message: `更新答案失败: ${error.message}`,
};
}
}
);
// 更新导入语句
ipcMain.handle("examing-start-paper", async (event, paperId) => {
try {
return await startPaperService(paperId);
} catch (error) {
console.error("开始考试失败:", error);
return {
success: false,
message: `开始考试失败: ${error.message}`,
};
}
});
// 提交考试
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-end-paper", async (event, paperId) => {
try {
return await endPaperService(paperId);
} catch (error) {
console.error("结束考试失败:", error);
return {
success: false,
message: `结束考试失败: ${error.message}`,
};
}
});
// 处理试卷
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-check-paper-answers", async (event, paperId) => {
try {
return await checkPaperAnswersService(paperId);
} catch (error) {
console.error("检查试卷答案失败:", error);
return {
success: false,
message: `检查试卷答案失败: ${error.message}`,
};
}
});
}

File diff suppressed because one or more lines are too long

2
output/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

145
package-lock.json generated
View File

@ -19,6 +19,7 @@
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
"element-plus": "^2.10.5", "element-plus": "^2.10.5",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"pdfkit": "^0.17.1",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"sqlite": "^5.1.1", "sqlite": "^5.1.1",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
@ -2464,6 +2465,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved": "https://registry.npmmirror.com/@swc/helpers/-/helpers-0.5.17.tgz",
"integrity": "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.8.0"
}
},
"node_modules/@szmarczak/http-timer": { "node_modules/@szmarczak/http-timer": {
"version": "4.0.6", "version": "4.0.6",
"resolved": "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", "resolved": "https://registry.npmmirror.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -3420,6 +3430,15 @@
"concat-map": "0.0.1" "concat-map": "0.0.1"
} }
}, },
"node_modules/brotli": {
"version": "1.3.3",
"resolved": "https://registry.npmmirror.com/brotli/-/brotli-1.3.3.tgz",
"integrity": "sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.1.2"
}
},
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.25.1", "version": "4.25.1",
"resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.1.tgz", "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.25.1.tgz",
@ -4152,6 +4171,12 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==",
"license": "MIT"
},
"node_modules/csstype": { "node_modules/csstype": {
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
@ -4344,6 +4369,12 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/dfa": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/dfa/-/dfa-1.2.0.tgz",
"integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==",
"license": "MIT"
},
"node_modules/dir-compare": { "node_modules/dir-compare": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmmirror.com/dir-compare/-/dir-compare-4.2.0.tgz", "resolved": "https://registry.npmmirror.com/dir-compare/-/dir-compare-4.2.0.tgz",
@ -5022,7 +5053,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-json-stable-stringify": { "node_modules/fast-json-stable-stringify": {
@ -5112,6 +5142,32 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/fontkit": {
"version": "2.0.4",
"resolved": "https://registry.npmmirror.com/fontkit/-/fontkit-2.0.4.tgz",
"integrity": "sha512-syetQadaUEDNdxdugga9CpEYVaQIxOwk7GlwZWWZ19//qW4zE5bknOKeMBDYAASwnpaSHKJITRLMF9m1fp3s6g==",
"license": "MIT",
"dependencies": {
"@swc/helpers": "^0.5.12",
"brotli": "^1.3.2",
"clone": "^2.1.2",
"dfa": "^1.2.0",
"fast-deep-equal": "^3.1.3",
"restructure": "^3.0.0",
"tiny-inflate": "^1.0.3",
"unicode-properties": "^1.4.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/fontkit/node_modules/clone": {
"version": "2.1.2",
"resolved": "https://registry.npmmirror.com/clone/-/clone-2.1.2.tgz",
"integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/foreground-child": { "node_modules/foreground-child": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", "resolved": "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz",
@ -5960,6 +6016,12 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/jpeg-exif": {
"version": "1.1.4",
"resolved": "https://registry.npmmirror.com/jpeg-exif/-/jpeg-exif-1.1.4.tgz",
"integrity": "sha512-a+bKEcCjtuW5WTdgeXFzswSrdqi0jk4XlEtZlx5A94wCoBpFjfFTbo/Tra5SpNCl/YFZPvcV1dJc+TAYeg6ROQ==",
"license": "MIT"
},
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
@ -6069,6 +6131,25 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/linebreak": {
"version": "1.1.0",
"resolved": "https://registry.npmmirror.com/linebreak/-/linebreak-1.1.0.tgz",
"integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "0.0.8",
"unicode-trie": "^2.0.0"
}
},
"node_modules/linebreak/node_modules/base64-js": {
"version": "0.0.8",
"resolved": "https://registry.npmmirror.com/base64-js/-/base64-js-0.0.8.tgz",
"integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
@ -6831,6 +6912,12 @@
"dev": true, "dev": true,
"license": "BlueOak-1.0.0" "license": "BlueOak-1.0.0"
}, },
"node_modules/pako": {
"version": "0.2.9",
"resolved": "https://registry.npmmirror.com/pako/-/pako-0.2.9.tgz",
"integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==",
"license": "MIT"
},
"node_modules/parse-ms": { "node_modules/parse-ms": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/parse-ms/-/parse-ms-4.0.0.tgz",
@ -6905,6 +6992,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfkit": {
"version": "0.17.1",
"resolved": "https://registry.npmmirror.com/pdfkit/-/pdfkit-0.17.1.tgz",
"integrity": "sha512-Kkf1I9no14O/uo593DYph5u3QwiMfby7JsBSErN1WqeyTgCBNJE3K4pXBn3TgkdKUIVu+buSl4bYUNC+8Up4xg==",
"license": "MIT",
"dependencies": {
"crypto-js": "^4.2.0",
"fontkit": "^2.0.4",
"jpeg-exif": "^1.1.4",
"linebreak": "^1.1.0",
"png-js": "^1.0.0"
}
},
"node_modules/pe-library": { "node_modules/pe-library": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmmirror.com/pe-library/-/pe-library-0.4.1.tgz", "resolved": "https://registry.npmmirror.com/pe-library/-/pe-library-0.4.1.tgz",
@ -6968,6 +7068,11 @@
"node": ">=10.4.0" "node": ">=10.4.0"
} }
}, },
"node_modules/png-js": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/png-js/-/png-js-1.0.0.tgz",
"integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g=="
},
"node_modules/popper.js": { "node_modules/popper.js": {
"version": "1.16.1", "version": "1.16.1",
"resolved": "https://registry.npmmirror.com/popper.js/-/popper.js-1.16.1.tgz", "resolved": "https://registry.npmmirror.com/popper.js/-/popper.js-1.16.1.tgz",
@ -7264,6 +7369,12 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/restructure": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/restructure/-/restructure-3.0.2.tgz",
"integrity": "sha512-gSfoiOEA0VPE6Tukkrr7I0RBdE0s7H1eFCDBk05l1KIQT1UIKNc5JZy6jdyW6eYH3aR3g5b3PuL77rq0hvwtAw==",
"license": "MIT"
},
"node_modules/retry": { "node_modules/retry": {
"version": "0.12.0", "version": "0.12.0",
"resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz", "resolved": "https://registry.npmmirror.com/retry/-/retry-0.12.0.tgz",
@ -8054,6 +8165,12 @@
"semver": "bin/semver" "semver": "bin/semver"
} }
}, },
"node_modules/tiny-inflate": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/tiny-inflate/-/tiny-inflate-1.0.3.tgz",
"integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==",
"license": "MIT"
},
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.14", "version": "0.2.14",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz", "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -8111,6 +8228,12 @@
"utf8-byte-length": "^1.0.1" "utf8-byte-length": "^1.0.1"
} }
}, },
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/tunnel-agent": { "node_modules/tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
"resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -8158,6 +8281,26 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/unicode-properties": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/unicode-properties/-/unicode-properties-1.4.1.tgz",
"integrity": "sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.0",
"unicode-trie": "^2.0.0"
}
},
"node_modules/unicode-trie": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/unicode-trie/-/unicode-trie-2.0.0.tgz",
"integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==",
"license": "MIT",
"dependencies": {
"pako": "^0.2.5",
"tiny-inflate": "^1.0.0"
}
},
"node_modules/unicorn-magic": { "node_modules/unicorn-magic": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz", "resolved": "https://registry.npmmirror.com/unicorn-magic/-/unicorn-magic-0.3.0.tgz",

View File

@ -27,6 +27,7 @@
"bootstrap": "^5.3.7", "bootstrap": "^5.3.7",
"element-plus": "^2.10.5", "element-plus": "^2.10.5",
"fs": "^0.0.1-security", "fs": "^0.0.1-security",
"pdfkit": "^0.17.1",
"popper.js": "^1.16.1", "popper.js": "^1.16.1",
"sqlite": "^5.1.1", "sqlite": "^5.1.1",
"sqlite3": "^5.1.7", "sqlite3": "^5.1.7",
@ -56,7 +57,9 @@
"target": [ "target": [
{ {
"target": "nsis", "target": "nsis",
"arch": ["ia32"] "arch": [
"ia32"
]
} }
], ],
"icon": "public/favicon.ico" "icon": "public/favicon.ico"

BIN
src/assets/bg.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
src/assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -4,8 +4,9 @@
<div class="row h-100 align-items-center justify-content-center"> <div class="row h-100 align-items-center justify-content-center">
<div class="col-auto"> <div class="col-auto">
<h2 class="display-6 m-0 d-flex align-items-center"> <h2 class="display-6 m-0 d-flex align-items-center">
<font-awesome-icon icon="graduation-cap" class="me-3" /> <!-- <font-awesome-icon icon="graduation-cap" class="me-3" /> -->
{{thisYear}}年抚顺市统计行业职工技能大赛考试系统 <img src="../../assets/logo.png" alt="logo" style="width:50px;height:50px;margin-right:1rem;">
{{thisYear}}<strong>年抚顺市统计行业职工技能大赛考试系统</strong>
</h2> </h2>
</div> </div>
</div> </div>
@ -16,7 +17,7 @@
<style scoped> <style scoped>
.header-container { .header-container {
width: 100%; width: 100%;
background-color: #1e40af; /* 深蓝色背景 */ background-color: #4f74ed; /* 深蓝色背景 */
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
height: 80px; /* 设置固定高度 */ height: 80px; /* 设置固定高度 */
padding: 0 2rem; padding: 0 2rem;

View File

@ -15,6 +15,10 @@ import ExamManagementView from '@/views/admin/ExamManagementView.vue'
// 导入ExamineeManagementView // 导入ExamineeManagementView
import ExamineeManagementView from '@/views/admin/ExamineeManagementView.vue' import ExamineeManagementView from '@/views/admin/ExamineeManagementView.vue'
// 首先在文件顶部导入EndView组件
import EndView from '@/views/user/EndView.vue'
// 然后在routes数组中添加新路由
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
routes: [ routes: [
@ -30,10 +34,16 @@ const router = createRouter({
}, },
// Modify the examing route to use lazy loading // Modify the examing route to use lazy loading
{ {
path: '/examinee/examing', path: '/examinee/examing/:paperId',
name: 'examinee-examing', name: 'examinee-examing',
component: ExamingView, component: ExamingView,
}, },
// 添加EndView路由
{
path: '/examinee/end',
name: 'examinee-end',
component: EndView,
},
// admin/AdminHomeView路由 // admin/AdminHomeView路由
{ {
path: '/admin/home', path: '/admin/home',

View File

@ -3,7 +3,9 @@ import { reactive, provide, inject } from 'vue'
// 创建用户状态 // 创建用户状态
const userState = reactive({ const userState = reactive({
examinee: null, examinee: null,
isLoggedIn: false isLoggedIn: false,
exam: null, // 考试信息属性
paper: null // 新增:试卷信息属性
}) })
// 创建状态管理工具 // 创建状态管理工具
@ -20,6 +22,16 @@ const userStore = {
clearExaminee() { clearExaminee() {
this.state.examinee = null this.state.examinee = null
this.state.isLoggedIn = false this.state.isLoggedIn = false
},
// 添加设置考试数据的方法
setExam(exam) {
this.state.exam = exam
},
// 新增:设置试卷数据的方法
setPaper(paper) {
this.state.paper = paper
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="welcome-container"> <div class="welcome-container" style="background-image: url('/src/assets/bg.jpeg'); background-size: cover; background-position: center;">
<el-container> <el-container>
<Header /> <Header />

269
src/views/user/EndView.vue Normal file
View File

@ -0,0 +1,269 @@
<template>
<div class="end-container">
<el-container>
<Header />
<!-- 主要内容区域 -->
<el-main>
<div style="width: 100%; height: 100%;">
<div class="d-flex align-items-center justify-content-center p-4" style="width:100%">
<!-- 交卷结果卡片 -->
<div class="bg-white rounded-4 shadow-lg p-4 w-100 max-w-2xl border-2 border-primary/20">
<el-result icon="success" title="考试已完成" sub-title="您已成功提交试卷感谢您的参与">
<template #extra>
<el-button type="primary" @click="goHome">返回首页</el-button>
</template>
</el-result>
<el-divider />
<div class="flex justify-between text-center mt-6 mb-4">
<h2 class="text-2xl font-bold text-primary">考生信息</h2>
</div>
<!-- 考生信息部分 -->
<el-descriptions class="margin-top" :column="3" :size="size" border>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon :style="iconStyle">
<User />
</el-icon>
姓名
</div>
</template>
{{ examinee?.examinee_name }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon :style="iconStyle">
<Postcard />
</el-icon>
身份证号
</div>
</template>
{{ formatIdCard(examinee?.examinee_id_card) }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon :style="iconStyle">
<Ticket />
</el-icon>
准考证号
</div>
</template>
{{ examinee?.examinee_admission_ticket }}
</el-descriptions-item>
</el-descriptions>
<el-divider />
<div class="flex justify-between text-center mt-6 mb-4">
<h2 class="text-2xl font-bold text-primary">考试信息</h2>
</div>
<!-- 试卷信息部分 -->
<el-descriptions class="margin-top" :column="2" :size="size" border>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon :style="iconStyle">
<Clock />
</el-icon>
开始时间
</div>
</template>
{{ formatDateTime(paper?.paper_start_time) }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon :style="iconStyle">
<Clock />
</el-icon>
结束时间
</div>
</template>
{{ formatDateTime(paper?.paper_end_time) }}
</el-descriptions-item>
<el-descriptions-item>
<template #label>
<div class="cell-item">
<el-icon :style="iconStyle">
<Timer />
</el-icon>
总使用时间
</div>
</template>
{{ formatDuration(paper?.paper_start_time, paper?.paper_end_time) }}
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</div>
</el-main>
<Footer />
</el-container>
</div>
</template>
<script setup>
//
import Header from '@/components/common/Header.vue'
import Footer from '@/components/common/Footer.vue'
import { useRouter } from 'vue-router'
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElDescriptions, ElDescriptionsItem, ElIcon, ElResult } from 'element-plus'
import { User, Postcard, Ticket, Clock, Timer } from '@element-plus/icons-vue'
//
import { useUserStore } from '@/store/userStore'
//
const userStore = useUserStore()
//
const examinee = computed(() => userStore.state.examinee)
//
const paper = computed(() => userStore.state.paper)
//
const isChecking = ref(false)
//
if (!userStore.state.isLoggedIn) {
//
router.push('/')
}
//
const router = useRouter()
const size = ref('default')
//
const iconStyle = computed(() => {
const marginMap = {
large: '8px',
default: '6px',
small: '4px',
}
return {
marginRight: marginMap[size.value] || marginMap.default,
}
})
// 11-15*
const formatIdCard = (idCard) => {
if (!idCard || idCard.length !== 18) return idCard
return idCard.substring(0, 9) + '*****' + idCard.substring(14)
}
//
const formatDateTime = (dateTime) => {
if (!dateTime) return '未知'
const date = new Date(dateTime)
return date.toLocaleString()
}
//
const formatDuration = (startTime, endTime) => {
if (!startTime || !endTime) return '未知'
const start = new Date(startTime)
const end = new Date(endTime)
const diff = end - start
const minutes = Math.floor(diff / (1000 * 60))
const hours = Math.floor(minutes / 60)
const mins = minutes % 60
return `${hours}小时${mins}分钟`
}
//
const goHome = () => {
router.push({ name: 'examinee-home' })
}
//
onMounted(() => {
console.log('EndView初始化')
//
if (!paper.value) {
ElMessage.warning('未找到试卷信息')
console.log('未找到试卷信息,准备跳转到首页')
//
setTimeout(() => {
goHome()
}, 2000)
} else {
//
checkAnswers()
}
})
//
const checkAnswers = async () => {
try {
isChecking.value = true
console.log('开始判卷试卷ID:', paper.value.id)
//
const result = await window.electronAPI.checkPaperAnswers(paper.value.id)
console.log('判卷接口返回结果:', result)
if (result.success) {
// generatePaperPdf result.data
const pdfPath = await window.electronAPI.generatePaperPdf(result.data)
console.log('生成的试卷PDF文件路径:', pdfPath)
// store
userStore.setPaper(JSON.stringify(result.data))
console.log('试卷数据已更新到store得分:', result.data.paper_score_real)
} else {
console.error('判卷失败:', result.message)
}
} catch (error) {
console.error('判卷过程发生错误:', error)
} finally {
isChecking.value = false
console.log('判卷完成')
}
}
</script>
<style scoped>
/* 自定义样式 */
.bg-primary {
background-color: #1E88E5 !important;
/* 蓝色主题与ExamineeHomeView保持一致 */
}
.text-primary {
color: #1E88E5 !important;
}
/* 确保容器占满高度 */
.end-container,
.el-container {
height: 100vh;
display: flex;
flex-direction: column;
}
/* 让主内容区自动扩展并居中 */
.el-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
/* 适配移动设备 */
@media (max-width: 640px) {
.max-w-2xl {
max-width: 100%;
}
}
.cell-item {
display: flex;
align-items: center;
}
.margin-top {
margin-top: 20px !important;
}
</style>

View File

@ -221,8 +221,10 @@ const fetchLastExam = async () => {
const examData = await window.electronAPI.fetchLastExam() const examData = await window.electronAPI.fetchLastExam()
// console.log(':', examData) // console.log(':', examData)
lastExam.value = examData lastExam.value = examData
// store
userStore.setExam(examData)
console.log('信息:', lastExam.value) console.log('信息:', lastExam.value)
// - exam_noticeJSON.parse //
if (lastExam.value && lastExam.value.exam_notice) { if (lastExam.value && lastExam.value.exam_notice) {
examNotices.value = lastExam.value.exam_notice examNotices.value = lastExam.value.exam_notice
} else { } else {
@ -298,6 +300,8 @@ const startExam = async () => {
if (result.success) { if (result.success) {
console.log('生成试卷成功:', result) console.log('生成试卷成功:', result)
// store
userStore.setPaper(result.data)
ElMessageBox.alert( ElMessageBox.alert(
'已完成组卷,点击"进入考试"开始答题', '已完成组卷,点击"进入考试"开始答题',
'组卷完成', '组卷完成',
@ -306,7 +310,11 @@ const startExam = async () => {
type: 'success' type: 'success'
} }
).then(() => { ).then(() => {
router.push('/examinee/examing') // 使
router.push({
name: 'examinee-examing',
params: { paperId: result.data.id }
})
}) })
} else { } else {
console.error('生成试卷失败:', result) console.error('生成试卷失败:', result)

File diff suppressed because it is too large Load Diff