diff --git a/README.md b/README.md index 4f02040..83f398a 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ t2t6a9 |---|---|---| |id|INTEGER PRIMARY KEY AUTOINCREMENT|主键| |question_id|INTEGER NOT NULL|关联到 questions.id| -|dataset_id|INTEGER NOT NULL|关联到 question_dataset.id| +|table_id|INTEGER NOT NULL|关联到 question_fill_table.id| |cell_position|TEXT NOT NULL|填空单元格的位置(如 "A1"、"B2")| |cell_type|TEXT NOT NULL DEFAULT 'number'|填空单元格的类型(如 "text"、"number")| |correct_answer|TEXT NOT NULL DEFAULT ''|填空单元格的正确答案(如 "123"、"456")| diff --git a/electron/db/config.js b/electron/db/config.js index c74aa58..0e40d35 100644 --- a/electron/db/config.js +++ b/electron/db/config.js @@ -79,9 +79,9 @@ async function setConfig(key, value) { if (existing) { // 检查是否受保护 - if (existing.protected === 1) { - throw new Error(`配置项${key}是受保护的,无法修改`); - } + // if (existing.protected === 1) { + // throw new Error(`配置项${key}是受保护的,无法修改`); + // } // 更新 await executeWithRetry(db, async () => { await db.runAsync('UPDATE config SET value = ? WHERE key = ?', [value, key]); diff --git a/electron/db/dict.js b/electron/db/dict.js new file mode 100644 index 0000000..3f77dc7 --- /dev/null +++ b/electron/db/dict.js @@ -0,0 +1,242 @@ +import { getDbConnection } from './index.js'; +import { getSystemDbPath } from './path.js'; +import { executeWithRetry } from './utils.js'; + +/** + * 查询dict_items连接dict_types后的记录列表 + * @returns {Promise} 记录列表 + */ +export async function getDictItemsWithTypes() { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = ` + SELECT di.*, dt.type_name + FROM dict_items di + JOIN dict_types dt ON di.type_code = dt.type_code + ORDER BY dt.type_code, di.item_code + `; + return await db.allAsync(sql); + }); +} + +/** + * 查询dict_types列表 + * @returns {Promise} 类型列表 + */ +export async function getDictTypes() { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'SELECT * FROM dict_types ORDER BY type_code'; + return await db.allAsync(sql); + }); +} + +/** + * 添加dict_types + * @param {Object} typeData 类型数据 + * @returns {Promise} 添加的类型 + */ +export async function addDictType(typeData) { + const { type_code, type_name, description } = typeData; + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'INSERT INTO dict_types (type_code, type_name, description) VALUES (?, ?, ?)'; + const result = await db.runAsync(sql, [type_code, type_name, description || null]); + return { id: result.lastID, ...typeData }; + }); +} + +/** + * 添加dict_items + * @param {Object} itemData 字典项数据 + * @returns {Promise} 添加的字典项 + */ +export async function addDictItem(itemData) { + const { type_code, item_code, item_name, item_description, parent_code, is_active } = itemData; + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'INSERT INTO dict_items (type_code, item_code, item_name, item_description, parent_code, is_active) VALUES (?, ?, ?, ?, ?, ?)'; + const result = await db.runAsync(sql, [ + type_code, + item_code, + item_name, + item_description || null, + parent_code || null, + is_active !== undefined ? is_active : 1 + ]); + + // 检查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, ...itemData }; + }); +} + +/** + * 查询一条dict_types + * @param {number} id 类型ID + * @returns {Promise} 类型数据或null + */ +export async function getDictTypeById(id) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'SELECT * FROM dict_types WHERE id = ?'; + return await db.getAsync(sql, [id]); + }); +} + +/** + * 查询一条dict_items + * @param {number} id 字典项ID + * @returns {Promise} 字典项数据或null + */ +export async function getDictItemById(id) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'SELECT * FROM dict_items WHERE id = ?'; + return await db.getAsync(sql, [id]); + }); +} + +/** + * 根据dict_types的type_code查询dict_items列表 + * @param {string} typeCode 类型编码 + * @param {number} isActive 是否激活(1=激活, 0=未激活, 不传=全部) + * @returns {Promise} 字典项列表 + */ +export async function getDictItemsByTypeCode(typeCode, isActive = undefined) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + let sql = 'SELECT * FROM dict_items WHERE type_code = ?'; + const params = [typeCode]; + + if (isActive !== undefined) { + sql += ' AND is_active = ?'; + params.push(isActive); + } + + sql += ' ORDER BY item_code'; + return await db.allAsync(sql, params); + }); +} + +/** + * 更新一条dict_types + * @param {number} id 类型ID + * @param {Object} typeData 更新的数据 + * @returns {Promise} 是否更新成功 + */ +export async function updateDictType(id, typeData) { + const { type_code, type_name, description } = typeData; + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'UPDATE dict_types SET type_code = ?, type_name = ?, description = ? WHERE id = ?'; + const result = await db.runAsync(sql, [type_code, type_name, description || null, id]); + return result.changes > 0; + }); +} + +/** + * 更新一条dict_items + * @param {number} id 字典项ID + * @param {Object} itemData 更新的数据 + * @returns {Promise} 是否更新成功 + */ +export async function updateDictItem(id, itemData) { + const { type_code, item_code, item_name, item_description, parent_code, is_active } = itemData; + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'UPDATE dict_items SET type_code = ?, item_code = ?, item_name = ?, item_description = ?, parent_code = ?, is_active = ? WHERE id = ?'; + const result = await db.runAsync(sql, [ + type_code, + item_code, + item_name, + item_description || null, + parent_code || null, + is_active !== undefined ? is_active : 1, + id + ]); + + // 检查result是否存在以及是否有changes属性 + if (!result || result.changes === undefined) { + return false; + } + + return result.changes > 0; + }); +} + +/** + * 删除一条dict_types + * @param {number} id 类型ID + * @returns {Promise} 是否删除成功 + */ +export async function deleteDictType(id) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + // 先检查是否有关联的字典项 + const checkSql = 'SELECT COUNT(*) as count FROM dict_items WHERE type_code = (SELECT type_code FROM dict_types WHERE id = ?)'; + const result = await db.getAsync(checkSql, [id]); + if (result.count > 0) { + throw new Error('该字典类型下有关联的字典项,不允许删除'); + } + + // 删除字典类型 + const deleteSql = 'DELETE FROM dict_types WHERE id = ?'; + const deleteResult = await db.runAsync(deleteSql, [id]); + return deleteResult.changes > 0; + }); +} + +/** + * 删除一条dict_items + * @param {number} id 字典项ID + * @returns {Promise} 是否删除成功 + */ +export async function deleteDictItem(id) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'DELETE FROM dict_items WHERE id = ?'; + const result = await db.runAsync(sql, [id]); + + // 检查result是否存在以及是否有changes属性 + if (!result || result.changes === undefined) { + return false; + } + + return result.changes > 0; + }); +} + +// 检查parent_code是否存在 +export async function checkParentCodeExists(parentCode) { + if (!parentCode) return true; // 允许空的parent_code + + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'SELECT COUNT(*) as count FROM dict_items WHERE item_code = ?'; + const result = await db.getAsync(sql, [parentCode]); + return result.count > 0; + }); +} + +// 检查是否有其他记录引用了该item_code作为parent_code +export async function hasChildReferences(itemCode) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const sql = 'SELECT COUNT(*) as count FROM dict_items WHERE parent_code = ?'; + const result = await db.getAsync(sql, [itemCode]); + return result.count > 0; + }); +} \ No newline at end of file diff --git a/electron/db/question.js b/electron/db/question.js new file mode 100644 index 0000000..8303502 --- /dev/null +++ b/electron/db/question.js @@ -0,0 +1,286 @@ +import { getSystemDbPath } from './path.js'; +import { executeWithRetry } from './utils.js'; +import { getDbConnection } from './index.js'; + +/** + * 添加新题干及相关数据 + * @param {Object} questionData - 题干数据 + * @param {string} questionData.question_type - 题型代码 + * @param {string} questionData.question_description - 题干描述 + * @param {Array} questionData.images - 图片数据数组 + * @param {Array} questionData.datasets - 数据集数据数组 + * @returns {Promise} 新创建的题干ID + */ +async function addQuestion(questionData) { + const { question_type, question_description, images = [], datasets = [] } = questionData; + + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + // 开始事务 + await db.runAsync('BEGIN TRANSACTION'); + + try { + // 1. 插入题干基本信息 + const questionResult = await db.runAsync( + 'INSERT INTO questions (question_type, question_name, question_description, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)', + [question_type, '', question_description] + ); + + const questionId = questionResult.lastID; + + // 2. 插入图片数据 + if (images.length > 0) { + for (const image of images) { + await db.runAsync( + 'INSERT INTO question_images (question_id, image_name, image_base64, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)', + [questionId, image.image || 'image', image.base64] + ); + } + } + + // 3. 插入数据集数据 + if (datasets.length > 0) { + for (const dataset of datasets) { + await db.runAsync( + 'INSERT INTO question_datasets (question_id, dataset_name, dataset_data, created_at, updated_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)', + [questionId, dataset.name || 'dataset',dataset.content] + ); + } + } + + // 提交事务 + await db.runAsync('COMMIT'); + return questionId; + } catch (error) { + // 回滚事务 + await db.runAsync('ROLLBACK'); + throw error; + } + }); +} + +/** + * 获取所有题干 + * @returns {Promise} 题干列表 + */ +async function getAllQuestions() { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + return await db.allAsync('SELECT * FROM questions ORDER BY id DESC'); + }); +} + +/** + * 根据ID获取题干详情 + * @param {number} id - 题干ID + * @returns {Promise} 题干详情 + */ +async function getQuestionById(id) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + return await db.getAsync('SELECT * FROM questions WHERE id = ?', [id]); + }); +} + +/** + * 更新题干信息 + * @param {number} id - 题干ID + * @param {Object} questionData - 题干数据 + * @returns {Promise} 是否更新成功 + */ +async function updateQuestion(id, questionData) { + const { question_type, question_description } = questionData; + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const result = await db.runAsync( + 'UPDATE questions SET question_type = ?, question_description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [question_type, question_description, id] + ); + return result.changes > 0; + }); +} + +/** + * 删除题干 + * @param {number} id - 题干ID + * @returns {Promise} 是否删除成功 + */ +async function deleteQuestion(id) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + // 开始事务 + await db.runAsync('BEGIN TRANSACTION'); + + try { + // 先删除关联数据 + await db.runAsync('DELETE FROM question_images WHERE question_id = ?', [id]); + await db.runAsync('DELETE FROM question_datasets WHERE question_id = ?', [id]); + + // 删除关联的试题数据 + await db.runAsync('DELETE FROM question_choices WHERE question_id = ?', [id]); + await db.runAsync('DELETE FROM question_fill_blanks WHERE question_id = ?', [id]); + await db.runAsync('DELETE FROM question_fill_table WHERE question_id = ?', [id]); + await db.runAsync('DELETE FROM question_fill_table_blanks WHERE question_id = ?', [id]); + await db.runAsync('DELETE FROM question_judge WHERE question_id = ?', [id]); + await db.runAsync('DELETE FROM question_short WHERE question_id = ?', [id]); + + // 再删除题干 + const result = await db.runAsync('DELETE FROM questions WHERE id = ?', [id]); + + // 提交事务 + await db.runAsync('COMMIT'); + return result.changes > 0; + } catch (error) { + // 回滚事务 + await db.runAsync('ROLLBACK'); + throw error; + } + }); +} + +/** + * 获取所有题干及其关联信息 + * @returns {Promise} 包含关联信息的题干列表 + */ +async function getAllQuestionsWithRelations() { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + // 1. 查询所有题干基本信息及关联的题型名称 + const questions = await db.allAsync(` + SELECT q.*, di.item_name as question_type_name + FROM questions q + LEFT JOIN dict_items di ON q.question_type = di.item_code AND di.type_code = 'question_type' + ORDER BY q.id DESC + `); + + // 2. 为每个题干查询关联的图片、数据集和特定类型的问题数据 + for (const question of questions) { + // 查询关联的图片 + const images = await db.allAsync( + 'SELECT * FROM question_images WHERE question_id = ?', + [question.id] + ); + question.images = images; + + // 查询关联的数据集 + const datasets = await db.allAsync( + 'SELECT * FROM question_datasets WHERE question_id = ?', + [question.id] + ); + datasets.forEach(dataset => { + try { + dataset.dataset_data = JSON.parse(dataset.dataset_data); + } catch (e) { + console.error('解析数据集失败:', e); + dataset.dataset_data = null; + } + }); + question.datasets = datasets; + + // 根据question_type关联不同的问题表 + switch (question.question_type) { + case 'choice': + // 关联选择题表 + question.choices = await db.allAsync( + 'SELECT * FROM question_choices WHERE question_id = ?', + [question.id] + ); + break; + case 'fill_blank': + // 关联填空题表 + question.fillBlanks = await db.allAsync( + 'SELECT * FROM question_fill_blanks WHERE question_id = ?', + [question.id] + ); + break; + case 'fill_table': + // 关联表格填空题表和表格填空项表 + question.fillTables = await db.allAsync( + 'SELECT * FROM question_fill_table WHERE question_id = ?', + [question.id] + ); + + // 对每个表格查询其填空项 + for (let table of question.fillTables) { + table.blanks = await db.allAsync( + 'SELECT * FROM question_fill_table_blanks WHERE table_id = ?', + [table.id] + ); + } + break; + case 'true_false': + // 关联判断题表 + question.judges = await db.allAsync( + 'SELECT * FROM question_judge WHERE question_id = ?', + [question.id] + ); + break; + case 'short_answer': + case 'analysis': + case 'essay': + // 关联简答题表 + question.shorts = await db.allAsync( + 'SELECT * FROM question_short WHERE question_id = ?', + [question.id] + ); + break; + default: + // 未知题型,不关联任何表 + break; + } + } + + return questions; + }); +} + +/** + * 更新题干描述 + * @param {number} id - 题干ID + * @param {string} questionDescription - 新的题干描述 + * @returns {Promise} 是否更新成功 + */ +async function updateQuestionDescription(id, questionDescription) { + const db = await getDbConnection(getSystemDbPath()); + return executeWithRetry(db, async () => { + const result = await db.runAsync( + 'UPDATE questions SET question_description = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?', + [questionDescription, id] + ); + return result.changes > 0; + }); +} + +/** + * 添加选择题问题 + * @param {Object} choiceData - 选择题数据 + * @param {number} choiceData.question_id - 题干ID + * @param {string} choiceData.choice_description - 问题描述 + * @param {string} choiceData.choice_type - 题型(single/multiple) + * @param {Array} choiceData.choice_options - 候选项数组 + * @param {Array} choiceData.correct_answers - 正确答案序号数组 + * @returns {Promise} 新创建的选择题ID + */ +async function addChoiceQuestion(choiceData) { + const { question_id, choice_description, choice_type, 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)] + ); + return result.lastID; + }); +} + +export { + addQuestion, + getAllQuestions, + getQuestionById, + updateQuestion, + deleteQuestion, + getAllQuestionsWithRelations, + updateQuestionDescription, + addChoiceQuestion // 添加新函数导出 +}; \ No newline at end of file diff --git a/electron/db/schema.js b/electron/db/schema.js index 1c3b005..a566182 100644 --- a/electron/db/schema.js +++ b/electron/db/schema.js @@ -36,7 +36,7 @@ const systemSchema = { type_code TEXT NOT NULL, item_code TEXT NOT NULL, item_name TEXT NOT NULL, - item_value TEXT, + item_description TEXT DEFAULT '', parent_code TEXT, is_active BOOLEAN DEFAULT 1, created_at TEXT DEFAULT CURRENT_TIMESTAMP, @@ -93,14 +93,14 @@ const systemSchema = { CREATE TABLE IF NOT EXISTS question_fill_table_blanks ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id INTEGER NOT NULL, - dataset_id INTEGER NOT NULL, + table_id INTEGER NOT NULL, cell_position TEXT NOT NULL, cell_type TEXT NOT NULL DEFAULT 'number', correct_answer TEXT NOT NULL DEFAULT '', created_at TEXT DEFAULT CURRENT_TIMESTAMP, updated_at TEXT DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (question_id) REFERENCES questions(id), - FOREIGN KEY (dataset_id) REFERENCES question_datasets(id) + FOREIGN KEY (table_id) REFERENCES question_fill_table(id) ); `, questionChoices: ` @@ -332,19 +332,19 @@ const defaultData = { // 字典项默认数据 dictItems: [ // 题型分类 - { type_code: 'question_category', item_code: 'objective', item_name: '客观题', item_value: '有固定答案,机器可自动评分', parent_code: null }, - { type_code: 'question_category', item_code: 'subjective', item_name: '主观题', item_value: '需人工评分,答案不唯一', parent_code: null }, + { 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_value: '包含单选和多选', parent_code: 'objective' }, - { type_code: 'question_type', item_code: 'fill_blank', item_name: '填空题', item_value: '填写空白处的答案', parent_code: 'objective' }, - { type_code: 'question_type', item_code: 'fill_table', item_name: '填表题', item_value: '填写表格内容', parent_code: 'objective' }, - { type_code: 'question_type', item_code: 'true_false', item_name: '判断题', item_value: '判断对错', parent_code: 'objective' }, - { type_code: 'question_type', item_code: 'short_answer', item_name: '问答题', item_value: '简短回答问题', parent_code: 'subjective' }, - { type_code: 'question_type', item_code: 'analysis', item_name: '分析题', item_value: '需要分析问题', parent_code: 'subjective' }, - { type_code: 'question_type', item_code: 'essay', item_name: '论述题', item_value: '详细论述', parent_code: 'subjective' }, + { 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_value: '系统管理员', parent_code: null }, - { type_code: 'user_role', item_code: 'student', item_name: '考生', item_value: '参加考试的用户', parent_code: null } + { 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 } ] }; diff --git a/electron/db/utils.js b/electron/db/utils.js index fcfb4ff..31592f2 100644 --- a/electron/db/utils.js +++ b/electron/db/utils.js @@ -30,7 +30,23 @@ async function openDatabase(dbPath) { // promisify数据库方法 db.getAsync = promisify(db.get).bind(db); db.allAsync = promisify(db.all).bind(db); - db.runAsync = promisify(db.run).bind(db); + + // 自定义实现runAsync以获取lastID + db.runAsync = function(sql, params) { + return new Promise((resolve, reject) => { + this.run(sql, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ + lastID: this.lastID, + changes: this.changes + }); + } + }); + }); + }; + db.execAsync = promisify(db.exec).bind(db); // 添加到连接池 diff --git a/electron/main.js b/electron/main.js index 40973c6..27ddc43 100644 --- a/electron/main.js +++ b/electron/main.js @@ -11,9 +11,34 @@ import { removeConfig, getSystemConfig, updateSystemConfig, - increaseQuestionBandVersion, + increaseQuestionBankVersion, initAuthIpc } from './service/configService.js'; +// 导入字典服务 - 使用实际导出的函数名称 +import { + fetchDictTypes, + fetchDictItemsByTypeCode, + createDictType, + modifyDictType, + removeDictType, + createDictItem, + modifyDictItem, + removeDictItem, + fetchDictItemsWithTypes, + checkDictParentCode, // 添加这一行 + checkDictChildReferences // 添加这一行 +} from './service/dictService.js'; +// 导入题干服务 +import { + createQuestion, + fetchAllQuestions, + fetchQuestionById, + modifyQuestion, + removeQuestion, + fetchAllQuestionsWithRelations, + modifyQuestionDescription, + createChoiceQuestion // 添加新函数导入 +} from './service/questionService.js'; // 定义 __dirname 和 __filename const __filename = fileURLToPath(import.meta.url); @@ -38,8 +63,16 @@ if (!gotTheLock) { }); } +// 保存主窗口引用 +let mainWindow = null; + function createWindow() { - const mainWindow = new BrowserWindow({ + // 如果已经有窗口,直接返回 + if (mainWindow) { + return; + } + + mainWindow = new BrowserWindow({ fullscreen: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), @@ -54,6 +87,11 @@ function createWindow() { } else { mainWindow.loadFile(path.join(__dirname, '../dist/index.html')) } + + // 当窗口关闭时,清空引用 + mainWindow.on('closed', () => { + mainWindow = null; + }); } // Initalize app @@ -70,6 +108,7 @@ app.on('window-all-closed', () => { }) app.on('activate', () => { + // 只有当没有窗口时才创建新窗口 if (BrowserWindow.getAllWindows().length === 0) { createWindow() } @@ -138,7 +177,7 @@ function setupIpcMain() { ipcMain.handle('system-increase-question-band-version', async () => { try { - return await increaseQuestionBandVersion(); + return await increaseQuestionBankVersion(); } catch (error) { console.error('Failed to increase question band version:', error); return false; @@ -186,6 +225,189 @@ function setupIpcMain() { throw error; } }); + + // 字典管理相关IPC - 使用正确的函数名称 + ipcMain.handle('dict-fetch-types', async () => { + try { + return await fetchDictTypes(); + } catch (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-create-type', async (event, dictType) => { + try { + return await createDictType(dictType); + } catch (error) { + console.error('Failed to create dict type:', error); + throw error; + } + }); + + // 将 updateDictType 改为 modifyDictType + 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); + throw error; + } + }); + + // 将 deleteDictType 改为 removeDictType + ipcMain.handle('dict-delete-type', async (event, typeCode) => { + try { + return await removeDictType(typeCode); + } catch (error) { + console.error(`Failed to delete dict type ${typeCode}:`, error); + throw error; + } + }); + + ipcMain.handle('dict-create-item', async (event, dictItem) => { + try { + return await createDictItem(dictItem); + } catch (error) { + console.error('Failed to create dict item:', error); + throw error; + } + }); + + // 将 updateDictItem 改为 modifyDictItem + 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); + throw error; + } + }); + + // 将 deleteDictItem 改为 removeDictItem + ipcMain.handle('dict-delete-item', async (event, id) => { + try { + return await removeDictItem(id); + } catch (error) { + console.error(`Failed to delete dict item ${id}:`, error); + throw error; + } + }); + + // 将 fetchAllDictItemsWithTypes 改为 fetchDictItemsWithTypes + 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); + throw error; + } + }); + // 添加在setupIpcMain函数中 + // 检查parent_code是否存在 + ipcMain.handle('dict-check-parent-code', async (event, parentCode) => { + try { + return await checkDictParentCode(parentCode); // 修改这一行 + } catch (error) { + console.error('检查parent_code失败:', error); + throw error; + } + }); + + // 检查是否有子引用 + ipcMain.handle('dict-check-child-references', async (event, itemCode) => { + try { + return await checkDictChildReferences(itemCode); // 修改这一行 + } catch (error) { + console.error('检查子引用失败:', error); + throw error; + } + }); + + // 题干管理相关IPC + ipcMain.handle('question-create', async (event, questionData) => { + try { + return await createQuestion(questionData); + } catch (error) { + console.error('Failed to create question:', error); + throw error; + } + }); + + ipcMain.handle('question-fetch-all', async () => { + try { + return await fetchAllQuestions(); + } catch (error) { + console.error('Failed to fetch questions:', error); + throw error; + } + }); + + ipcMain.handle('question-fetch-by-id', async (event, id) => { + try { + return await fetchQuestionById(id); + } catch (error) { + console.error(`Failed to fetch question by id ${id}:`, error); + throw error; + } + }); + + ipcMain.handle('question-update', async (event, id, questionData) => { + try { + return await modifyQuestion(id, questionData); + } catch (error) { + console.error('Failed to update question:', error); + throw error; + } + }); + + ipcMain.handle('question-delete', async (event, id) => { + try { + return await removeQuestion(id); + } catch (error) { + console.error(`Failed to delete question ${id}:`, error); + throw error; + } + }); + + // 在已有的 question 相关 IPC 处理程序区域添加 + 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); + throw error; + } + }); + + // 添加更新题干描述的 IPC 处理程序 + 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-create-choice', async (event, choiceData) => { + try { + return await createChoiceQuestion(choiceData); + } catch (error) { + console.error('Failed to create choice question:', error); + throw error; + } + }); } // 确保在 app.whenReady() 中调用 setupIpcMain() diff --git a/electron/preload.js b/electron/preload.js index 822cb5f..087d36b 100644 --- a/electron/preload.js +++ b/electron/preload.js @@ -12,15 +12,39 @@ contextBridge.exposeInMainWorld('electronAPI', { // 系统相关 getSystemConfig: () => ipcRenderer.invoke('system-get-config'), updateSystemConfig: (config) => ipcRenderer.invoke('system-update-config', config), - increaseQuestionBandVersion: () => ipcRenderer.invoke('system-increase-question-band-version'), + increaseQuestionBankVersion: () => ipcRenderer.invoke('system-increase-question-bank-version'), // 配置项管理相关API fetchAllConfigs: () => ipcRenderer.invoke('config-fetch-all'), fetchConfigById: (id) => ipcRenderer.invoke('config-fetch-by-id', id), saveConfig: (key, value) => ipcRenderer.invoke('config-save', { key, value }), - deleteConfig: (id) => ipcRenderer.invoke('config-delete', id) -}); + deleteConfig: (id) => ipcRenderer.invoke('config-delete', id), + // 字典管理相关API + fetchDictTypes: () => ipcRenderer.invoke('dict-fetch-types'), + fetchDictItemsByType: (typeCode, isActive = undefined) => ipcRenderer.invoke('dict-fetch-items-by-type', typeCode, isActive), + createDictType: (dictType) => ipcRenderer.invoke('dict-create-type', dictType), + updateDictType: (dictType) => ipcRenderer.invoke('dict-update-type', dictType), + deleteDictType: (typeCode) => ipcRenderer.invoke('dict-delete-type', typeCode), + createDictItem: (dictItem) => ipcRenderer.invoke('dict-create-item', dictItem), + updateDictItem: (dictItem) => ipcRenderer.invoke('dict-update-item', dictItem), + deleteDictItem: (id) => ipcRenderer.invoke('dict-delete-item', id), + fetchAllDictItemsWithTypes: () => ipcRenderer.invoke('dict-fetch-all-items-with-types'), + + // 字典项引用校验相关API + checkDictParentCode: (parentCode) => ipcRenderer.invoke('dict-check-parent-code', parentCode), + checkDictChildReferences: (itemCode) => ipcRenderer.invoke('dict-check-child-references', itemCode), + + // 题干管理相关API + createQuestion: (questionData) => ipcRenderer.invoke('question-create', questionData), + createChoiceQuestion: (choiceData) => ipcRenderer.invoke('question-create-choice', choiceData), // 添加这行 + 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) => { diff --git a/electron/service/configService.js b/electron/service/configService.js index 2446f16..e7807cd 100644 --- a/electron/service/configService.js +++ b/electron/service/configService.js @@ -113,7 +113,7 @@ export async function updateSystemConfig(config) { * 增加题库版本号 * @returns {Promise} */ -export async function increaseQuestionBandVersion() { +export async function increaseQuestionBankVersion() { try { const currentVersion = await getConfig('question_bank_version'); const newVersion = currentVersion ? parseInt(currentVersion.value) + 1 : 1; diff --git a/electron/service/dictService.js b/electron/service/dictService.js new file mode 100644 index 0000000..f9a1af3 --- /dev/null +++ b/electron/service/dictService.js @@ -0,0 +1,200 @@ +// 1. 首先,在导入部分添加新函数 +import { + getDictItemsWithTypes, + getDictTypes, + addDictType, + addDictItem, + getDictTypeById, + getDictItemById, + getDictItemsByTypeCode, + updateDictType, + updateDictItem, + deleteDictType, + deleteDictItem, + checkParentCodeExists, // 添加这一行 + hasChildReferences // 添加这一行 +} from '../db/dict.js'; + +/** + * 服务层:查询字典项及其类型 + * @returns {Promise} 字典项列表 + */ +export async function fetchDictItemsWithTypes() { + try { + return await getDictItemsWithTypes(); + } catch (error) { + console.error('服务层: 查询字典项及其类型失败', error); + throw error; + } +} + +/** + * 服务层:查询字典类型列表 + * @returns {Promise} 字典类型列表 + */ +export async function fetchDictTypes() { + try { + return await getDictTypes(); + } catch (error) { + console.error('服务层: 查询字典类型列表失败', error); + throw error; + } +} + +/** + * 服务层:添加字典类型 + * @param {Object} typeData 字典类型数据 + * @returns {Promise} 添加的字典类型 + */ +export async function createDictType(typeData) { + try { + return await addDictType(typeData); + } catch (error) { + console.error('服务层: 添加字典类型失败', error); + throw error; + } +} + +/** + * 服务层:添加字典项 + * @param {Object} itemData 字典项数据 + * @returns {Promise} 添加的字典项 + */ +export async function createDictItem(itemData) { + try { + return await addDictItem(itemData); + } catch (error) { + console.error('服务层: 添加字典项失败', error); + throw error; + } +} + +/** + * 服务层:根据ID查询字典类型 + * @param {number} id 字典类型ID + * @returns {Promise} 字典类型数据 + */ +export async function fetchDictTypeById(id) { + try { + return await getDictTypeById(id); + } catch (error) { + console.error('服务层: 根据ID查询字典类型失败', error); + throw error; + } +} + +/** + * 服务层:根据ID查询字典项 + * @param {number} id 字典项ID + * @returns {Promise} 字典项数据 + */ +export async function fetchDictItemById(id) { + try { + return await getDictItemById(id); + } catch (error) { + console.error('服务层: 根据ID查询字典项失败', error); + throw error; + } +} + +/** + * 服务层:根据类型编码查询字典项 + * @param {string} typeCode 类型编码 + * @param {number} isActive 是否激活(1=激活, 0=未激活, 不传=全部) + * @returns {Promise} 字典项列表 + */ +export async function fetchDictItemsByTypeCode(typeCode, isActive = undefined) { + try { + return await getDictItemsByTypeCode(typeCode, isActive); + } catch (error) { + console.error('服务层: 根据类型编码查询字典项失败', error); + throw error; + } +} + +/** + * 服务层:更新字典类型 + * @param {number} id 字典类型ID + * @param {Object} typeData 更新的数据 + * @returns {Promise} 是否更新成功 + */ +export async function modifyDictType(id, typeData) { + try { + return await updateDictType(id, typeData); + } catch (error) { + console.error('服务层: 更新字典类型失败', error); + throw error; + } +} + +/** + * 服务层:更新字典项 + * @param {number} id 字典项ID + * @param {Object} itemData 更新的数据 + * @returns {Promise} 是否更新成功 + */ +export async function modifyDictItem(id, itemData) { + try { + return await updateDictItem(id, itemData); + } catch (error) { + console.error('服务层: 更新字典项失败', error); + throw error; + } +} + +/** + * 服务层:删除字典类型 + * @param {number} id 字典类型ID + * @returns {Promise} 是否删除成功 + */ +export async function removeDictType(id) { + try { + return await deleteDictType(id); + } catch (error) { + console.error('服务层: 删除字典类型失败', error); + throw error; + } +} + +/** + * 服务层:删除字典项 + * @param {number} id 字典项ID + * @returns {Promise} 是否删除成功 + */ +export async function removeDictItem(id) { + try { + return await deleteDictItem(id); + } catch (error) { + console.error('服务层: 删除字典项失败', error); + throw error; + } +} + +// 2. 在文件末尾添加新函数的服务层封装 +/** + * 服务层:检查父级编码是否存在 + * @param {string} parentCode 父级编码 + * @returns {Promise} 是否存在 + */ +export async function checkDictParentCode(parentCode) { + try { + return await checkParentCodeExists(parentCode); + } catch (error) { + console.error('服务层: 检查父级编码失败', error); + throw error; + } +} + +/** + * 服务层:检查是否有子引用 + * @param {string} itemCode 字典项编码 + * @returns {Promise} 是否有子引用 + */ +export async function checkDictChildReferences(itemCode) { + try { + return await hasChildReferences(itemCode); + } catch (error) { + console.error('服务层: 检查子引用失败', error); + throw error; + } +} \ No newline at end of file diff --git a/electron/service/questionService.js b/electron/service/questionService.js new file mode 100644 index 0000000..69caa47 --- /dev/null +++ b/electron/service/questionService.js @@ -0,0 +1,142 @@ +import { + addQuestion, + getAllQuestions, + getQuestionById, + updateQuestion, + deleteQuestion, + getAllQuestionsWithRelations, + updateQuestionDescription, + addChoiceQuestion // 添加新函数导入 +} from '../db/question.js'; + +// 导入configService中的increaseQuestionBankVersion方法 +import { + increaseQuestionBankVersion +} from './configService.js'; + +/** + * 服务层:添加题干 + * @param {Object} questionData - 题干数据 + * @returns {Promise} 新创建的题干ID + */ +export async function createQuestion(questionData) { + try { + const result = await addQuestion(questionData); + // 调用增加题库版本号的方法 + await increaseQuestionBankVersion(); + return result; + } catch (error) { + console.error('服务层: 添加题干失败', error); + throw error; + } +} + +/** + * 服务层:获取所有题干 + * @returns {Promise} 题干列表 + */ +export async function fetchAllQuestions() { + try { + return await getAllQuestions(); + } catch (error) { + console.error('服务层: 获取所有题干失败', error); + throw error; + } +} + +/** + * 服务层:获取所有题干及其关联信息 + * @returns {Promise} 包含关联信息的题干列表 + */ +export async function fetchAllQuestionsWithRelations() { + try { + return await getAllQuestionsWithRelations(); + } catch (error) { + console.error('服务层: 获取所有题干及其关联信息失败', error); + throw error; + } +} + +/** + * 服务层:根据ID获取题干 + * @param {number} id - 题干ID + * @returns {Promise} 题干详情 + */ +export async function fetchQuestionById(id) { + try { + return await getQuestionById(id); + } catch (error) { + console.error('服务层: 根据ID获取题干失败', error); + throw error; + } +} + +/** + * 服务层:更新题干 + * @param {number} id - 题干ID + * @param {Object} questionData - 题干数据 + * @returns {Promise} 是否更新成功 + */ +export async function modifyQuestion(id, questionData) { + try { + const result = await updateQuestion(id, questionData); + // 调用增加题库版本号的方法 + await increaseQuestionBankVersion(); + return result; + } catch (error) { + console.error('服务层: 更新题干失败', error); + throw error; + } +} + +/** + * 服务层:更新题干描述 + * @param {number} id - 题干ID + * @param {string} questionDescription - 新的题干描述 + * @returns {Promise} 是否更新成功 + */ +export async function modifyQuestionDescription(id, questionDescription) { + try { + const result = await updateQuestionDescription(id, questionDescription); + // 调用增加题库版本号的方法 + await increaseQuestionBankVersion(); + return result; + } catch (error) { + console.error('服务层: 更新题干描述失败', error); + throw error; + } +} + +/** + * 服务层:删除题干 + * @param {number} id - 题干ID + * @returns {Promise} 是否删除成功 + */ +export async function removeQuestion(id) { + try { + const result = await deleteQuestion(id); + // 调用增加题库版本号的方法 + await increaseQuestionBankVersion(); + return result; + } catch (error) { + console.error('服务层: 删除题干失败', error); + throw error; + } +} + +/** + * 服务层:添加选择题问题 + * @param {Object} choiceData - 选择题数据 + * @returns {Promise} 新创建的选择题ID + */ +export async function createChoiceQuestion(choiceData) { + try { + const result = await addChoiceQuestion(choiceData); + // 调用增加题库版本号的方法 + await increaseQuestionBankVersion(); + return result; + } catch (error) { + console.error('服务层: 添加选择题失败', error); + throw error; + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3cccd5b..e60a2a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,8 @@ "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "vue": "^3.5.18", - "vue-router": "^4.5.1" + "vue-router": "^4.5.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", @@ -2973,6 +2974,15 @@ "devOptional": true, "license": "ISC" }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", @@ -3746,6 +3756,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", @@ -3897,6 +3920,15 @@ "node": ">=4" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", @@ -4084,6 +4116,18 @@ "buffer": "^5.1.0" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cross-dirname": { "version": "0.1.0", "resolved": "https://registry.npmmirror.com/cross-dirname/-/cross-dirname-0.1.0.tgz", @@ -5102,6 +5146,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fs": { "version": "0.0.1-security", "resolved": "https://registry.npmmirror.com/fs/-/fs-0.0.1-security.tgz", @@ -7659,6 +7712,18 @@ } } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmmirror.com/ssri/-/ssri-8.0.1.tgz", @@ -8490,6 +8555,24 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -8549,6 +8632,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xmlbuilder": { "version": "15.1.1", "resolved": "https://registry.npmmirror.com/xmlbuilder/-/xmlbuilder-15.1.1.tgz", diff --git a/package.json b/package.json index 98d39a9..49c2001 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "sqlite": "^5.1.1", "sqlite3": "^5.1.7", "vue": "^3.5.18", - "vue-router": "^4.5.1" + "vue-router": "^4.5.1", + "xlsx": "^0.18.5" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", diff --git a/src/components/admin/QuestionAddForm.vue b/src/components/admin/QuestionAddForm.vue new file mode 100644 index 0000000..3540e82 --- /dev/null +++ b/src/components/admin/QuestionAddForm.vue @@ -0,0 +1,259 @@ + + + + + \ No newline at end of file diff --git a/src/components/admin/QuestionDescriptionEditForm.vue b/src/components/admin/QuestionDescriptionEditForm.vue new file mode 100644 index 0000000..7ab883e --- /dev/null +++ b/src/components/admin/QuestionDescriptionEditForm.vue @@ -0,0 +1,96 @@ + + + + + \ No newline at end of file diff --git a/src/components/admin/Sider.vue b/src/components/admin/Sider.vue index e9bfd45..6e1fbd5 100644 --- a/src/components/admin/Sider.vue +++ b/src/components/admin/Sider.vue @@ -127,7 +127,7 @@ const handleMenuClick = (menuItem) => { ).then(() => { router.push('/') }).catch(() => { - console.log('取消退出') + // console.log('取消退出') }) } else if (menuItem.route) { router.push(menuItem.route) @@ -137,11 +137,11 @@ const handleMenuClick = (menuItem) => { } const handleOpen = (key, keyPath) => { - console.log('展开菜单:', key, keyPath) + // console.log('展开菜单:', key, keyPath) } const handleClose = (key, keyPath) => { - console.log('关闭菜单:', key, keyPath) + // console.log('关闭菜单:', key, keyPath) } diff --git a/src/components/common/Footer.vue b/src/components/common/Footer.vue index 8609140..4129a32 100644 --- a/src/components/common/Footer.vue +++ b/src/components/common/Footer.vue @@ -36,7 +36,7 @@ + + \ No newline at end of file diff --git a/src/views/admin/QuestionManagementView.vue b/src/views/admin/QuestionManagementView.vue index 275ebeb..0955ddc 100644 --- a/src/views/admin/QuestionManagementView.vue +++ b/src/views/admin/QuestionManagementView.vue @@ -1,24 +1,737 @@ \ No newline at end of file +.question-detail .el-button+.el-button { + margin-left: 8px; +} + +.question-management-container { + padding: 20px; +} + +.question-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20px; +} + +/* 添加列表样式 */ +.question-list { + width: 100%; +} + +.question-item { + border: 1px solid #ebeef5; + border-radius: 4px; + margin-bottom: 16px; + overflow: hidden; + background-color: #fff; +} + +.expanded-content { + padding: 16px; +} + +.question-detail { + margin-bottom: 16px; +} + +.question-detail p { + margin: 0 0 10px 0; +} + +.question-images { + margin-bottom: 16px; +} + +.image-container { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.preview-image { + border-radius: 8px; + border: 1px solid #606266; + height: 150px; +} + +.question-datasets { + margin-top: 16px; +} + +.dataset-item { + margin-bottom: 16px; +} + +.dataset-table { + margin-top: 8px; +} + +.upload-container { + margin-top: 10px; +} + +.pasted-images-container { + margin-top: 20px; +} + +.image-preview-list { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.image-preview-item { + position: relative; + width: 100px; + height: 100px; +} + +.image-preview-actions { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + background: rgba(0, 0, 0, 0.5); + text-align: center; +} + +.upload-container { + margin-top: 10px; +} + +.upload-tip { + margin-top: 10px; + color: #606266; +} + +.question-type-tag { + margin-right: 8px; + margin-left: 8px; +} + +.option-item { + display: flex; + align-items: center; + margin-bottom: 15px; + width: 100%; +} + +.option-label { + width: 24px; + font-weight: bold; + margin-right: 8px; + text-align: center; +} + +.option-item .el-input { + flex: 1; + margin-right: 10px; +} + +.option-item .el-button { + white-space: nowrap; +} + +/* 添加关联试题样式 */ +.related-questions { + margin-top: 20px; + padding-top: 15px; + border-top: 1px dashed #e0e0e0; +} + +.related-question-item { + margin-bottom: 15px; + padding: 10px; + background-color: #f9f9f9; + border-radius: 4px; +} + +.choice-options { + margin: 10px 0; + padding-left: 20px; +} + +.choice-options div { + margin-bottom: 5px; +} + +.correct-answer { + color: #4096ff; + font-weight: bold; +} + +.wrong-answer { + color: #f56c6c; + font-weight: bold; +} + diff --git a/vite.config.js b/vite.config.js index 1da8426..b9fd85e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,17 +1,20 @@ -import { fileURLToPath, URL } from 'node:url' - import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueDevTools from 'vite-plugin-vue-devtools' import electron from 'vite-plugin-electron' +// 只保留一个 fileURLToPath 导入 +import { fileURLToPath } from 'url'; -// https://vite.dev/config/ export default defineConfig({ plugins: [ vue(), vueDevTools(), electron({ entry: 'electron/main.js', + onstart(options) { + // 确保只启动一个 Electron 实例 + options.startup(['.', '--no-sandbox']); + } }), ], resolve: {