后端迁移完成,做了welcome页
@ -1,5 +1,15 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: [
|
||||||
'@vue/cli-plugin-babel/preset'
|
'@vue/cli-plugin-babel/preset',
|
||||||
|
['@babel/preset-env', {
|
||||||
|
targets: {
|
||||||
|
node: 'current',
|
||||||
|
browsers: ['> 1%', 'last 2 versions', 'not dead']
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
'@babel/plugin-proposal-class-properties'
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
1159
package-lock.json
generated
@ -24,8 +24,9 @@
|
|||||||
"bootstrap": "4.6.0",
|
"bootstrap": "4.6.0",
|
||||||
"core-js": "3.6.5",
|
"core-js": "3.6.5",
|
||||||
"element-ui": "2.15.14",
|
"element-ui": "2.15.14",
|
||||||
|
"fontkit": "1.8.1",
|
||||||
"jquery": "3.5.1",
|
"jquery": "3.5.1",
|
||||||
"pdfkit": "^0.15.0",
|
"pdfkit": "0.11.0",
|
||||||
"popper.js": "1.16.1",
|
"popper.js": "1.16.1",
|
||||||
"sqlite3": "4.2.0",
|
"sqlite3": "4.2.0",
|
||||||
"vue": "2.6.11",
|
"vue": "2.6.11",
|
||||||
@ -34,6 +35,10 @@
|
|||||||
"xlsx": "^0.18.5"
|
"xlsx": "^0.18.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.28.3",
|
||||||
|
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
||||||
|
"@babel/plugin-proposal-optional-chaining": "^7.21.0",
|
||||||
|
"@babel/preset-env": "^7.28.3",
|
||||||
"@electron/rebuild": "3.2.13",
|
"@electron/rebuild": "3.2.13",
|
||||||
"@vue/cli-plugin-babel": "4.5.19",
|
"@vue/cli-plugin-babel": "4.5.19",
|
||||||
"@vue/cli-plugin-router": "4.5.17",
|
"@vue/cli-plugin-router": "4.5.17",
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 264 KiB |
BIN
public/favicon.png
Normal file
After Width: | Height: | Size: 111 KiB |
17
src/App.vue
@ -1,9 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<div id="nav">
|
|
||||||
<router-link to="/">Home</router-link> |
|
|
||||||
<router-link to="/about">About</router-link>
|
|
||||||
</div>
|
|
||||||
<router-view/>
|
<router-view/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -16,17 +12,4 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: #2c3e50;
|
color: #2c3e50;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nav {
|
|
||||||
padding: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav a {
|
|
||||||
font-weight: bold;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
#nav a.router-link-exact-active {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
31
src/assets/base.css
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
/* color palette from <https://github.com/vuejs/theme> */
|
||||||
|
:root {
|
||||||
|
--primary-color: #4096ff; /* 定义主色调,与Element Plus保持一致 */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
color: var(--color-text);
|
||||||
|
background: var(--color-background);
|
||||||
|
transition:
|
||||||
|
color 0.5s,
|
||||||
|
background-color 0.5s;
|
||||||
|
line-height: 1.6;
|
||||||
|
font-family:
|
||||||
|
Inter,
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
'Segoe UI',
|
||||||
|
Roboto,
|
||||||
|
Oxygen,
|
||||||
|
Ubuntu,
|
||||||
|
Cantarell,
|
||||||
|
'Fira Sans',
|
||||||
|
'Droid Sans',
|
||||||
|
'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
BIN
src/assets/bg.jpeg
Normal file
After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 13 KiB |
1
src/assets/logo.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
After Width: | Height: | Size: 276 B |
1
src/assets/main.css
Normal file
@ -0,0 +1 @@
|
|||||||
|
@import './base.css';
|
136
src/background/db/exam.js
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
const { executeWithRetry } = require('./utils.js');
|
||||||
|
const { openDatabase } = require('./utils.js');
|
||||||
|
const { getSystemDbPath } = require('./path.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有考试
|
||||||
|
* @returns {Promise<Array>} 考试列表
|
||||||
|
*/
|
||||||
|
async function getAllExams() {
|
||||||
|
const db = await openDatabase(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'SELECT * FROM exam ORDER BY created_at DESC';
|
||||||
|
return await db.allAsync(sql);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询考试
|
||||||
|
* @param {number} id 考试ID
|
||||||
|
* @returns {Promise<Object|null>} 考试数据或null
|
||||||
|
*/
|
||||||
|
async function getExamById(id) {
|
||||||
|
const db = await openDatabase(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'SELECT * FROM exam WHERE id = ?';
|
||||||
|
return await db.getAsync(sql, [id]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询ID最大的考试记录
|
||||||
|
* @returns {Promise<Object|null>} 考试数据或null
|
||||||
|
*/
|
||||||
|
async function getLastExam() {
|
||||||
|
const db = await openDatabase(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'SELECT * FROM exam ORDER BY id DESC LIMIT 1';
|
||||||
|
return await db.getAsync(sql);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加考试
|
||||||
|
* @param {Object} examData 考试数据
|
||||||
|
* @returns {Promise<Object>} 添加的考试
|
||||||
|
*/
|
||||||
|
async function createExam(examData) {
|
||||||
|
const { exam_name, exam_description, exam_examinee_type, exam_notice, exam_minutes, exam_minutes_min } = examData;
|
||||||
|
const db = await openDatabase(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'INSERT INTO exam (exam_name, exam_description, exam_examinee_type, exam_notice, exam_minutes, exam_minutes_min) VALUES (?, ?, ?, ?, ?, ?)';
|
||||||
|
const result = await db.runAsync(sql, [
|
||||||
|
exam_name,
|
||||||
|
exam_description || '',
|
||||||
|
exam_examinee_type,
|
||||||
|
exam_notice || '',
|
||||||
|
exam_minutes,
|
||||||
|
exam_minutes_min || 0
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 检查result是否存在,如果不存在则获取最后插入的ID
|
||||||
|
let lastId;
|
||||||
|
if (result && result.lastID) {
|
||||||
|
lastId = result.lastID;
|
||||||
|
} else {
|
||||||
|
// 使用另一种方式获取最后插入的ID
|
||||||
|
const idResult = await db.getAsync('SELECT last_insert_rowid() as id');
|
||||||
|
lastId = idResult ? idResult.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastId) {
|
||||||
|
throw new Error('无法获取插入的考试ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: lastId, ...examData };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新考试
|
||||||
|
* @param {number} id 考试ID
|
||||||
|
* @param {Object} examData 更新的数据
|
||||||
|
* @returns {Promise<boolean>} 是否更新成功
|
||||||
|
*/
|
||||||
|
async function updateExam(id, examData) {
|
||||||
|
const { exam_name, exam_description, exam_examinee_type, exam_notice, exam_minutes, exam_minutes_min } = examData;
|
||||||
|
const db = await openDatabase(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'UPDATE exam SET exam_name = ?, exam_description = ?, exam_examinee_type = ?, exam_notice = ?, exam_minutes = ?, exam_minutes_min = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?';
|
||||||
|
const result = await db.runAsync(sql, [
|
||||||
|
exam_name,
|
||||||
|
exam_description || '',
|
||||||
|
exam_examinee_type,
|
||||||
|
exam_notice || '',
|
||||||
|
exam_minutes,
|
||||||
|
exam_minutes_min || 0,
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 检查result是否存在以及是否有changes属性
|
||||||
|
if (!result || result.changes === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.changes > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除考试
|
||||||
|
* @param {number} id 考试ID
|
||||||
|
* @returns {Promise<boolean>} 是否删除成功
|
||||||
|
*/
|
||||||
|
async function deleteExam(id) {
|
||||||
|
const db = await openDatabase(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'DELETE FROM exam WHERE id = ?';
|
||||||
|
const result = await db.runAsync(sql, [id]);
|
||||||
|
|
||||||
|
// 检查result是否存在以及是否有changes属性
|
||||||
|
if (!result || result.changes === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.changes > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getAllExams,
|
||||||
|
getExamById,
|
||||||
|
getLastExam,
|
||||||
|
createExam,
|
||||||
|
updateExam,
|
||||||
|
deleteExam
|
||||||
|
};
|
190
src/background/db/examinee.js
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
const { executeWithRetry } = require('./utils.js');
|
||||||
|
const { getDbConnection } = require('./index.js');
|
||||||
|
const { getSystemDbPath } = require('./index.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有考生列表
|
||||||
|
* @returns {Promise<Array>} 考生列表
|
||||||
|
*/
|
||||||
|
async function getAllExaminees() {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'SELECT * FROM examinee ORDER BY created_at DESC';
|
||||||
|
return await db.allAsync(sql);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询考生
|
||||||
|
* @param {number} id 考生ID
|
||||||
|
* @returns {Promise<Object|null>} 考生数据或null
|
||||||
|
*/
|
||||||
|
async function getExamineeById(id) {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'SELECT * FROM examinee WHERE id = ?';
|
||||||
|
return await db.getAsync(sql, [id]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据身份证号和准考证号查询考生
|
||||||
|
* @param {string} idCard 身份证号
|
||||||
|
* @param {string} admissionTicket 准考证号
|
||||||
|
* @returns {Promise<Object|null>} 考生数据或null
|
||||||
|
*/
|
||||||
|
async function getExamineeByIdCardAndAdmissionTicket(idCard, admissionTicket) {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'SELECT * FROM examinee WHERE examinee_id_card = ? AND examinee_admission_ticket = ?';
|
||||||
|
return await db.getAsync(sql, [idCard, admissionTicket]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加考生
|
||||||
|
* @param {Object} examineeData 考生数据
|
||||||
|
* @returns {Promise<Object>} 添加的考生
|
||||||
|
*/
|
||||||
|
async function createExaminee(examineeData) {
|
||||||
|
const {
|
||||||
|
examinee_name,
|
||||||
|
examinee_gender,
|
||||||
|
examinee_unit,
|
||||||
|
written_exam_room,
|
||||||
|
written_exam_seat,
|
||||||
|
computer_exam_room,
|
||||||
|
computer_exam_seat,
|
||||||
|
examinee_id_card,
|
||||||
|
examinee_admission_ticket
|
||||||
|
} = examineeData;
|
||||||
|
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = `INSERT INTO examinee (
|
||||||
|
examinee_name,
|
||||||
|
examinee_gender,
|
||||||
|
examinee_unit,
|
||||||
|
written_exam_room,
|
||||||
|
written_exam_seat,
|
||||||
|
computer_exam_room,
|
||||||
|
computer_exam_seat,
|
||||||
|
examinee_id_card,
|
||||||
|
examinee_admission_ticket
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
||||||
|
|
||||||
|
const result = await db.runAsync(sql, [
|
||||||
|
examinee_name,
|
||||||
|
examinee_gender || '',
|
||||||
|
examinee_unit || '',
|
||||||
|
written_exam_room || '',
|
||||||
|
written_exam_seat || '',
|
||||||
|
computer_exam_room || '',
|
||||||
|
computer_exam_seat || '',
|
||||||
|
examinee_id_card || '',
|
||||||
|
examinee_admission_ticket || ''
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 检查result是否存在,如果不存在则获取最后插入的ID
|
||||||
|
let lastId;
|
||||||
|
if (result && result.lastID) {
|
||||||
|
lastId = result.lastID;
|
||||||
|
} else {
|
||||||
|
// 使用另一种方式获取最后插入的ID
|
||||||
|
const idResult = await db.getAsync('SELECT last_insert_rowid() as id');
|
||||||
|
lastId = idResult ? idResult.id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!lastId) {
|
||||||
|
throw new Error('无法获取插入的考生ID');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: lastId, ...examineeData };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新考生
|
||||||
|
* @param {number} id 考生ID
|
||||||
|
* @param {Object} examineeData 更新的数据
|
||||||
|
* @returns {Promise<boolean>} 是否更新成功
|
||||||
|
*/
|
||||||
|
async function updateExaminee(id, examineeData) {
|
||||||
|
const {
|
||||||
|
examinee_name,
|
||||||
|
examinee_gender,
|
||||||
|
examinee_unit,
|
||||||
|
written_exam_room,
|
||||||
|
written_exam_seat,
|
||||||
|
computer_exam_room,
|
||||||
|
computer_exam_seat,
|
||||||
|
examinee_id_card,
|
||||||
|
examinee_admission_ticket
|
||||||
|
} = examineeData;
|
||||||
|
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = `UPDATE examinee SET
|
||||||
|
examinee_name = ?,
|
||||||
|
examinee_gender = ?,
|
||||||
|
examinee_unit = ?,
|
||||||
|
written_exam_room = ?,
|
||||||
|
written_exam_seat = ?,
|
||||||
|
computer_exam_room = ?,
|
||||||
|
computer_exam_seat = ?,
|
||||||
|
examinee_id_card = ?,
|
||||||
|
examinee_admission_ticket = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ?`;
|
||||||
|
|
||||||
|
const result = await db.runAsync(sql, [
|
||||||
|
examinee_name,
|
||||||
|
examinee_gender || '',
|
||||||
|
examinee_unit || '',
|
||||||
|
written_exam_room || '',
|
||||||
|
written_exam_seat || '',
|
||||||
|
computer_exam_room || '',
|
||||||
|
computer_exam_seat || '',
|
||||||
|
examinee_id_card || '',
|
||||||
|
examinee_admission_ticket || '',
|
||||||
|
id
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 检查result是否存在以及是否有changes属性
|
||||||
|
if (!result || result.changes === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.changes > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除考生
|
||||||
|
* @param {number} id 考生ID
|
||||||
|
* @returns {Promise<boolean>} 是否删除成功
|
||||||
|
*/
|
||||||
|
async function deleteExaminee(id) {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
return executeWithRetry(db, async () => {
|
||||||
|
const sql = 'DELETE FROM examinee WHERE id = ?';
|
||||||
|
const result = await db.runAsync(sql, [id]);
|
||||||
|
|
||||||
|
// 检查result是否存在以及是否有changes属性
|
||||||
|
if (!result || result.changes === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.changes > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出使用CommonJS格式
|
||||||
|
module.exports = {
|
||||||
|
getAllExaminees,
|
||||||
|
getExamineeById,
|
||||||
|
getExamineeByIdCardAndAdmissionTicket,
|
||||||
|
createExaminee,
|
||||||
|
updateExaminee,
|
||||||
|
deleteExaminee
|
||||||
|
};
|
1175
src/background/db/examing.js
Normal file
@ -15,6 +15,14 @@ const { initConfigIpc } = require('./service/configService.js');
|
|||||||
const { initDictIpc } = require('./service/dictService.js');
|
const { initDictIpc } = require('./service/dictService.js');
|
||||||
// 导入试题服务
|
// 导入试题服务
|
||||||
const { initQuestionIpc } = require('./service/questionService.js');
|
const { initQuestionIpc } = require('./service/questionService.js');
|
||||||
|
// 导入考试服务
|
||||||
|
const { initExamIpc } = require('./service/examService.js');
|
||||||
|
// 导入考生服务
|
||||||
|
const { initExamineeIpc } = require('./service/examineeService.js');
|
||||||
|
// 导入考生考试服务
|
||||||
|
const { initExamingIpc } = require('./service/examingService.js');
|
||||||
|
// 导入文件服务
|
||||||
|
const { initFileIpc } = require('./service/fileService.js');
|
||||||
|
|
||||||
// Scheme must be registered before the app is ready
|
// Scheme must be registered before the app is ready
|
||||||
protocol.registerSchemesAsPrivileged([
|
protocol.registerSchemesAsPrivileged([
|
||||||
@ -83,57 +91,20 @@ app.on('ready', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 测试SQLite连接
|
|
||||||
try {
|
|
||||||
console.log('尝试连接SQLite...');
|
|
||||||
const db = new sqlite3.Database(':memory:', (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('SQLite连接失败:', err.message);
|
|
||||||
} else {
|
|
||||||
console.log('SQLite连接成功!');
|
|
||||||
|
|
||||||
// 执行简单的SQL操作
|
|
||||||
db.serialize(() => {
|
|
||||||
console.log('创建测试表...');
|
|
||||||
db.run('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, name TEXT)', (err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('创建表失败:', err.message);
|
|
||||||
} else {
|
|
||||||
console.log('表创建成功');
|
|
||||||
|
|
||||||
// 插入测试数据
|
|
||||||
db.run('INSERT INTO test (name) VALUES (?)', ['测试数据'], function(err) {
|
|
||||||
if (err) {
|
|
||||||
console.error('插入数据失败:', err.message);
|
|
||||||
} else {
|
|
||||||
console.log('数据插入成功,ID:', this.lastID);
|
|
||||||
|
|
||||||
// 查询数据
|
|
||||||
db.all('SELECT * FROM test', [], (err, rows) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('查询失败:', err.message);
|
|
||||||
} else {
|
|
||||||
console.log('查询结果:', rows);
|
|
||||||
}
|
|
||||||
db.close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('SQLite测试异常:', error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化配置相关的IPC处理程序
|
// 初始化配置相关的IPC处理程序
|
||||||
initConfigIpc(ipcMain);
|
initConfigIpc(ipcMain);
|
||||||
// 初始化字典相关的IPC处理程序
|
// 初始化字典相关的IPC处理程序
|
||||||
initDictIpc(ipcMain);
|
initDictIpc(ipcMain);
|
||||||
// 初始化试题相关的IPC处理程序
|
// 初始化试题相关的IPC处理程序
|
||||||
await initQuestionIpc(ipcMain);
|
initQuestionIpc(ipcMain);
|
||||||
|
// 初始化考试相关的IPC处理程序
|
||||||
|
initExamIpc(ipcMain);
|
||||||
|
// 初始化考生相关的IPC处理程序
|
||||||
|
initExamineeIpc(ipcMain);
|
||||||
|
// 初始化考生考试相关的IPC处理程序
|
||||||
|
initExamingIpc(ipcMain);
|
||||||
|
// 初始化文件相关的IPC处理程序
|
||||||
|
initFileIpc(ipcMain);
|
||||||
|
|
||||||
createWindow();
|
createWindow();
|
||||||
});
|
});
|
||||||
|
262
src/background/service/examService.js
Normal file
@ -0,0 +1,262 @@
|
|||||||
|
const {
|
||||||
|
createExam,
|
||||||
|
getAllExams,
|
||||||
|
getExamById,
|
||||||
|
updateExam,
|
||||||
|
deleteExam,
|
||||||
|
getLastExam
|
||||||
|
} = require('../db/exam.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:创建考试
|
||||||
|
* @param {Object} examData 考试数据
|
||||||
|
* @returns {Promise<Object>} 创建的考试
|
||||||
|
*/
|
||||||
|
async function createNewExam(examData) {
|
||||||
|
try {
|
||||||
|
// 数据验证 - 修改为exam_minutes和exam_minutes_min必填
|
||||||
|
if (!examData.exam_minutes || !examData.exam_minutes_min) {
|
||||||
|
throw new Error("考试时长和最少考试时间为必填项");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除默认值设置,因为现在是必填项
|
||||||
|
if (typeof examData.exam_minutes_min !== "number") {
|
||||||
|
throw new Error("最少考试时间必须是数字");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保最少考试时间不大于考试时长
|
||||||
|
if (examData.exam_minutes_min > examData.exam_minutes) {
|
||||||
|
throw new Error("最少考试时间不能大于考试时长");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createExam(examData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 创建考试失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:查询所有考试
|
||||||
|
* @returns {Promise<Array>} 考试列表
|
||||||
|
*/
|
||||||
|
async function fetchAllExams() {
|
||||||
|
try {
|
||||||
|
return await getAllExams();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 查询所有考试失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:根据ID查询考试
|
||||||
|
* @param {number} id 考试ID
|
||||||
|
* @returns {Promise<Object|null>} 考试数据
|
||||||
|
*/
|
||||||
|
async function fetchExamById(id) {
|
||||||
|
try {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("考试ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
const exam = await getExamById(id);
|
||||||
|
if (!exam) {
|
||||||
|
throw new Error("未找到指定考试");
|
||||||
|
}
|
||||||
|
|
||||||
|
return exam;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 根据ID查询考试失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:更新考试
|
||||||
|
* @param {number} id 考试ID
|
||||||
|
* @param {Object} examData 更新的数据
|
||||||
|
* @returns {Promise<boolean>} 是否更新成功
|
||||||
|
*/
|
||||||
|
async function modifyExam(id, examData) {
|
||||||
|
try {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("考试ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证考试是否存在
|
||||||
|
const existingExam = await getExamById(id);
|
||||||
|
if (!existingExam) {
|
||||||
|
throw new Error("未找到指定考试");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据验证 - 修改为exam_minutes和exam_minutes_min必填
|
||||||
|
if (!examData.exam_minutes || !examData.exam_minutes_min) {
|
||||||
|
throw new Error("考试时长和最少考试时间为必填项");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除默认值设置,因为现在是必填项
|
||||||
|
if (typeof examData.exam_minutes_min !== "number") {
|
||||||
|
throw new Error("最少考试时间必须是数字");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确保最少考试时间不大于考试时长
|
||||||
|
if (examData.exam_minutes_min > examData.exam_minutes) {
|
||||||
|
throw new Error("最少考试时间不能大于考试时长");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await updateExam(id, examData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 更新考试失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:删除考试
|
||||||
|
* @param {number} id 考试ID
|
||||||
|
* @returns {Promise<boolean>} 是否删除成功
|
||||||
|
*/
|
||||||
|
async function removeExam(id) {
|
||||||
|
try {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("考试ID不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证考试是否存在
|
||||||
|
const existingExam = await getExamById(id);
|
||||||
|
if (!existingExam) {
|
||||||
|
throw new Error("未找到指定考试");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await deleteExam(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 删除考试失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:查询ID最大的考试记录
|
||||||
|
* @returns {Promise<Object|null>} 考试数据
|
||||||
|
*/
|
||||||
|
async function fetchLastExam() {
|
||||||
|
try {
|
||||||
|
return await getLastExam();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 查询ID最大的考试失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化考试相关的IPC处理程序
|
||||||
|
* @param {Object} ipcMain Electron的ipcMain实例
|
||||||
|
*/
|
||||||
|
function initExamIpc(ipcMain) {
|
||||||
|
// 考试管理相关IPC
|
||||||
|
ipcMain.handle("exam-create", async (event, examData) => {
|
||||||
|
try {
|
||||||
|
// 确保exam_notice是序列化的JSON字符串
|
||||||
|
if (examData.exam_notice && typeof examData.exam_notice === "object") {
|
||||||
|
examData.exam_notice = JSON.stringify(examData.exam_notice);
|
||||||
|
}
|
||||||
|
return await createNewExam(examData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create exam:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("exam-update", async (event, { id, examData }) => {
|
||||||
|
try {
|
||||||
|
// 确保exam_notice是序列化的JSON字符串
|
||||||
|
if (examData.exam_notice && typeof examData.exam_notice === "object") {
|
||||||
|
examData.exam_notice = JSON.stringify(examData.exam_notice);
|
||||||
|
}
|
||||||
|
return await modifyExam(id, examData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update exam:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("exam-fetch-last", async () => {
|
||||||
|
try {
|
||||||
|
const exam = await fetchLastExam();
|
||||||
|
// 将exam_notice字符串解析为数组
|
||||||
|
if (exam && exam.exam_notice) {
|
||||||
|
try {
|
||||||
|
exam.exam_notice = JSON.parse(exam.exam_notice);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("解析考试须知失败:", e);
|
||||||
|
exam.exam_notice = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exam;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch last exam:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("exam-fetch-all", async () => {
|
||||||
|
try {
|
||||||
|
const exams = await fetchAllExams();
|
||||||
|
// 处理每个考试的exam_notice
|
||||||
|
return exams.map(exam => {
|
||||||
|
if (exam.exam_notice) {
|
||||||
|
try {
|
||||||
|
exam.exam_notice = JSON.parse(exam.exam_notice);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`解析考试(ID: ${exam.id})须知失败:`, e);
|
||||||
|
exam.exam_notice = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return exam;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch all exams:", error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("exam-fetch-by-id", async (event, id) => {
|
||||||
|
try {
|
||||||
|
const exam = await fetchExamById(id);
|
||||||
|
// 处理exam_notice
|
||||||
|
if (exam && exam.exam_notice) {
|
||||||
|
try {
|
||||||
|
exam.exam_notice = JSON.parse(exam.exam_notice);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`解析考试(ID: ${id})须知失败:`, e);
|
||||||
|
exam.exam_notice = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { success: true, data: exam };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch exam by id ${id}:`, error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("exam-delete", async (event, id) => {
|
||||||
|
try {
|
||||||
|
const result = await removeExam(id);
|
||||||
|
return { success: result, data: { id } };
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete exam ${id}:`, error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
createNewExam,
|
||||||
|
fetchAllExams,
|
||||||
|
fetchExamById,
|
||||||
|
modifyExam,
|
||||||
|
removeExam,
|
||||||
|
fetchLastExam,
|
||||||
|
initExamIpc
|
||||||
|
};
|
187
src/background/service/examineeService.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
const {
|
||||||
|
getAllExaminees,
|
||||||
|
getExamineeById,
|
||||||
|
createExaminee,
|
||||||
|
updateExaminee,
|
||||||
|
deleteExaminee,
|
||||||
|
getExamineeByIdCardAndAdmissionTicket
|
||||||
|
} = require('../db/examinee.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:获取所有考生列表
|
||||||
|
* @returns {Promise<Array>} 考生列表
|
||||||
|
*/
|
||||||
|
async function fetchAllExamineesService() {
|
||||||
|
try {
|
||||||
|
return await getAllExaminees();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 获取所有考生列表失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:根据ID查询考生
|
||||||
|
* @param {number} id 考生ID
|
||||||
|
* @returns {Promise<Object|null>} 考生数据
|
||||||
|
*/
|
||||||
|
async function fetchExamineeByIdService(id) {
|
||||||
|
try {
|
||||||
|
return await getExamineeById(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 根据ID查询考生失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:添加考生
|
||||||
|
* @param {Object} examineeData 考生数据
|
||||||
|
* @returns {Promise<Object>} 添加的考生
|
||||||
|
*/
|
||||||
|
async function createExamineeService(examineeData) {
|
||||||
|
try {
|
||||||
|
// 数据验证
|
||||||
|
if (!examineeData.examinee_name || !examineeData.examinee_id_card) {
|
||||||
|
throw new Error('考生姓名和身份证号为必填项');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await createExaminee(examineeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 添加考生失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:更新考生
|
||||||
|
* @param {number} id 考生ID
|
||||||
|
* @param {Object} examineeData 更新的数据
|
||||||
|
* @returns {Promise<boolean>} 是否更新成功
|
||||||
|
*/
|
||||||
|
async function updateExamineeService(id, examineeData) {
|
||||||
|
try {
|
||||||
|
if (!id) {
|
||||||
|
throw new Error('考生ID不能为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证考生是否存在
|
||||||
|
const existingExaminee = await getExamineeById(id);
|
||||||
|
if (!existingExaminee) {
|
||||||
|
throw new Error('未找到指定考生');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 数据验证
|
||||||
|
if (!examineeData.examinee_name || !examineeData.examinee_id_card) {
|
||||||
|
throw new Error('考生姓名和身份证号为必填项');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await updateExaminee(id, examineeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 更新考生失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:删除考生
|
||||||
|
* @param {number} id 考生ID
|
||||||
|
* @returns {Promise<boolean>} 是否删除成功
|
||||||
|
*/
|
||||||
|
async function deleteExamineeService(id) {
|
||||||
|
try {
|
||||||
|
return await deleteExaminee(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 删除考生失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:考生登录
|
||||||
|
* @param {string} idCard 身份证号
|
||||||
|
* @param {string} admissionTicket 准考证号
|
||||||
|
* @returns {Promise<Object|null>} 考生数据或null
|
||||||
|
*/
|
||||||
|
async function fetchExamineeByIdCardAndAdmissionTicketService(idCard, admissionTicket) {
|
||||||
|
try {
|
||||||
|
if (!idCard || !admissionTicket) {
|
||||||
|
throw new Error('身份证号和准考证号不能为空');
|
||||||
|
}
|
||||||
|
return await getExamineeByIdCardAndAdmissionTicket(idCard, admissionTicket);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 考生登录失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化考生相关的IPC处理程序
|
||||||
|
* @param {Object} ipcMain Electron的ipcMain实例
|
||||||
|
*/
|
||||||
|
function initExamineeIpc(ipcMain) {
|
||||||
|
ipcMain.handle("examinee-fetch-all", async (event) => {
|
||||||
|
try {
|
||||||
|
return await fetchAllExamineesService();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch all examinees:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("user-login", async (event, {idCard, admissionTicket}) => {
|
||||||
|
try {
|
||||||
|
return await fetchExamineeByIdCardAndAdmissionTicketService(idCard, admissionTicket);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to login examinee:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("examinee-fetch-by-id", async (event, id) => {
|
||||||
|
try {
|
||||||
|
return await fetchExamineeByIdService(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch examinee by id:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("examinee-create", async (event, examineeData) => {
|
||||||
|
try {
|
||||||
|
return await createExamineeService(examineeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create examinee:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("examinee-update", async (event, {id, examineeData}) => {
|
||||||
|
try {
|
||||||
|
return await updateExamineeService(id, examineeData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to update examinee:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("examinee-delete", async (event, id) => {
|
||||||
|
try {
|
||||||
|
return await deleteExamineeService(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete examinee:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出使用CommonJS格式
|
||||||
|
module.exports = {
|
||||||
|
fetchAllExamineesService,
|
||||||
|
fetchExamineeByIdService,
|
||||||
|
createExamineeService,
|
||||||
|
updateExamineeService,
|
||||||
|
deleteExamineeService,
|
||||||
|
fetchExamineeByIdCardAndAdmissionTicketService,
|
||||||
|
initExamineeIpc
|
||||||
|
};
|
499
src/background/service/examingService.js
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
const {
|
||||||
|
generateExamineePaper,
|
||||||
|
loadPaperSerial,
|
||||||
|
getQuestionByRelatedId,
|
||||||
|
updateExamineeAnswer,
|
||||||
|
startPaper,
|
||||||
|
submitPaper,
|
||||||
|
endPaper,
|
||||||
|
processPaper,
|
||||||
|
checkPaperAnswers,
|
||||||
|
getPaper
|
||||||
|
} = require('../db/examing.js');
|
||||||
|
const { getDbConnection, closeAllConnections } = require('../db/index.js');
|
||||||
|
const { getUserDbPath } = require('../db/path.js');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:生成考生试卷
|
||||||
|
* @param {Object} examineeData - 考生数据
|
||||||
|
* @param {number} examDuration - 考试时长(分钟)
|
||||||
|
* @returns {Promise<Object>} - 包含试卷ID和状态的对象
|
||||||
|
*/
|
||||||
|
async function generateExamineePaperService(examineeData, examData) {
|
||||||
|
try {
|
||||||
|
// 数据验证
|
||||||
|
if (!examineeData || !examineeData.id || !examineeData.examinee_name) {
|
||||||
|
throw new Error("考生数据不完整,必须包含ID和姓名");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!examData || !examData.exam_minutes || examData.exam_minutes <= 0) {
|
||||||
|
throw new Error("考试时长必须为正数");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
examData.exam_minutes_min === undefined ||
|
||||||
|
examData.exam_minutes_min < 0
|
||||||
|
) {
|
||||||
|
throw new Error("最短考试时长必须为非负数");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await generateExamineePaper(examineeData, examData);
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 生成考生试卷失败", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `生成试卷失败: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:获取考生试卷状态
|
||||||
|
* @param {number} examineeId - 考生ID
|
||||||
|
* @returns {Promise<Object|null>} - 试卷状态信息
|
||||||
|
*/
|
||||||
|
async function getExamineePaperStatusService(examineeId) {
|
||||||
|
try {
|
||||||
|
if (!examineeId || examineeId <= 0) {
|
||||||
|
throw new Error("考生ID必须为正数");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDb = await getDbConnection(getUserDbPath());
|
||||||
|
const paperStatus = await userDb.getAsync(
|
||||||
|
"SELECT * FROM examinee_papers WHERE examinee_id = ?",
|
||||||
|
[examineeId]
|
||||||
|
);
|
||||||
|
|
||||||
|
return paperStatus;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 获取考生试卷状态失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:更新试卷状态
|
||||||
|
* @param {number} paperId - 试卷ID
|
||||||
|
* @param {Object} statusData - 状态数据
|
||||||
|
* @returns {Promise<boolean>} - 是否更新成功
|
||||||
|
*/
|
||||||
|
async function updatePaperStatusService(paperId, statusData) {
|
||||||
|
try {
|
||||||
|
if (!paperId || paperId <= 0) {
|
||||||
|
throw new Error("试卷ID必须为正数");
|
||||||
|
}
|
||||||
|
|
||||||
|
const userDb = await getDbConnection(getUserDbPath());
|
||||||
|
|
||||||
|
// 构建更新字段
|
||||||
|
const fields = [];
|
||||||
|
const values = [];
|
||||||
|
|
||||||
|
if (statusData.paper_start_time !== undefined) {
|
||||||
|
fields.push("paper_start_time = ?");
|
||||||
|
values.push(statusData.paper_start_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusData.paper_last_time !== undefined) {
|
||||||
|
fields.push("paper_last_time = ?");
|
||||||
|
values.push(statusData.paper_last_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusData.paper_submit_time !== undefined) {
|
||||||
|
fields.push("paper_submit_time = ?");
|
||||||
|
values.push(statusData.paper_submit_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusData.paper_end_time !== undefined) {
|
||||||
|
fields.push("paper_end_time = ?");
|
||||||
|
values.push(statusData.paper_end_time);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusData.paper_status !== undefined) {
|
||||||
|
fields.push("paper_status = ?");
|
||||||
|
values.push(statusData.paper_status);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusData.paper_score_real !== undefined) {
|
||||||
|
fields.push("paper_score_real = ?");
|
||||||
|
values.push(statusData.paper_score_real);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fields.length === 0) {
|
||||||
|
return true; // 没有需要更新的字段
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加WHERE条件的值
|
||||||
|
values.push(paperId);
|
||||||
|
|
||||||
|
const sql = `UPDATE examinee_papers SET ${fields.join(", ")} WHERE id = ?`;
|
||||||
|
await userDb.runAsync(sql, values);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("服务层: 更新试卷状态失败", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:加载试卷试题序列
|
||||||
|
* @param {number} paperId - 试卷ID
|
||||||
|
* @returns {Promise<Array>} - 包含试题序列的数组
|
||||||
|
*/
|
||||||
|
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>} - 包含试题数据的对象
|
||||||
|
*/
|
||||||
|
async function getQuestionByRelatedIdService(tableName, id) {
|
||||||
|
try {
|
||||||
|
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>} - 包含更新结果的对象
|
||||||
|
*/
|
||||||
|
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>} - 包含操作结果的对象
|
||||||
|
*/
|
||||||
|
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>} - 包含操作结果的对象
|
||||||
|
*/
|
||||||
|
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>} - 包含操作结果的对象
|
||||||
|
*/
|
||||||
|
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>} - 包含操作结果的对象
|
||||||
|
*/
|
||||||
|
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>} - 包含操作结果和试卷数据的对象
|
||||||
|
*/
|
||||||
|
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处理程序
|
||||||
|
* @param {import('electron').IpcMain} ipcMain - IPC主进程实例
|
||||||
|
*/
|
||||||
|
function initExamingIpc(ipcMain) {
|
||||||
|
// 生成考生试卷
|
||||||
|
ipcMain.handle(
|
||||||
|
"examing-generate-paper",
|
||||||
|
async (event, { examineeData, examData }) => {
|
||||||
|
try {
|
||||||
|
return await generateExamineePaperService(examineeData, examData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("生成考生试卷失败:", error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `生成试卷失败: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 获取考生试卷状态
|
||||||
|
ipcMain.handle("examing-get-paper-status", async (event, examineeId) => {
|
||||||
|
try {
|
||||||
|
return await getExamineePaperStatusService(examineeId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("获取考生试卷状态失败:", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新试卷状态
|
||||||
|
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 {
|
||||||
|
return await loadPaperSerialService(paperId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载试卷试题序列失败:", error);
|
||||||
|
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}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出使用CommonJS格式
|
||||||
|
module.exports = {
|
||||||
|
generateExamineePaperService,
|
||||||
|
getExamineePaperStatusService,
|
||||||
|
updatePaperStatusService,
|
||||||
|
loadPaperSerialService,
|
||||||
|
getQuestionByRelatedIdService,
|
||||||
|
updateExamineeAnswerService,
|
||||||
|
startPaperService,
|
||||||
|
submitPaperService,
|
||||||
|
endPaperService,
|
||||||
|
processPaperService,
|
||||||
|
checkPaperAnswersService,
|
||||||
|
initExamingIpc
|
||||||
|
};
|
849
src/background/service/fileService.js
Normal file
@ -0,0 +1,849 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const PDFDocument = require('pdfkit');
|
||||||
|
const { app } = require('electron');
|
||||||
|
|
||||||
|
// 使用更可靠的方式获取应用路径
|
||||||
|
const appPath = app.getAppPath();
|
||||||
|
|
||||||
|
// 修正字体路径常量 - 从应用根路径开始构建
|
||||||
|
const FONT_PATH = path.join(appPath, 'src', 'background', 'font');
|
||||||
|
// 优先使用SourceHanSansSC字体
|
||||||
|
const primaryFontPath = path.join(FONT_PATH, 'SourceHanSansSC-Regular.otf');
|
||||||
|
const boldFontPath = path.join(FONT_PATH, 'SourceHanSansSC-Bold.otf');
|
||||||
|
// 保留宋体作为备选
|
||||||
|
const simsunPath = path.join(FONT_PATH, 'simsun.ttf');
|
||||||
|
const fallbackFontPath = path.join(FONT_PATH, 'simsun.ttc'); // 备选字体路径
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:获取所有考生列表
|
||||||
|
* @returns {Promise<Array>} 考生列表
|
||||||
|
*/
|
||||||
|
exports.createFileService = async function() {
|
||||||
|
try {
|
||||||
|
// TODO 测试用
|
||||||
|
return '文件服务测试成功';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 创建文件失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成PDF文件并保存到合适的目录
|
||||||
|
* @param {Object} pdfData - PDF数据
|
||||||
|
* @param {string} fileName - 文件名
|
||||||
|
* @returns {Promise<string>} 文件保存路径
|
||||||
|
*/
|
||||||
|
exports.generatePdfService = async function(pdfData, fileName) {
|
||||||
|
try {
|
||||||
|
// 获取合适的保存目录
|
||||||
|
const appDir = path.join(getAppSaveDir(), '..');
|
||||||
|
const filePath = path.join(appDir, `${fileName || 'document'}.pdf`);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 创建PDF文档
|
||||||
|
const doc = new PDFDocument();
|
||||||
|
|
||||||
|
// 加载中文字体的标志
|
||||||
|
let chineseFontLoaded = false;
|
||||||
|
let boldFontLoaded = false;
|
||||||
|
|
||||||
|
// 保存当前字体
|
||||||
|
let currentFont = null;
|
||||||
|
|
||||||
|
// 修改字体加载逻辑
|
||||||
|
try {
|
||||||
|
// 1. 尝试加载SourceHanSansSC常规字体
|
||||||
|
if (fs.existsSync(primaryFontPath)) {
|
||||||
|
try {
|
||||||
|
doc.registerFont('SourceHanSans', primaryFontPath);
|
||||||
|
doc.font('SourceHanSans');
|
||||||
|
currentFont = 'SourceHanSans';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
console.log('成功加载SourceHanSansSC-Regular.otf字体');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载SourceHanSansSC-Regular.otf字体失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试加载SourceHanSansSC粗体字体(用于标题)
|
||||||
|
if (fs.existsSync(boldFontPath)) {
|
||||||
|
try {
|
||||||
|
doc.registerFont('SourceHanSansBold', boldFontPath);
|
||||||
|
boldFontLoaded = true;
|
||||||
|
console.log('成功加载SourceHanSansSC-Bold.otf字体');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载SourceHanSansSC-Bold.otf字体失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果SourceHanSansSC字体加载失败,尝试加载宋体
|
||||||
|
if (!chineseFontLoaded) {
|
||||||
|
if (fs.existsSync(simsunPath)) {
|
||||||
|
try {
|
||||||
|
doc.registerFont('SimSun', simsunPath);
|
||||||
|
doc.font('SimSun');
|
||||||
|
currentFont = 'SimSun';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
console.log('成功加载simsun.ttf字体');
|
||||||
|
} catch (ttfError) {
|
||||||
|
console.error('加载simsun.ttf字体失败:', ttfError);
|
||||||
|
// 尝试加载备选TTC字体
|
||||||
|
if (fs.existsSync(fallbackFontPath)) {
|
||||||
|
try {
|
||||||
|
doc.registerFont('SimSun', fallbackFontPath);
|
||||||
|
doc.font('SimSun');
|
||||||
|
currentFont = 'SimSun';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
console.log('成功加载simsun.ttc字体');
|
||||||
|
} catch (ttcError) {
|
||||||
|
console.error('加载simsun.ttc字体失败:', ttcError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(`未找到simsun.ttf字体文件: ${simsunPath}`);
|
||||||
|
// 检查是否有备选TTC字体
|
||||||
|
if (fs.existsSync(fallbackFontPath)) {
|
||||||
|
try {
|
||||||
|
doc.registerFont('SimSun', fallbackFontPath);
|
||||||
|
doc.font('SimSun');
|
||||||
|
currentFont = 'SimSun';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
console.log('成功加载simsun.ttc字体');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载simsun.ttc字体失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chineseFontLoaded) {
|
||||||
|
console.warn('无法加载中文字体,将使用默认字体,可能导致中文显示异常');
|
||||||
|
// 在macOS上尝试使用系统字体
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
try {
|
||||||
|
doc.font('Arial Unicode MS'); // macOS内置支持多语言的字体
|
||||||
|
currentFont = 'Arial Unicode MS';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
console.log('成功加载系统Arial Unicode MS字体');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载系统字体失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载字体失败:', error);
|
||||||
|
console.warn('将使用默认字体,可能导致中文显示异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到文件
|
||||||
|
const writeStream = fs.createWriteStream(filePath);
|
||||||
|
doc.pipe(writeStream);
|
||||||
|
|
||||||
|
// 设置文档标题
|
||||||
|
if (pdfData.title) {
|
||||||
|
// 保存当前字体
|
||||||
|
const tempFont = currentFont;
|
||||||
|
try {
|
||||||
|
// 尝试使用粗体字体
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.fontSize(20).font('SourceHanSansBold').text(pdfData.title, { align: 'center' }).moveDown();
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
doc.fontSize(20).font('Arial Unicode MS Bold').text(pdfData.title, { align: 'center' }).moveDown();
|
||||||
|
} else {
|
||||||
|
doc.fontSize(20).text(pdfData.title, { align: 'center' }).moveDown();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置标题字体失败:', error);
|
||||||
|
doc.fontSize(20).text(pdfData.title, { align: 'center' }).moveDown();
|
||||||
|
}
|
||||||
|
// 恢复字体
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加内容
|
||||||
|
if (pdfData.content) {
|
||||||
|
pdfData.content.forEach(item => {
|
||||||
|
if (item.type === 'text') {
|
||||||
|
doc.fontSize(item.fontSize || 12).text(item.text, item.options || {}).moveDown();
|
||||||
|
} else if (item.type === 'heading') {
|
||||||
|
// 保存当前字体
|
||||||
|
const tempFont = currentFont;
|
||||||
|
try {
|
||||||
|
// 尝试使用SourceHanSansBold粗体字体
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.fontSize(item.fontSize || 16).font('SourceHanSansBold').text(item.text, item.options || {}).moveDown();
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
doc.fontSize(item.fontSize || 16).font('Arial Unicode MS Bold').text(item.text, item.options || {}).moveDown();
|
||||||
|
} else {
|
||||||
|
// 如果没有粗体字体,使用当前字体加大字号
|
||||||
|
doc.fontSize(item.fontSize || 16).text(item.text, item.options || {}).moveDown();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('切换到粗体字体失败:', error);
|
||||||
|
doc.fontSize(item.fontSize || 16).text(item.text, item.options || {}).moveDown();
|
||||||
|
}
|
||||||
|
// 恢复之前的字体
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
} else if (item.type === 'table') {
|
||||||
|
// 改进表格实现
|
||||||
|
const { headers, rows } = item;
|
||||||
|
const cellWidth = 100;
|
||||||
|
const baseCellHeight = 25; // 增加基础单元格高度,更好地适应中文
|
||||||
|
const marginLeft = 50;
|
||||||
|
let currentY = doc.y;
|
||||||
|
const fontSize = 12;
|
||||||
|
|
||||||
|
// 辅助函数:计算文本在指定宽度内的行数
|
||||||
|
const calculateLines = (text, width) => {
|
||||||
|
// 估算每行字符数(假设平均字符宽度为字体大小的一半)
|
||||||
|
const charsPerLine = Math.floor(width / (fontSize / 2));
|
||||||
|
const lines = [];
|
||||||
|
let currentText = text;
|
||||||
|
|
||||||
|
while (currentText.length > 0) {
|
||||||
|
// 找到合适的换行位置
|
||||||
|
let splitIndex = Math.min(currentText.length, charsPerLine);
|
||||||
|
// 尝试在空格处换行
|
||||||
|
if (currentText.length > splitIndex && currentText[splitIndex] !== ' ') {
|
||||||
|
const lastSpace = currentText.lastIndexOf(' ', splitIndex);
|
||||||
|
if (lastSpace > 0) {
|
||||||
|
splitIndex = lastSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(currentText.substring(0, splitIndex).trim());
|
||||||
|
currentText = currentText.substring(splitIndex).trim();
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绘制表头
|
||||||
|
headers.forEach((header, i) => {
|
||||||
|
// 计算单元格实际高度(考虑换行)
|
||||||
|
const lines = calculateLines(header, cellWidth - 10);
|
||||||
|
const cellHeight = Math.max(baseCellHeight, lines.length * 15);
|
||||||
|
|
||||||
|
doc.rect(marginLeft + i * cellWidth, currentY, cellWidth, cellHeight).stroke();
|
||||||
|
|
||||||
|
// 保存当前字体
|
||||||
|
const tempFont = currentFont;
|
||||||
|
try {
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.font('SourceHanSansBold');
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
doc.font('Arial Unicode MS Bold');
|
||||||
|
}
|
||||||
|
// 垂直居中显示文本
|
||||||
|
doc.fontSize(fontSize).text(header, marginLeft + i * cellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, {
|
||||||
|
width: cellWidth - 10,
|
||||||
|
height: cellHeight - 10
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('设置表头字体失败:', error);
|
||||||
|
doc.fontSize(fontSize).text(header, marginLeft + i * cellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, {
|
||||||
|
width: cellWidth - 10,
|
||||||
|
height: cellHeight - 10
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 恢复字体
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// 移动到下一行,考虑最高的表头单元格高度
|
||||||
|
const headerLines = headers.map(header => calculateLines(header, cellWidth - 10).length);
|
||||||
|
const maxHeaderLines = Math.max(...headerLines);
|
||||||
|
currentY += Math.max(baseCellHeight, maxHeaderLines * 15) + 5; // 添加一些间距
|
||||||
|
|
||||||
|
// 绘制行
|
||||||
|
rows.forEach(row => {
|
||||||
|
// 计算这一行中最高的单元格
|
||||||
|
const rowLines = row.map(cell => calculateLines(cell.toString(), cellWidth - 10).length);
|
||||||
|
const maxRowLines = Math.max(...rowLines);
|
||||||
|
const rowHeight = Math.max(baseCellHeight, maxRowLines * 15);
|
||||||
|
|
||||||
|
row.forEach((cell, i) => {
|
||||||
|
doc.rect(marginLeft + i * cellWidth, currentY, cellWidth, rowHeight).stroke();
|
||||||
|
const cellLines = calculateLines(cell.toString(), cellWidth - 10);
|
||||||
|
doc.fontSize(fontSize).text(cell.toString(), marginLeft + i * cellWidth + 5, currentY + (rowHeight - cellLines.length * 15) / 2, {
|
||||||
|
width: cellWidth - 10,
|
||||||
|
height: rowHeight - 10
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移动到下一行
|
||||||
|
currentY += rowHeight + 5; // 添加一些间距
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新文档的当前Y位置
|
||||||
|
doc.y = currentY;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 结束文档
|
||||||
|
doc.end();
|
||||||
|
|
||||||
|
// 监听完成事件
|
||||||
|
writeStream.on('finish', () => {
|
||||||
|
resolve(filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误事件
|
||||||
|
writeStream.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 生成PDF失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化文件相关IPC服务
|
||||||
|
* @param {ipcMain} ipcMain - Electron IPC主进程实例
|
||||||
|
*/
|
||||||
|
exports.initFileIpc = function(ipcMain) {
|
||||||
|
// 测试用接口
|
||||||
|
ipcMain.handle('file-test', async () => {
|
||||||
|
try {
|
||||||
|
// 测试用
|
||||||
|
return '文件服务测试成功';
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 文件测试失败:', error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成PDF文件接口
|
||||||
|
ipcMain.handle('file-generate-pdf', async (event, pdfData, fileName) => {
|
||||||
|
try {
|
||||||
|
const filePath = await exports.generatePdfService(pdfData, fileName);
|
||||||
|
return { success: true, filePath };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 生成PDF失败:', error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成试卷PDF文件接口
|
||||||
|
ipcMain.handle('file-generate-paper-pdf', async (event, jsonString) => {
|
||||||
|
try {
|
||||||
|
const filePath = await exports.generatePaperPdf(jsonString);
|
||||||
|
return { success: true, filePath };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 生成试卷PDF失败:', error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 复制文件到桌面接口
|
||||||
|
ipcMain.handle('file-copy-to-desktop', async (event, filePath) => {
|
||||||
|
try {
|
||||||
|
const destPath = await exports.copyToDesk(filePath);
|
||||||
|
return { success: true, filePath: destPath };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 复制文件到桌面失败:', error);
|
||||||
|
return { success: false, message: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用保存目录,适配不同操作系统和环境
|
||||||
|
* @returns {string} 保存目录路径
|
||||||
|
*/
|
||||||
|
function getAppSaveDir() {
|
||||||
|
// 判断是否为开发环境
|
||||||
|
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境:使用项目根目录
|
||||||
|
const outputDir = path.join(process.cwd(), 'output');
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return outputDir;
|
||||||
|
} else {
|
||||||
|
// 检测是否为便携模式
|
||||||
|
const exePath = app.getPath('exe');
|
||||||
|
const appDir = path.dirname(exePath);
|
||||||
|
const portableFlagPath = path.join(appDir, 'portable.txt');
|
||||||
|
const isPortable = fs.existsSync(portableFlagPath);
|
||||||
|
|
||||||
|
// 便携模式:使用应用根目录
|
||||||
|
if (isPortable) {
|
||||||
|
return appDir;
|
||||||
|
} else {
|
||||||
|
// 非便携模式:使用应用同级的output目录
|
||||||
|
const outputDir = path.join(appDir, 'output');
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return outputDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取用户桌面目录路径
|
||||||
|
* @returns {string} 用户桌面绝对路径
|
||||||
|
*/
|
||||||
|
function getDesktopDir() {
|
||||||
|
try {
|
||||||
|
// 使用Electron的app.getPath方法获取桌面路径
|
||||||
|
return app.getPath('desktop');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取桌面路径失败:', error);
|
||||||
|
// 发生错误时,返回当前工作目录作为备选
|
||||||
|
return process.cwd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将文件复制到用户桌面
|
||||||
|
* @param {string} filePath - 要复制的文件的绝对路径
|
||||||
|
* @returns {Promise<string>} 复制后的文件路径
|
||||||
|
*/
|
||||||
|
exports.copyToDesk = async function(filePath) {
|
||||||
|
try {
|
||||||
|
const desktopDir = getDesktopDir();
|
||||||
|
const fileName = path.basename(filePath);
|
||||||
|
const destPath = path.join(desktopDir, fileName);
|
||||||
|
|
||||||
|
// 使用fs.promises进行文件复制
|
||||||
|
await fs.promises.copyFile(filePath, destPath);
|
||||||
|
console.log(`文件已成功复制到桌面: ${destPath}`);
|
||||||
|
return destPath;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('复制文件到桌面失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成试卷PDF文件
|
||||||
|
* @param {string} jsonString - 包含试卷信息的JSON字符串
|
||||||
|
* @returns {Promise<string>} - 生成的PDF文件绝对路径
|
||||||
|
*/
|
||||||
|
exports.generatePaperPdf = async function(jsonString) {
|
||||||
|
try {
|
||||||
|
// 解析JSON字符串
|
||||||
|
const paperData = JSON.parse(jsonString);
|
||||||
|
|
||||||
|
// 提取考生信息
|
||||||
|
const { examinee } = paperData;
|
||||||
|
const examineeName = examinee.examinee_name;
|
||||||
|
const idCard = examinee.examinee_id_card;
|
||||||
|
const admissionTicket = examinee.examinee_admission_ticket;
|
||||||
|
|
||||||
|
// 提取考试时间信息
|
||||||
|
const startTime = paperData.paper_start_time;
|
||||||
|
const endTime = paperData.paper_end_time;
|
||||||
|
// 截取考试日期 (假设格式为 'YYYY-MM-DD HH:mm:ss')
|
||||||
|
const examDate = startTime.split(' ')[0];
|
||||||
|
// 计算用时(分钟)
|
||||||
|
const start = new Date(startTime.replace(/-/g, '/'));
|
||||||
|
const end = new Date(endTime.replace(/-/g, '/'));
|
||||||
|
const durationMinutes = Math.round((end - start) / (1000 * 60));
|
||||||
|
|
||||||
|
// 提取试卷信息
|
||||||
|
// 计算总题量(每个question下的choice题和fill_blank题的数量之和)
|
||||||
|
let totalQuestions = 0;
|
||||||
|
paperData.questions.forEach(question => {
|
||||||
|
if (question.choices && question.choices.length > 0) {
|
||||||
|
totalQuestions += question.choices.length;
|
||||||
|
}
|
||||||
|
if (question.blanks && question.blanks.length > 0) {
|
||||||
|
totalQuestions += question.blanks.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const totalScore = paperData.paper_score;
|
||||||
|
const realScore = paperData.paper_score_real;
|
||||||
|
|
||||||
|
// 对questions列表按照question_type和id进行排序
|
||||||
|
paperData.questions.sort((a, b) => {
|
||||||
|
// 先按题型排序
|
||||||
|
if (a.question_type !== b.question_type) {
|
||||||
|
return a.question_type.localeCompare(b.question_type);
|
||||||
|
}
|
||||||
|
// 再按id排序
|
||||||
|
return a.id - b.id;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成文件名
|
||||||
|
// 格式化paper_end_time,移除特殊字符
|
||||||
|
const endDate = new Date(endTime.replace(/-/g, '/'));
|
||||||
|
const formattedEndTime = [
|
||||||
|
endDate.getFullYear(),
|
||||||
|
String(endDate.getMonth() + 1).padStart(2, '0'),
|
||||||
|
String(endDate.getDate()).padStart(2, '0'),
|
||||||
|
String(endDate.getHours()).padStart(2, '0'),
|
||||||
|
String(endDate.getMinutes()).padStart(2, '0'),
|
||||||
|
String(endDate.getSeconds()).padStart(2, '0')
|
||||||
|
].join('');
|
||||||
|
const fileName = `${examineeName}_${idCard}_${formattedEndTime}.pdf`;
|
||||||
|
|
||||||
|
// 获取保存路径
|
||||||
|
const appDir = getAppSaveDir();
|
||||||
|
// 确保目录存在
|
||||||
|
if (!fs.existsSync(appDir)) {
|
||||||
|
fs.mkdirSync(appDir, { recursive: true });
|
||||||
|
}
|
||||||
|
const filePath = path.join(appDir, fileName);
|
||||||
|
|
||||||
|
// 创建PDF文档,保留默认边距
|
||||||
|
const doc = new PDFDocument({
|
||||||
|
size: 'A4',
|
||||||
|
margin: 50
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加载中文字体
|
||||||
|
const FONT_PATH = path.join(__dirname, '..', 'font');
|
||||||
|
const primaryFontPath = path.join(FONT_PATH, 'SourceHanSansSC-Regular.otf');
|
||||||
|
const boldFontPath = path.join(FONT_PATH, 'SourceHanSansSC-Bold.otf');
|
||||||
|
const simsunPath = path.join(FONT_PATH, 'simsun.ttf');
|
||||||
|
const fallbackFontPath = path.join(FONT_PATH, 'simsun.ttc');
|
||||||
|
|
||||||
|
let chineseFontLoaded = false;
|
||||||
|
let boldFontLoaded = false;
|
||||||
|
let currentFont = null;
|
||||||
|
|
||||||
|
// 尝试加载字体
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(primaryFontPath)) {
|
||||||
|
doc.registerFont('SourceHanSans', primaryFontPath);
|
||||||
|
doc.font('SourceHanSans');
|
||||||
|
currentFont = 'SourceHanSans';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
} else if (fs.existsSync(simsunPath)) {
|
||||||
|
doc.registerFont('SimSun', simsunPath);
|
||||||
|
doc.font('SimSun');
|
||||||
|
currentFont = 'SimSun';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
} else if (fs.existsSync(fallbackFontPath)) {
|
||||||
|
doc.registerFont('SimSun', fallbackFontPath);
|
||||||
|
doc.font('SimSun');
|
||||||
|
currentFont = 'SimSun';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
doc.font('Arial Unicode MS');
|
||||||
|
currentFont = 'Arial Unicode MS';
|
||||||
|
chineseFontLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fs.existsSync(boldFontPath)) {
|
||||||
|
doc.registerFont('SourceHanSansBold', boldFontPath);
|
||||||
|
boldFontLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chineseFontLoaded) {
|
||||||
|
console.warn('无法加载中文字体,可能导致中文显示异常');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载字体失败:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到文件
|
||||||
|
const writeStream = fs.createWriteStream(filePath);
|
||||||
|
doc.pipe(writeStream);
|
||||||
|
|
||||||
|
// 添加标题
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.font('SourceHanSansBold');
|
||||||
|
} else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') {
|
||||||
|
doc.font('Arial Unicode MS Bold');
|
||||||
|
}
|
||||||
|
doc.fontSize(24).text('机考试卷', { align: 'center' }).moveDown(2);
|
||||||
|
|
||||||
|
// 恢复常规字体
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:绘制表格
|
||||||
|
function drawTable(headers, rows, cellWidth = 120, baseCellHeight = 25) {
|
||||||
|
// 从左边距开始
|
||||||
|
const marginLeft = doc.page.margins.left;
|
||||||
|
let currentY = doc.y;
|
||||||
|
const fontSize = 12;
|
||||||
|
const pageWidth = doc.page.width - doc.page.margins.left - doc.page.margins.right;
|
||||||
|
|
||||||
|
// 计算文本在指定宽度内的行数
|
||||||
|
const calculateLines = (text, width) => {
|
||||||
|
const charsPerLine = Math.floor(width / (fontSize / 2));
|
||||||
|
const lines = [];
|
||||||
|
let currentText = text;
|
||||||
|
|
||||||
|
while (currentText.length > 0) {
|
||||||
|
let splitIndex = Math.min(currentText.length, charsPerLine);
|
||||||
|
if (currentText.length > splitIndex && currentText[splitIndex] !== ' ') {
|
||||||
|
const lastSpace = currentText.lastIndexOf(' ', splitIndex);
|
||||||
|
if (lastSpace > 0) {
|
||||||
|
splitIndex = lastSpace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.push(currentText.substring(0, splitIndex).trim());
|
||||||
|
currentText = currentText.substring(splitIndex).trim();
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 确保表格不会超出页面宽度
|
||||||
|
const totalTableWidth = headers.length * cellWidth;
|
||||||
|
const adjustedCellWidth = totalTableWidth > pageWidth ? pageWidth / headers.length : cellWidth;
|
||||||
|
|
||||||
|
// 绘制表头
|
||||||
|
headers.forEach((header, i) => {
|
||||||
|
const lines = calculateLines(header, adjustedCellWidth - 10);
|
||||||
|
const cellHeight = Math.max(baseCellHeight, lines.length * 15);
|
||||||
|
|
||||||
|
doc.rect(marginLeft + i * adjustedCellWidth, currentY, adjustedCellWidth, cellHeight).stroke();
|
||||||
|
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.font('SourceHanSansBold');
|
||||||
|
} else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') {
|
||||||
|
doc.font('Arial Unicode MS Bold');
|
||||||
|
}
|
||||||
|
|
||||||
|
doc.fontSize(fontSize).text(header, marginLeft + i * adjustedCellWidth + 5, currentY + (cellHeight - lines.length * 15) / 2, {
|
||||||
|
width: adjustedCellWidth - 10,
|
||||||
|
height: cellHeight - 10
|
||||||
|
});
|
||||||
|
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 移动到下一行(无间隙)
|
||||||
|
const headerLines = headers.map(header => calculateLines(header, adjustedCellWidth - 10).length);
|
||||||
|
const maxHeaderLines = Math.max(...headerLines);
|
||||||
|
currentY += Math.max(baseCellHeight, maxHeaderLines * 15);
|
||||||
|
|
||||||
|
// 绘制行
|
||||||
|
rows.forEach(row => {
|
||||||
|
const rowLines = row.map(cell => calculateLines(cell.toString(), adjustedCellWidth - 10).length);
|
||||||
|
const maxRowLines = Math.max(...rowLines);
|
||||||
|
const rowHeight = Math.max(baseCellHeight, maxRowLines * 15);
|
||||||
|
|
||||||
|
row.forEach((cell, i) => {
|
||||||
|
doc.rect(marginLeft + i * adjustedCellWidth, currentY, adjustedCellWidth, rowHeight).stroke();
|
||||||
|
const cellLines = calculateLines(cell.toString(), adjustedCellWidth - 10);
|
||||||
|
doc.fontSize(fontSize).text(cell.toString(), marginLeft + i * adjustedCellWidth + 5, currentY + (rowHeight - cellLines.length * 15) / 2, {
|
||||||
|
width: adjustedCellWidth - 10,
|
||||||
|
height: rowHeight - 10
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
currentY += rowHeight; // 无间隙
|
||||||
|
});
|
||||||
|
|
||||||
|
doc.y = currentY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:添加base64图片
|
||||||
|
function addBase64Image(base64String, maxWidth = 400, maxHeight = 300) {
|
||||||
|
try {
|
||||||
|
// 移除base64前缀
|
||||||
|
const base64Data = base64String.replace(/^data:image\/\w+;base64,/, '');
|
||||||
|
// 将base64转换为缓冲区
|
||||||
|
const imageBuffer = Buffer.from(base64Data, 'base64');
|
||||||
|
// 获取图片尺寸
|
||||||
|
const image = doc.openImage(imageBuffer);
|
||||||
|
// 计算缩放比例
|
||||||
|
const scale = Math.min(maxWidth / image.width, maxHeight / image.height, 1);
|
||||||
|
// 添加图片,从左边距开始
|
||||||
|
doc.image(image, doc.page.margins.left, doc.y, {
|
||||||
|
width: image.width * scale,
|
||||||
|
height: image.height * scale
|
||||||
|
});
|
||||||
|
// 移动文档指针
|
||||||
|
doc.y += image.height * scale + 10;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('添加图片失败:', error);
|
||||||
|
doc.fontSize(10).text('图片加载失败', doc.page.margins.left, doc.y).moveDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制考生信息表格
|
||||||
|
drawTable(
|
||||||
|
['姓名', '身份证号', '准考证号'],
|
||||||
|
[[examineeName, idCard, admissionTicket]]
|
||||||
|
);
|
||||||
|
doc.moveDown();
|
||||||
|
|
||||||
|
// 绘制考试信息表格
|
||||||
|
drawTable(
|
||||||
|
['考试日期', '开始时间', '结束时间', '用时(分钟)'],
|
||||||
|
[[examDate, startTime.split(' ')[1], endTime.split(' ')[1], durationMinutes.toString()]]
|
||||||
|
);
|
||||||
|
doc.moveDown();
|
||||||
|
|
||||||
|
// 绘制试卷信息表格
|
||||||
|
drawTable(
|
||||||
|
['总题量', '试卷总分', '考试得分'],
|
||||||
|
[[totalQuestions.toString(), totalScore.toString(), realScore.toString()]]
|
||||||
|
);
|
||||||
|
doc.moveDown(2);
|
||||||
|
|
||||||
|
// 添加题目信息
|
||||||
|
paperData.questions.forEach((question, index) => {
|
||||||
|
// 题目类型和描述
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.font('SourceHanSansBold');
|
||||||
|
} else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') {
|
||||||
|
doc.font('Arial Unicode MS Bold');
|
||||||
|
}
|
||||||
|
doc.fontSize(14).text(`第 ${index + 1} 题 (${question.question_type_name})`, doc.page.margins.left).moveDown();
|
||||||
|
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
// 确保题干描述从左边距开始
|
||||||
|
doc.fontSize(12).text(question.question_description, {
|
||||||
|
x: doc.page.margins.left,
|
||||||
|
width: doc.page.width - doc.page.margins.left - doc.page.margins.right
|
||||||
|
}).moveDown();
|
||||||
|
|
||||||
|
// 添加图片
|
||||||
|
if (question.images && question.images.length > 0) {
|
||||||
|
doc.fontSize(12).text('图片:', doc.page.margins.left).moveDown();
|
||||||
|
question.images.forEach((image) => {
|
||||||
|
if (image.image_base64) {
|
||||||
|
addBase64Image(image.image_base64);
|
||||||
|
} else {
|
||||||
|
doc.fontSize(10).text(`图片: ${image.image_name || '无名称'}`, doc.page.margins.left).moveDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加数据集
|
||||||
|
if (question.datasets && question.datasets.length > 0) {
|
||||||
|
doc.fontSize(12).text('数据集:', doc.page.margins.left).moveDown();
|
||||||
|
question.datasets.forEach((dataset, dataIndex) => {
|
||||||
|
doc.fontSize(10).text(`数据集 ${dataIndex + 1} (${dataset.dataset_name || '无名称'})`, doc.page.margins.left).moveDown();
|
||||||
|
// 尝试解析数据集数据为表格
|
||||||
|
try {
|
||||||
|
const datasetData = JSON.parse(dataset.dataset_data);
|
||||||
|
if (Array.isArray(datasetData) && datasetData.length > 0) {
|
||||||
|
// 假设第一行是表头
|
||||||
|
const headers = Object.keys(datasetData[0]);
|
||||||
|
const rows = datasetData.map(item => headers.map(header => item[header]));
|
||||||
|
// 缩小单元格宽度以适应页面
|
||||||
|
drawTable(headers, rows, 80, 20);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('解析数据集失败:', error);
|
||||||
|
doc.fontSize(10).text(`数据集内容: ${dataset.dataset_data.substring(0, 100)}...`, doc.page.margins.left).moveDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加填空题
|
||||||
|
if (question.blanks && question.blanks.length > 0) {
|
||||||
|
question.blanks.forEach((blank, blankIndex) => {
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.font('SourceHanSansBold');
|
||||||
|
} else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') {
|
||||||
|
doc.font('Arial Unicode MS Bold');
|
||||||
|
}
|
||||||
|
doc.fontSize(12).text(`填空 ${blankIndex + 1}`, doc.page.margins.left).moveDown();
|
||||||
|
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
// 确保问题描述从左边距开始
|
||||||
|
doc.fontSize(12).text(blank.blank_description, {
|
||||||
|
x: doc.page.margins.left,
|
||||||
|
width: doc.page.width - doc.page.margins.left - doc.page.margins.right
|
||||||
|
}).moveDown();
|
||||||
|
drawTable(
|
||||||
|
['正确答案', '考生答案', '分值', '得分'],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
Array.isArray(blank.correct_answers) ? blank.correct_answers.join(', ') : blank.correct_answers,
|
||||||
|
Array.isArray(blank.examinee_answers) ? blank.examinee_answers.join(', ') : blank.examinee_answers,
|
||||||
|
blank.score.toString(),
|
||||||
|
blank.score_real.toString()
|
||||||
|
]
|
||||||
|
],
|
||||||
|
100
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加选择题
|
||||||
|
if (question.choices && question.choices.length > 0) {
|
||||||
|
question.choices.forEach((choice, choiceIndex) => {
|
||||||
|
if (boldFontLoaded) {
|
||||||
|
doc.font('SourceHanSansBold');
|
||||||
|
} else if (process.platform === 'darwin' && currentFont === 'Arial Unicode MS') {
|
||||||
|
doc.font('Arial Unicode MS Bold');
|
||||||
|
}
|
||||||
|
doc.fontSize(12).text(`选择题 ${choiceIndex + 1}`, doc.page.margins.left).moveDown();
|
||||||
|
|
||||||
|
if (currentFont) {
|
||||||
|
doc.font(currentFont);
|
||||||
|
}
|
||||||
|
// 确保问题描述从左边距开始
|
||||||
|
doc.fontSize(12).text(choice.choice_description, {
|
||||||
|
x: doc.page.margins.left,
|
||||||
|
width: doc.page.width - doc.page.margins.left - doc.page.margins.right
|
||||||
|
}).moveDown();
|
||||||
|
// 显示选项
|
||||||
|
if (choice.choice_options && choice.choice_options.length > 0) {
|
||||||
|
const optionsText = choice.choice_options.map((option, i) => {
|
||||||
|
const optionLabel = String.fromCharCode(65 + i); // A, B, C, D...
|
||||||
|
return `${optionLabel}. ${option}`;
|
||||||
|
}).join(' ');
|
||||||
|
doc.fontSize(10).text(`选项: ${optionsText}`, doc.page.margins.left).moveDown();
|
||||||
|
}
|
||||||
|
drawTable(
|
||||||
|
['正确答案', '考生答案', '分值', '得分'],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
Array.isArray(choice.correct_answers) ? choice.correct_answers.join(', ') : choice.correct_answers,
|
||||||
|
Array.isArray(choice.examinee_answers) ? choice.examinee_answers.join(', ') : choice.examinee_answers,
|
||||||
|
choice.score.toString(),
|
||||||
|
choice.score_real.toString()
|
||||||
|
]
|
||||||
|
],
|
||||||
|
100
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
|
if (doc.y > 700) {
|
||||||
|
doc.addPage();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 结束文档
|
||||||
|
doc.end();
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
writeStream.on('finish', async () => {
|
||||||
|
try {
|
||||||
|
// 调用copyToDesk方法将文件复制到桌面,获取新的路径
|
||||||
|
const newFilePath = await exports.copyToDesk(filePath);
|
||||||
|
resolve(newFilePath);
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
writeStream.on('error', reject);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成PDF失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
@ -248,7 +248,7 @@ async function fetchQuestionsCountAndScore() {
|
|||||||
* 初始化试题相关IPC通信
|
* 初始化试题相关IPC通信
|
||||||
* @param {Electron.IpcMain} ipcMain - IPC主进程实例
|
* @param {Electron.IpcMain} ipcMain - IPC主进程实例
|
||||||
*/
|
*/
|
||||||
async function initQuestionIpc(ipcMain) {
|
function initQuestionIpc(ipcMain) {
|
||||||
// 题干管理相关IPC
|
// 题干管理相关IPC
|
||||||
ipcMain.handle("question-create", async (event, questionData) => {
|
ipcMain.handle("question-create", async (event, questionData) => {
|
||||||
try {
|
try {
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="hello">
|
|
||||||
<h1>{{ msg }}</h1>
|
|
||||||
<p>
|
|
||||||
For a guide and recipes on how to configure / customize this project,<br>
|
|
||||||
check out the
|
|
||||||
<a href="https://cli.vuejs.org" target="_blank" rel="noopener">vue-cli documentation</a>.
|
|
||||||
</p>
|
|
||||||
<h3>Installed CLI Plugins</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-babel" target="_blank" rel="noopener">babel</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-router" target="_blank" rel="noopener">router</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-vuex" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Essential Links</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://vuejs.org" target="_blank" rel="noopener">Core Docs</a></li>
|
|
||||||
<li><a href="https://forum.vuejs.org" target="_blank" rel="noopener">Forum</a></li>
|
|
||||||
<li><a href="https://chat.vuejs.org" target="_blank" rel="noopener">Community Chat</a></li>
|
|
||||||
<li><a href="https://twitter.com/vuejs" target="_blank" rel="noopener">Twitter</a></li>
|
|
||||||
<li><a href="https://news.vuejs.org" target="_blank" rel="noopener">News</a></li>
|
|
||||||
</ul>
|
|
||||||
<h3>Ecosystem</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a href="https://router.vuejs.org" target="_blank" rel="noopener">vue-router</a></li>
|
|
||||||
<li><a href="https://vuex.vuejs.org" target="_blank" rel="noopener">vuex</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/vue-devtools#vue-devtools" target="_blank" rel="noopener">vue-devtools</a></li>
|
|
||||||
<li><a href="https://vue-loader.vuejs.org" target="_blank" rel="noopener">vue-loader</a></li>
|
|
||||||
<li><a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">awesome-vue</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
name: 'HelloWorld',
|
|
||||||
props: {
|
|
||||||
msg: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Add "scoped" attribute to limit CSS to this component only -->
|
|
||||||
<style scoped>
|
|
||||||
h3 {
|
|
||||||
margin: 40px 0 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style-type: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 0 10px;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #42b983;
|
|
||||||
}
|
|
||||||
</style>
|
|
53
src/components/common/Footer.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<footer class="w-100 py-3 mt-auto footer-container">
|
||||||
|
<el-row type="flex" justify="space-between" align="middle" height="100%">
|
||||||
|
<el-col :span="12">
|
||||||
|
<p class="ml-3" style="text-align: left !important;">© {{ thisYear }} 抚顺市统计局</p>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="12">
|
||||||
|
<p class="mr-3" style="text-align: right !important;">题库版本:{{ questionBankVersion || '未知' }}</p>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Footer',
|
||||||
|
components: {
|
||||||
|
ElRow: require('element-ui').Row,
|
||||||
|
ElCol: require('element-ui').Col
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
thisYear: new Date().getFullYear(),
|
||||||
|
questionBankVersion: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.fetchQuestionBankVersion()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async fetchQuestionBankVersion() {
|
||||||
|
try {
|
||||||
|
// 调用electronAPI获取系统配置
|
||||||
|
const config = await window.electronAPI.systemGetConfig()
|
||||||
|
// 设置题库版本
|
||||||
|
this.questionBankVersion = config.question_bank_version || '未知'
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取题库版本失败:', error)
|
||||||
|
this.questionBankVersion = '获取失败'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.footer-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
color: #6c757d;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
</style>
|
36
src/components/common/Header.vue
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header-container text-white shadow-md">
|
||||||
|
<div class="h-100">
|
||||||
|
<div class="row h-100 align-items-center justify-content-center">
|
||||||
|
<div class="col-auto">
|
||||||
|
<h2 class="display-6 m-0 d-flex align-items-center">
|
||||||
|
<img src="../../assets/logo.png" alt="logo" style="width:50px;height:50px;margin-right:1rem;">
|
||||||
|
{{ thisYear }}<strong>年抚顺市统计行业职工技能大赛考试系统</strong>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Header',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
thisYear: new Date().getFullYear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header-container {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #4f74ed; /* 深蓝色背景 */
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
height: 80px; /* 设置固定高度 */
|
||||||
|
padding: 0 2rem;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
</style>
|
52
src/components/common/Loading.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="loading-overlay" v-if="visible">
|
||||||
|
<div class="loading-spinner">
|
||||||
|
<el-loading-spinner></el-loading-spinner>
|
||||||
|
<p class="loading-text">{{ text || '加载中...' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'Loading',
|
||||||
|
props: {
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.loading-overlay {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
background-color: white;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
@ -47,6 +47,41 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
questionGetStatistics: () => ipcRenderer.invoke('question-get-statistics'),
|
questionGetStatistics: () => ipcRenderer.invoke('question-get-statistics'),
|
||||||
questionGetQuestionById: (questionId) => ipcRenderer.invoke('question-get-question-by-id', questionId),
|
questionGetQuestionById: (questionId) => ipcRenderer.invoke('question-get-question-by-id', questionId),
|
||||||
|
|
||||||
|
// 考试服务相关接口
|
||||||
|
examCreate: (examData) => ipcRenderer.invoke('exam-create', examData),
|
||||||
|
examUpdate: (id, examData) => ipcRenderer.invoke('exam-update', { id, examData }),
|
||||||
|
examFetchLast: () => ipcRenderer.invoke('exam-fetch-last'),
|
||||||
|
examFetchAll: () => ipcRenderer.invoke('exam-fetch-all'),
|
||||||
|
examFetchById: (id) => ipcRenderer.invoke('exam-fetch-by-id', id),
|
||||||
|
examDelete: (id) => ipcRenderer.invoke('exam-delete', id),
|
||||||
|
|
||||||
|
// 考生服务相关接口
|
||||||
|
examineeFetchAll: () => ipcRenderer.invoke('examinee-fetch-all'),
|
||||||
|
userLogin: ({ idCard, admissionTicket }) => ipcRenderer.invoke('user-login', { idCard, admissionTicket }),
|
||||||
|
examineeFetchById: (id) => ipcRenderer.invoke('examinee-fetch-by-id', id),
|
||||||
|
examineeCreate: (examineeData) => ipcRenderer.invoke('examinee-create', examineeData),
|
||||||
|
examineeUpdate: (id, examineeData) => ipcRenderer.invoke('examinee-update', { id, examineeData }),
|
||||||
|
examineeDelete: (id) => ipcRenderer.invoke('examinee-delete', id),
|
||||||
|
|
||||||
|
// 考生考试服务相关接口
|
||||||
|
examingGeneratePaper: ({ examineeId, examId }) => ipcRenderer.invoke('examing-generate-paper', { examineeId, examId }),
|
||||||
|
examingGetPaperStatus: ({ examineeId, examId }) => ipcRenderer.invoke('examing-get-paper-status', { examineeId, examId }),
|
||||||
|
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 }),
|
||||||
|
examingStartPaper: ({ paperId }) => ipcRenderer.invoke('examing-start-paper', { paperId }),
|
||||||
|
examingSubmitPaper: ({ paperId }) => ipcRenderer.invoke('examing-submit-paper', { paperId }),
|
||||||
|
examingEndPaper: ({ paperId }) => ipcRenderer.invoke('examing-end-paper', { paperId }),
|
||||||
|
examingProcessPaper: ({ paperId }) => ipcRenderer.invoke('examing-process-paper', { paperId }),
|
||||||
|
examingCheckPaperAnswers: ({ paperId }) => ipcRenderer.invoke('examing-check-paper-answers', { paperId }),
|
||||||
|
|
||||||
|
// 文件服务相关接口
|
||||||
|
fileTest: () => ipcRenderer.invoke('file-test'),
|
||||||
|
fileGeneratePdf: (pdfData, fileName) => ipcRenderer.invoke('file-generate-pdf', pdfData, fileName),
|
||||||
|
fileGeneratePaperPdf: (jsonString) => ipcRenderer.invoke('file-generate-paper-pdf', jsonString),
|
||||||
|
fileCopyToDesktop: (filePath) => ipcRenderer.invoke('file-copy-to-desktop', filePath),
|
||||||
|
|
||||||
// 保留原有的ipcRenderer接口,确保兼容性
|
// 保留原有的ipcRenderer接口,确保兼容性
|
||||||
ipcRenderer: {
|
ipcRenderer: {
|
||||||
invoke: (channel, data) => ipcRenderer.invoke(channel, data)
|
invoke: (channel, data) => ipcRenderer.invoke(channel, data)
|
||||||
|
@ -1,22 +1,14 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Home from '../views/Home.vue'
|
import WelcomeView from '../views/WelcomeView.vue'
|
||||||
|
|
||||||
Vue.use(VueRouter)
|
Vue.use(VueRouter)
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'Home',
|
name: 'Welcome',
|
||||||
component: Home
|
component: WelcomeView
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/about',
|
|
||||||
name: 'About',
|
|
||||||
// route level code-splitting
|
|
||||||
// this generates a separate chunk (about.[hash].js) for this route
|
|
||||||
// which is lazy-loaded when the route is visited.
|
|
||||||
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5,8 +5,18 @@ Vue.use(Vuex)
|
|||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
|
examinee: null, // 保存考生信息
|
||||||
|
isLoggedIn: false // 登录状态
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
setExaminee(state, examineeInfo) {
|
||||||
|
state.examinee = examineeInfo
|
||||||
|
state.isLoggedIn = true
|
||||||
|
},
|
||||||
|
clearExaminee(state) {
|
||||||
|
state.examinee = null
|
||||||
|
state.isLoggedIn = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
},
|
},
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="about">
|
|
||||||
<h1>This is an about page</h1>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
@ -1,627 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="home">
|
|
||||||
<h1>Argon2 Test</h1>
|
|
||||||
|
|
||||||
<!-- 测试FontAwesome图标 -->
|
|
||||||
<div class="icon-test">
|
|
||||||
<h3>FontAwesome 图标测试</h3>
|
|
||||||
<div class="icons">
|
|
||||||
<font-awesome-icon :icon="['fas', 'calculator']" class="icon" size="3x" />
|
|
||||||
<font-awesome-icon :icon="['far', 'file-alt']" class="icon" size="3x" />
|
|
||||||
<font-awesome-icon :icon="['fab', 'vuejs']" class="icon" size="3x" />
|
|
||||||
<font-awesome-icon :icon="['fas', 'database']" class="icon" size="3x" />
|
|
||||||
<font-awesome-icon :icon="['fas', 'lock']" class="icon" size="3x" />
|
|
||||||
<font-awesome-icon :icon="['fas', 'key']" class="icon" size="3x" />
|
|
||||||
<font-awesome-icon :icon="['fas', 'user']" class="icon" size="3x" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 测试Element UI组件 -->
|
|
||||||
<div class="element-test">
|
|
||||||
<h3>Element UI 组件测试</h3>
|
|
||||||
<el-card class="card-test">
|
|
||||||
<template slot="header">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>Argon2哈希计算</span>
|
|
||||||
<el-button type="primary" size="small" @click="showDialog">打开对话框</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="input-container">
|
|
||||||
<el-input
|
|
||||||
v-model="inputString"
|
|
||||||
placeholder="请输入要计算哈希的字符串"
|
|
||||||
@input="onInputChange"
|
|
||||||
prefix-icon="el-icon-lock"
|
|
||||||
clearable
|
|
||||||
style="width: 300px;"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="result" class="result-container">
|
|
||||||
<el-alert title="计算结果" type="success" :closable="false">
|
|
||||||
<pre>{{ result }}</pre>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="error" class="error-container">
|
|
||||||
<el-alert :title="error" type="error" show-icon :closable="false" />
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 测试Element UI对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
title="组件测试对话框"
|
|
||||||
:visible.sync="dialogVisible"
|
|
||||||
width="50%"
|
|
||||||
:before-close="handleClose"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p>这是一个Element UI对话框组件</p>
|
|
||||||
<!-- 切换为Element UI图标选择 -->
|
|
||||||
<el-select v-model="selectedIconId" placeholder="选择一个图标">
|
|
||||||
<el-option v-for="icon in simplifiedIconOptions" :key="icon.id" :label="icon.label" :value="icon.id" />
|
|
||||||
</el-select>
|
|
||||||
<div style="margin-top: 20px;">
|
|
||||||
<!-- 使用Element UI图标 -->
|
|
||||||
<i v-if="selectedIconId" :class="getElementIcon(selectedIconId)" style="font-size: 32px;"></i>
|
|
||||||
<span v-else>请选择一个图标</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span slot="footer" class="dialog-footer">
|
|
||||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
|
||||||
<el-button type="primary" @click="dialogVisible = false">确 定</el-button>
|
|
||||||
</span>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 数据库初始化测试 -->
|
|
||||||
<div class="database-test">
|
|
||||||
<h3>数据库初始化测试</h3>
|
|
||||||
<el-card class="card-test">
|
|
||||||
<template slot="header">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>数据库状态</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="database-status">
|
|
||||||
<el-alert
|
|
||||||
:title="`数据库状态: ${dbInitialized ? '已初始化' : '未初始化'}`"
|
|
||||||
:type="dbInitialized ? 'success' : 'warning'"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="!dbInitialized" class="initialize-button-container">
|
|
||||||
<el-button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
@click="initializeDatabase"
|
|
||||||
:loading="initializing"
|
|
||||||
style="margin-top: 20px;"
|
|
||||||
>
|
|
||||||
{{ initializing ? '正在初始化...' : '初始化数据库' }}
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="dbInitResult" class="init-result">
|
|
||||||
<el-alert
|
|
||||||
:title="dbInitResult.success ? '初始化成功' : '初始化失败'"
|
|
||||||
:type="dbInitResult.success ? 'success' : 'error'"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
>
|
|
||||||
<template slot="desc" v-if="!dbInitResult.success">
|
|
||||||
失败原因: {{ dbInitResult.error }}
|
|
||||||
</template>
|
|
||||||
</el-alert>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="dbInitError" class="init-error">
|
|
||||||
<el-alert
|
|
||||||
:title="`获取数据库状态失败: ${dbInitError}`"
|
|
||||||
type="error"
|
|
||||||
show-icon
|
|
||||||
:closable="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 配置列表显示 -->
|
|
||||||
<div class="config-list-test" v-if="dbInitialized">
|
|
||||||
<h3>配置列表</h3>
|
|
||||||
<el-card class="card-test">
|
|
||||||
<template slot="header">
|
|
||||||
<div class="card-header">
|
|
||||||
<span>系统配置项</span>
|
|
||||||
<el-button type="primary" size="small" @click="fetchConfigList">刷新列表</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div v-if="loadingConfigList" class="loading-container">
|
|
||||||
<el-row type="flex" justify="center">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-empty description="正在加载配置..." />
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="configList.length === 0" class="empty-container">
|
|
||||||
<el-row type="flex" justify="center">
|
|
||||||
<el-col :span="6">
|
|
||||||
<el-empty description="暂无配置项" />
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else class="config-table-container">
|
|
||||||
<el-table :data="configList" stripe border style="width: 100%">
|
|
||||||
<el-table-column prop="id" label="ID" width="80" align="center" />
|
|
||||||
<el-table-column prop="key" label="配置键" min-width="180" />
|
|
||||||
<el-table-column prop="value" label="配置值" min-width="200">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<div v-if="editingConfigId === scope.row.id">
|
|
||||||
<el-input v-model="configEdit[scope.row.id]" placeholder="配置值" />
|
|
||||||
</div>
|
|
||||||
<div v-else class="config-value-display">
|
|
||||||
{{ scope.row.value }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="description" label="描述" min-width="250" />
|
|
||||||
<el-table-column prop="created_at" label="创建时间" width="180" align="center" />
|
|
||||||
<el-table-column prop="updated_at" label="更新时间" width="180" align="center" />
|
|
||||||
<el-table-column label="操作" width="150" align="center">
|
|
||||||
<template slot-scope="scope">
|
|
||||||
<el-button v-if="editingConfigId !== scope.row.id" type="primary" size="small" @click="startEditConfig(scope.row)">编辑</el-button>
|
|
||||||
<div v-else>
|
|
||||||
<el-button type="success" size="small" @click="saveConfig(scope.row)">保存</el-button>
|
|
||||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="configListError" class="config-list-error">
|
|
||||||
<el-alert :title="`获取配置列表失败: ${configListError}`" type="error" show-icon :closable="false" />
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bootstrap组件测试 -->
|
|
||||||
<div class="bootstrap-test">
|
|
||||||
<h3>Bootstrap 组件测试</h3>
|
|
||||||
|
|
||||||
<!-- 1. 测试Bootstrap按钮 -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4>按钮样式</h4>
|
|
||||||
<button type="button" class="btn btn-primary mr-2">Primary</button>
|
|
||||||
<button type="button" class="btn btn-secondary mr-2">Secondary</button>
|
|
||||||
<button type="button" class="btn btn-success mr-2">Success</button>
|
|
||||||
<button type="button" class="btn btn-danger mr-2">Danger</button>
|
|
||||||
<button type="button" class="btn btn-warning mr-2">Warning</button>
|
|
||||||
<button type="button" class="btn btn-info mr-2">Info</button>
|
|
||||||
<button type="button" class="btn btn-light">Light</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 2. 测试Bootstrap卡片 -->
|
|
||||||
<div class="card mb-4" style="max-width: 500px; margin: 0 auto;">
|
|
||||||
<div class="card-header bg-primary text-white">Bootstrap 卡片标题</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<h5 class="card-title">这是一个Bootstrap卡片</h5>
|
|
||||||
<p class="card-text">卡片内容区域,可以放置文本、图片等各种内容。</p>
|
|
||||||
<a href="#" class="btn btn-primary" @click.prevent="showBootstrapModal">打开模态框</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 3. 测试Bootstrap表单 -->
|
|
||||||
<div class="card mb-4" style="max-width: 500px; margin: 0 auto;">
|
|
||||||
<div class="card-header bg-secondary text-white">Bootstrap 表单</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="exampleInputEmail1">邮箱地址</label>
|
|
||||||
<input type="email" class="form-control" id="exampleInputEmail1" aria-describedby="emailHelp" placeholder="请输入邮箱">
|
|
||||||
<small id="emailHelp" class="form-text text-muted">我们不会分享您的邮箱给任何人</small>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="exampleInputPassword1">密码</label>
|
|
||||||
<input type="password" class="form-control" id="exampleInputPassword1" placeholder="密码">
|
|
||||||
</div>
|
|
||||||
<div class="form-check">
|
|
||||||
<input type="checkbox" class="form-check-input" id="exampleCheck1">
|
|
||||||
<label class="form-check-label" for="exampleCheck1">记住我</label>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary mt-2">提交</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 4. 测试Bootstrap导航 -->
|
|
||||||
<div class="mb-4">
|
|
||||||
<h4>导航栏</h4>
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-light bg-light">
|
|
||||||
<a class="navbar-brand" href="#">Bootstrap</a>
|
|
||||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
|
||||||
<span class="navbar-toggler-icon"></span>
|
|
||||||
</button>
|
|
||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
|
||||||
<ul class="navbar-nav">
|
|
||||||
<li class="nav-item active">
|
|
||||||
<a class="nav-link" href="#">首页 <span class="sr-only">(current)</span></a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#">特性</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#">关于</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bootstrap模态框 -->
|
|
||||||
<div class="modal fade" id="bootstrapModal" tabindex="-1" role="dialog" aria-labelledby="bootstrapModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="bootstrapModalLabel">Bootstrap模态框</h5>
|
|
||||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
|
||||||
<span aria-hidden="true">×</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
这是一个Bootstrap模态框,它使用JavaScript来控制显示和隐藏。
|
|
||||||
如果您能看到这个模态框,说明Bootstrap的JavaScript组件正常工作!
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">关闭</button>
|
|
||||||
<button type="button" class="btn btn-primary">保存更改</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// 这里不需要导入FontAwesome了,但为了保留页面顶部的图标测试,暂时保留
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Home',
|
|
||||||
components: {
|
|
||||||
FontAwesomeIcon
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
inputString: '',
|
|
||||||
result: '',
|
|
||||||
error: '',
|
|
||||||
dialogVisible: false,
|
|
||||||
// 使用简单的字符串ID代替复杂的数组
|
|
||||||
selectedIconId: 'file',
|
|
||||||
// 简化的图标选项配置
|
|
||||||
simplifiedIconOptions: [
|
|
||||||
{ id: 'file', label: '文件', elementIcon: 'el-icon-document' },
|
|
||||||
{ id: 'lock', label: '锁', elementIcon: 'el-icon-lock' },
|
|
||||||
{ id: 'key', label: '钥匙', elementIcon: 'el-icon-key' },
|
|
||||||
{ id: 'user', label: '用户', elementIcon: 'el-icon-user' },
|
|
||||||
{ id: 'database', label: '数据库', elementIcon: 'el-icon-data-line' },
|
|
||||||
{ id: 'search', label: '搜索', elementIcon: 'el-icon-search' },
|
|
||||||
{ id: 'edit', label: '编辑', elementIcon: 'el-icon-edit' },
|
|
||||||
{ id: 'delete', label: '删除', elementIcon: 'el-icon-delete' }
|
|
||||||
],
|
|
||||||
// 数据库相关状态
|
|
||||||
dbInitialized: null,
|
|
||||||
dbInitResult: null,
|
|
||||||
dbInitError: null,
|
|
||||||
initializing: false,
|
|
||||||
// 配置列表相关状态
|
|
||||||
configList: [],
|
|
||||||
loadingConfigList: false,
|
|
||||||
configListError: null,
|
|
||||||
editingConfigId: null,
|
|
||||||
configEdit: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async onInputChange() {
|
|
||||||
if (this.inputString.trim()) {
|
|
||||||
try {
|
|
||||||
this.error = ''
|
|
||||||
|
|
||||||
// 安全地检查 window.electron 是否可用
|
|
||||||
if (typeof window !== 'undefined' && window.electron && window.electron.ipcRenderer) {
|
|
||||||
const result = await window.electron.ipcRenderer.invoke('hashTest', this.inputString)
|
|
||||||
this.result = result
|
|
||||||
} else {
|
|
||||||
throw new Error('Electron IPC 不可用')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.error = `计算失败:${err.message}`
|
|
||||||
this.result = ''
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this.result = ''
|
|
||||||
this.error = ''
|
|
||||||
}
|
|
||||||
},
|
|
||||||
showDialog() {
|
|
||||||
this.dialogVisible = true
|
|
||||||
},
|
|
||||||
handleClose(done) {
|
|
||||||
this.$confirm('确认关闭?')
|
|
||||||
.then(_ => {
|
|
||||||
done()
|
|
||||||
})
|
|
||||||
.catch(_ => {})
|
|
||||||
},
|
|
||||||
// 根据ID获取Element UI图标类名的辅助方法
|
|
||||||
getElementIcon(iconId) {
|
|
||||||
const icon = this.simplifiedIconOptions.find(item => item.id === iconId)
|
|
||||||
return icon ? icon.elementIcon : ''
|
|
||||||
},
|
|
||||||
// 显示Bootstrap模态框
|
|
||||||
showBootstrapModal() {
|
|
||||||
$('#bootstrapModal').modal('show')
|
|
||||||
},
|
|
||||||
// 检查数据库初始化状态
|
|
||||||
async checkDatabaseInitialized() {
|
|
||||||
try {
|
|
||||||
this.dbInitError = null
|
|
||||||
|
|
||||||
// 安全地检查 window.electronAPI 是否可用
|
|
||||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
|
||||||
const initialized = await window.electronAPI.checkDatabaseInitialized()
|
|
||||||
this.dbInitialized = initialized
|
|
||||||
} else {
|
|
||||||
throw new Error('Electron API 不可用')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.dbInitError = err.message
|
|
||||||
console.error('检查数据库初始化状态失败:', err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 初始化数据库
|
|
||||||
async initializeDatabase() {
|
|
||||||
try {
|
|
||||||
this.dbInitResult = null
|
|
||||||
this.initializing = true
|
|
||||||
|
|
||||||
// 安全地检查 window.electronAPI 是否可用
|
|
||||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
|
||||||
const result = await window.electronAPI.initializeDatabase()
|
|
||||||
this.dbInitResult = result
|
|
||||||
|
|
||||||
// 如果初始化成功,更新状态
|
|
||||||
if (result && result.success) {
|
|
||||||
this.dbInitialized = true
|
|
||||||
// 初始化成功后立即获取配置列表
|
|
||||||
await this.fetchConfigList()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new Error('Electron API 不可用')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.dbInitResult = { success: false, error: err.message }
|
|
||||||
console.error('数据库初始化失败:', err)
|
|
||||||
} finally {
|
|
||||||
this.initializing = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取配置列表
|
|
||||||
async fetchConfigList() {
|
|
||||||
try {
|
|
||||||
this.loadingConfigList = true
|
|
||||||
this.configListError = null
|
|
||||||
|
|
||||||
// 安全地检查 window.electronAPI 是否可用
|
|
||||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
|
||||||
const configList = await window.electronAPI.configFetchAll()
|
|
||||||
this.configList = configList
|
|
||||||
} else {
|
|
||||||
throw new Error('Electron API 不可用')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.configListError = err.message
|
|
||||||
console.error('获取配置列表失败:', err)
|
|
||||||
} finally {
|
|
||||||
this.loadingConfigList = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 开始编辑配置
|
|
||||||
startEditConfig(config) {
|
|
||||||
this.editingConfigId = config.id
|
|
||||||
// 存储当前值用于编辑
|
|
||||||
this.$set(this.configEdit, config.id, config.value)
|
|
||||||
},
|
|
||||||
|
|
||||||
// 保存配置
|
|
||||||
async saveConfig(config) {
|
|
||||||
try {
|
|
||||||
// 安全地检查 window.electronAPI 是否可用
|
|
||||||
if (typeof window !== 'undefined' && window.electronAPI) {
|
|
||||||
const updatedConfig = {
|
|
||||||
id: config.id,
|
|
||||||
key: config.key,
|
|
||||||
value: this.configEdit[config.id],
|
|
||||||
description: config.description
|
|
||||||
}
|
|
||||||
|
|
||||||
await window.electronAPI.configSave(updatedConfig)
|
|
||||||
|
|
||||||
// 保存成功后更新本地数据
|
|
||||||
const index = this.configList.findIndex(item => item.id === config.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
this.configList[index].value = this.configEdit[config.id]
|
|
||||||
this.configList[index].updated_at = new Date().toLocaleString()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$message.success('配置保存成功')
|
|
||||||
this.cancelEdit()
|
|
||||||
} else {
|
|
||||||
throw new Error('Electron API 不可用')
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
this.$message.error(`保存失败: ${err.message}`)
|
|
||||||
console.error('保存配置失败:', err)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 取消编辑
|
|
||||||
cancelEdit() {
|
|
||||||
this.editingConfigId = null
|
|
||||||
this.configEdit = {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 组件挂载后检查数据库状态
|
|
||||||
async mounted() {
|
|
||||||
await this.checkDatabaseInitialized()
|
|
||||||
// 数据库初始化成功后,获取配置列表
|
|
||||||
if (this.dbInitialized) {
|
|
||||||
await this.fetchConfigList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.home {
|
|
||||||
text-align: center;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-test {
|
|
||||||
margin: 20px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icons {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 20px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
color: #409EFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
.element-test {
|
|
||||||
margin-top: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-test {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-container {
|
|
||||||
margin: 20px 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.result-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-wrap: break-word;
|
|
||||||
background-color: #f5f5f5;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer button:first-child {
|
|
||||||
margin-right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 数据库测试样式 */
|
|
||||||
.database-test {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.database-status {
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.initialize-button-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.init-result,
|
|
||||||
.init-error {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bootstrap-test {
|
|
||||||
margin-top: 40px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bootstrap-test h3 {
|
|
||||||
margin-bottom: 30px;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bootstrap-test h4 {
|
|
||||||
margin: 20px 0 10px 0;
|
|
||||||
color: #555;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 确保样式不与Element UI冲突 */
|
|
||||||
.bootstrap-test .btn {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 配置列表样式 */
|
|
||||||
.config-list-test {
|
|
||||||
margin-top: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container,
|
|
||||||
.empty-container {
|
|
||||||
padding: 40px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-table-container {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-list-error {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 配置值显示样式 */
|
|
||||||
.config-value-display {
|
|
||||||
padding: 5px 0;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
</style>
|
|
325
src/views/WelcomeView.vue
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
<template>
|
||||||
|
<div class="welcome-container main-background">
|
||||||
|
<el-container>
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<el-main>
|
||||||
|
<div class="d-flex align-items-center justify-content-center p-4" style="padding: 0; width: 600px;">
|
||||||
|
<!-- 数据库初始化提示卡片 -->
|
||||||
|
<div class="login-card bg-white rounded shadow-lg p-5 w-100 max-w-md" id="init-section" v-show="!isDatabaseInitialized">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-6">
|
||||||
|
<i class="fa fa-database text-primary" style="font-size: 64px;"></i>
|
||||||
|
</div>
|
||||||
|
<h2 class="display-6 mb-4">系统未初始化</h2>
|
||||||
|
<p class="fs-5 mb-6 text-muted">请点击下方按钮进行系统初始化,初始化完成后将自动显示登录界面</p>
|
||||||
|
<button id="initialize-db" @click="initializeDatabase" class="btn btn-primary px-8 py-3 fs-5" :disabled="isInitializing">
|
||||||
|
<i v-if="isInitializing" class="fa fa-spinner fa-spin me-2"></i>
|
||||||
|
<i v-else class="fa fa-refresh me-2"></i>
|
||||||
|
{{ isInitializing ? '初始化中...' : '数据初始化' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bootstrap登录卡片 -->
|
||||||
|
<div class="login-card bg-white rounded shadow-lg p-5 w-100 max-w-md" id="login-section" style="height: 500px;" v-show="isDatabaseInitialized">
|
||||||
|
<!-- 登录类型切换标签页 -->
|
||||||
|
<ul class="nav nav-tabs fs-4" id="loginTab" role="tablist">
|
||||||
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
|
<button class="nav-link rounded-0 active w-100" id="exam-tab" data-toggle="tab" data-target="#exam-login" type="button" role="tab" aria-controls="exam-login" aria-selected="true">
|
||||||
|
<i class="fa fa-graduation-cap me-2"></i>
|
||||||
|
考生登录
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item flex-fill" role="presentation">
|
||||||
|
<button class="nav-link rounded-0 w-100" id="admin-tab" data-toggle="tab" data-target="#admin-login" type="button" role="tab" aria-controls="admin-login" aria-selected="false">
|
||||||
|
<i class="fa fa-cog me-2"></i>
|
||||||
|
系统管理
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<!-- 登录表单内容 -->
|
||||||
|
<div class="tab-content fs-5 p-4 border border-left border-right border-bottom" id="loginTabContent" style="height: calc(100% - 60px);">
|
||||||
|
<!-- 考生登录表单 -->
|
||||||
|
<div class="h-100 tab-pane fade show active" id="exam-login" role="tabpanel" aria-labelledby="exam-tab">
|
||||||
|
<form @submit.prevent="handleExamineeLogin" class="d-flex flex-column h-100">
|
||||||
|
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
|
||||||
|
<label for="examineeIdCard" class="form-label">身份证号</label>
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<font-awesome-icon :icon="['fas', 'id-card']" />
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" id="examineeIdCard" v-model="examineeIdCard" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
|
||||||
|
<label for="examineeAdmissionTicket" class="form-label">准考证号</label>
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<font-awesome-icon :icon="['fas', 'key']" />
|
||||||
|
</span>
|
||||||
|
<input type="text" class="form-control" id="examineeAdmissionTicket" v-model="examineeAdmissionTicket" required>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex-grow-1 d-flex flex-column justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100 py-2 fs-5">
|
||||||
|
<i class="fa fa-sign-in me-2"></i>
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 管理员登录表单 -->
|
||||||
|
<div class="h-100 tab-pane fade" id="admin-login" role="tabpanel" aria-labelledby="admin-tab">
|
||||||
|
<form @submit.prevent="handleAdminLogin" class="d-flex flex-column h-100">
|
||||||
|
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
|
||||||
|
<label for="password" class="form-label">管理员密码</label>
|
||||||
|
<div class="input-group input-group-lg">
|
||||||
|
<span class="input-group-text">
|
||||||
|
<font-awesome-icon :icon="['fas', 'lock']" />
|
||||||
|
</span>
|
||||||
|
<input type="password" class="form-control" id="password" v-model="adminPassword" required>
|
||||||
|
</div>
|
||||||
|
<div id="admin-error-message" class="text-danger mt-2" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 flex-grow-1 d-flex flex-column justify-content-end">
|
||||||
|
<button type="submit" class="btn btn-primary w-100 py-2 fs-5" :disabled="isLoading">
|
||||||
|
<i v-if="isLoading" class="fa fa-spinner fa-spin me-2"></i>
|
||||||
|
<i v-else class="fa fa-sign-in me-2"></i>
|
||||||
|
{{ isLoading ? '登录中...' : '登录' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 导入组件
|
||||||
|
import Header from '../components/common/Header.vue'
|
||||||
|
import Footer from '../components/common/Footer.vue'
|
||||||
|
import { Message } from 'element-ui'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'WelcomeView',
|
||||||
|
components: {
|
||||||
|
Header,
|
||||||
|
Footer
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
examineeIdCard: '', // 身份证号
|
||||||
|
examineeAdmissionTicket: '', // 准考证号
|
||||||
|
adminPassword: '',
|
||||||
|
isDatabaseInitialized: false,
|
||||||
|
isInitializing: false,
|
||||||
|
isLoading: false // 添加加载状态
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.checkDatabaseStatus()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async checkDatabaseStatus() {
|
||||||
|
try {
|
||||||
|
console.log('组件挂载 - 开始检查数据库初始化状态');
|
||||||
|
const initialized = await window.electronAPI.checkDatabaseInitialized();
|
||||||
|
console.log('组件挂载 - 数据库初始化状态检查完成:', initialized);
|
||||||
|
this.isDatabaseInitialized = initialized;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查数据库初始化状态失败:', error);
|
||||||
|
Message.error('检查数据库初始化状态失败,请重试');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async initializeDatabase() {
|
||||||
|
try {
|
||||||
|
console.log('初始化数据库 - 开始');
|
||||||
|
this.isInitializing = true;
|
||||||
|
Message.info('开始初始化数据库...');
|
||||||
|
|
||||||
|
const result = await window.electronAPI.initializeDatabase();
|
||||||
|
console.log('初始化数据库 - 结果:', result);
|
||||||
|
|
||||||
|
// 修复:同时处理布尔值true和带success属性的对象
|
||||||
|
if (result === true || (result && result.success)) {
|
||||||
|
Message.success('数据库初始化成功!');
|
||||||
|
console.log('初始化数据库 - 成功,更新初始化状态');
|
||||||
|
this.isDatabaseInitialized = true;
|
||||||
|
} else {
|
||||||
|
const errorMessage = result && result.error ? result.error : '未知错误';
|
||||||
|
Message.error(`数据库初始化失败: ${errorMessage}`);
|
||||||
|
console.error('初始化数据库 - 失败:', errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('数据库初始化失败:', error);
|
||||||
|
Message.error(`数据库初始化失败: ${error.message || '未知错误'}`);
|
||||||
|
} finally {
|
||||||
|
console.log('初始化数据库 - 结束');
|
||||||
|
this.isInitializing = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleExamineeLogin() {
|
||||||
|
console.log('考生登录 - 开始', {
|
||||||
|
examineeIdCard: this.examineeIdCard,
|
||||||
|
examineeAdmissionTicket: this.examineeAdmissionTicket
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除首尾空格
|
||||||
|
const idCard = this.examineeIdCard.trim();
|
||||||
|
const admissionTicket = this.examineeAdmissionTicket.trim();
|
||||||
|
|
||||||
|
// 前端验证
|
||||||
|
if (!idCard || !admissionTicket) {
|
||||||
|
console.warn('考生登录 - 验证失败: 身份证号和准考证号不能为空');
|
||||||
|
Message.error('请输入身份证号和准考证号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置加载状态
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用登录API
|
||||||
|
const result = await window.electronAPI.userLogin(idCard, admissionTicket);
|
||||||
|
console.log(result);
|
||||||
|
|
||||||
|
if (result && result.id) {
|
||||||
|
console.log('考生登录 - 成功', result);
|
||||||
|
// 保存用户信息到store - 在Vue 2中通常使用Vuex
|
||||||
|
if (this.$store && this.$store.commit) {
|
||||||
|
this.$store.commit('setExaminee', result);
|
||||||
|
}
|
||||||
|
Message.success('登录成功');
|
||||||
|
// 跳转到考生首页
|
||||||
|
this.$router.push('/examinee/home');
|
||||||
|
} else {
|
||||||
|
console.warn('考生登录 - 失败:', result);
|
||||||
|
Message.error(result.error || '登录失败,请检查身份证号和准考证号');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('考生登录 - 异常:', error);
|
||||||
|
Message.error('登录失败,请重试');
|
||||||
|
} finally {
|
||||||
|
// 无论成功失败,都关闭加载状态
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async handleAdminLogin() {
|
||||||
|
console.log('管理员登录 - 开始', { passwordLength: this.adminPassword.length });
|
||||||
|
|
||||||
|
// 前端密码验证
|
||||||
|
const passwordError = this.validateAdminPassword(this.adminPassword);
|
||||||
|
if (passwordError) {
|
||||||
|
console.warn('管理员登录 - 验证失败:', passwordError);
|
||||||
|
const errorElement = document.getElementById('admin-error-message');
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.textContent = passwordError;
|
||||||
|
errorElement.style.display = 'block';
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除之前的错误信息
|
||||||
|
const errorElement = document.getElementById('admin-error-message');
|
||||||
|
if (errorElement) {
|
||||||
|
errorElement.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('管理员登录 - 调用主进程登录方法');
|
||||||
|
// 使用新的adminLogin方法
|
||||||
|
const result = await window.electronAPI.adminLogin({
|
||||||
|
username: 'admin',
|
||||||
|
password: this.adminPassword
|
||||||
|
});
|
||||||
|
console.log('管理员登录 - 登录结果:', result);
|
||||||
|
|
||||||
|
if (result && result.success) {
|
||||||
|
console.log('管理员登录 - 成功,跳转到管理首页');
|
||||||
|
Message.success('登录成功');
|
||||||
|
this.$router.push('/admin/home');
|
||||||
|
} else {
|
||||||
|
const errorMessage = result && result.message ? result.message : '登录失败';
|
||||||
|
console.warn('管理员登录 - 失败:', errorMessage);
|
||||||
|
Message.error(errorMessage);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('管理员登录 - 异常:', error);
|
||||||
|
Message.error(`登录异常: ${error.message || '未知错误'}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
validateAdminPassword(password) {
|
||||||
|
// 检查密码是否为空
|
||||||
|
if (!password) {
|
||||||
|
return '请输入管理员密码';
|
||||||
|
}
|
||||||
|
// 检查密码长度
|
||||||
|
if (password.length < 4 || password.length > 32) {
|
||||||
|
return '密码长度必须在4-32个字符之间';
|
||||||
|
}
|
||||||
|
// 检查密码是否只包含英文大小写和数字
|
||||||
|
const regex = /^[A-Za-z0-9]+$/;
|
||||||
|
if (!regex.test(password)) {
|
||||||
|
return '密码只能包含英文大小写字母和数字';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 自定义样式 */
|
||||||
|
.bg-primary {
|
||||||
|
background-color: #1E88E5 !important;
|
||||||
|
/* 蓝色主题 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-primary {
|
||||||
|
color: #1E88E5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 确保容器占满高度 */
|
||||||
|
.welcome-container,
|
||||||
|
.el-container {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 让主内容区自动扩展并居中 */
|
||||||
|
.el-main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-background {
|
||||||
|
background-image: url('../assets/bg.jpeg');
|
||||||
|
background-size: 100% 100%; /* 拉伸背景图以填满容器 */
|
||||||
|
background-position: center; /* 保持居中 */
|
||||||
|
background-repeat: no-repeat; /* 避免重复 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 适配移动设备 */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.max-w-md {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -4,11 +4,34 @@ module.exports = {
|
|||||||
nodeIntegration: false,
|
nodeIntegration: false,
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
preload: 'src/preload.js',
|
preload: 'src/preload.js',
|
||||||
// 添加这一行,指定主进程文件路径
|
|
||||||
mainProcessFile: 'src/background/main.js',
|
mainProcessFile: 'src/background/main.js',
|
||||||
// If you want to use ESLint for your preload script,
|
lintPreloadFiles: false,
|
||||||
// set lintPreloadFiles to true
|
// 将externals改为数组格式
|
||||||
lintPreloadFiles: false
|
externals: ['fontkit', 'pdfkit'],
|
||||||
|
chainWebpackMainProcess: (config) => {
|
||||||
|
config.module
|
||||||
|
.rule('babel')
|
||||||
|
.test(/\.js$/)
|
||||||
|
.use('babel-loader')
|
||||||
|
.loader('babel-loader')
|
||||||
|
.options({
|
||||||
|
presets: ['@babel/preset-env'],
|
||||||
|
plugins: [
|
||||||
|
'@babel/plugin-proposal-optional-chaining',
|
||||||
|
'@babel/plugin-proposal-class-properties'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
.end()
|
||||||
|
|
||||||
|
// 添加对mjs文件的处理
|
||||||
|
config.module
|
||||||
|
.rule('mjs')
|
||||||
|
.test(/\.mjs$/)
|
||||||
|
.include
|
||||||
|
.add(/node_modules/)
|
||||||
|
.end()
|
||||||
|
.type('javascript/auto')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|