后端迁移完成,做了welcome页

This commit is contained in:
chenqiang 2025-08-28 01:26:34 +08:00
parent 6022e8e17a
commit 366e5596bb
31 changed files with 4507 additions and 1331 deletions

View File

@ -1,5 +1,15 @@
module.exports = {
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

File diff suppressed because it is too large Load Diff

View File

@ -24,8 +24,9 @@
"bootstrap": "4.6.0",
"core-js": "3.6.5",
"element-ui": "2.15.14",
"fontkit": "1.8.1",
"jquery": "3.5.1",
"pdfkit": "^0.15.0",
"pdfkit": "0.11.0",
"popper.js": "1.16.1",
"sqlite3": "4.2.0",
"vue": "2.6.11",
@ -34,6 +35,10 @@
"xlsx": "^0.18.5"
},
"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",
"@vue/cli-plugin-babel": "4.5.19",
"@vue/cli-plugin-router": "4.5.17",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 264 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

View File

@ -1,9 +1,5 @@
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
@ -16,17 +12,4 @@
text-align: center;
color: #2c3e50;
}
#nav {
padding: 30px;
}
#nav a {
font-weight: bold;
color: #2c3e50;
}
#nav a.router-link-exact-active {
color: #42b983;
}
</style>

31
src/assets/base.css Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 13 KiB

1
src/assets/logo.svg Normal file
View 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
View File

@ -0,0 +1 @@
@import './base.css';

136
src/background/db/exam.js Normal file
View 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
};

View 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

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,14 @@ const { initConfigIpc } = require('./service/configService.js');
const { initDictIpc } = require('./service/dictService.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
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处理程序
initConfigIpc(ipcMain);
// 初始化字典相关的IPC处理程序
initDictIpc(ipcMain);
// 初始化试题相关的IPC处理程序
await initQuestionIpc(ipcMain);
initQuestionIpc(ipcMain);
// 初始化考试相关的IPC处理程序
initExamIpc(ipcMain);
// 初始化考生相关的IPC处理程序
initExamineeIpc(ipcMain);
// 初始化考生考试相关的IPC处理程序
initExamingIpc(ipcMain);
// 初始化文件相关的IPC处理程序
initFileIpc(ipcMain);
createWindow();
});

View 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
};

View 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
};

View 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
};

View 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;
}
};

View File

@ -248,7 +248,7 @@ async function fetchQuestionsCountAndScore() {
* 初始化试题相关IPC通信
* @param {Electron.IpcMain} ipcMain - IPC主进程实例
*/
async function initQuestionIpc(ipcMain) {
function initQuestionIpc(ipcMain) {
// 题干管理相关IPC
ipcMain.handle("question-create", async (event, questionData) => {
try {

View File

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

View 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>

View 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>

View 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>

View File

@ -47,6 +47,41 @@ contextBridge.exposeInMainWorld('electronAPI', {
questionGetStatistics: () => ipcRenderer.invoke('question-get-statistics'),
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: {
invoke: (channel, data) => ipcRenderer.invoke(channel, data)

View File

@ -1,22 +1,14 @@
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import WelcomeView from '../views/WelcomeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
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')
name: 'Welcome',
component: WelcomeView
}
]

View File

@ -5,8 +5,18 @@ Vue.use(Vuex)
export default new Vuex.Store({
state: {
examinee: null, // 保存考生信息
isLoggedIn: false // 登录状态
},
mutations: {
setExaminee(state, examineeInfo) {
state.examinee = examineeInfo
state.isLoggedIn = true
},
clearExaminee(state) {
state.examinee = null
state.isLoggedIn = false
}
},
actions: {
},

View File

@ -1,5 +0,0 @@
<template>
<div class="about">
<h1>This is an about page</h1>
</div>
</template>

View File

@ -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">&times;</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(_ => {})
},
// IDElement 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
View 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);
// truesuccess
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>

View File

@ -4,11 +4,34 @@ module.exports = {
nodeIntegration: false,
contextIsolation: true,
preload: 'src/preload.js',
// 添加这一行,指定主进程文件路径
mainProcessFile: 'src/background/main.js',
// If you want to use ESLint for your preload script,
// set lintPreloadFiles to true
lintPreloadFiles: false
lintPreloadFiles: false,
// 将externals改为数组格式
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')
}
}
}
}