From 1db2e19fe80fccb389095f3dadac661bd434e972 Mon Sep 17 00:00:00 2001 From: chenqiang Date: Sun, 14 Sep 2025 11:24:26 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=AF=95=E5=8D=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- background/db/paper.js | 289 +++++ background/main.js | 28 +- background/preload.js | 17 +- background/service/paperService.js | 172 +++ src/components/admin/Sider.vue | 72 +- src/router/index.js | 5 +- src/views/admin/AdminHomeView.vue | 56 - src/views/admin/UserPaperManagementView.vue | 1190 +++++++++++++++++++ 8 files changed, 1746 insertions(+), 83 deletions(-) create mode 100644 background/db/paper.js create mode 100644 background/service/paperService.js create mode 100644 src/views/admin/UserPaperManagementView.vue diff --git a/background/db/paper.js b/background/db/paper.js new file mode 100644 index 0000000..47ddfc9 --- /dev/null +++ b/background/db/paper.js @@ -0,0 +1,289 @@ +const { getDbConnection } = require('./index.js'); +// 添加缺少的getUserDbPath导入 +const { getUserDbPath } = require('./path.js'); + +/** + * 查询所有考生试卷记录 + * @returns {Promise} 试卷记录数组 + */ +exports.getAllExamineePapers = async function getAllExamineePapers() { + try { + const db = await getDbConnection(getUserDbPath()); + const sql = ` + SELECT + ep.*, + e.examinee_name, + e.examinee_id_card, + e.examinee_admission_ticket, + + -- 关联试卷问题 + GROUP_CONCAT(DISTINCT pq.question_name) as question_names, + + -- 计算选择题总数 + (SELECT COUNT(*) FROM question_choices qc + JOIN paper_questions pq1 ON qc.question_id = pq1.id + WHERE pq1.paper_id = ep.id) as total_choices, + + -- 计算填空题总数 + (SELECT COUNT(*) FROM question_fill_blanks qfb + JOIN paper_questions pq1 ON qfb.question_id = pq1.id + WHERE pq1.paper_id = ep.id) as total_fill_blanks, + + -- 计算已回答的选择题数 + (SELECT COUNT(*) FROM question_choices qc + JOIN paper_questions pq1 ON qc.question_id = pq1.id + WHERE pq1.paper_id = ep.id AND qc.examinee_answers != '') as answered_choices, + + -- 计算已回答的填空题数 + (SELECT COUNT(*) FROM question_fill_blanks qfb + JOIN paper_questions pq1 ON qfb.question_id = pq1.id + WHERE pq1.paper_id = ep.id AND qfb.examinee_answers != '') as answered_fill_blanks + + FROM examinee_papers ep + LEFT JOIN examinee e ON ep.examinee_id = e.id + LEFT JOIN paper_questions pq ON ep.id = pq.paper_id + GROUP BY ep.id + ORDER BY ep.paper_start_time DESC + `; + const papers = await db.allAsync(sql); + return papers; + } catch (error) { + console.error('查询所有考生试卷记录失败:', error); + throw error; + } +}; + +/** + * 根据试卷ID查询试卷详情 + * @param {number} paperId - 试卷ID + * @returns {Promise} 试卷详情对象 + */ +exports.getExamineePaperById = async function getExamineePaperById(paperId) { + try { + const db = await getDbConnection(getUserDbPath()); + const sql = ` + SELECT + ep.*, + e.examinee_name, + e.examinee_id_card, + e.examinee_admission_ticket, + + -- 获取试卷问题详情 + pq.*, + + -- 获取选择题详情 + qc.id as choice_id, + qc.question_id, + qc.choice_description, + qc.choice_type, + qc.choice_options, + qc.correct_answers, + qc.examinee_answers, + qc.score as choice_score, + qc.score_real as choice_score_real, + + -- 获取填空题详情 + qfb.id as blank_id, + qfb.question_id as blank_question_id, + qfb.blank_description, + qfb.blank_count, + qfb.correct_answers as blank_correct_answers, + qfb.examinee_answers as blank_examinee_answers, + qfb.score as blank_score, + qfb.score_real as blank_score_real + + FROM examinee_papers ep + LEFT JOIN examinee e ON ep.examinee_id = e.id + LEFT JOIN paper_questions pq ON ep.id = pq.paper_id + LEFT JOIN question_choices qc ON pq.id = qc.question_id + LEFT JOIN question_fill_blanks qfb ON pq.id = qfb.question_id + WHERE ep.id = ? + `; + const papers = await db.allAsync(sql, [paperId]); + + // 格式化结果,将问题组织到paper对象中 + if (papers.length > 0) { + const paper = { + ...papers[0], + questions: [], + // 直接添加question_choices和question_fill_blanks数组,方便前端使用 + question_choices: [], + question_fill_blanks: [] + }; + + // 提取唯一的问题并添加相关的选择题和填空题 + const questionMap = new Map(); + + papers.forEach(row => { + if (row.question_name && !questionMap.has(row.question_id)) { + questionMap.set(row.question_id, { + id: row.question_id, + question_name: row.question_name, + question_description: row.question_description, + question_type: row.question_type, + question_choices: [], + question_fill_blanks: [] + }); + } + + // 添加选择题 + if (row.choice_id) { + const question = questionMap.get(row.question_id); + const choiceData = { + id: row.choice_id, + question_id: row.question_id, + question_content: row.question_description, + option_a: row.choice_options?.split('|')[0] || '', + option_b: row.choice_options?.split('|')[1] || '', + option_c: row.choice_options?.split('|')[2] || '', + option_d: row.choice_options?.split('|')[3] || '', + correct_answer: row.correct_answers, + user_answer: row.examinee_answers, + score: row.choice_score, + user_score: row.choice_score_real + }; + // 添加检查,确保question存在再push到question.question_choices + if (question) { + question.question_choices.push(choiceData); + } + // 同时添加到顶层的question_choices数组 + paper.question_choices.push(choiceData); + } + + // 添加填空题 + if (row.blank_id) { + const question = questionMap.get(row.blank_question_id); + const blankData = { + id: row.blank_id, + question_id: row.blank_question_id, + question_content: row.question_description, + blank_count: row.blank_count, + correct_answers: row.blank_correct_answers ? JSON.parse(row.blank_correct_answers) : [], + user_answers: row.blank_examinee_answers ? JSON.parse(row.blank_examinee_answers) : [], + score: row.blank_score, + user_score: row.blank_score_real + }; + if (question) { + question.question_fill_blanks.push(blankData); + } + // 同时添加到顶层的question_fill_blanks数组 + paper.question_fill_blanks.push(blankData); + } + }); + + paper.questions = Array.from(questionMap.values()); + return paper; + } + + return null; + } catch (error) { + console.error(`根据ID查询试卷详情失败 (ID: ${paperId}):`, error); + throw error; + } +}; + +/** + * 查询考生的试卷记录 + * @param {number} examineeId - 考生ID + * @returns {Promise} 该考生的试卷记录数组 + */ +exports.getExamineePapersByExamineeId = async function getExamineePapersByExamineeId(examineeId) { + try { + const db = await getDbConnection(getUserDbPath()); + const sql = ` + SELECT + ep.*, + e.examinee_name, + e.examinee_id_card, + e.examinee_admission_ticket, + + -- 关联试卷问题 + GROUP_CONCAT(DISTINCT pq.question_name) as question_names, + + -- 计算选择题总数 + (SELECT COUNT(*) FROM question_choices qc + JOIN paper_questions pq1 ON qc.question_id = pq1.id + WHERE pq1.paper_id = ep.id) as total_choices, + + -- 计算填空题总数 + (SELECT COUNT(*) FROM question_fill_blanks qfb + JOIN paper_questions pq1 ON qfb.question_id = pq1.id + WHERE pq1.paper_id = ep.id) as total_fill_blanks, + + -- 计算已回答的选择题数 + (SELECT COUNT(*) FROM question_choices qc + JOIN paper_questions pq1 ON qc.question_id = pq1.id + WHERE pq1.paper_id = ep.id AND qc.examinee_answers != '') as answered_choices, + + -- 计算已回答的填空题数 + (SELECT COUNT(*) FROM question_fill_blanks qfb + JOIN paper_questions pq1 ON qfb.question_id = pq1.id + WHERE pq1.paper_id = ep.id AND qfb.examinee_answers != '') as answered_fill_blanks + + FROM examinee_papers ep + LEFT JOIN examinee e ON ep.examinee_id = e.id + LEFT JOIN paper_questions pq ON ep.id = pq.paper_id + WHERE ep.examinee_id = ? + GROUP BY ep.id + ORDER BY ep.paper_start_time DESC + `; + const papers = await db.allAsync(sql, [examineeId]); + return papers; + } catch (error) { + console.error(`查询考生试卷记录失败 (ExamineeID: ${examineeId}):`, error); + throw error; + } +}; + +/** + * 根据状态查询试卷记录 + * @param {number} status - 试卷状态 + * @returns {Promise} 符合条件的试卷记录数组 + */ +exports.getExamineePapersByStatus = async function getExamineePapersByStatus(status) { + try { + const db = await getDbConnection(getUserDbPath()); + const sql = ` + SELECT + ep.*, + e.examinee_name, + e.examinee_id_card, + e.examinee_admission_ticket, + + -- 关联试卷问题 + GROUP_CONCAT(DISTINCT pq.question_name) as question_names, + + -- 计算选择题总数 + (SELECT COUNT(*) FROM question_choices qc + JOIN paper_questions pq1 ON qc.question_id = pq1.id + WHERE pq1.paper_id = ep.id) as total_choices, + + -- 计算填空题总数 + (SELECT COUNT(*) FROM question_fill_blanks qfb + JOIN paper_questions pq1 ON qfb.question_id = pq1.id + WHERE pq1.paper_id = ep.id) as total_fill_blanks, + + -- 计算已回答的选择题数 + (SELECT COUNT(*) FROM question_choices qc + JOIN paper_questions pq1 ON qc.question_id = pq1.id + WHERE pq1.paper_id = ep.id AND qc.examinee_answers != '') as answered_choices, + + -- 计算已回答的填空题数 + (SELECT COUNT(*) FROM question_fill_blanks qfb + JOIN paper_questions pq1 ON qfb.question_id = pq1.id + WHERE pq1.paper_id = ep.id AND qfb.examinee_answers != '') as answered_fill_blanks + + FROM examinee_papers ep + LEFT JOIN examinee e ON ep.examinee_id = e.id + LEFT JOIN paper_questions pq ON ep.id = pq.paper_id + WHERE ep.paper_status = ? + GROUP BY ep.id + ORDER BY ep.paper_start_time DESC + `; + const papers = await db.allAsync(sql, [status]); + return papers; + } catch (error) { + console.error(`根据状态查询试卷记录失败 (Status: ${status}):`, error); + throw error; + } +} \ No newline at end of file diff --git a/background/main.js b/background/main.js index fe2e08a..d19ae1a 100644 --- a/background/main.js +++ b/background/main.js @@ -1,7 +1,7 @@ "use strict"; -import { app, protocol, BrowserWindow, ipcMain, dialog } from "electron"; -import { createProtocol } from "vue-cli-plugin-electron-builder/lib"; +const { app, protocol, BrowserWindow, ipcMain, dialog, shell } = require("electron"); +const { createProtocol } = require("vue-cli-plugin-electron-builder/lib"); // 替换argon2为bcryptjs const bcrypt = require("bcryptjs"); const isDevelopment = process.env.NODE_ENV !== "production"; @@ -31,6 +31,8 @@ const { initExamineeIpc } = require("./service/examineeService.js"); const { initExamingIpc } = require("./service/examingService.js"); // 导入文件服务 const { initFileIpc } = require("./service/fileService.js"); +// 试卷 +const { initPaperIpc } = require("./service/paperService.js"); // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ @@ -210,6 +212,8 @@ app.on("ready", async () => { initExamingIpc(ipcMain); // 初始化文件相关的IPC处理程序 initFileIpc(ipcMain); + // 初始化试卷相关的IPC处理程序 + initPaperIpc(ipcMain); // 检查数据库是否初始化 try { @@ -366,3 +370,23 @@ ipcMain.handle("get-database-paths", (event) => { return { systemDbPath: '获取失败', userDbPath: '获取失败' }; } }); + +// 在现有的IPC处理程序后面添加 +ipcMain.handle("file-open-file", async (event, filePath) => { + try { + console.log('尝试打开文件:', filePath); + // 使用shell.openPath打开文件,这会使用系统默认应用打开指定文件 + const result = await shell.openPath(filePath); + // 在Windows上,result是打开的文件路径;在macOS和Linux上,成功时返回空字符串 + if (process.platform === 'win32' || result === '') { + console.log('文件打开成功:', filePath); + return { success: true, message: '文件打开成功' }; + } else { + console.error('文件打开失败:', result); + return { success: false, message: result }; + } + } catch (error) { + console.error('打开文件时发生错误:', error); + return { success: false, message: error.message }; + } +}); diff --git a/background/preload.js b/background/preload.js index 2c909bf..7b12801 100644 --- a/background/preload.js +++ b/background/preload.js @@ -160,16 +160,23 @@ contextBridge.exposeInMainWorld("electronAPI", { ipcRenderer.invoke("file-generate-pdf", pdfData, fileName), fileCopyToDesktop: (filePath) => ipcRenderer.invoke("file-copy-to-desktop", filePath), + // 试卷管理相关接口 + paperGetAll: () => ipcRenderer.invoke('paper-get-all'), + paperGetById: (paperId) => ipcRenderer.invoke('paper-get-by-id', paperId), + paperGetByExamineeId: (examineeId) => ipcRenderer.invoke('paper-get-by-examinee-id', examineeId), + paperGetByStatus: (status) => ipcRenderer.invoke('paper-get-by-status', status), + // 新增:获取可执行文件路径的API + getExePath: () => ipcRenderer.invoke("get-exe-path"), + // 添加文件打开API + fileOpenFile: (filePath) => ipcRenderer.invoke("file-open-file", filePath), + + // 新增:获取数据库路径的API + getDatabasePaths: () => ipcRenderer.invoke("get-database-paths"), // 保留原有的ipcRenderer接口,确保兼容性 ipcRenderer: { invoke: (channel, data) => ipcRenderer.invoke(channel, data), }, - // 新增:获取可执行文件路径的API - getExePath: () => ipcRenderer.invoke("get-exe-path"), - - // 新增:获取数据库路径的API - getDatabasePaths: () => ipcRenderer.invoke("get-database-paths"), }); // 也保留原来的electron对象,确保现有功能正常 diff --git a/background/service/paperService.js b/background/service/paperService.js new file mode 100644 index 0000000..ba2e36d --- /dev/null +++ b/background/service/paperService.js @@ -0,0 +1,172 @@ +const { + getAllExamineePapers, + getExamineePaperById, + getExamineePapersByExamineeId, + getExamineePapersByStatus +} = require('../db/paper.js'); + +/** + * 服务层:查询所有考生试卷记录 + * @returns {Promise} 包含状态、消息和数据的对象 + */ +exports.getAllExamineePapersService = async function getAllExamineePapersService() { + try { + const papers = await getAllExamineePapers(); + return { + success: true, + message: '查询成功', + data: papers + }; + } catch (error) { + console.error('服务层: 查询所有考生试卷记录失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: [] + }; + } +}; + +/** + * 服务层:根据试卷ID查询试卷详情 + * @param {number} paperId - 试卷ID + * @returns {Promise} 包含状态、消息和数据的对象 + */ +exports.getExamineePaperByIdService = async function getExamineePaperByIdService(paperId) { + try { + if (!paperId || paperId <= 0) { + throw new Error('试卷ID必须为正整数'); + } + const paper = await getExamineePaperById(paperId); + if (!paper) { + return { + success: false, + message: '未找到该试卷记录', + data: null + }; + } + return { + success: true, + message: '查询成功', + data: paper + }; + } catch (error) { + console.error('服务层: 根据ID查询试卷详情失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: null + }; + } +}; + +/** + * 服务层:查询考生的试卷记录 + * @param {number} examineeId - 考生ID + * @returns {Promise} 包含状态、消息和数据的对象 + */ +exports.getExamineePapersByExamineeIdService = async function getExamineePapersByExamineeIdService(examineeId) { + try { + if (!examineeId || examineeId <= 0) { + throw new Error('考生ID必须为正整数'); + } + const papers = await getExamineePapersByExamineeId(examineeId); + return { + success: true, + message: '查询成功', + data: papers + }; + } catch (error) { + console.error('服务层: 查询考生试卷记录失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: [] + }; + } +}; + +/** + * 服务层:根据状态查询试卷记录 + * @param {number} status - 试卷状态 + * @returns {Promise} 包含状态、消息和数据的对象 + */ +exports.getExamineePapersByStatusService = async function getExamineePapersByStatusService(status) { + try { + const papers = await getExamineePapersByStatus(status); + return { + success: true, + message: '查询成功', + data: papers + }; + } catch (error) { + console.error('服务层: 根据状态查询试卷记录失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: [] + }; + } +}; + +/** + * 注册试卷管理相关的IPC处理程序 + * @param {Electron.IpcMain} ipcMain - Electron的IpcMain实例 + */ +exports.initPaperIpc = function initPaperIpc(ipcMain) { + // 查询所有考生试卷记录 + ipcMain.handle('paper-get-all', async (event) => { + try { + return await exports.getAllExamineePapersService(); + } catch (error) { + console.error('IPC处理程序: 查询所有考生试卷记录失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: [] + }; + } + }); + + // 根据试卷ID查询试卷详情 + ipcMain.handle('paper-get-by-id', async (event, paperId) => { + try { + return await exports.getExamineePaperByIdService(paperId); + } catch (error) { + console.error('IPC处理程序: 根据ID查询试卷详情失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: null + }; + } + }); + + // 查询考生的试卷记录 + ipcMain.handle('paper-get-by-examinee-id', async (event, examineeId) => { + try { + return await exports.getExamineePapersByExamineeIdService(examineeId); + } catch (error) { + console.error('IPC处理程序: 查询考生试卷记录失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: [] + }; + } + }); + + // 根据状态查询试卷记录 + ipcMain.handle('paper-get-by-status', async (event, status) => { + try { + return await exports.getExamineePapersByStatusService(status); + } catch (error) { + console.error('IPC处理程序: 根据状态查询试卷记录失败', error); + return { + success: false, + message: `查询失败: ${error.message}`, + data: [] + }; + } + }); +} \ No newline at end of file diff --git a/src/components/admin/Sider.vue b/src/components/admin/Sider.vue index ad3645d..1484f83 100644 --- a/src/components/admin/Sider.vue +++ b/src/components/admin/Sider.vue @@ -15,29 +15,42 @@ active-text-color="#409EFF" :collapse="isCollapse" @select="handleMenuSelect" + unique-opened > - - - 后台首页 - - - - 试题管理 - - - - 考生管理 - - - - 考试管理 - - + + + + + 后台首页 + + + + 试题管理 + + + + 考生管理 + + + + 考试管理 + + + + + + + + 用户试卷 + + + + + 退出登录 - @@ -47,7 +60,8 @@ export default { components: { ElAside: require('element-ui').Aside, ElMenu: require('element-ui').Menu, - ElMenuItem: require('element-ui').MenuItem + ElMenuItem: require('element-ui').MenuItem, + ElMenuItemGroup: require('element-ui').MenuItemGroup }, data() { return { @@ -132,11 +146,26 @@ export default { width: 100%; } +/* 菜单分组标题样式 */ +.el-menu-item-group__title { + color: #8392a5 !important; + font-size: 12px; + padding: 12px 20px 8px; + line-height: 1; + text-transform: uppercase; +} + /* 确保菜单中的图标和文字正确显示 */ .el-menu-item i { margin-right: 10px; } +/* 退出登录菜单项样式 */ +.logout-item { + margin-top: 20px; + border-top: 1px solid #2d3a4b; +} + /* 滚动条样式 */ .sider-container::-webkit-scrollbar { width: 6px; @@ -154,4 +183,9 @@ export default { .sider-container::-webkit-scrollbar-thumb:hover { background: #587ba0; } + +/* 折叠状态下隐藏分组标题 */ +.el-menu--collapse .el-menu-item-group__title { + display: none; +} \ No newline at end of file diff --git a/src/router/index.js b/src/router/index.js index c2e1f7c..a4e4fb9 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -13,6 +13,8 @@ import ExamineeHomeView from '../views/user/ExamineeHomeView.vue' import ExamingView from '../views/user/ExamingView.vue' // 添加EndView组件导入 import EndView from '../views/user/EndView.vue' +// 在文件顶部导入新组件 +import UserPaperManagementView from '../views/admin/UserPaperManagementView.vue' Vue.use(VueRouter) @@ -31,7 +33,8 @@ const routes = [ { path: 'home', name: 'AdminHome', component: AdminHomeView }, { path: 'question', name: 'QuestionManagement', component: QuestionManagementView }, { path: 'examinee', name: 'ExamineeManagement', component: ExamineeManagementView }, - { path: 'exam', name: 'ExamManagement', component: ExamManagementView } + { path: 'exam', name: 'ExamManagement', component: ExamManagementView }, + { path: 'userPaper', name: 'UserPaperManagement', component: UserPaperManagementView } ] }, // 添加考生相关路由 diff --git a/src/views/admin/AdminHomeView.vue b/src/views/admin/AdminHomeView.vue index 884e294..95ec643 100644 --- a/src/views/admin/AdminHomeView.vue +++ b/src/views/admin/AdminHomeView.vue @@ -45,62 +45,6 @@ - - -
-
-
快速操作
-
-
-
-
- - 考生管理 - -
-
- - 试题管理 - -
-
- - 考试管理 - -
-
- - 系统设置 - -
-
- - 数据统计 - -
-
- - 退出登录 - -
-
-
-
- - -
-
-
最近活动
-
-
- - - - - - -
-
diff --git a/src/views/admin/UserPaperManagementView.vue b/src/views/admin/UserPaperManagementView.vue new file mode 100644 index 0000000..830c1b7 --- /dev/null +++ b/src/views/admin/UserPaperManagementView.vue @@ -0,0 +1,1190 @@ + + + + + \ No newline at end of file