准备尝试一次打包
This commit is contained in:
parent
52f8af2b63
commit
637906b241
22
README.md
22
README.md
@ -96,7 +96,7 @@ Windows 10 64位 单机版
|
||||
+ 考试管理:提供考试的添加、删除、修改、查询等功能,考试信息包括考试名称、时间、各类型试题题量(各类题型的小题数)、分值设置(各类试题的小题分值)。
|
||||
+ 系统初始化
|
||||
+ 参加考试:提供考试页面,包括试题展示、答题、提交等功能。
|
||||
+ 输入考生信息页:输入正确的考生姓名、准考证号才可以进入考试系统,信息来源于人员管理中维护的数据。
|
||||
+ 输入考生信息页:输入正确的考生姓名、身份证号、手机号才可以进入考试系统,信息来源于人员管理中维护的数据。
|
||||
+ 考试确认页:随机抽题组生成本次试卷,结合考生信息、考试时间等形成唯一考试数据,展示考试说明(固定内容),提供“开始考试”入口。
|
||||
+ 考试页面:展示试题,支持单选题、多选题、填空题、问答题的答题功能。考试页面强制全屏,具备防切出机制,切出5次后,在第6次切出时强制结束考试。考试页面具备倒计时功能,
|
||||
+ 单选题:支持单选项选择。
|
||||
@ -140,7 +140,6 @@ t2t6a9
|
||||
> 已知的配置项:
|
||||
+ 管理员密码(admin_password)
|
||||
+ 题库版本号(question_bank_version)
|
||||
+ 考试版本号(exam_version)
|
||||
|
||||
##### 数据字典
|
||||
|
||||
@ -309,18 +308,6 @@ t2t6a9
|
||||
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP)|
|
||||
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP)|
|
||||
|
||||
###### exam_question_set 考试题型配置表
|
||||
|
||||
|字段名|字段类型|字段描述|
|
||||
|---|---|---|
|
||||
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|
||||
|exam_id|INTEGER NOT NULL|关联到 exam.id|
|
||||
|question_type|TEXT NOT NULL|问题类型(关联到 dict_types.type_code)|
|
||||
|question_count|INTEGER NOT NULL DEFAULT 0|该类型问题的数量|
|
||||
|question_score|INTEGER NOT NULL DEFAULT 0|该类型问题的总分数|
|
||||
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP)|
|
||||
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP)|
|
||||
|
||||
##### 考生
|
||||
|
||||
###### examinee 考生表
|
||||
@ -330,9 +317,12 @@ t2t6a9
|
||||
|id|INTEGER PRIMARY KEY AUTOINCREMENT|主键|
|
||||
|examinee_type|TEXT NOT NULL DEFAULT ''|考生类型(如 "student"、"teacher")|
|
||||
|examinee_name|TEXT NOT NULL DEFAULT ''|考生姓名|
|
||||
|examinee_account|TEXT NOT NULL DEFAULT ''|考生学号/工号|
|
||||
|examinee_phone|TEXT NOT NULL DEFAULT ''|考生手机号|
|
||||
|examinee_gender|TEXT NOT NULL DEFAULT ''|考生性别|
|
||||
|examinee_unit|TEXT NOT NULL DEFAULT ''|考生单位|
|
||||
|examinee_position|TEXT NOT NULL DEFAULT ''|考生职务|
|
||||
|examinee_id_card|TEXT NOT NULL DEFAULT ''|考生身份证号|
|
||||
|examinee_phone|TEXT NOT NULL DEFAULT ''|考生手机号|
|
||||
|examinee_event|TEXT NOT NULL DEFAULT ''|考生报名赛项|
|
||||
|created_at|TEXT|记录创建时间(默认 CURRENT_TIMESTAMP)|
|
||||
|updated_at|TEXT|记录更新时间(默认 CURRENT_TIMESTAMP)|
|
||||
|
||||
|
127
electron/db/exam.js
Normal file
127
electron/db/exam.js
Normal file
@ -0,0 +1,127 @@
|
||||
import { getSystemDbPath } from './path.js';
|
||||
import { executeWithRetry } from './utils.js';
|
||||
import { openDatabase } from './utils.js';
|
||||
|
||||
/**
|
||||
* 查询所有考试
|
||||
* @returns {Promise<Array>} 考试列表
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export 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>} 添加的考试
|
||||
*/
|
||||
export 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>} 是否更新成功
|
||||
*/
|
||||
export 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>} 是否删除成功
|
||||
*/
|
||||
export 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;
|
||||
});
|
||||
}
|
180
electron/db/examinee.js
Normal file
180
electron/db/examinee.js
Normal file
@ -0,0 +1,180 @@
|
||||
import { getSystemDbPath } from './path.js';
|
||||
import { executeWithRetry } from './utils.js';
|
||||
import { getDbConnection } from './index.js';
|
||||
|
||||
/**
|
||||
* 查询所有考生列表
|
||||
* @returns {Promise<Array>} 考生列表
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export 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>} 添加的考生
|
||||
*/
|
||||
export 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>} 是否更新成功
|
||||
*/
|
||||
export 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>} 是否删除成功
|
||||
*/
|
||||
export 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;
|
||||
});
|
||||
}
|
@ -103,9 +103,6 @@ async function initializeSystemDatabase() {
|
||||
await systemDb.execAsync(systemSchema.exam.trim());
|
||||
console.log('创建 exam 表成功');
|
||||
|
||||
await systemDb.execAsync(systemSchema.examQuestionSet.trim());
|
||||
console.log('创建 exam_question_set 表成功');
|
||||
|
||||
await systemDb.execAsync(systemSchema.examinee.trim());
|
||||
console.log('创建 examinee 表成功');
|
||||
|
||||
|
@ -262,18 +262,114 @@ async function updateQuestionDescription(id, questionDescription) {
|
||||
* @returns {Promise<number>} 新创建的选择题ID
|
||||
*/
|
||||
async function addChoiceQuestion(choiceData) {
|
||||
const { question_id, choice_description, choice_type, choice_options, correct_answers } = choiceData;
|
||||
const { question_id, choice_description, choice_type, score, choice_options, correct_answers } = choiceData;
|
||||
|
||||
const db = await getDbConnection(getSystemDbPath());
|
||||
return executeWithRetry(db, async () => {
|
||||
const result = await db.runAsync(
|
||||
'INSERT INTO question_choices (question_id, choice_description, choice_type, choice_options, correct_answers, created_at, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
|
||||
[question_id, choice_description, choice_type, JSON.stringify(choice_options), JSON.stringify(correct_answers)]
|
||||
'INSERT INTO question_choices (question_id, choice_description, choice_type, score, choice_options, correct_answers, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
|
||||
[question_id, choice_description, choice_type, score, JSON.stringify(choice_options), JSON.stringify(correct_answers)]
|
||||
);
|
||||
return result.lastID;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选择题问题
|
||||
* @param {number} id - 选择题ID
|
||||
* @param {Object} choiceData - 选择题数据
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
async function updateChoiceQuestion(id, choiceData) {
|
||||
const { choice_description, choice_type, score, choice_options, correct_answers } = choiceData;
|
||||
|
||||
const db = await getDbConnection(getSystemDbPath());
|
||||
return executeWithRetry(db, async () => {
|
||||
const result = await db.runAsync(
|
||||
'UPDATE question_choices SET choice_description = ?, choice_type = ?, score = ?, choice_options = ?, correct_answers = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[choice_description, choice_type, score, JSON.stringify(choice_options), JSON.stringify(correct_answers), id]
|
||||
);
|
||||
return result.changes > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加填空题问题
|
||||
* @param {Object} fillBlankData - 填空题数据
|
||||
* @param {number} fillBlankData.question_id - 题干ID
|
||||
* @param {string} fillBlankData.blank_description - 问题描述
|
||||
* @param {number} fillBlankData.blank_count - 空白数量
|
||||
* @param {Array} fillBlankData.correct_answers - 正确答案数组
|
||||
* @param {number} fillBlankData.score - 分值
|
||||
* @returns {Promise<number>} 新创建的填空题ID
|
||||
*/
|
||||
async function addFillBlankQuestion(fillBlankData) {
|
||||
const { question_id, blank_description, blank_count, correct_answers, score } = fillBlankData;
|
||||
|
||||
const db = await getDbConnection(getSystemDbPath());
|
||||
return executeWithRetry(db, async () => {
|
||||
const result = await db.runAsync(
|
||||
'INSERT INTO question_fill_blanks (question_id, blank_description, blank_count, correct_answers, score, created_at, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)',
|
||||
[question_id, blank_description, blank_count, JSON.stringify(correct_answers), score]
|
||||
);
|
||||
return result.lastID;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新填空题问题
|
||||
* @param {number} id - 填空题ID
|
||||
* @param {Object} fillBlankData - 填空题数据
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
async function updateFillBlankQuestion(id, fillBlankData) {
|
||||
const { blank_description, blank_count, correct_answers, score } = fillBlankData;
|
||||
|
||||
const db = await getDbConnection(getSystemDbPath());
|
||||
return executeWithRetry(db, async () => {
|
||||
const result = await db.runAsync(
|
||||
'UPDATE question_fill_blanks SET blank_description = ?, blank_count = ?, correct_answers = ?, score = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?',
|
||||
[blank_description, blank_count, JSON.stringify(correct_answers), score, id]
|
||||
);
|
||||
return result.changes > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除填空题问题
|
||||
* @param {number} id - 填空题ID
|
||||
* @returns {Promise<boolean>} 是否删除成功
|
||||
*/
|
||||
async function deleteFillBlankQuestion(id) {
|
||||
const db = await getDbConnection(getSystemDbPath());
|
||||
return executeWithRetry(db, async () => {
|
||||
const result = await db.runAsync('DELETE FROM question_fill_blanks WHERE id = ?', [id]);
|
||||
return result.changes > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据题干ID查询填空题问题
|
||||
* @param {number} questionId - 题干ID
|
||||
* @returns {Promise<Array>} 填空题列表
|
||||
*/
|
||||
async function getFillBlankQuestionsByQuestionId(questionId) {
|
||||
const db = await getDbConnection(getSystemDbPath());
|
||||
return executeWithRetry(db, async () => {
|
||||
const questions = await db.allAsync('SELECT * FROM question_fill_blanks WHERE question_id = ?', [questionId]);
|
||||
// 解析correct_answers为数组
|
||||
questions.forEach(question => {
|
||||
try {
|
||||
question.correct_answers = JSON.parse(question.correct_answers);
|
||||
} catch (e) {
|
||||
console.error('解析填空题答案失败:', e);
|
||||
question.correct_answers = [];
|
||||
}
|
||||
});
|
||||
return questions;
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
addQuestion,
|
||||
getAllQuestions,
|
||||
@ -282,5 +378,10 @@ export {
|
||||
deleteQuestion,
|
||||
getAllQuestionsWithRelations,
|
||||
updateQuestionDescription,
|
||||
addChoiceQuestion // 添加新函数导出
|
||||
addChoiceQuestion,
|
||||
updateChoiceQuestion,
|
||||
addFillBlankQuestion,
|
||||
updateFillBlankQuestion,
|
||||
deleteFillBlankQuestion,
|
||||
getFillBlankQuestionsByQuestionId
|
||||
};
|
@ -1,8 +1,7 @@
|
||||
|
||||
// 辅助函数:将db.run包装为Promise
|
||||
const runAsync = (db, sql, params = []) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.run(sql, params, function(err) {
|
||||
db.run(sql, params, function (err) {
|
||||
if (err) reject(err);
|
||||
else resolve(this);
|
||||
});
|
||||
@ -13,7 +12,7 @@ export { runAsync };
|
||||
|
||||
// 系统数据库表结构
|
||||
const systemSchema = {
|
||||
config: `
|
||||
config: `
|
||||
CREATE TABLE IF NOT EXISTS config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
key TEXT NOT NULL,
|
||||
@ -21,7 +20,7 @@ const systemSchema = {
|
||||
protected INTEGER DEFAULT 0
|
||||
);
|
||||
`,
|
||||
dictTypes: `
|
||||
dictTypes: `
|
||||
CREATE TABLE IF NOT EXISTS dict_types (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type_code TEXT NOT NULL UNIQUE,
|
||||
@ -30,7 +29,7 @@ const systemSchema = {
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
dictItems: `
|
||||
dictItems: `
|
||||
CREATE TABLE IF NOT EXISTS dict_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type_code TEXT NOT NULL,
|
||||
@ -44,7 +43,7 @@ const systemSchema = {
|
||||
FOREIGN KEY (type_code) REFERENCES dict_types(type_code)
|
||||
);
|
||||
`,
|
||||
questions: `
|
||||
questions: `
|
||||
CREATE TABLE IF NOT EXISTS questions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_type TEXT NOT NULL,
|
||||
@ -55,7 +54,7 @@ const systemSchema = {
|
||||
FOREIGN KEY (question_type) REFERENCES dict_types(type_code)
|
||||
);
|
||||
`,
|
||||
questionDatasets: `
|
||||
questionDatasets: `
|
||||
CREATE TABLE IF NOT EXISTS question_datasets (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
@ -66,7 +65,7 @@ const systemSchema = {
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
questionImages: `
|
||||
questionImages: `
|
||||
CREATE TABLE IF NOT EXISTS question_images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
@ -77,19 +76,20 @@ const systemSchema = {
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
questionFillTable: `
|
||||
questionFillTable: `
|
||||
CREATE TABLE IF NOT EXISTS question_fill_table (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
table_name TEXT NOT NULL DEFAULT '',
|
||||
table_data TEXT NOT NULL,
|
||||
table_description TEXT NOT NULL DEFAULT '',
|
||||
score REAL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
questionFillTableBlanks: `
|
||||
questionFillTableBlanks: `
|
||||
CREATE TABLE IF NOT EXISTS question_fill_table_blanks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
@ -103,7 +103,7 @@ const systemSchema = {
|
||||
FOREIGN KEY (table_id) REFERENCES question_fill_table(id)
|
||||
);
|
||||
`,
|
||||
questionChoices: `
|
||||
questionChoices: `
|
||||
CREATE TABLE IF NOT EXISTS question_choices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
@ -111,24 +111,26 @@ const systemSchema = {
|
||||
choice_type TEXT NOT NULL DEFAULT 'single',
|
||||
choice_options TEXT NOT NULL,
|
||||
correct_answers TEXT NOT NULL,
|
||||
score REAL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
questionFillBlanks: `
|
||||
questionFillBlanks: `
|
||||
CREATE TABLE IF NOT EXISTS question_fill_blanks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
blank_description TEXT NOT NULL DEFAULT '',
|
||||
blank_count INTEGER NOT NULL DEFAULT 0,
|
||||
correct_answers TEXT NOT NULL DEFAULT '',
|
||||
score REAL,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
questionJudge: `
|
||||
questionJudge: `
|
||||
CREATE TABLE IF NOT EXISTS question_judge (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
@ -139,7 +141,7 @@ const systemSchema = {
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
questionShort: `
|
||||
questionShort: `
|
||||
CREATE TABLE IF NOT EXISTS question_short (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
question_id INTEGER NOT NULL,
|
||||
@ -150,7 +152,7 @@ const systemSchema = {
|
||||
FOREIGN KEY (question_id) REFERENCES questions(id)
|
||||
);
|
||||
`,
|
||||
exam: `
|
||||
exam: `
|
||||
CREATE TABLE IF NOT EXISTS exam (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
exam_name TEXT NOT NULL DEFAULT '',
|
||||
@ -158,40 +160,32 @@ const systemSchema = {
|
||||
exam_examinee_type TEXT NOT NULL DEFAULT '',
|
||||
exam_notice TEXT NOT NULL DEFAULT '',
|
||||
exam_minutes INTEGER NOT NULL DEFAULT 0,
|
||||
exam_minutes_min INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
examQuestionSet: `
|
||||
CREATE TABLE IF NOT EXISTS exam_question_set (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
exam_id INTEGER NOT NULL,
|
||||
question_type TEXT NOT NULL,
|
||||
question_count INTEGER NOT NULL DEFAULT 0,
|
||||
question_score INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (exam_id) REFERENCES exam(id),
|
||||
FOREIGN KEY (question_type) REFERENCES dict_types(type_code)
|
||||
);
|
||||
`,
|
||||
examinee: `
|
||||
examinee: `
|
||||
CREATE TABLE IF NOT EXISTS examinee (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
examinee_type TEXT NOT NULL DEFAULT '',
|
||||
examinee_name TEXT NOT NULL DEFAULT '',
|
||||
examinee_account TEXT NOT NULL DEFAULT '',
|
||||
examinee_phone TEXT NOT NULL DEFAULT '',
|
||||
examinee_gender TEXT NOT NULL DEFAULT '',
|
||||
examinee_unit TEXT NOT NULL DEFAULT '',
|
||||
written_exam_room TEXT NOT NULL DEFAULT '',
|
||||
written_exam_seat TEXT NOT NULL DEFAULT '',
|
||||
computer_exam_room TEXT NOT NULL DEFAULT '',
|
||||
computer_exam_seat TEXT NOT NULL DEFAULT '',
|
||||
examinee_id_card TEXT NOT NULL DEFAULT '',
|
||||
examinee_admission_ticket TEXT NOT NULL DEFAULT '',
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
`,
|
||||
};
|
||||
|
||||
// 用户数据库表结构
|
||||
const userSchema = {
|
||||
examineeLog: `
|
||||
examineeLog: `
|
||||
CREATE TABLE IF NOT EXISTS examinee_log (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
examinee_id INTEGER NOT NULL,
|
||||
@ -200,7 +194,7 @@ const userSchema = {
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
examineeExam: `
|
||||
examineeExam: `
|
||||
CREATE TABLE IF NOT EXISTS examinee_exam (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
examinee_id INTEGER NOT NULL,
|
||||
@ -217,7 +211,7 @@ const userSchema = {
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
examineePapers: `
|
||||
examineePapers: `
|
||||
CREATE TABLE IF NOT EXISTS examinee_papers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
examinee_id INTEGER NOT NULL,
|
||||
@ -227,7 +221,7 @@ const userSchema = {
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
paperQuestions: `
|
||||
paperQuestions: `
|
||||
CREATE TABLE IF NOT EXISTS paper_questions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
examinee_paper_id INTEGER NOT NULL,
|
||||
@ -238,7 +232,7 @@ const userSchema = {
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`,
|
||||
paperQuestionChoices: `
|
||||
paperQuestionChoices: `
|
||||
CREATE TABLE IF NOT EXISTS paper_question_choices (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_question_id INTEGER NOT NULL,
|
||||
@ -249,7 +243,7 @@ const userSchema = {
|
||||
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
|
||||
);
|
||||
`,
|
||||
paperQuestionBlanks: `
|
||||
paperQuestionBlanks: `
|
||||
CREATE TABLE IF NOT EXISTS paper_question_blanks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_question_id INTEGER NOT NULL,
|
||||
@ -260,7 +254,7 @@ const userSchema = {
|
||||
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
|
||||
);
|
||||
`,
|
||||
paperQuestionJudge: `
|
||||
paperQuestionJudge: `
|
||||
CREATE TABLE IF NOT EXISTS paper_question_judge (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_question_id INTEGER NOT NULL,
|
||||
@ -271,7 +265,7 @@ const userSchema = {
|
||||
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
|
||||
);
|
||||
`,
|
||||
paperQuestionFillTable: `
|
||||
paperQuestionFillTable: `
|
||||
CREATE TABLE IF NOT EXISTS paper_question_fill_table (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_question_id INTEGER NOT NULL,
|
||||
@ -282,7 +276,7 @@ const userSchema = {
|
||||
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
|
||||
);
|
||||
`,
|
||||
paperQuestionFillTableBlanks: `
|
||||
paperQuestionFillTableBlanks: `
|
||||
CREATE TABLE IF NOT EXISTS paper_question_fill_table_blanks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_question_fill_table_id INTEGER NOT NULL,
|
||||
@ -294,7 +288,7 @@ const userSchema = {
|
||||
FOREIGN KEY (paper_question_fill_table_id) REFERENCES paper_question_fill_table(id)
|
||||
);
|
||||
`,
|
||||
paperQuestionSubjective: `
|
||||
paperQuestionSubjective: `
|
||||
CREATE TABLE IF NOT EXISTS paper_question_subjective (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
paper_question_id INTEGER NOT NULL,
|
||||
@ -307,50 +301,135 @@ const userSchema = {
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (paper_question_id) REFERENCES paper_questions(id)
|
||||
);
|
||||
`
|
||||
`,
|
||||
};
|
||||
|
||||
// 初始化默认数据
|
||||
// 系统配置默认数据
|
||||
const plainPassword = 't2t6a9'; // 明文密码变量定义
|
||||
const plainPassword = "t2t6a9"; // 明文密码变量定义
|
||||
|
||||
// 注意:在实际初始化数据库时,需要使用argon2对plainPassword进行哈希
|
||||
// 这里只定义默认数据结构,哈希操作应在index.js中的初始化函数中完成
|
||||
const defaultData = {
|
||||
config: [
|
||||
{ key: 'admin_password', value: plainPassword, protected: 1 },
|
||||
{ key: 'question_bank_version', value: '1', protected: 1 },
|
||||
{ key: 'exam_version', value: '1', protected: 1 },
|
||||
{ key: 'initialized', value: '1', protected: 1 }
|
||||
],
|
||||
// 字典类型默认数据
|
||||
dictTypes: [
|
||||
{ type_code: 'question_category', type_name: '题型分类', description: '用于区分客观题和主观题' },
|
||||
{ type_code: 'question_type', type_name: '题型', description: '存储所有题型(选择题、填空题等)' },
|
||||
{ type_code: 'user_role', type_name: '用户角色', description: '区分不同用户类型' }
|
||||
],
|
||||
// 字典项默认数据
|
||||
dictItems: [
|
||||
// 题型分类
|
||||
{ type_code: 'question_category', item_code: 'objective', item_name: '客观题', item_description: '有固定答案,机器可自动评分', is_active: 1, parent_code: null },
|
||||
{ type_code: 'question_category', item_code: 'subjective', item_name: '主观题', item_description: '需人工评分,答案不唯一', is_active: 1, parent_code: null },
|
||||
// 题型
|
||||
{ type_code: 'question_type', item_code: 'choice', item_name: '选择题', item_description: '包含单选和多选', is_active: 1, parent_code: 'objective' },
|
||||
{ type_code: 'question_type', item_code: 'fill_blank', item_name: '填空题', item_description: '填写空白处的答案', is_active: 0, parent_code: 'objective' },
|
||||
{ type_code: 'question_type', item_code: 'fill_table', item_name: '填表题', item_description: '填写表格内容', is_active: 0, parent_code: 'objective' },
|
||||
{ type_code: 'question_type', item_code: 'true_false', item_name: '判断题', item_description: '判断对错', is_active: 0, parent_code: 'objective' },
|
||||
{ type_code: 'question_type', item_code: 'short_answer', item_name: '问答题', item_description: '简短回答问题', is_active: 0, parent_code: 'subjective' },
|
||||
{ type_code: 'question_type', item_code: 'analysis', item_name: '分析题', item_description: '需要分析问题', is_active: 0, parent_code: 'subjective' },
|
||||
{ type_code: 'question_type', item_code: 'essay', item_name: '论述题', item_description: '详细论述', is_active: 0, parent_code: 'subjective' },
|
||||
// 用户角色
|
||||
{ type_code: 'user_role', item_code: 'admin', item_name: '管理员', item_description: '系统管理员', is_active: 1, parent_code: null },
|
||||
{ type_code: 'user_role', item_code: 'student', item_name: '考生', item_description: '参加考试的用户', is_active: 1, parent_code: null }
|
||||
]
|
||||
config: [
|
||||
{ key: "admin_password", value: plainPassword, protected: 1 },
|
||||
{ key: "question_bank_version", value: "1", protected: 1 },
|
||||
{ key: "exam_version", value: "1", protected: 1 },
|
||||
{ key: "initialized", value: "1", protected: 1 },
|
||||
],
|
||||
// 字典类型默认数据
|
||||
dictTypes: [
|
||||
{
|
||||
type_code: "question_category",
|
||||
type_name: "题型分类",
|
||||
description: "用于区分客观题和主观题",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
type_name: "题型",
|
||||
description: "存储所有题型(选择题、填空题等)",
|
||||
},
|
||||
{
|
||||
type_code: "user_role",
|
||||
type_name: "用户角色",
|
||||
description: "区分不同用户类型",
|
||||
},
|
||||
],
|
||||
// 字典项默认数据
|
||||
dictItems: [
|
||||
// 题型分类
|
||||
{
|
||||
type_code: "question_category",
|
||||
item_code: "objective",
|
||||
item_name: "客观题",
|
||||
item_description: "有固定答案,机器可自动评分",
|
||||
is_active: 1,
|
||||
parent_code: null,
|
||||
},
|
||||
{
|
||||
type_code: "question_category",
|
||||
item_code: "subjective",
|
||||
item_name: "主观题",
|
||||
item_description: "需人工评分,答案不唯一",
|
||||
is_active: 1,
|
||||
parent_code: null,
|
||||
},
|
||||
// 题型
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "choice",
|
||||
item_name: "选择题",
|
||||
item_description: "包含单选和多选",
|
||||
is_active: 1,
|
||||
parent_code: "objective",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "fill_blank",
|
||||
item_name: "填空题",
|
||||
item_description: "填写空白处的答案",
|
||||
is_active: 0,
|
||||
parent_code: "objective",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "fill_table",
|
||||
item_name: "填表题",
|
||||
item_description: "填写表格内容",
|
||||
is_active: 0,
|
||||
parent_code: "objective",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "true_false",
|
||||
item_name: "判断题",
|
||||
item_description: "判断对错",
|
||||
is_active: 0,
|
||||
parent_code: "objective",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "short_answer",
|
||||
item_name: "问答题",
|
||||
item_description: "简短回答问题",
|
||||
is_active: 0,
|
||||
parent_code: "subjective",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "analysis",
|
||||
item_name: "分析题",
|
||||
item_description: "需要分析问题",
|
||||
is_active: 0,
|
||||
parent_code: "subjective",
|
||||
},
|
||||
{
|
||||
type_code: "question_type",
|
||||
item_code: "essay",
|
||||
item_name: "论述题",
|
||||
item_description: "详细论述",
|
||||
is_active: 0,
|
||||
parent_code: "subjective",
|
||||
},
|
||||
// 用户角色
|
||||
{
|
||||
type_code: "user_role",
|
||||
item_code: "admin",
|
||||
item_name: "管理员",
|
||||
item_description: "系统管理员",
|
||||
is_active: 1,
|
||||
parent_code: null,
|
||||
},
|
||||
{
|
||||
type_code: "user_role",
|
||||
item_code: "student",
|
||||
item_name: "考生",
|
||||
item_description: "参加考试的用户",
|
||||
is_active: 1,
|
||||
parent_code: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// 导出对象
|
||||
export {
|
||||
systemSchema,
|
||||
userSchema,
|
||||
defaultData
|
||||
};
|
||||
export { systemSchema, userSchema, defaultData };
|
||||
|
475
electron/main.js
475
electron/main.js
@ -1,8 +1,8 @@
|
||||
// 确保所有导入都使用 ES 模块语法
|
||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { checkDatabaseInitialized, initializeDatabase } from './db/index.js';
|
||||
import { app, BrowserWindow, ipcMain } from "electron";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { checkDatabaseInitialized, initializeDatabase } from "./db/index.js";
|
||||
// 导入配置项服务
|
||||
import {
|
||||
fetchAllConfigs,
|
||||
@ -12,8 +12,8 @@ import {
|
||||
getSystemConfig,
|
||||
updateSystemConfig,
|
||||
increaseQuestionBankVersion,
|
||||
initAuthIpc
|
||||
} from './service/configService.js';
|
||||
initAuthIpc,
|
||||
} from "./service/configService.js";
|
||||
// 导入字典服务 - 使用实际导出的函数名称
|
||||
import {
|
||||
fetchDictTypes,
|
||||
@ -25,9 +25,9 @@ import {
|
||||
modifyDictItem,
|
||||
removeDictItem,
|
||||
fetchDictItemsWithTypes,
|
||||
checkDictParentCode, // 添加这一行
|
||||
checkDictChildReferences // 添加这一行
|
||||
} from './service/dictService.js';
|
||||
checkDictParentCode, // 添加这一行
|
||||
checkDictChildReferences, // 添加这一行
|
||||
} from "./service/dictService.js";
|
||||
// 导入题干服务
|
||||
import {
|
||||
createQuestion,
|
||||
@ -37,8 +37,32 @@ import {
|
||||
removeQuestion,
|
||||
fetchAllQuestionsWithRelations,
|
||||
modifyQuestionDescription,
|
||||
createChoiceQuestion // 添加新函数导入
|
||||
createChoiceQuestion,
|
||||
modifyChoiceQuestion,
|
||||
createFillBlankQuestion,
|
||||
modifyFillBlankQuestion,
|
||||
removeFillBlankQuestion,
|
||||
fetchFillBlankQuestionsByQuestionId
|
||||
} from './service/questionService.js';
|
||||
import {
|
||||
createNewExam,
|
||||
fetchAllExams,
|
||||
fetchExamById,
|
||||
modifyExam,
|
||||
removeExam,
|
||||
fetchLastExam, // 添加这一行
|
||||
} from "./service/examService.js";
|
||||
|
||||
// 添加考生服务导入
|
||||
// 在文件开头的导入语句中添加新函数
|
||||
import {
|
||||
fetchAllExaminees,
|
||||
fetchExamineeById,
|
||||
createExamineeService,
|
||||
updateExamineeService,
|
||||
deleteExamineeService,
|
||||
fetchExamineeByIdCardAndAdmissionTicket,
|
||||
} from "./service/examineeService.js";
|
||||
|
||||
// 定义 __dirname 和 __filename
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
@ -53,7 +77,7 @@ if (!gotTheLock) {
|
||||
app.quit();
|
||||
} else {
|
||||
// 设置第二个实例启动时的处理
|
||||
app.on('second-instance', (event, commandLine, workingDirectory) => {
|
||||
app.on("second-instance", (event, commandLine, workingDirectory) => {
|
||||
// 当用户尝试启动第二个实例时,聚焦到已有的主窗口
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
if (mainWindow) {
|
||||
@ -75,111 +99,127 @@ function createWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
fullscreen: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
preload: path.join(__dirname, "../electron/preload.js"),
|
||||
nodeIntegration: false,
|
||||
contextIsolation: true
|
||||
}
|
||||
})
|
||||
contextIsolation: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
mainWindow.loadURL('http://localhost:5173/')
|
||||
mainWindow.webContents.openDevTools()
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
mainWindow.loadURL("http://localhost:5173/");
|
||||
mainWindow.webContents.openDevTools();
|
||||
} else {
|
||||
mainWindow.loadFile(path.join(__dirname, '../dist/index.html'))
|
||||
mainWindow.loadFile(path.join(__dirname, "../dist/index.html"));
|
||||
}
|
||||
|
||||
// 当窗口关闭时,清空引用
|
||||
mainWindow.on('closed', () => {
|
||||
mainWindow.on("closed", () => {
|
||||
mainWindow = null;
|
||||
});
|
||||
}
|
||||
|
||||
// Initalize app
|
||||
app.whenReady().then(() => {
|
||||
setupApp()
|
||||
createWindow()
|
||||
setupIpcMain()
|
||||
})
|
||||
setupApp();
|
||||
createWindow();
|
||||
setupIpcMain();
|
||||
});
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin') {
|
||||
app.quit()
|
||||
app.on("window-all-closed", () => {
|
||||
if (process.platform !== "darwin") {
|
||||
app.quit();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
app.on("activate", () => {
|
||||
// 只有当没有窗口时才创建新窗口
|
||||
if (BrowserWindow.getAllWindows().length === 0) {
|
||||
createWindow()
|
||||
createWindow();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
// Check database initialization status
|
||||
async function setupApp() {
|
||||
try {
|
||||
console.log('应用启动 - 检查数据库初始化状态...');
|
||||
try {
|
||||
console.log("应用启动 - 检查数据库初始化状态...");
|
||||
|
||||
// 使用全局变量防止重复初始化检查
|
||||
if (global.dbInitCheck) {
|
||||
console.log('数据库初始化检查已完成');
|
||||
return;
|
||||
}
|
||||
global.dbInitCheck = true;
|
||||
|
||||
const isInitialized = await checkDatabaseInitialized();
|
||||
console.log('数据库初始化状态:', isInitialized);
|
||||
|
||||
// 只检查状态,不自动初始化
|
||||
} catch (error) {
|
||||
console.error('数据库检查过程中出错:', error);
|
||||
// 使用全局变量防止重复初始化检查
|
||||
if (global.dbInitCheck) {
|
||||
console.log("数据库初始化检查已完成");
|
||||
return;
|
||||
}
|
||||
global.dbInitCheck = true;
|
||||
|
||||
const isInitialized = await checkDatabaseInitialized();
|
||||
console.log("数据库初始化状态:", isInitialized);
|
||||
|
||||
// 只检查状态,不自动初始化
|
||||
} catch (error) {
|
||||
console.error("数据库检查过程中出错:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Setup IPC communication
|
||||
function setupIpcMain() {
|
||||
// 考生登录IPC
|
||||
ipcMain.handle("user-login", async (event, {idCard, admissionTicket}) => {
|
||||
// return {data: 'hello world'};
|
||||
try {
|
||||
const examinee = await fetchExamineeByIdCardAndAdmissionTicket(idCard, admissionTicket);
|
||||
console.log(examinee);
|
||||
// const examinee = 'hello world';
|
||||
if (examinee) {
|
||||
return { success: true, data: examinee };
|
||||
} else {
|
||||
return { success: false, error: "未找到匹配的考生信息" };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to login examinee:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
// 数据库相关
|
||||
ipcMain.handle('check-database-initialized', async () => {
|
||||
ipcMain.handle("check-database-initialized", async () => {
|
||||
try {
|
||||
return await checkDatabaseInitialized();
|
||||
} catch (error) {
|
||||
console.error('Failed to check database initialization:', error);
|
||||
console.error("Failed to check database initialization:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('initialize-database', async () => {
|
||||
try {
|
||||
return await initializeDatabase();
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize database:', error);
|
||||
return false;
|
||||
}
|
||||
ipcMain.handle("initialize-database", async () => {
|
||||
try {
|
||||
return await initializeDatabase();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize database:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
// 系统相关
|
||||
ipcMain.handle('system-get-config', async () => {
|
||||
ipcMain.handle("system-get-config", async () => {
|
||||
try {
|
||||
return await getSystemConfig();
|
||||
} catch (error) {
|
||||
console.error('Failed to get system config:', error);
|
||||
console.error("Failed to get system config:", error);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('system-update-config', async (event, config) => {
|
||||
ipcMain.handle("system-update-config", async (event, config) => {
|
||||
try {
|
||||
return await updateSystemConfig(config);
|
||||
} catch (error) {
|
||||
console.error('Failed to update system config:', error);
|
||||
console.error("Failed to update system config:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('system-increase-question-band-version', async () => {
|
||||
ipcMain.handle("system-increase-question-band-version", async () => {
|
||||
try {
|
||||
return await increaseQuestionBankVersion();
|
||||
} catch (error) {
|
||||
console.error('Failed to increase question band version:', error);
|
||||
console.error("Failed to increase question band version:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@ -188,16 +228,16 @@ function setupIpcMain() {
|
||||
initAuthIpc(ipcMain);
|
||||
|
||||
// 配置项管理相关IPC
|
||||
ipcMain.handle('config-fetch-all', async () => {
|
||||
ipcMain.handle("config-fetch-all", async () => {
|
||||
try {
|
||||
return await fetchAllConfigs();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch all configs:', error);
|
||||
console.error("Failed to fetch all configs:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('config-fetch-by-id', async (event, id) => {
|
||||
ipcMain.handle("config-fetch-by-id", async (event, id) => {
|
||||
try {
|
||||
return await fetchConfigById(id);
|
||||
} catch (error) {
|
||||
@ -206,7 +246,7 @@ function setupIpcMain() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('config-save', async (event, { key, value }) => {
|
||||
ipcMain.handle("config-save", async (event, { key, value }) => {
|
||||
try {
|
||||
await saveConfig(key, value);
|
||||
return true;
|
||||
@ -216,7 +256,7 @@ function setupIpcMain() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('config-delete', async (event, id) => {
|
||||
ipcMain.handle("config-delete", async (event, id) => {
|
||||
try {
|
||||
await removeConfig(id);
|
||||
return true;
|
||||
@ -227,45 +267,48 @@ function setupIpcMain() {
|
||||
});
|
||||
|
||||
// 字典管理相关IPC - 使用正确的函数名称
|
||||
ipcMain.handle('dict-fetch-types', async () => {
|
||||
ipcMain.handle("dict-fetch-types", async () => {
|
||||
try {
|
||||
return await fetchDictTypes();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch dict types:', error);
|
||||
console.error("Failed to fetch dict types:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('dict-fetch-items-by-type', async (event, typeCode, isActive = undefined) => {
|
||||
try {
|
||||
return await fetchDictItemsByTypeCode(typeCode, isActive);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch dict items by type ${typeCode}:`, error);
|
||||
throw error;
|
||||
ipcMain.handle(
|
||||
"dict-fetch-items-by-type",
|
||||
async (event, typeCode, isActive = undefined) => {
|
||||
try {
|
||||
return await fetchDictItemsByTypeCode(typeCode, isActive);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch dict items by type ${typeCode}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
ipcMain.handle('dict-create-type', async (event, dictType) => {
|
||||
ipcMain.handle("dict-create-type", async (event, dictType) => {
|
||||
try {
|
||||
return await createDictType(dictType);
|
||||
} catch (error) {
|
||||
console.error('Failed to create dict type:', error);
|
||||
console.error("Failed to create dict type:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 将 updateDictType 改为 modifyDictType
|
||||
ipcMain.handle('dict-update-type', async (event, dictType) => {
|
||||
ipcMain.handle("dict-update-type", async (event, dictType) => {
|
||||
try {
|
||||
return await modifyDictType(dictType.id, dictType);
|
||||
} catch (error) {
|
||||
console.error('Failed to update dict type:', error);
|
||||
console.error("Failed to update dict type:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 将 deleteDictType 改为 removeDictType
|
||||
ipcMain.handle('dict-delete-type', async (event, typeCode) => {
|
||||
ipcMain.handle("dict-delete-type", async (event, typeCode) => {
|
||||
try {
|
||||
return await removeDictType(typeCode);
|
||||
} catch (error) {
|
||||
@ -274,27 +317,27 @@ function setupIpcMain() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('dict-create-item', async (event, dictItem) => {
|
||||
ipcMain.handle("dict-create-item", async (event, dictItem) => {
|
||||
try {
|
||||
return await createDictItem(dictItem);
|
||||
} catch (error) {
|
||||
console.error('Failed to create dict item:', error);
|
||||
console.error("Failed to create dict item:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 将 updateDictItem 改为 modifyDictItem
|
||||
ipcMain.handle('dict-update-item', async (event, dictItem) => {
|
||||
ipcMain.handle("dict-update-item", async (event, dictItem) => {
|
||||
try {
|
||||
return await modifyDictItem(dictItem.id, dictItem);
|
||||
} catch (error) {
|
||||
console.error('Failed to update dict item:', error);
|
||||
console.error("Failed to update dict item:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 将 deleteDictItem 改为 removeDictItem
|
||||
ipcMain.handle('dict-delete-item', async (event, id) => {
|
||||
ipcMain.handle("dict-delete-item", async (event, id) => {
|
||||
try {
|
||||
return await removeDictItem(id);
|
||||
} catch (error) {
|
||||
@ -304,55 +347,55 @@ function setupIpcMain() {
|
||||
});
|
||||
|
||||
// 将 fetchAllDictItemsWithTypes 改为 fetchDictItemsWithTypes
|
||||
ipcMain.handle('dict-fetch-all-items-with-types', async () => {
|
||||
ipcMain.handle("dict-fetch-all-items-with-types", async () => {
|
||||
try {
|
||||
return await fetchDictItemsWithTypes();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch all dict items with types:', error);
|
||||
console.error("Failed to fetch all dict items with types:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
// 添加在setupIpcMain函数中
|
||||
// 检查parent_code是否存在
|
||||
ipcMain.handle('dict-check-parent-code', async (event, parentCode) => {
|
||||
ipcMain.handle("dict-check-parent-code", async (event, parentCode) => {
|
||||
try {
|
||||
return await checkDictParentCode(parentCode); // 修改这一行
|
||||
return await checkDictParentCode(parentCode); // 修改这一行
|
||||
} catch (error) {
|
||||
console.error('检查parent_code失败:', error);
|
||||
console.error("检查parent_code失败:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否有子引用
|
||||
ipcMain.handle('dict-check-child-references', async (event, itemCode) => {
|
||||
ipcMain.handle("dict-check-child-references", async (event, itemCode) => {
|
||||
try {
|
||||
return await checkDictChildReferences(itemCode); // 修改这一行
|
||||
return await checkDictChildReferences(itemCode); // 修改这一行
|
||||
} catch (error) {
|
||||
console.error('检查子引用失败:', error);
|
||||
console.error("检查子引用失败:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 题干管理相关IPC
|
||||
ipcMain.handle('question-create', async (event, questionData) => {
|
||||
ipcMain.handle("question-create", async (event, questionData) => {
|
||||
try {
|
||||
return await createQuestion(questionData);
|
||||
} catch (error) {
|
||||
console.error('Failed to create question:', error);
|
||||
console.error("Failed to create question:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('question-fetch-all', async () => {
|
||||
ipcMain.handle("question-fetch-all", async () => {
|
||||
try {
|
||||
return await fetchAllQuestions();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch questions:', error);
|
||||
console.error("Failed to fetch questions:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('question-fetch-by-id', async (event, id) => {
|
||||
ipcMain.handle("question-fetch-by-id", async (event, id) => {
|
||||
try {
|
||||
return await fetchQuestionById(id);
|
||||
} catch (error) {
|
||||
@ -361,16 +404,16 @@ function setupIpcMain() {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('question-update', async (event, id, questionData) => {
|
||||
ipcMain.handle("question-update", async (event, id, questionData) => {
|
||||
try {
|
||||
return await modifyQuestion(id, questionData);
|
||||
} catch (error) {
|
||||
console.error('Failed to update question:', error);
|
||||
console.error("Failed to update question:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('question-delete', async (event, id) => {
|
||||
ipcMain.handle("question-delete", async (event, id) => {
|
||||
try {
|
||||
return await removeQuestion(id);
|
||||
} catch (error) {
|
||||
@ -380,52 +423,238 @@ function setupIpcMain() {
|
||||
});
|
||||
|
||||
// 在已有的 question 相关 IPC 处理程序区域添加
|
||||
ipcMain.handle('question-fetch-all-with-relations', async (event) => {
|
||||
ipcMain.handle("question-fetch-all-with-relations", async (event) => {
|
||||
try {
|
||||
return await fetchAllQuestionsWithRelations();
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch all questions with relations:', error);
|
||||
console.error("Failed to fetch all questions with relations:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加更新题干描述的 IPC 处理程序
|
||||
ipcMain.handle('question-update-description', async (event, id, questionDescription) => {
|
||||
ipcMain.handle(
|
||||
"question-update-description",
|
||||
async (event, id, questionDescription) => {
|
||||
try {
|
||||
return await modifyQuestionDescription(id, questionDescription);
|
||||
} catch (error) {
|
||||
console.error("Failed to update question description:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 添加选择题问题的IPC处理程序
|
||||
ipcMain.handle('question-update-choice', async (event, id, choiceData) => {
|
||||
try {
|
||||
return await modifyQuestionDescription(id, questionDescription);
|
||||
return await modifyChoiceQuestion(id, choiceData);
|
||||
} catch (error) {
|
||||
console.error('Failed to update question description:', error);
|
||||
console.error('Failed to update choice question:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加选择题问题的IPC处理程序
|
||||
ipcMain.handle('question-create-choice', async (event, choiceData) => {
|
||||
ipcMain.handle("question-create-choice", async (event, choiceData) => {
|
||||
try {
|
||||
return await createChoiceQuestion(choiceData);
|
||||
} catch (error) {
|
||||
console.error('Failed to create choice question:', error);
|
||||
console.error("Failed to create choice question:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 添加填空题问题的IPC处理程序
|
||||
ipcMain.handle('question-create-fill-blank', async (event, fillBlankData) => {
|
||||
try {
|
||||
return await createFillBlankQuestion(fillBlankData);
|
||||
} catch (error) {
|
||||
console.error('Failed to create fill blank question:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 更新填空题问题的IPC处理程序
|
||||
ipcMain.handle('question-update-fill-blank', async (event, id, fillBlankData) => {
|
||||
try {
|
||||
return await modifyFillBlankQuestion(id, fillBlankData);
|
||||
} catch (error) {
|
||||
console.error('Failed to update fill blank question:', error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 删除填空题问题的IPC处理程序
|
||||
ipcMain.handle('question-delete-fill-blank', async (event, id) => {
|
||||
try {
|
||||
return await removeFillBlankQuestion(id);
|
||||
} catch (error) {
|
||||
console.error(`Failed to delete fill blank question ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 根据题干ID查询填空题问题的IPC处理程序
|
||||
ipcMain.handle('question-fetch-fill-blank-by-question-id', async (event, questionId) => {
|
||||
try {
|
||||
return await fetchFillBlankQuestionsByQuestionId(questionId);
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch fill blank questions by question id ${questionId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
// 考试管理相关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 {
|
||||
return { success: true, data: await fetchAllExams() };
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch all exams:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("exam-fetch-last", async () => {
|
||||
try {
|
||||
return { success: true, data: await fetchLastExam() };
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch last exam:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("exam-fetch-by-id", async (event, id) => {
|
||||
try {
|
||||
return { success: true, data: await fetchExamById(id) };
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch exam by id ${id}:`, error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("exam-update", async (event, { id, examData }) => {
|
||||
try {
|
||||
const result = await modifyExam(id, examData);
|
||||
return { success: result, data: { id, ...examData } };
|
||||
} catch (error) {
|
||||
console.error(`Failed to update exam ${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 };
|
||||
}
|
||||
});
|
||||
|
||||
// 考生管理相关IPC
|
||||
ipcMain.handle("examinee-create", async (event, examineeData) => {
|
||||
try {
|
||||
return await createExamineeService(examineeData);
|
||||
} catch (error) {
|
||||
console.error("Failed to create examinee:", error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("examinee-fetch-all", async () => {
|
||||
try {
|
||||
return await fetchAllExaminees();
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch all examinees:", error);
|
||||
return [];
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("examinee-fetch-by-id", async (event, id) => {
|
||||
try {
|
||||
return await fetchExamineeById(id);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch examinee by id:", 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 false;
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle("examinee-delete", async (event, id) => {
|
||||
try {
|
||||
return await deleteExamineeService(id);
|
||||
} catch (error) {
|
||||
console.error("Failed to delete examinee:", error);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// 确保在 app.whenReady() 中调用 setupIpcMain()
|
||||
app.whenReady().then(() => {
|
||||
setupApp()
|
||||
createWindow()
|
||||
setupIpcMain()
|
||||
setupApp();
|
||||
createWindow();
|
||||
setupIpcMain();
|
||||
});
|
||||
|
||||
// 删除下面这段重复的代码
|
||||
// app.whenReady().then(() => {
|
||||
// setupApp()
|
||||
// createWindow()
|
||||
// setupIpcMain()
|
||||
// });
|
||||
|
||||
// 在应用退出前关闭所有数据库连接
|
||||
app.on('will-quit', () => {
|
||||
console.log('应用即将退出...');
|
||||
// closeAllConnections();
|
||||
});
|
||||
app.on("will-quit", () => {
|
||||
console.log("应用即将退出...");
|
||||
// closeAllConnections();
|
||||
});
|
||||
|
@ -37,22 +37,29 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
|
||||
// 题干管理相关API
|
||||
createQuestion: (questionData) => ipcRenderer.invoke('question-create', questionData),
|
||||
createChoiceQuestion: (choiceData) => ipcRenderer.invoke('question-create-choice', choiceData), // 添加这行
|
||||
createChoiceQuestion: (choiceData) => ipcRenderer.invoke('question-create-choice', choiceData),
|
||||
updateChoiceQuestion: (id, choiceData) => ipcRenderer.invoke('question-update-choice', id, choiceData),
|
||||
createFillBlankQuestion: (fillBlankData) => ipcRenderer.invoke('question-create-fill-blank', fillBlankData),
|
||||
fetchAllQuestions: () => ipcRenderer.invoke('question-fetch-all'),
|
||||
fetchAllQuestionsWithRelations: () => ipcRenderer.invoke('question-fetch-all-with-relations'),
|
||||
fetchQuestionById: (id) => ipcRenderer.invoke('question-fetch-by-id', id),
|
||||
updateQuestion: (id, questionData) => ipcRenderer.invoke('question-update', id, questionData),
|
||||
updateQuestionDescription: (id, questionDescription) => ipcRenderer.invoke('question-update-description', id, questionDescription), // 添加新API
|
||||
deleteQuestion: (id) => ipcRenderer.invoke('question-delete', id)
|
||||
});
|
||||
// 这里可以添加预加载脚本
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const replaceText = (selector, text) => {
|
||||
const element = document.getElementById(selector)
|
||||
if (element) element.innerText = text
|
||||
}
|
||||
|
||||
for (const dependency of ['chrome', 'node', 'electron']) {
|
||||
replaceText(`${dependency}-version`, process.versions[dependency])
|
||||
}
|
||||
})
|
||||
updateQuestionDescription: (id, questionDescription) => ipcRenderer.invoke('question-update-description', id, questionDescription),
|
||||
deleteQuestion: (id) => ipcRenderer.invoke('question-delete', id),
|
||||
updateFillBlankQuestion: (id, fillBlankData) => ipcRenderer.invoke('question-update-fill-blank', id, fillBlankData),
|
||||
deleteFillBlankQuestion: (id) => ipcRenderer.invoke('question-delete-fill-blank', id),
|
||||
fetchFillBlankQuestionsByQuestionId: (questionId) => ipcRenderer.invoke('question-fetch-fill-blank-by-question-id', questionId),
|
||||
// 考试管理相关API
|
||||
createExam: (examData) => ipcRenderer.invoke('exam-create', examData),
|
||||
fetchAllExams: () => ipcRenderer.invoke('exam-fetch-all'),
|
||||
fetchExamById: (id) => ipcRenderer.invoke('exam-fetch-by-id', id),
|
||||
updateExam: (id, examData) => ipcRenderer.invoke('exam-update', { id, examData }),
|
||||
deleteExam: (id) => ipcRenderer.invoke('exam-delete', id),
|
||||
fetchLastExam: () => ipcRenderer.invoke('exam-fetch-last'), // 添加这一行
|
||||
// 考生管理相关API
|
||||
fetchAllExaminees: () => ipcRenderer.invoke('examinee-fetch-all'),
|
||||
createExaminee: (examineeData) => ipcRenderer.invoke('examinee-create', examineeData),
|
||||
updateExaminee: (id, examineeData) => ipcRenderer.invoke('examinee-update', id, examineeData),
|
||||
deleteExaminee: (id) => ipcRenderer.invoke('examinee-delete', id),
|
||||
userLogin: (idCard, admissionTicket) => ipcRenderer.invoke('user-login', { idCard, admissionTicket })
|
||||
});
|
150
electron/service/examService.js
Normal file
150
electron/service/examService.js
Normal file
@ -0,0 +1,150 @@
|
||||
import {
|
||||
createExam,
|
||||
getAllExams,
|
||||
getExamById,
|
||||
updateExam,
|
||||
deleteExam,
|
||||
getLastExam // 添加这一行
|
||||
} from '../db/exam.js';
|
||||
|
||||
/**
|
||||
* 服务层:创建考试
|
||||
* @param {Object} examData 考试数据
|
||||
* @returns {Promise<Object>} 创建的考试
|
||||
*/
|
||||
export 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>} 考试列表
|
||||
*/
|
||||
export async function fetchAllExams() {
|
||||
try {
|
||||
return await getAllExams();
|
||||
} catch (error) {
|
||||
console.error('服务层: 查询所有考试失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:根据ID查询考试
|
||||
* @param {number} id 考试ID
|
||||
* @returns {Promise<Object|null>} 考试数据
|
||||
*/
|
||||
export 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>} 是否更新成功
|
||||
*/
|
||||
export 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>} 是否删除成功
|
||||
*/
|
||||
export 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>} 考试数据
|
||||
*/
|
||||
export async function fetchLastExam() {
|
||||
try {
|
||||
return await getLastExam();
|
||||
} catch (error) {
|
||||
console.error('服务层: 查询ID最大的考试失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
118
electron/service/examineeService.js
Normal file
118
electron/service/examineeService.js
Normal file
@ -0,0 +1,118 @@
|
||||
// 在文件开头的导入语句中添加新函数
|
||||
import {
|
||||
getAllExaminees,
|
||||
getExamineeById,
|
||||
createExaminee,
|
||||
updateExaminee,
|
||||
deleteExaminee,
|
||||
getExamineeByIdCardAndAdmissionTicket
|
||||
} from '../db/examinee.js';
|
||||
|
||||
/**
|
||||
* 服务层:获取所有考生列表
|
||||
* @returns {Promise<Array>} 考生列表
|
||||
*/
|
||||
export async function fetchAllExaminees() {
|
||||
try {
|
||||
return await getAllExaminees();
|
||||
} catch (error) {
|
||||
console.error('服务层: 获取所有考生列表失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 服务层:根据ID查询考生
|
||||
* @param {number} id 考生ID
|
||||
* @returns {Promise<Object|null>} 考生数据
|
||||
*/
|
||||
export async function fetchExamineeById(id) {
|
||||
try {
|
||||
return await getExamineeById(id);
|
||||
} catch (error) {
|
||||
console.error('服务层: 根据ID查询考生失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:添加考生
|
||||
* @param {Object} examineeData 考生数据
|
||||
* @returns {Promise<Object>} 添加的考生
|
||||
*/
|
||||
export 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>} 是否更新成功
|
||||
*/
|
||||
export 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>} 是否删除成功
|
||||
*/
|
||||
export 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
|
||||
*/
|
||||
export async function fetchExamineeByIdCardAndAdmissionTicket(idCard, admissionTicket) {
|
||||
try {
|
||||
if (!idCard || !admissionTicket) {
|
||||
throw new Error('身份证号和准考证号不能为空');
|
||||
}
|
||||
return await getExamineeByIdCardAndAdmissionTicket(idCard, admissionTicket);
|
||||
} catch (error) {
|
||||
console.error('服务层: 考生登录失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -6,7 +6,12 @@ import {
|
||||
deleteQuestion,
|
||||
getAllQuestionsWithRelations,
|
||||
updateQuestionDescription,
|
||||
addChoiceQuestion // 添加新函数导入
|
||||
addChoiceQuestion,
|
||||
updateChoiceQuestion, // 添加这一行
|
||||
addFillBlankQuestion,
|
||||
updateFillBlankQuestion,
|
||||
deleteFillBlankQuestion,
|
||||
getFillBlankQuestionsByQuestionId
|
||||
} from '../db/question.js';
|
||||
|
||||
// 导入configService中的increaseQuestionBankVersion方法
|
||||
@ -139,4 +144,88 @@ export async function createChoiceQuestion(choiceData) {
|
||||
console.error('服务层: 添加选择题失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:添加选择题问题
|
||||
* @param {number} id - 选择题ID
|
||||
* @param {Object} choiceData - 选择题数据
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
export async function modifyChoiceQuestion(id, choiceData) {
|
||||
try {
|
||||
const result = await updateChoiceQuestion(id, choiceData);
|
||||
// 调用增加题库版本号的方法
|
||||
await increaseQuestionBankVersion();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('服务层: 更新选择题失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:添加填空题问题
|
||||
* @param {Object} fillBlankData - 填空题数据
|
||||
* @returns {Promise<number>} 新创建的填空题ID
|
||||
*/
|
||||
export async function createFillBlankQuestion(fillBlankData) {
|
||||
try {
|
||||
const result = await addFillBlankQuestion(fillBlankData);
|
||||
// 调用增加题库版本号的方法
|
||||
await increaseQuestionBankVersion();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('服务层: 添加填空题失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:更新填空题问题
|
||||
* @param {number} id - 填空题ID
|
||||
* @param {Object} fillBlankData - 填空题数据
|
||||
* @returns {Promise<boolean>} 是否更新成功
|
||||
*/
|
||||
export async function modifyFillBlankQuestion(id, fillBlankData) {
|
||||
try {
|
||||
const result = await updateFillBlankQuestion(id, fillBlankData);
|
||||
// 调用增加题库版本号的方法
|
||||
await increaseQuestionBankVersion();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('服务层: 更新填空题失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:删除填空题问题
|
||||
* @param {number} id - 填空题ID
|
||||
* @returns {Promise<boolean>} 是否删除成功
|
||||
*/
|
||||
export async function removeFillBlankQuestion(id) {
|
||||
try {
|
||||
const result = await deleteFillBlankQuestion(id);
|
||||
// 调用增加题库版本号的方法
|
||||
await increaseQuestionBankVersion();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('服务层: 删除填空题失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务层:根据题干ID查询填空题问题
|
||||
* @param {number} questionId - 题干ID
|
||||
* @returns {Promise<Array>} 填空题列表
|
||||
*/
|
||||
export async function fetchFillBlankQuestionsByQuestionId(questionId) {
|
||||
try {
|
||||
return await getFillBlankQuestionsByQuestionId(questionId);
|
||||
} catch (error) {
|
||||
console.error('服务层: 根据题干ID查询填空题失败', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
31
package.json
31
package.json
@ -41,5 +41,36 @@
|
||||
"vite": "^7.0.6",
|
||||
"vite-plugin-electron": "^0.29.0",
|
||||
"vite-plugin-vue-devtools": "^8.0.0"
|
||||
},
|
||||
"build": {
|
||||
"appId": "com.example.electron-exam",
|
||||
"productName": "电子考试系统",
|
||||
"directories": {
|
||||
"output": "dist-electron"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"electron/**/*"
|
||||
],
|
||||
"win": {
|
||||
"target": [
|
||||
{
|
||||
"target": "nsis",
|
||||
"arch": ["ia32"]
|
||||
}
|
||||
],
|
||||
"icon": "public/favicon.ico"
|
||||
},
|
||||
"nsis": {
|
||||
"oneClick": false,
|
||||
"allowElevation": true,
|
||||
"allowToChangeInstallationDirectory": true,
|
||||
"installerIcon": "public/favicon.ico",
|
||||
"uninstallerIcon": "public/favicon.ico",
|
||||
"installerHeaderIcon": "public/favicon.ico",
|
||||
"createDesktopShortcut": true,
|
||||
"createStartMenuShortcut": true,
|
||||
"shortcutName": "电子考试系统"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
14
src/App.vue
14
src/App.vue
@ -1,14 +1,10 @@
|
||||
<script setup>
|
||||
import { RouterView } from 'vue-router'
|
||||
import { provideUserStore } from './store/userStore'
|
||||
|
||||
// 提供用户状态管理
|
||||
provideUserStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<RouterView />
|
||||
</div>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 可以在这里添加全局样式 */
|
||||
|
||||
</style>
|
||||
|
@ -82,7 +82,7 @@ const menuData = [
|
||||
index: '6',
|
||||
label: '考生管理',
|
||||
icon: 'users',
|
||||
route: '/admin/student-management'
|
||||
route: '/admin/examinee-management'
|
||||
},
|
||||
{
|
||||
index: '7',
|
||||
|
@ -2,7 +2,7 @@
|
||||
<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>© {{ thisYear }} 统计技能考试系统 - 版权所有</p>
|
||||
<p>© {{ thisYear }} 抚顺市统计局</p>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<p style="text-align: right !important;">题库版本:{{ questionBankVersion || '未知' }}</p>
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="col-auto">
|
||||
<h2 class="display-6 m-0 d-flex align-items-center">
|
||||
<font-awesome-icon icon="graduation-cap" class="me-3" />
|
||||
统计技能考试系统
|
||||
{{thisYear}}年抚顺市统计行业职工技能大赛考试系统
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
@ -26,4 +26,8 @@
|
||||
|
||||
<script setup>
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { ref } from 'vue'
|
||||
|
||||
// 使用ref定义响应式数据
|
||||
const thisYear = ref(new Date().getFullYear())
|
||||
</script>
|
@ -8,6 +8,10 @@ import QuestionManagementView from '@/views/admin/QuestionManagementView.vue'
|
||||
import ConfigManagementView from '@/views/admin/ConfigManagementView.vue'
|
||||
// 导入DictManagementView
|
||||
import DictManagementView from '@/views/admin/DictManagementView.vue'
|
||||
// 导入ExamManagementView
|
||||
import ExamManagementView from '@/views/admin/ExamManagementView.vue'
|
||||
// 导入ExamineeManagementView
|
||||
import ExamineeManagementView from '@/views/admin/ExamineeManagementView.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
@ -18,7 +22,7 @@ const router = createRouter({
|
||||
component: WelcomeView,
|
||||
},
|
||||
{
|
||||
path: '/student-home',
|
||||
path: '/student/home',
|
||||
name: 'student-home',
|
||||
component: StudentHomeView,
|
||||
},
|
||||
@ -46,6 +50,18 @@ const router = createRouter({
|
||||
name: 'admin-dict-management',
|
||||
component: DictManagementView,
|
||||
},
|
||||
// 添加ExamManagementView路由
|
||||
{
|
||||
path: '/admin/exam-management',
|
||||
name: 'admin-exam-management',
|
||||
component: ExamManagementView,
|
||||
},
|
||||
// 添加ExamineeManagementView路由
|
||||
{
|
||||
path: '/admin/examinee-management',
|
||||
name: 'admin-examinee-management',
|
||||
component: ExamineeManagementView,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
41
src/store/userStore.js
Normal file
41
src/store/userStore.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { reactive, provide, inject } from 'vue'
|
||||
|
||||
// 创建用户状态
|
||||
const userState = reactive({
|
||||
examinee: null,
|
||||
isLoggedIn: false
|
||||
})
|
||||
|
||||
// 创建状态管理工具
|
||||
const userStore = {
|
||||
state: userState,
|
||||
|
||||
// 设置考生数据
|
||||
setExaminee(examinee) {
|
||||
this.state.examinee = examinee
|
||||
this.state.isLoggedIn = true
|
||||
},
|
||||
|
||||
// 清除考生数据
|
||||
clearExaminee() {
|
||||
this.state.examinee = null
|
||||
this.state.isLoggedIn = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提供注入键
|
||||
const UserStoreKey = Symbol('UserStore')
|
||||
|
||||
// 提供状态管理
|
||||
export function provideUserStore() {
|
||||
provide(UserStoreKey, userStore)
|
||||
}
|
||||
|
||||
// 注入状态管理
|
||||
export function useUserStore() {
|
||||
const store = inject(UserStoreKey)
|
||||
if (!store) {
|
||||
throw new Error('useUserStore must be called within a provideUserStore provider')
|
||||
}
|
||||
return store
|
||||
}
|
@ -23,7 +23,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Bootstrap登录卡片 -->
|
||||
<div class="login-card bg-white rounded-4 shadow-lg p-5 w-100 max-w-md" id="login-section" style="height: 480px;" v-show="isDatabaseInitialized">
|
||||
<div class="login-card bg-white rounded-4 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 nav-justified" id="loginTab" role="tablist">
|
||||
@ -46,33 +46,33 @@
|
||||
<!-- 原有表单内容保持不变 -->
|
||||
<!-- 考生登录表单 -->
|
||||
<div class="h-100 tab-pane fade show active" id="exam-login" role="tabpanel" aria-labelledby="exam-tab">
|
||||
<form @submit.prevent="handleStudentLogin" class="d-flex flex-column h-100">
|
||||
<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="examId" class="form-label">准考证号</label>
|
||||
<div class="input-group input-group-lg">
|
||||
<span class="input-group-text">
|
||||
<FontAwesomeIcon icon="fa-solid fa-id-card" />
|
||||
</span>
|
||||
<input type="text" class="form-control" id="examId" v-model="studentId" required>
|
||||
</div>
|
||||
</div><!-- 结束:.mb-3.flex-grow-1.d-flex.flex-column.justify-content-center -->
|
||||
<div class="mb-3 flex-grow-1 d-flex flex-column justify-content-center">
|
||||
<label for="idCard" class="form-label">身份证号</label>
|
||||
<label for="examineeIdCard" class="form-label">身份证号</label>
|
||||
<div class="input-group input-group-lg">
|
||||
<span class="input-group-text">
|
||||
<FontAwesomeIcon icon="fa-solid fa-id-card-alt" />
|
||||
</span>
|
||||
<input type="text" class="form-control" id="idCard" v-model="studentName" required>
|
||||
<input type="text" class="form-control" id="examineeIdCard" v-model="examineeIdCard" required>
|
||||
</div>
|
||||
</div><!-- 结束:.mb-3.flex-grow-1.d-flex.flex-column.justify-content-center -->
|
||||
</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">
|
||||
<FontAwesomeIcon icon="fa-solid fa-ticket-alt" />
|
||||
</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">
|
||||
<FontAwesomeIcon icon="fa-solid fa-sign-in-alt" class="me-2" />
|
||||
登录
|
||||
</button>
|
||||
</div><!-- 结束:.mt-4.flex-grow-1.d-flex.flex-column.justify-content-end -->
|
||||
</div>
|
||||
</form>
|
||||
</div><!-- 结束:#exam-login -->
|
||||
</div>
|
||||
|
||||
<!-- 管理员登录表单 -->
|
||||
<div class="h-100 tab-pane fade" id="admin-login" role="tabpanel" aria-labelledby="admin-tab">
|
||||
@ -88,9 +88,10 @@
|
||||
<div id="admin-error-message" class="text-danger mt-2" style="display: none;"></div>
|
||||
</div><!-- 结束:.mb-3.flex-grow-1.d-flex.flex-column.justify-content-center -->
|
||||
<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">
|
||||
<FontAwesomeIcon icon="fa-solid fa-sign-in-alt" class="me-2" />
|
||||
登录
|
||||
<button type="submit" class="btn btn-primary w-100 py-2 fs-5" :disabled="isLoading">
|
||||
<FontAwesomeIcon v-if="isLoading" icon="fa-solid fa-spinner fa-spin" class="me-2" />
|
||||
<FontAwesomeIcon v-else icon="fa-solid fa-sign-in-alt" class="me-2" />
|
||||
{{ isLoading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</div><!-- 结束:.mt-4.flex-grow-1.d-flex.flex-column.justify-content-end -->
|
||||
</form>
|
||||
@ -116,11 +117,12 @@ import { ElMessage } from 'element-plus'
|
||||
|
||||
// 响应式数据
|
||||
const router = useRouter()
|
||||
const studentName = ref('') // 身份证号
|
||||
const studentId = ref('') // 准考证号
|
||||
const examineeIdCard = ref('') // 身份证号
|
||||
const examineeAdmissionTicket = ref('') // 准考证号
|
||||
const adminPassword = ref('')
|
||||
const isDatabaseInitialized = ref(false)
|
||||
const isInitializing = ref(false)
|
||||
const isLoading = ref(false) // 添加加载状态
|
||||
|
||||
// 在组件挂载时检查数据库初始化状态
|
||||
onMounted(async () => {
|
||||
@ -164,16 +166,57 @@ const initializeDatabase = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// 导入用户状态管理
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
// 获取用户状态管理
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 考生登录方法
|
||||
const handleStudentLogin = () => {
|
||||
console.log('考生登录 - 开始', { studentId: studentId.value, studentName: studentName.value });
|
||||
if (!studentName.value || !studentId.value) {
|
||||
const handleExamineeLogin = async () => {
|
||||
console.log('考生登录 - 开始', {
|
||||
examineeIdCard: examineeIdCard.value,
|
||||
examineeAdmissionTicket: examineeAdmissionTicket.value
|
||||
});
|
||||
|
||||
// 清除首尾空格
|
||||
const idCard = examineeIdCard.value.trim();
|
||||
const admissionTicket = examineeAdmissionTicket.value.trim();
|
||||
|
||||
// 前端验证
|
||||
if (!idCard || !admissionTicket) {
|
||||
console.warn('考生登录 - 验证失败: 身份证号和准考证号不能为空');
|
||||
alert('请输入身份证号和准考证号');
|
||||
ElMessage.error('请输入身份证号和准考证号');
|
||||
return;
|
||||
}
|
||||
console.log('考生登录 - 验证通过,跳转到学生首页');
|
||||
router.push('/student-home');
|
||||
|
||||
// 设置加载状态
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
// 调用登录API
|
||||
const result = await window.electronAPI.userLogin(idCard, admissionTicket);
|
||||
|
||||
if (result.success) {
|
||||
console.log('考生登录 - 成功', result.data);
|
||||
// 保存用户信息到store - 修改这里
|
||||
userStore.setExaminee({
|
||||
...result.data
|
||||
});
|
||||
ElMessage.success('登录成功');
|
||||
// 跳转到考生首页
|
||||
router.push('/student/home');
|
||||
} else {
|
||||
console.warn('考生登录 - 失败:', result.error);
|
||||
ElMessage.error(result.error || '登录失败,请检查身份证号和准考证号');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('考生登录 - 异常:', error);
|
||||
ElMessage.error('登录失败,请重试');
|
||||
} finally {
|
||||
// 无论成功失败,都关闭加载状态
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 管理员登录方法
|
||||
|
237
src/views/admin/ExamManagementView.vue
Normal file
237
src/views/admin/ExamManagementView.vue
Normal file
@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<div class="exam-management-container">
|
||||
<div class="exam-header">
|
||||
<h1>考试管理</h1>
|
||||
<el-button type="primary" @click="handleAddExam">+ 添加考试</el-button>
|
||||
</div>
|
||||
<div class="exam-content">
|
||||
<el-table :data="examList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="exam_name" label="考试名称" width="200" />
|
||||
<el-table-column prop="exam_description" label="考试描述">
|
||||
<template #default="scope">
|
||||
<div class="ellipsis-cell" :title="scope.row.exam_description">{{ scope.row.exam_description }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="exam_minutes" label="考试时长(分钟)" width="120" />
|
||||
<el-table-column prop="created_at" label="创建时间" width="180" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEditExam(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDeleteExam(scope.row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑考试弹窗 -->
|
||||
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
|
||||
<el-form :model="formData" label-width="120px">
|
||||
<el-form-item label="考试名称" prop="exam_name">
|
||||
<el-input v-model="formData.exam_name" placeholder="请输入考试名称(选填)" />
|
||||
</el-form-item>
|
||||
<el-form-item label="考试描述" prop="exam_description">
|
||||
<el-input v-model="formData.exam_description" type="textarea" placeholder="请输入考试描述" :rows="4" />
|
||||
</el-form-item>
|
||||
<el-form-item label="考试须知" prop="exam_notice">
|
||||
<el-input v-model="formData.exam_notice" type="textarea" placeholder="请输入考试须知,每行一条" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="考试时长(分钟)" prop="exam_minutes" :rules="[{ required: true, message: '请输入考试时长', trigger: 'blur' }, { type: 'number', min: 1, message: '考试时长必须大于0分钟' }]">
|
||||
<el-input v-model.number="formData.exam_minutes" type="number" placeholder="请输入考试时长" />
|
||||
</el-form-item>
|
||||
<el-form-item label="最少考试时间(分钟)" prop="exam_minutes_min" :rules="[{ required: true, message: '请输入最少考试时间', trigger: 'blur' }, { type: 'number', min: 0, message: '最少考试时间不能小于0分钟' }]">
|
||||
<el-input v-model.number="formData.exam_minutes_min" type="number" placeholder="请输入最少考试时间" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveExam">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import AdminLayout from '@/components/admin/AdminLayout.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// 数据和状态
|
||||
const examList = ref([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加考试')
|
||||
const isEdit = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
id: null,
|
||||
exam_name: '',
|
||||
exam_description: '',
|
||||
exam_notice: '',
|
||||
exam_minutes: 60,
|
||||
exam_minutes_min: 0, // 添加最少考试时间字段
|
||||
exam_examinee_type: ''
|
||||
})
|
||||
|
||||
|
||||
// 获取考试列表
|
||||
const fetchExams = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await window.electronAPI.fetchAllExams()
|
||||
if (result.success) {
|
||||
// 格式化日期
|
||||
const formattedExams = result.data.map(exam => ({
|
||||
...exam,
|
||||
created_at: formatDate(exam.created_at)
|
||||
}))
|
||||
examList.value = formattedExams
|
||||
} else {
|
||||
ElMessage.error('获取考试列表失败: ' + result.error)
|
||||
console.error('获取考试列表失败', result.error)
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('获取考试列表失败: ' + error.message)
|
||||
console.error('获取考试列表失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return ''
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleString()
|
||||
}
|
||||
|
||||
|
||||
// 处理添加考试
|
||||
const handleAddExam = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加考试'
|
||||
formData.value = {
|
||||
exam_name: '',
|
||||
exam_description: '',
|
||||
exam_notice: '',
|
||||
exam_minutes: null, // 修改为null,因为现在是必填项
|
||||
exam_minutes_min: null, // 修改为null,因为现在是必填项
|
||||
exam_examinee_type: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理编辑考试
|
||||
const handleEditExam = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑考试'
|
||||
formData.value = {...row}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理保存考试
|
||||
const handleSaveExam = async () => {
|
||||
try {
|
||||
// 将Proxy对象转换为普通对象
|
||||
const examData = {...formData.value};
|
||||
|
||||
// 将考试须知字符串转换为数组并序列化
|
||||
if (examData.exam_notice) {
|
||||
const noticeArray = examData.exam_notice.split('\n').filter(item => item.trim() !== '');
|
||||
examData.exam_notice = JSON.stringify(noticeArray);
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await window.electronAPI.updateExam(examData.id, examData)
|
||||
ElMessage.success('考试更新成功')
|
||||
} else {
|
||||
await window.electronAPI.createExam(examData)
|
||||
ElMessage.success('考试添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchExams() // 重新加载列表
|
||||
} catch (error) {
|
||||
ElMessage.error('保存考试失败: ' + error.message)
|
||||
console.error('保存考试失败', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理删除考试
|
||||
const handleDeleteExam = (id) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除该考试吗?',
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
await window.electronAPI.deleteExam(id)
|
||||
ElMessage.success('考试删除成功')
|
||||
fetchExams() // 重新加载列表
|
||||
} catch (error) {
|
||||
ElMessage.error('删除考试失败: ' + error.message)
|
||||
console.error('删除考试失败', error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
fetchExams()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
exam-management-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
exam-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
exam-content {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 添加省略号样式 */
|
||||
.ellipsis-cell {
|
||||
white-space: nowrap; /* 不自动换行 */
|
||||
overflow: hidden; /* 超出部分隐藏 */
|
||||
text-overflow: ellipsis; /* 超出部分显示省略号 */
|
||||
}
|
||||
|
||||
/* 确保表格单元格内容不换行 */
|
||||
:deep(.el-table__cell) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
227
src/views/admin/ExamineeManagementView.vue
Normal file
227
src/views/admin/ExamineeManagementView.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<AdminLayout>
|
||||
<div class="examinee-management-container">
|
||||
<div class="examinee-header">
|
||||
<h1>考生管理</h1>
|
||||
<el-button type="primary" @click="handleAddExaminee">+ 添加考生</el-button>
|
||||
</div>
|
||||
<div class="examinee-content">
|
||||
<el-table :data="examineeList" style="width: 100%" v-loading="loading">
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column prop="examinee_name" label="姓名" width="120" />
|
||||
<el-table-column prop="examinee_id_card" label="身份证号" width="180" />
|
||||
<el-table-column prop="examinee_admission_ticket" label="准考证号" width="180" />
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="handleEditExaminee(scope.row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="handleDeleteExaminee(scope.row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑考生弹窗 -->
|
||||
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef">
|
||||
<el-form-item label="考生姓名" prop="examinee_name" :rules="[{ required: true, message: '请输入考生姓名', trigger: 'blur' }]">
|
||||
<el-input v-model="formData.examinee_name" placeholder="请输入考生姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="性别" prop="examinee_gender">
|
||||
<el-select v-model="formData.examinee_gender" placeholder="请选择性别">
|
||||
<el-option label="男" value="男" />
|
||||
<el-option label="女" value="女" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="examinee_unit">
|
||||
<el-input v-model="formData.examinee_unit" placeholder="请输入单位" />
|
||||
</el-form-item>
|
||||
<el-form-item label="笔试考场" prop="written_exam_room">
|
||||
<el-input v-model="formData.written_exam_room" placeholder="请输入笔试考场" />
|
||||
</el-form-item>
|
||||
<el-form-item label="笔试座位号" prop="written_exam_seat">
|
||||
<el-input v-model="formData.written_exam_seat" placeholder="请输入笔试座位号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="机试考场" prop="computer_exam_room">
|
||||
<el-input v-model="formData.computer_exam_room" placeholder="请输入机试考场" />
|
||||
</el-form-item>
|
||||
<el-form-item label="机试座位号" prop="computer_exam_seat">
|
||||
<el-input v-model="formData.computer_exam_seat" placeholder="请输入机试座位号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="身份证号" prop="examinee_id_card" :rules="[{ required: true, message: '请输入身份证号', trigger: 'blur' }]">
|
||||
<el-input v-model="formData.examinee_id_card" placeholder="请输入身份证号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="准考证号" prop="examinee_admission_ticket" :rules="[{ required: true, message: '请输入准考证号', trigger: 'blur' }]">
|
||||
<el-input v-model="formData.examinee_admission_ticket" placeholder="请输入准考证号" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveExaminee">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import AdminLayout from '@/components/admin/AdminLayout.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
// 数据和状态
|
||||
const examineeList = ref([])
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('添加考生')
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const formData = ref({
|
||||
id: null,
|
||||
examinee_name: '',
|
||||
examinee_gender: '',
|
||||
examinee_unit: '',
|
||||
written_exam_room: '',
|
||||
written_exam_seat: '',
|
||||
computer_exam_room: '',
|
||||
computer_exam_seat: '',
|
||||
examinee_id_card: '',
|
||||
examinee_admission_ticket: ''
|
||||
})
|
||||
|
||||
// 获取考生列表
|
||||
const fetchExaminees = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await window.electronAPI.fetchAllExaminees()
|
||||
examineeList.value = result
|
||||
} catch (error) {
|
||||
ElMessage.error('获取考生列表失败: ' + error.message)
|
||||
console.error('获取考生列表失败', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理添加考生
|
||||
const handleAddExaminee = () => {
|
||||
isEdit.value = false
|
||||
dialogTitle.value = '添加考生'
|
||||
formData.value = {
|
||||
id: null,
|
||||
examinee_name: '',
|
||||
examinee_gender: '',
|
||||
examinee_unit: '',
|
||||
written_exam_room: '',
|
||||
written_exam_seat: '',
|
||||
computer_exam_room: '',
|
||||
computer_exam_seat: '',
|
||||
examinee_id_card: '',
|
||||
examinee_admission_ticket: ''
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理编辑考生
|
||||
const handleEditExaminee = (row) => {
|
||||
isEdit.value = true
|
||||
dialogTitle.value = '编辑考生'
|
||||
formData.value = {...row}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理保存考生
|
||||
const handleSaveExaminee = async () => {
|
||||
formRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 将Proxy对象转换为普通对象
|
||||
const examineeData = {...formData.value};
|
||||
|
||||
if (isEdit.value) {
|
||||
await window.electronAPI.updateExaminee(examineeData.id, examineeData)
|
||||
ElMessage.success('考生更新成功')
|
||||
} else {
|
||||
await window.electronAPI.createExaminee(examineeData)
|
||||
ElMessage.success('考生添加成功')
|
||||
}
|
||||
dialogVisible.value = false
|
||||
fetchExaminees() // 重新加载列表
|
||||
} catch (error) {
|
||||
ElMessage.error('保存考生失败: ' + error.message)
|
||||
console.error('保存考生失败', error)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 处理删除考生
|
||||
const handleDeleteExaminee = (id) => {
|
||||
ElMessageBox.confirm(
|
||||
'确定要删除该考生吗?',
|
||||
'确认删除',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
try {
|
||||
await window.electronAPI.deleteExaminee(id)
|
||||
ElMessage.success('考生删除成功')
|
||||
fetchExaminees() // 重新加载列表
|
||||
} catch (error) {
|
||||
ElMessage.error('删除考生失败: ' + error.message)
|
||||
console.error('删除考生失败', error)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// 取消删除
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
fetchExaminees()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
examinee-management-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
examinee-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
examinee-content {
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 确保表格单元格内容不换行 */
|
||||
:deep(.el-table__cell) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
@ -55,22 +55,24 @@
|
||||
<!-- 选择题 -->
|
||||
<div v-if="question.choices && question.choices.length > 0">
|
||||
<div v-for="(choice, idx) in question.choices" :key="idx" class="related-question-item">
|
||||
<p><strong>{{ choice.choice_type === 'single' ? '单选题' : '多选题' }} {{ idx + 1 }}:</strong> {{ choice.choice_description }}</p>
|
||||
<div class="question-item-header">
|
||||
<p><strong>{{ choice.choice_type === 'single' ? '单选题' : '多选题' }} {{ idx + 1 }}({{ choice.score }}分):</strong> {{ choice.choice_description }}</p>
|
||||
<el-button type="primary" size="small" @click="handleEditChoiceQuestion(choice, idx)">编辑</el-button>
|
||||
</div>
|
||||
<div class="choice-options">
|
||||
<div v-for="(option, optIdx) in JSON.parse(choice.choice_options)" :key="optIdx">
|
||||
<span :class="{ 'correct-answer': isCorrectAnswer(optIdx + 1, JSON.parse(choice.correct_answers)) }">{{ String.fromCharCode(65 + optIdx) }}. {{ option }}</span>
|
||||
<span :class="{ 'correct-answer': isCorrectAnswer(optIdx + 1, JSON.parse(choice.correct_answers), JSON.parse(choice.choice_options)) }">{{ String.fromCharCode(65 + optIdx) }}. {{ option }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p><strong>正确答案:</strong> {{ formatCorrectAnswers(JSON.parse(choice.correct_answers), JSON.parse(choice.choice_options).length) }}</p>
|
||||
<p><strong>正确答案:</strong> {{ formatCorrectAnswers(JSON.parse(choice.correct_answers)) }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 填空题 -->
|
||||
<div v-else-if="question.fillBlanks && question.fillBlanks.length > 0">
|
||||
<div v-for="(fillBlank, idx) in question.fillBlanks" :key="idx" class="related-question-item">
|
||||
<p><strong>填空题 {{ idx + 1 }}:</strong></p>
|
||||
<p><strong>空白数量:</strong> {{ fillBlank.blank_count }}</p>
|
||||
<p><strong>答案:</strong> {{ fillBlank.answers }}</p>
|
||||
<p><strong>填空题 {{ idx + 1 }}({{ fillBlank.score }}分):</strong>{{ fillBlank.blank_description }}</p>
|
||||
<p><strong>正确答案:</strong> {{ fillBlank.correct_answers }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -78,7 +80,9 @@
|
||||
<div v-else-if="question.judges && question.judges.length > 0">
|
||||
<div v-for="(judge, idx) in question.judges" :key="idx" class="related-question-item">
|
||||
<p><strong>判断题 {{ idx + 1 }}:</strong> {{ judge.content }}</p>
|
||||
<p><strong>正确答案:</strong> <span :class="judge.is_correct ? 'correct-answer' : 'wrong-answer'">{{ judge.is_correct ? '正确' : '错误' }}</span></p>
|
||||
<p><strong>正确答案:</strong> <span :class="judge.is_correct ? 'correct-answer' : 'wrong-answer'">{{
|
||||
judge.is_correct
|
||||
? '正确' : '错误' }}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -104,11 +108,8 @@
|
||||
@update:formData="newData => formData = newData" @onSave="handleSaveQuestion" @onCancel="handleDialogClose" />
|
||||
</template>
|
||||
<template v-else-if="dialogFormType === 'editDescription'">
|
||||
<QuestionDescriptionEditForm
|
||||
v-model="editDescriptionFormData"
|
||||
@submit="handleSaveQuestionDescription"
|
||||
@onCancel="handleDialogClose"
|
||||
/>
|
||||
<QuestionDescriptionEditForm v-model="editDescriptionFormData" @submit="handleSaveQuestionDescription"
|
||||
@onCancel="handleDialogClose" />
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
@ -118,6 +119,9 @@
|
||||
<el-form-item label="问题描述" prop="choice_description">
|
||||
<el-input v-model="choiceQuestionFormData.choice_description" type="textarea" placeholder="请输入问题描述"
|
||||
:rows="3" /></el-form-item>
|
||||
<el-form-item label="分值" prop="score">
|
||||
<el-input v-model="choiceQuestionFormData.score" type="number" placeholder="请输入分值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="题目类型" prop="choice_type">
|
||||
<el-radio-group v-model="choiceQuestionFormData.choice_type">
|
||||
<el-radio :label="'single'">单选题</el-radio>
|
||||
@ -138,13 +142,13 @@
|
||||
<template v-if="choiceQuestionFormData.choice_type === 'single'">
|
||||
<el-select v-model="choiceQuestionFormData.correct_answers[0]" placeholder="请选择正确答案">
|
||||
<el-option v-for="(option, index) in choiceQuestionFormData.choice_options" :key="index"
|
||||
:label="option.value" :value="index + 1" />
|
||||
:label="option.value" :value="option.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-checkbox-group v-model="choiceQuestionFormData.correct_answers">
|
||||
<el-checkbox v-for="(option, index) in choiceQuestionFormData.choice_options" :key="index"
|
||||
:label="index + 1">{{ option.value }}</el-checkbox>
|
||||
:label="option.value">{{ option.value }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
</el-form-item>
|
||||
@ -155,15 +159,66 @@
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑选择题弹窗 -->
|
||||
<el-dialog :title="editChoiceQuestionDialogTitle" v-model="editChoiceQuestionDialogVisible" width="800px">
|
||||
<el-form ref="editChoiceQuestionFormRef" :model="editChoiceQuestionFormData" label-width="120px">
|
||||
<el-form-item label="问题描述" prop="choice_description">
|
||||
<el-input v-model="editChoiceQuestionFormData.choice_description" type="textarea" placeholder="请输入问题描述" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分值" prop="score">
|
||||
<el-input v-model="editChoiceQuestionFormData.score" type="number" placeholder="请输入分值" />
|
||||
</el-form-item>
|
||||
<el-form-item label="题目类型" prop="choice_type">
|
||||
<el-radio-group v-model="editChoiceQuestionFormData.choice_type">
|
||||
<el-radio :label="'single'">单选题</el-radio>
|
||||
<el-radio :label="'multiple'">多选题</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="选项列表">
|
||||
<div v-for="(option, index) in editChoiceQuestionFormData.choice_options" :key="index" class="option-item">
|
||||
<div class="option-label">{{ String.fromCharCode(65 + index) }}.</div>
|
||||
<el-input v-model="option.value" placeholder="请输入选项内容" style="width: calc(100% - 40px)" />
|
||||
<el-button type="danger" size="small" @click="removeEditOption(index)" v-if="editChoiceQuestionFormData.choice_options.length > 1">删除</el-button>
|
||||
</div>
|
||||
<el-button type="primary" size="small" @click="addEditOption">+ 添加选项</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="正确答案" prop="correct_answers">
|
||||
<template v-if="editChoiceQuestionFormData.choice_type === 'single'">
|
||||
<el-select v-model="editChoiceQuestionFormData.correct_answers[0]" placeholder="请选择正确答案">
|
||||
<el-option v-for="(option, index) in editChoiceQuestionFormData.choice_options" :key="index" :label="option.value" :value="option.value" />
|
||||
</el-select>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-checkbox-group v-model="editChoiceQuestionFormData.correct_answers">
|
||||
<el-checkbox v-for="(option, index) in editChoiceQuestionFormData.choice_options" :key="index" :label="option.value">{{ option.value }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editChoiceQuestionDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleSaveEditedChoiceQuestion">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 填空题弹窗 -->
|
||||
<el-dialog title="添加填空题" v-model="fillBlankQuestionDialogVisible" width="800px">
|
||||
<el-form ref="fillBlankQuestionFormRef" :model="fillBlankQuestionFormData" label-width="120px">
|
||||
<el-form-item label="空白数量" prop="blank_count">
|
||||
<el-input v-model.number="fillBlankQuestionFormData.blank_count" placeholder="请输入空白数量" type="number"
|
||||
min="1" /></el-form-item>
|
||||
<el-form-item label="答案" prop="answers">
|
||||
<el-input v-model="fillBlankQuestionFormData.answers" type="textarea" placeholder="请输入答案,多个答案用逗号分隔"
|
||||
:rows="3" /></el-form-item>
|
||||
<el-form-item label="问题描述" prop="blank_description" required>
|
||||
<el-input v-model="fillBlankQuestionFormData.blank_description" type="textarea" placeholder="请输入问题描述" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="空白数量" prop="blank_count" required>
|
||||
<el-input v-model.number="fillBlankQuestionFormData.blank_count" placeholder="请输入空白数量" type="number" min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="正确答案" prop="correct_answers">
|
||||
<div v-for="index in fillBlankQuestionFormData.blank_count" :key="index" class="answer-item">
|
||||
<div class="answer-label">答案 {{ index }}:</div>
|
||||
<el-input v-model="fillBlankQuestionFormData.correct_answers[index-1]" placeholder="正确答案" style="width: calc(100% - 60px)" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="分值" prop="score" required>
|
||||
<el-input v-model.number="fillBlankQuestionFormData.score" placeholder="请输入分值" type="number" min="0" step="0.5" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="fillBlankQuestionDialogVisible = false">取消</el-button>
|
||||
@ -209,7 +264,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { ref, onMounted, nextTick, watch } from 'vue'
|
||||
import AdminLayout from '@/components/admin/AdminLayout.vue'
|
||||
import QuestionAddForm from '@/components/admin/QuestionAddForm.vue'
|
||||
import QuestionDescriptionEditForm from '@/components/admin/QuestionDescriptionEditForm.vue'
|
||||
@ -254,8 +309,8 @@ const loadQuestionTypes = async () => {
|
||||
const fetchQuestions = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const questions = await window.electronAPI.fetchAllQuestionsWithRelations();
|
||||
console.log('questions', questions);
|
||||
const questions = await window.electronAPI.fetchAllQuestionsWithRelations()
|
||||
console.log('questions', questions)
|
||||
const processedQuestions = questions.map(question => ({
|
||||
...question,
|
||||
image_count: question.images ? question.images.length : 0,
|
||||
@ -325,21 +380,21 @@ const handleEditQuestionDescription = (question) => {
|
||||
// 保存题干描述修改
|
||||
const handleSaveQuestionDescription = async (validatedData) => {
|
||||
try {
|
||||
if (!validatedData) return;
|
||||
if (!validatedData) return
|
||||
|
||||
await window.electronAPI.updateQuestionDescription(
|
||||
validatedData.id,
|
||||
validatedData.question_description
|
||||
);
|
||||
)
|
||||
|
||||
dialogVisible.value = false;
|
||||
ElMessage.success('更新题干描述成功');
|
||||
fetchQuestions();
|
||||
dialogVisible.value = false
|
||||
ElMessage.success('更新题干描述成功')
|
||||
fetchQuestions()
|
||||
} catch (error) {
|
||||
console.error('Failed to update question description:', error);
|
||||
ElMessage.error('更新题干描述失败');
|
||||
console.error('Failed to update question description:', error)
|
||||
ElMessage.error('更新题干描述失败')
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetAddForm = () => {
|
||||
@ -410,29 +465,150 @@ const choiceQuestionFormData = ref({
|
||||
{ value: '' },
|
||||
{ value: '' }
|
||||
],
|
||||
correct_answers: [] // 正确答案(序号数组)
|
||||
correct_answers: [], // 正确答案(字符串数组)
|
||||
score: null
|
||||
})
|
||||
|
||||
// 编辑选择题相关数据
|
||||
const editChoiceQuestionDialogVisible = ref(false)
|
||||
const editChoiceQuestionDialogTitle = ref('编辑选择题')
|
||||
const editChoiceQuestionFormData = ref({
|
||||
id: null,
|
||||
question_id: null,
|
||||
choice_description: '',
|
||||
choice_type: 'single',
|
||||
choice_options: [
|
||||
{ value: '' },
|
||||
{ value: '' },
|
||||
{ value: '' },
|
||||
{ value: '' }
|
||||
],
|
||||
correct_answers: [],
|
||||
score: null
|
||||
})
|
||||
const editChoiceQuestionFormRef = ref(null)
|
||||
|
||||
// 添加选项
|
||||
const addOption = () => {
|
||||
choiceQuestionFormData.value.choice_options.push({ value: '' })
|
||||
}
|
||||
|
||||
// 添加编辑选项
|
||||
const addEditOption = () => {
|
||||
editChoiceQuestionFormData.value.choice_options.push({ value: '' })
|
||||
}
|
||||
|
||||
// 删除选项
|
||||
const removeOption = (index) => {
|
||||
// 移除选项
|
||||
choiceQuestionFormData.value.choice_options.splice(index, 1)
|
||||
// 移除相关的正确答案
|
||||
choiceQuestionFormData.value.correct_answers = choiceQuestionFormData.value.correct_answers.filter(
|
||||
answer => answer !== index + 1
|
||||
answer => answer !== choiceQuestionFormData.value.choice_options[index].value
|
||||
)
|
||||
}
|
||||
|
||||
// 删除编辑选项
|
||||
const removeEditOption = (index) => {
|
||||
// 移除选项
|
||||
editChoiceQuestionFormData.value.choice_options.splice(index, 1)
|
||||
// 移除相关的正确答案
|
||||
editChoiceQuestionFormData.value.correct_answers = editChoiceQuestionFormData.value.correct_answers.filter(
|
||||
answer => answer !== editChoiceQuestionFormData.value.choice_options[index].value
|
||||
)
|
||||
}
|
||||
|
||||
// 处理编辑选择题按钮点击
|
||||
const handleEditChoiceQuestion = (choice, index) => {
|
||||
// 填充编辑表单数据
|
||||
editChoiceQuestionFormData.value = {
|
||||
id: choice.id,
|
||||
question_id: choice.question_id,
|
||||
choice_description: choice.choice_description,
|
||||
choice_type: choice.choice_type,
|
||||
choice_options: JSON.parse(choice.choice_options).map(option => ({ value: option })),
|
||||
correct_answers: JSON.parse(choice.correct_answers),
|
||||
score: choice.score
|
||||
}
|
||||
editChoiceQuestionDialogTitle.value = `编辑${choice.choice_type === 'single' ? '单选题' : '多选题'} ${index + 1}`
|
||||
editChoiceQuestionDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 保存编辑后的选择题 (空实现)
|
||||
const handleSaveEditedChoiceQuestion = () => {
|
||||
editChoiceQuestionFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 收集表单数据并确保可序列化
|
||||
const choiceData = {
|
||||
choice_description: String(editChoiceQuestionFormData.value.choice_description),
|
||||
choice_type: String(editChoiceQuestionFormData.value.choice_type),
|
||||
score: String(editChoiceQuestionFormData.value.score),
|
||||
choice_options: editChoiceQuestionFormData.value.choice_options
|
||||
.map(option => String(option.value)), // 确保每个选项都是字符串
|
||||
correct_answers: editChoiceQuestionFormData.value.correct_answers
|
||||
.map(answer => String(answer)) // 确保正确答案是字符串类型
|
||||
};
|
||||
|
||||
// 调用更新选择题接口
|
||||
await window.electronAPI.updateChoiceQuestion(
|
||||
editChoiceQuestionFormData.value.id,
|
||||
choiceData
|
||||
);
|
||||
|
||||
// 关闭弹窗并显示成功消息
|
||||
editChoiceQuestionDialogVisible.value = false;
|
||||
ElMessage.success('选择题更新成功');
|
||||
|
||||
// 刷新题干列表
|
||||
fetchQuestions();
|
||||
|
||||
// 重置表单
|
||||
editChoiceQuestionFormData.value = {
|
||||
id: null,
|
||||
question_id: null,
|
||||
choice_description: '',
|
||||
choice_type: 'single',
|
||||
choice_options: [
|
||||
{ value: '' },
|
||||
{ value: '' },
|
||||
{ value: '' },
|
||||
{ value: '' }
|
||||
],
|
||||
correct_answers: [],
|
||||
score: null
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to update choice question:', error);
|
||||
ElMessage.error('选择题更新失败: ' + (error.message || '未知错误'));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const fillBlankQuestionFormData = ref({
|
||||
blank_description: '',
|
||||
blank_count: 1,
|
||||
answers: ''
|
||||
correct_answers: [''],
|
||||
score: 0
|
||||
})
|
||||
|
||||
// 监听blank_count变化,动态调整correct_answers数组长度
|
||||
watch(
|
||||
() => fillBlankQuestionFormData.value.blank_count,
|
||||
(newCount, oldCount) => {
|
||||
const answers = fillBlankQuestionFormData.value.correct_answers
|
||||
if (newCount > oldCount) {
|
||||
// 增加答案输入框
|
||||
for (let i = oldCount; i < newCount; i++) {
|
||||
answers.push('')
|
||||
}
|
||||
} else if (newCount < oldCount) {
|
||||
// 减少答案输入框
|
||||
answers.splice(newCount)
|
||||
}
|
||||
}
|
||||
)
|
||||
const judgeQuestionFormData = ref({
|
||||
content: '',
|
||||
is_correct: true
|
||||
@ -487,10 +663,11 @@ const handleSaveChoiceQuestion = () => {
|
||||
question_id: Number(currentQuestionId.value), // 确保是数字类型
|
||||
choice_description: String(choiceQuestionFormData.value.choice_description),
|
||||
choice_type: String(choiceQuestionFormData.value.choice_type),
|
||||
score: String(choiceQuestionFormData.value.score),
|
||||
choice_options: choiceQuestionFormData.value.choice_options
|
||||
.map(option => String(option.value)), // 确保每个选项都是字符串
|
||||
correct_answers: choiceQuestionFormData.value.correct_answers
|
||||
.map(answer => Number(answer)) // 确保正确答案是数字类型
|
||||
.map(answer => String(answer)) // 确保正确答案是字符串类型
|
||||
}
|
||||
|
||||
// 调用添加选择题接口
|
||||
@ -513,7 +690,8 @@ const handleSaveChoiceQuestion = () => {
|
||||
{ value: '' },
|
||||
{ value: '' }
|
||||
],
|
||||
correct_answers: []
|
||||
correct_answers: [],
|
||||
score: null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create choice question:', error)
|
||||
@ -523,9 +701,43 @@ const handleSaveChoiceQuestion = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 修改保存填空题的方法
|
||||
const handleSaveFillBlankQuestion = () => {
|
||||
fillBlankQuestionDialogVisible.value = false
|
||||
ElMessage.success('填空题添加成功')
|
||||
fillBlankQuestionFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
// 确保correct_answers是一个纯数组
|
||||
const correctAnswers = Array.isArray(fillBlankQuestionFormData.value.correct_answers)
|
||||
? fillBlankQuestionFormData.value.correct_answers.map(answer => String(answer).trim())
|
||||
: [];
|
||||
|
||||
const fillBlankData = {
|
||||
question_id: Number(currentQuestionId.value),
|
||||
blank_description: fillBlankQuestionFormData.value.blank_description,
|
||||
blank_count: Number(fillBlankQuestionFormData.value.blank_count),
|
||||
correct_answers: correctAnswers,
|
||||
score: Number(fillBlankQuestionFormData.value.score)
|
||||
};
|
||||
|
||||
// 验证数据是否可序列化
|
||||
try {
|
||||
JSON.stringify(fillBlankData);
|
||||
} catch (e) {
|
||||
console.error('数据序列化失败:', e);
|
||||
ElMessage.error('数据格式错误,无法保存');
|
||||
return;
|
||||
}
|
||||
|
||||
await window.electronAPI.createFillBlankQuestion(fillBlankData)
|
||||
ElMessage.success('填空题添加成功')
|
||||
fillBlankQuestionDialogVisible.value = false
|
||||
fetchQuestions()
|
||||
} catch (error) {
|
||||
console.error('保存填空题失败:', error)
|
||||
ElMessage.error('保存填空题失败: ' + (error.message || '未知错误'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveJudgeQuestion = () => {
|
||||
@ -551,20 +763,22 @@ const hasRelatedQuestions = (question) => {
|
||||
(question.fillBlanks && question.fillBlanks.length > 0) ||
|
||||
(question.judges && question.judges.length > 0) ||
|
||||
(question.shorts && question.shorts.length > 0)
|
||||
);
|
||||
};
|
||||
)
|
||||
}
|
||||
|
||||
// 添加格式化正确答案的方法
|
||||
const formatCorrectAnswers = (correctAnswers, totalOptions) => {
|
||||
if (!correctAnswers || correctAnswers.length === 0) return '无';
|
||||
return correctAnswers.map(answer => String.fromCharCode(64 + answer)).join(', ');
|
||||
};
|
||||
if (!correctAnswers || correctAnswers.length === 0) return '无'
|
||||
return correctAnswers.join(', ')
|
||||
}
|
||||
|
||||
// 添加判断是否为正确答案的方法
|
||||
const isCorrectAnswer = (optionIndex, correctAnswers) => {
|
||||
if (!correctAnswers || correctAnswers.length === 0) return false;
|
||||
return correctAnswers.includes(optionIndex);
|
||||
};
|
||||
const isCorrectAnswer = (optionIndex, correctAnswers, choiceOptions) => {
|
||||
if (!correctAnswers || correctAnswers.length === 0) return false
|
||||
// 获取选项值
|
||||
const optionValue = choiceOptions[optionIndex - 1].value
|
||||
return correctAnswers.includes(optionValue)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -679,6 +893,13 @@ const isCorrectAnswer = (optionIndex, correctAnswers) => {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.question-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,91 +1,287 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex flex-col bg-gray-50">
|
||||
<!-- 顶部标题栏 -->
|
||||
<header class="bg-primary text-white py-4 px-6 shadow-md">
|
||||
<div class="container mx-auto">
|
||||
<h1 class="text-2xl font-bold text-center flex items-center justify-center gap-2">
|
||||
<FontAwesomeIcon icon="fa-solid fa-user-graduate" /> 考试系统 - 考生首页
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
<div class="student-home-container">
|
||||
<el-container>
|
||||
<Header />
|
||||
<!-- 主要内容区域 -->
|
||||
<el-main>
|
||||
<div style="width: 100%; height: 100%;">
|
||||
<div class="d-flex align-items-center justify-content-center p-4" style="width:100%">
|
||||
<!-- 考试须知卡片 -->
|
||||
<div class="bg-white rounded-4 shadow-lg p-4 w-100 max-w-2xl border-2 border-primary/20">
|
||||
<div class="flex justify-between text-center mt-6 mb-4">
|
||||
<h2 class="text-2xl font-bold text-primary">考生信息</h2>
|
||||
|
||||
<!-- 中间内容区 -->
|
||||
<main class="flex-grow flex items-center justify-center p-6">
|
||||
<div class="container mx-auto">
|
||||
<div class="max-w-2xl mx-auto bg-white rounded-xl shadow-lg overflow-hidden border-2 border-primary/20">
|
||||
<div class="p-8">
|
||||
<div class="flex items-center justify-center mb-6">
|
||||
<div class="w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mr-3">
|
||||
<FontAwesomeIcon icon="fa-solid fa-file-question" class="text-primary" />
|
||||
</div>
|
||||
<h2 class="text-xl font-semibold text-gray-800">考试须知</h2>
|
||||
</div>
|
||||
<div class="prose max-w-none mb-6 bg-gray-50 p-4 rounded-lg border border-gray-100">
|
||||
<ol class="list-decimal pl-5 space-y-3 text-gray-600">
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-primary font-medium">•</span>
|
||||
<span>请确保网络连接稳定,避免考试过程中断网。</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-primary font-medium">•</span>
|
||||
<span>考试时间为60分钟,超时系统将自动提交。</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-primary font-medium">•</span>
|
||||
<span>请独立完成考试,严禁作弊行为。</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-primary font-medium">•</span>
|
||||
<span>考试过程中请勿刷新页面,否则可能导致答案丢失。</span>
|
||||
</li>
|
||||
<li class="flex items-start gap-2">
|
||||
<span class="text-primary font-medium">•</span>
|
||||
<span>遇到技术问题,请及时联系监考老师。</span>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="flex justify-center mt-6">
|
||||
<el-button type="primary" size="large" @click="startExam" class="flex items-center justify-center gap-2 w-full md:w-auto">
|
||||
<FontAwesomeIcon icon="fa-solid fa-calendar-check" /> 开始考试
|
||||
</el-button>
|
||||
<!-- 考生信息部分修改 -->
|
||||
<el-descriptions class="margin-top" :column="3" :size="size" border>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon :style="iconStyle">
|
||||
<User />
|
||||
</el-icon>
|
||||
姓名
|
||||
</div>
|
||||
</template>
|
||||
{{ examinee?.examinee_name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon :style="iconStyle">
|
||||
<Postcard />
|
||||
</el-icon>
|
||||
身份证号
|
||||
</div>
|
||||
</template>
|
||||
{{ formatIdCard(examinee?.examinee_id_card) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon :style="iconStyle">
|
||||
<Ticket />
|
||||
</el-icon>
|
||||
准考证号
|
||||
</div>
|
||||
</template>
|
||||
{{ examinee?.examinee_admission_ticket }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<div class="flex justify-between text-center mt-6 mb-4">
|
||||
<h2 class="text-2xl font-bold text-primary">考试信息</h2>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<el-descriptions class="margin-top" :column="3" :size="size" border>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon :style="iconStyle">
|
||||
<Clock />
|
||||
</el-icon>
|
||||
考试时长
|
||||
</div>
|
||||
</template>
|
||||
{{ formatExamDuration(lastExam?.exam_minutes) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon :style="iconStyle">
|
||||
<ScaleToOriginal />
|
||||
</el-icon>
|
||||
考试总分
|
||||
</div>
|
||||
</template>
|
||||
<span class="text-gray-500">待设置</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item>
|
||||
<template #label>
|
||||
<div class="cell-item">
|
||||
<el-icon :style="iconStyle">
|
||||
<Timer />
|
||||
</el-icon>
|
||||
最短考试时长
|
||||
</div>
|
||||
</template>
|
||||
{{ formatExamDuration(lastExam?.exam_minutes_min) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
<el-divider />
|
||||
<div class="flex justify-between text-center mt-6 mb-4">
|
||||
<h2 class="text-2xl font-bold text-primary">考试须知</h2>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div v-if="!examLoading && examNotices.length > 0" class="space-y-2 pl-4">
|
||||
<ol class="list-decimal pl-4 space-y-2">
|
||||
<li v-for="(notice, index) in examNotices" :key="index" class="text-gray-700">
|
||||
{{ notice }}
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div v-else-if="examLoading" class="text-center text-gray-500 py-4">
|
||||
加载考试信息中...
|
||||
</div>
|
||||
<div v-else class="text-center text-gray-500 py-4">
|
||||
暂无考试须知
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</el-main>
|
||||
|
||||
<!-- 底部页脚 -->
|
||||
<footer class="bg-gray-800 text-white py-4 px-6 text-center text-sm">
|
||||
<div class="container mx-auto">
|
||||
<p>© 2023 考试系统. 版权所有. 版本 v1.0.0</p>
|
||||
</div>
|
||||
</footer>
|
||||
<Footer />
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 导入组件
|
||||
import Header from '@/components/common/Header.vue'
|
||||
import Footer from '@/components/common/Footer.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElButton } from 'element-plus'
|
||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||
import { ref, computed, onMounted, watch } from 'vue' // 添加watch导入
|
||||
import { ElMessage, ElDescriptions, ElDescriptionsItem, ElIcon } from 'element-plus'
|
||||
import { User, Postcard, Ticket, Clock, ScaleToOriginal, Timer } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
// 导入用户状态管理
|
||||
import { useUserStore } from '@/store/userStore'
|
||||
|
||||
const startExam = () => {
|
||||
// 这里可以添加开始考试的逻辑
|
||||
alert('即将开始考试!')
|
||||
// 获取用户状态管理
|
||||
const userStore = useUserStore()
|
||||
// 响应式引用考生数据
|
||||
const examinee = computed(() => userStore.state.examinee)
|
||||
|
||||
// 检查是否已登录
|
||||
if (!userStore.state.isLoggedIn) {
|
||||
// 如果未登录,重定向到欢迎页
|
||||
router.push('/')
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const router = useRouter()
|
||||
const isLoading = ref(false)
|
||||
const size = ref('default')
|
||||
const lastExam = ref(null)
|
||||
const examLoading = ref(true)
|
||||
const examNotices = ref([])
|
||||
|
||||
// 添加watch监听lastExam变化
|
||||
watch(lastExam, (newValue) => {
|
||||
if (newValue) {
|
||||
examLoading.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 图标样式计算属性
|
||||
const iconStyle = computed(() => {
|
||||
const marginMap = {
|
||||
large: '8px',
|
||||
default: '6px',
|
||||
small: '4px',
|
||||
}
|
||||
return {
|
||||
marginRight: marginMap[size.value] || marginMap.default,
|
||||
}
|
||||
})
|
||||
|
||||
// 格式化身份证号(第11-15位替换为*)
|
||||
const formatIdCard = (idCard) => {
|
||||
if (!idCard || idCard.length !== 18) return idCard
|
||||
return idCard.substring(0, 9) + '*****' + idCard.substring(14)
|
||||
}
|
||||
|
||||
// 格式化考试时长(分钟转小时)
|
||||
const formatExamDuration = (minutes) => {
|
||||
if (!minutes) return '未知'
|
||||
const hours = Math.floor(minutes / 60)
|
||||
const mins = minutes % 60
|
||||
return `${hours}小时${mins}分钟`
|
||||
}
|
||||
|
||||
// 获取最新考试信息
|
||||
const fetchLastExam = async () => {
|
||||
examLoading.value = true
|
||||
try {
|
||||
// 调用Electron API获取最新考试信息
|
||||
const examData = await window.electronAPI.fetchLastExam()
|
||||
// console.log('获取考试信息成功:', examData)
|
||||
lastExam.value = examData
|
||||
console.log('信息:', lastExam.value)
|
||||
// 解析考试须知数组 - 注意:根据控制台日志,exam_notice已经是数组,不需要再JSON.parse
|
||||
if (lastExam.value && lastExam.value.exam_notice) {
|
||||
examNotices.value = lastExam.value.exam_notice
|
||||
} else {
|
||||
examNotices.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取考试信息失败:', error)
|
||||
ElMessage.error(`获取考试信息失败: ${error.message || '未知错误'}`)
|
||||
} finally {
|
||||
examLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 开始考试方法
|
||||
const startExam = async () => {
|
||||
if (!lastExam.value) {
|
||||
ElMessage.warning('请先获取考试信息')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('开始考试 - 调用接口')
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
// 这里可以添加开始考试的逻辑
|
||||
// 例如,调用API获取考试信息
|
||||
// const examInfo = await window.electronAPI.startExam(lastExam.value.id)
|
||||
|
||||
// 模拟API调用延迟
|
||||
setTimeout(() => {
|
||||
console.log('开始考试 - 成功')
|
||||
ElMessage.success('即将开始考试!')
|
||||
// 跳转到考试页面
|
||||
// router.push('/exam')
|
||||
isLoading.value = false
|
||||
}, 1000)
|
||||
} catch (error) {
|
||||
console.error('开始考试 - 异常:', error)
|
||||
ElMessage.error(`无法开始考试: ${error.message || '未知错误'}`)
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取最新考试信息
|
||||
onMounted(() => {
|
||||
fetchLastExam()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 使用Bootstrap样式并自定义 */
|
||||
@import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
/* 自定义样式 */
|
||||
.bg-primary {
|
||||
background-color: var(--primary-color) !important;
|
||||
background-color: #1E88E5 !important;
|
||||
/* 蓝色主题,与WelcomeView保持一致 */
|
||||
}
|
||||
|
||||
.text-primary {
|
||||
color: var(--primary-color) !important;
|
||||
color: #1E88E5 !important;
|
||||
}
|
||||
|
||||
/* 确保容器占满高度 */
|
||||
.student-home-container,
|
||||
.el-container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 让主内容区自动扩展并居中 */
|
||||
.el-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 适配移动设备 */
|
||||
@media (max-width: 640px) {
|
||||
.max-w-2xl {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cell-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.margin-top {
|
||||
margin-top: 20px !important;
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue
Block a user