From f3f302da49111140c6c7962767d36647e582021f Mon Sep 17 00:00:00 2001 From: chenqiang Date: Wed, 3 Sep 2025 14:20:29 +0800 Subject: [PATCH] =?UTF-8?q?ExamingView.vue=E8=80=83=E8=AF=95=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=EF=BC=8C=E5=90=8C=E6=97=B6=E8=AE=BE=E7=BD=AE=E4=BA=86?= =?UTF-8?q?=E8=BD=AF=E4=BB=B6=E6=A0=87=E9=A2=98=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E9=80=80=E5=87=BA=E6=8F=90=E9=86=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/background/db/examing.js | 46 +- src/background/main.js | 69 +- src/background/service/examingService.js | 18 +- src/components/user/ExamineeLayout.vue | 1 + src/preload.js | 3 +- src/router/index.js | 4 + src/store/index.js | 11 +- src/views/user/ExamineeHomeView.vue | 51 +- src/views/user/ExamingView.vue | 1185 ++++++++++++++++++++++ vue.config.js | 9 + 10 files changed, 1348 insertions(+), 49 deletions(-) create mode 100644 src/views/user/ExamingView.vue diff --git a/src/background/db/examing.js b/src/background/db/examing.js index b469314..7da34a6 100644 --- a/src/background/db/examing.js +++ b/src/background/db/examing.js @@ -15,6 +15,48 @@ function formatDateTime(date) { return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; } +/** + * 批量插入数据到指定表 + * @param {sqlite3.Database} db - 数据库连接对象 + * @param {string} tableName - 表名 + * @param {Array} dataArray - 要插入的数据数组 + * @returns {Promise} + */ +async function batchInsert(db, tableName, dataArray) { + if (!dataArray || dataArray.length === 0) { + return; + } + + // 开始事务以提高性能 + await db.runAsync('BEGIN TRANSACTION'); + + try { + // 获取第一个对象的字段名 + const fields = Object.keys(dataArray[0]); + const placeholders = dataArray.map(() => `(${fields.map(() => '?').join(', ')})`).join(', '); + const sql = `INSERT INTO ${tableName} (${fields.join(', ')}) VALUES ${placeholders}`; + + // 扁平化所有值 + const values = []; + dataArray.forEach(item => { + fields.forEach(field => { + values.push(item[field]); + }); + }); + + // 执行批量插入 + await db.runAsync(sql, values); + + // 提交事务 + await db.runAsync('COMMIT'); + } catch (error) { + // 发生错误时回滚事务 + await db.runAsync('ROLLBACK'); + console.error(`批量插入数据到${tableName}失败:`, error); + throw error; + } +} + /** * 生成考生试卷 * @param {Object} examineeData - 考生数据 @@ -398,6 +440,7 @@ async function generateExamineePaper(examineeData, examData) { * @returns {Promise} - 包含试题序列的数组 */ async function loadPaperSerial(paperId) { + console.log('0903调试(loadPaperSerial): ', paperId); try { // 1. 获取数据库连接 const userDb = await getDbConnection(getUserDbPath()); @@ -827,7 +870,7 @@ async function processPaper(paperId) { `SELECT * FROM examinee_papers WHERE id = ?`, [paperId] ); - console.log('更新试卷最后操作时间成功:', paper); + // console.log('更新试卷最后操作时间成功:', paper); return { success: true, message: '试卷处理时间已更新', @@ -1162,6 +1205,7 @@ async function getPaper(paperId) { // 在文件末尾添加所有导出 module.exports = { formatDateTime, + batchInsert, generateExamineePaper, loadPaperSerial, getQuestionByRelatedId, diff --git a/src/background/main.js b/src/background/main.js index 47ab92c..66e99f0 100644 --- a/src/background/main.js +++ b/src/background/main.js @@ -1,6 +1,6 @@ 'use strict' -import { app, protocol, BrowserWindow, ipcMain } from 'electron' +import { app, protocol, BrowserWindow, ipcMain, dialog } from 'electron' import { createProtocol } from 'vue-cli-plugin-electron-builder/lib' import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' // 替换argon2为bcryptjs @@ -29,9 +29,12 @@ protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } } ]) +// 添加全局变量保存窗口引用 +let mainWindow = null + async function createWindow() { // Create the browser window. - const win = new BrowserWindow({ + mainWindow = new BrowserWindow({ width: 800, // 默认宽度(实际会被最大化覆盖) height: 600, // 默认高度(实际会被最大化覆盖) show: false, // 先隐藏窗口,避免闪烁 @@ -44,21 +47,21 @@ async function createWindow() { }) // 设置隐藏菜单栏 - win.setMenu(null); + mainWindow.setMenu(null); // 在窗口显示前设置最大化 - win.maximize(); + mainWindow.maximize(); // 然后显示窗口 - win.show(); + mainWindow.show(); if (process.env.WEBPACK_DEV_SERVER_URL) { // Load the url of the dev server if in development mode - await win.loadURL(process.env.WEBPACK_DEV_SERVER_URL) - if (!process.env.IS_TEST) win.webContents.openDevTools() + await mainWindow.loadURL(process.env.WEBPACK_DEV_SERVER_URL) + if (!process.env.IS_TEST) mainWindow.webContents.openDevTools() } else { createProtocol('app') // Load the index.html when not in development - win.loadURL('app://./index.html') + mainWindow.loadURL('app://./index.html') } } @@ -77,6 +80,39 @@ app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) +// 添加退出确认逻辑 +app.on('before-quit', (event) => { + // 如果是MacOS系统,让系统默认处理 + if (process.platform === 'darwin') { + return + } + + // 如果没有窗口,直接退出 + if (!mainWindow) { + return + } + + // 阻止默认的退出行为 + event.preventDefault() + + // 显示确认对话框 + dialog.showMessageBox(mainWindow, { + type: 'warning', + title: '确认关闭', + message: '系统关闭后,已进行的试题将不会被保存,是否确认要关闭?', + buttons: ['取消', '确认关闭'], + defaultId: 0, + cancelId: 0 + }).then((result) => { + // 如果用户确认关闭,则退出应用 + if (result.response === 1) { + app.exit(0) + } + }).catch((err) => { + console.error('显示退出确认对话框失败:', err) + }) +}) + // This method will be called when Electron has finished // initialization and is ready to create browser windows. // Some APIs can only be used after this event occurs. @@ -85,6 +121,8 @@ const sqlite3 = require('sqlite3').verbose(); // 在app.on('ready')事件中添加 app.on('ready', async () => { + // 禁用Vue DevTools扩展 + /* if (isDevelopment && !process.env.IS_TEST) { // Install Vue Devtools try { @@ -93,6 +131,7 @@ app.on('ready', async () => { console.error('Vue Devtools failed to install:', e.toString()) } } + */ // 初始化配置相关的IPC处理程序 initConfigIpc(ipcMain); @@ -109,8 +148,18 @@ app.on('ready', async () => { // 初始化文件相关的IPC处理程序 initFileIpc(ipcMain); - createWindow(); -}); + // 检查数据库是否初始化 + try { + const isInitialized = await checkDatabaseInitialized(); + if (!isInitialized) { + await initializeDatabase(); + } + } catch (error) { + console.error('数据库初始化失败:', error); + } + + createWindow() +}) // Exit cleanly on request from parent process in development mode. if (isDevelopment) { diff --git a/src/background/service/examingService.js b/src/background/service/examingService.js index 2297bce..95ab075 100644 --- a/src/background/service/examingService.js +++ b/src/background/service/examingService.js @@ -143,6 +143,7 @@ async function updatePaperStatusService(paperId, statusData) { * @returns {Promise} - 包含试题序列的数组 */ async function loadPaperSerialService(paperId) { + console.log("0903调试(loadPaperSerialService): ", paperId); try { if (!paperId || paperId <= 0) { throw new Error("试卷ID必须为正数"); @@ -372,8 +373,9 @@ function initExamingIpc(ipcMain) { ); // 加载试卷试题序列 - ipcMain.handle("examing-load-paper-serial", async (event, paperId) => { + ipcMain.handle("examing-load-paper-serial", async (event, { paperId }) => { try { + console.log("0903调试:", paperId); return await loadPaperSerialService(paperId); } catch (error) { console.error("加载试卷试题序列失败:", error); @@ -387,9 +389,9 @@ function initExamingIpc(ipcMain) { // 根据表名和ID获取完整的试题数据 ipcMain.handle( "examing-get-question-by-related-id", - async (event, { tableName, id }) => { + async (event, { tableName, relatedId }) => { try { - return await getQuestionByRelatedIdService(tableName, id); + return await getQuestionByRelatedIdService(tableName, relatedId); } catch (error) { console.error("获取试题数据失败:", error); return { @@ -417,7 +419,7 @@ function initExamingIpc(ipcMain) { ); // 开始考试 - ipcMain.handle("examing-start-paper", async (event, paperId) => { + ipcMain.handle("examing-start-paper", async (event, { paperId }) => { try { return await startPaperService(paperId); } catch (error) { @@ -430,7 +432,7 @@ function initExamingIpc(ipcMain) { }); // 提交考试 - ipcMain.handle("examing-submit-paper", async (event, paperId) => { + ipcMain.handle("examing-submit-paper", async (event, { paperId }) => { try { return await submitPaperService(paperId); } catch (error) { @@ -443,7 +445,7 @@ function initExamingIpc(ipcMain) { }); // 结束考试 - ipcMain.handle("examing-end-paper", async (event, paperId) => { + ipcMain.handle("examing-end-paper", async (event, { paperId }) => { try { return await endPaperService(paperId); } catch (error) { @@ -456,7 +458,7 @@ function initExamingIpc(ipcMain) { }); // 处理试卷 - ipcMain.handle("examing-process-paper", async (event, paperId) => { + ipcMain.handle("examing-process-paper", async (event, { paperId }) => { try { return await processPaperService(paperId); } catch (error) { @@ -469,7 +471,7 @@ function initExamingIpc(ipcMain) { }); // 检查试卷答案并计算得分 - ipcMain.handle("examing-check-paper-answers", async (event, paperId) => { + ipcMain.handle("examing-check-paper-answers", async (event, { paperId }) => { try { return await checkPaperAnswersService(paperId); } catch (error) { diff --git a/src/components/user/ExamineeLayout.vue b/src/components/user/ExamineeLayout.vue index 79092c0..5a6e72e 100644 --- a/src/components/user/ExamineeLayout.vue +++ b/src/components/user/ExamineeLayout.vue @@ -37,5 +37,6 @@ export default { flex: 1; padding: 20px; overflow-y: auto; + background-color: #f0f2f5; } \ No newline at end of file diff --git a/src/preload.js b/src/preload.js index fc69451..e9f0159 100644 --- a/src/preload.js +++ b/src/preload.js @@ -78,7 +78,8 @@ contextBridge.exposeInMainWorld('electronAPI', { examingUpdatePaperStatus: ({ paperId, status }) => ipcRenderer.invoke('examing-update-paper-status', { paperId, status }), examingLoadPaperSerial: ({ paperId }) => ipcRenderer.invoke('examing-load-paper-serial', { paperId }), examingGetQuestionByRelatedId: ({ tableName, relatedId }) => ipcRenderer.invoke('examing-get-question-by-related-id', { tableName, relatedId }), - examingUpdateAnswer: ({ paperId, questionId, answer }) => ipcRenderer.invoke('examing-update-answer', { paperId, questionId, answer }), + // 修复:examingUpdateAnswer接口的参数结构 + examingUpdateAnswer: ({ tableName, id, answers }) => ipcRenderer.invoke('examing-update-answer', { tableName, id, answers }), examingStartPaper: ({ paperId }) => ipcRenderer.invoke('examing-start-paper', { paperId }), examingSubmitPaper: ({ paperId }) => ipcRenderer.invoke('examing-submit-paper', { paperId }), examingEndPaper: ({ paperId }) => ipcRenderer.invoke('examing-end-paper', { paperId }), diff --git a/src/router/index.js b/src/router/index.js index 321a347..36c81c5 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -9,6 +9,8 @@ import ExamineeManagementView from '../views/admin/ExamineeManagementView.vue' import ExamManagementView from '../views/admin/ExamManagementView.vue' // 导入考生相关组件 import ExamineeHomeView from '../views/user/ExamineeHomeView.vue' +// 添加ExamingView组件导入 +import ExamingView from '../views/user/ExamingView.vue' Vue.use(VueRouter) @@ -38,6 +40,8 @@ const routes = [ meta: { requiresAuth: true }, children: [ { path: 'home', name: 'ExamineeHome', component: ExamineeHomeView }, + // 添加考试页面路由 + { path: 'exam/:paperId', name: 'Examing', component: ExamingView }, // 可以在这里添加更多考生相关的路由 ] } diff --git a/src/store/index.js b/src/store/index.js index ac5f09d..77dfee7 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -8,7 +8,8 @@ export default new Vuex.Store({ examinee: null, // 保存考生信息 admin: null, // 保存管理员信息 isLoggedIn: false, // 通用登录状态 - userType: null // 用户类型: 'examinee' 或 'admin' + userType: null, // 用户类型: 'examinee' 或 'admin' + paper: null // 新增:保存当前考试的试卷信息 }, mutations: { setExaminee(state, examineeInfo) { @@ -28,6 +29,14 @@ export default new Vuex.Store({ state.admin = null state.isLoggedIn = false state.userType = null + }, + // 新增:设置试卷信息 + setPaper(state, paperInfo) { + state.paper = paperInfo + }, + // 新增:清除试卷信息 + clearPaper(state) { + state.paper = null } }, actions: { diff --git a/src/views/user/ExamineeHomeView.vue b/src/views/user/ExamineeHomeView.vue index fbdae80..08c531d 100644 --- a/src/views/user/ExamineeHomeView.vue +++ b/src/views/user/ExamineeHomeView.vue @@ -3,9 +3,9 @@
-
-
-

考生信息

+
+
+

考生信息

@@ -38,8 +38,8 @@ -
-

考试信息

+
+

考试信息

@@ -82,8 +82,8 @@
-
-

考试须知

+
+

考试须知

@@ -262,17 +262,17 @@ export default { return } - if (!this.examinee) { - this.$message.warning('未获取到考生信息') + if (!this.examinee || !this.examinee.id || !this.examinee.examinee_name) { + this.$message.warning('未获取到完整的考生信息') return } this.isLoading = true try { - console.log('开始生成试卷...') + console.log('开始生成试卷...', { examinee: this.examinee, exam: this.lastExam }) - // 创建可序列化的考生数据对象 + // 创建可序列化的完整考生数据对象 const examineeData = { id: this.examinee.id, examinee_name: this.examinee.examinee_name || '', @@ -296,15 +296,18 @@ export default { exam_notice: this.lastExam.exam_notice || [] })) - // 调用生成试卷API - const result = await window.electronAPI.examingGeneratePaper({ - examineeId: examineeData.id, - examId: examData.id + // 直接调用ipcRenderer接口,跳过preload.js中定义的不匹配方法 + const result = await window.electronAPI.ipcRenderer.invoke('examing-generate-paper', { + examineeData: examineeData, + examData: examData }) if (result && result.success) { console.log('生成试卷成功:', result) - // 由于Vuex store中没有setPaper方法,这里可以考虑添加或者直接传递参数 + + // 新增:保存试卷信息到store + this.$store.commit('setPaper', result.data) + this.$alert( '已完成组卷,点击"进入考试"开始答题', '组卷完成', @@ -313,9 +316,9 @@ export default { type: 'success' } ).then(() => { - // 使用命名路由传递参数 + // 使用正确的路由名称进行跳转 this.$router.push({ - name: 'examinee-examing', + name: 'Examing', params: { paperId: result.data.id } }) }) @@ -348,7 +351,6 @@ export default { /* 确保容器占满可用空间 */ .student-home-container { height: 100%; - min-height: 100vh; display: flex; flex-direction: column; } @@ -357,17 +359,17 @@ export default { .content-wrapper { flex: 1; width: 100%; + height: 100vh; display: flex; align-items: center; justify-content: center; - padding: 1rem; box-sizing: border-box; } /* 考试卡片样式 - 响应式设计 */ .exam-card { width: 100%; - min-height: calc(100vh - 4rem); + margin: 0px 0px 20px 0px; background-color: white; display: flex; flex-direction: column; @@ -378,11 +380,6 @@ export default { @media (max-width: 768px) { .exam-card { max-width: 100%; - min-height: calc(100vh - 2rem); - } - - .content-wrapper { - padding: 0.5rem; } } @@ -390,7 +387,6 @@ export default { @media (min-width: 1200px) { .exam-card { max-width: 80vw; - max-height: 90vh; } } @@ -398,7 +394,6 @@ export default { @media (min-width: 1600px) { .exam-card { max-width: 90vw; - max-height: 90vh; } } diff --git a/src/views/user/ExamingView.vue b/src/views/user/ExamingView.vue new file mode 100644 index 0000000..ad02f27 --- /dev/null +++ b/src/views/user/ExamingView.vue @@ -0,0 +1,1185 @@ + + + + + \ No newline at end of file diff --git a/vue.config.js b/vue.config.js index 90df805..5c19788 100644 --- a/vue.config.js +++ b/vue.config.js @@ -1,4 +1,13 @@ module.exports = { + // 添加应用标题配置 + chainWebpack: config => { + config + .plugin('html') + .tap(args => { + args[0].title = '统计数据考试系统' + return args + }) + }, pluginOptions: { electronBuilder: { nodeIntegration: false,