ExamingView.vue考试页面,同时设置了软件标题,增加了退出提醒

This commit is contained in:
chenqiang 2025-09-03 14:20:29 +08:00
parent 3f61b5eb72
commit f3f302da49
10 changed files with 1348 additions and 49 deletions

View File

@ -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<void>}
*/
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<Array>} - 包含试题序列的数组
*/
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,

View File

@ -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) {

View File

@ -143,6 +143,7 @@ async function updatePaperStatusService(paperId, statusData) {
* @returns {Promise<Array>} - 包含试题序列的数组
*/
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) {

View File

@ -37,5 +37,6 @@ export default {
flex: 1;
padding: 20px;
overflow-y: auto;
background-color: #f0f2f5;
}
</style>

View File

@ -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 }),

View File

@ -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 },
// 可以在这里添加更多考生相关的路由
]
}

View File

@ -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: {

View File

@ -3,9 +3,9 @@
<div class="student-home-container">
<div class="content-wrapper">
<!-- 考试须知卡片 -->
<div class="exam-card rounded-lg shadow-lg p-4 border-2">
<div class="flex justify-between text-center mb-2">
<h2 class="text-2xl font-bold text-primary">考生信息</h2>
<div class="exam-card rounded-lg shadow-lg p-4 border">
<div class="flex justify-between text-center">
<h3 class="font-bold text-primary">考生信息</h3>
</div>
<!-- 考生信息部分 -->
<el-descriptions class="margin-top" :column="3" :size="size" border>
@ -38,8 +38,8 @@
</el-descriptions-item>
</el-descriptions>
<el-divider />
<div class="flex justify-between text-center mt-4 mb-2">
<h2 class="text-2xl font-bold text-primary">考试信息</h2>
<div class="flex justify-between text-center mt-2">
<h3 class="font-bold text-primary">考试信息</h3>
</div>
<div class="space-y-4">
<el-descriptions class="margin-top" :column="2" :size="size" border>
@ -82,8 +82,8 @@
</el-descriptions>
</div>
<el-divider />
<div class="flex justify-between text-center mt-4 mb-4">
<h2 class="text-2xl font-bold text-primary">考试须知</h2>
<div class="flex justify-between text-center mt-2">
<h3 class="font-bold text-primary">考试须知</h3>
</div>
<div class="space-y-4">
<div v-if="!examLoading && examNotices.length > 0" class="space-y-2 pl-4">
@ -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
// ipcRendererpreload.js
const result = await window.electronAPI.ipcRenderer.invoke('examing-generate-paper', {
examineeData: examineeData,
examData: examData
})
if (result && result.success) {
console.log('生成试卷成功:', result)
// Vuex storesetPaper
// 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,13 @@
module.exports = {
// 添加应用标题配置
chainWebpack: config => {
config
.plugin('html')
.tap(args => {
args[0].title = '统计数据考试系统'
return args
})
},
pluginOptions: {
electronBuilder: {
nodeIntegration: false,