准备尝试一次打包

This commit is contained in:
chenqiang 2025-08-09 07:38:55 +08:00
parent 52f8af2b63
commit 637906b241
23 changed files with 2472 additions and 393 deletions

View File

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

View File

@ -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 表成功');

View File

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

View File

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

View File

@ -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();
});

View File

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

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

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

View File

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

View File

@ -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": "电子考试系统"
}
}
}

View File

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

View File

@ -82,7 +82,7 @@ const menuData = [
index: '6',
label: '考生管理',
icon: 'users',
route: '/admin/student-management'
route: '/admin/examinee-management'
},
{
index: '7',

View File

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

View File

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

View File

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

View File

@ -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;
}
};
//

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

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

View File

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

View File

@ -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([])
// watchlastExam
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_noticeJSON.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>