后台QuestionManagement试题管理中实现选择题的添加,其他题型暂未实现,实现字典管理页面,修改了question_fill_table_blanks表结构,修改了dict_items表结构

This commit is contained in:
chenqiang 2025-08-07 21:48:17 +08:00
parent f8c1321eda
commit 52f8af2b63
21 changed files with 2644 additions and 48 deletions

View File

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

View File

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

242
electron/db/dict.js Normal file
View File

@ -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<Array>} 记录列表
*/
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<Array>} 类型列表
*/
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<Object>} 添加的类型
*/
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<Object>} 添加的字典项
*/
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<Object|null>} 类型数据或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<Object|null>} 字典项数据或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<Array>} 字典项列表
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否删除成功
*/
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<boolean>} 是否删除成功
*/
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;
});
}

286
electron/db/question.js Normal file
View File

@ -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<number>} 新创建的题干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<Array>} 题干列表
*/
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<Object>} 题干详情
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否删除成功
*/
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<Array>} 包含关联信息的题干列表
*/
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<boolean>} 是否更新成功
*/
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<number>} 新创建的选择题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 // 添加新函数导出
};

View File

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

View File

@ -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);
// 添加到连接池

View File

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

View File

@ -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) => {

View File

@ -113,7 +113,7 @@ export async function updateSystemConfig(config) {
* 增加题库版本号
* @returns {Promise<boolean>}
*/
export async function increaseQuestionBandVersion() {
export async function increaseQuestionBankVersion() {
try {
const currentVersion = await getConfig('question_bank_version');
const newVersion = currentVersion ? parseInt(currentVersion.value) + 1 : 1;

View File

@ -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<Array>} 字典项列表
*/
export async function fetchDictItemsWithTypes() {
try {
return await getDictItemsWithTypes();
} catch (error) {
console.error('服务层: 查询字典项及其类型失败', error);
throw error;
}
}
/**
* 服务层查询字典类型列表
* @returns {Promise<Array>} 字典类型列表
*/
export async function fetchDictTypes() {
try {
return await getDictTypes();
} catch (error) {
console.error('服务层: 查询字典类型列表失败', error);
throw error;
}
}
/**
* 服务层添加字典类型
* @param {Object} typeData 字典类型数据
* @returns {Promise<Object>} 添加的字典类型
*/
export async function createDictType(typeData) {
try {
return await addDictType(typeData);
} catch (error) {
console.error('服务层: 添加字典类型失败', error);
throw error;
}
}
/**
* 服务层添加字典项
* @param {Object} itemData 字典项数据
* @returns {Promise<Object>} 添加的字典项
*/
export async function createDictItem(itemData) {
try {
return await addDictItem(itemData);
} catch (error) {
console.error('服务层: 添加字典项失败', error);
throw error;
}
}
/**
* 服务层根据ID查询字典类型
* @param {number} id 字典类型ID
* @returns {Promise<Object|null>} 字典类型数据
*/
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<Object|null>} 字典项数据
*/
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<Array>} 字典项列表
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否删除成功
*/
export async function removeDictType(id) {
try {
return await deleteDictType(id);
} catch (error) {
console.error('服务层: 删除字典类型失败', error);
throw error;
}
}
/**
* 服务层删除字典项
* @param {number} id 字典项ID
* @returns {Promise<boolean>} 是否删除成功
*/
export async function removeDictItem(id) {
try {
return await deleteDictItem(id);
} catch (error) {
console.error('服务层: 删除字典项失败', error);
throw error;
}
}
// 2. 在文件末尾添加新函数的服务层封装
/**
* 服务层检查父级编码是否存在
* @param {string} parentCode 父级编码
* @returns {Promise<boolean>} 是否存在
*/
export async function checkDictParentCode(parentCode) {
try {
return await checkParentCodeExists(parentCode);
} catch (error) {
console.error('服务层: 检查父级编码失败', error);
throw error;
}
}
/**
* 服务层检查是否有子引用
* @param {string} itemCode 字典项编码
* @returns {Promise<boolean>} 是否有子引用
*/
export async function checkDictChildReferences(itemCode) {
try {
return await hasChildReferences(itemCode);
} catch (error) {
console.error('服务层: 检查子引用失败', error);
throw error;
}
}

View File

@ -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<number>} 新创建的题干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<Array>} 题干列表
*/
export async function fetchAllQuestions() {
try {
return await getAllQuestions();
} catch (error) {
console.error('服务层: 获取所有题干失败', error);
throw error;
}
}
/**
* 服务层获取所有题干及其关联信息
* @returns {Promise<Array>} 包含关联信息的题干列表
*/
export async function fetchAllQuestionsWithRelations() {
try {
return await getAllQuestionsWithRelations();
} catch (error) {
console.error('服务层: 获取所有题干及其关联信息失败', error);
throw error;
}
}
/**
* 服务层根据ID获取题干
* @param {number} id - 题干ID
* @returns {Promise<Object|null>} 题干详情
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否更新成功
*/
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<boolean>} 是否删除成功
*/
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<number>} 新创建的选择题ID
*/
export async function createChoiceQuestion(choiceData) {
try {
const result = await addChoiceQuestion(choiceData);
// 调用增加题库版本号的方法
await increaseQuestionBankVersion();
return result;
} catch (error) {
console.error('服务层: 添加选择题失败', error);
throw error;
}
}

106
package-lock.json generated
View File

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

View File

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

View File

@ -0,0 +1,259 @@
<template>
<el-form ref="questionFormRef" :model="formData" label-width="120px">
<el-form-item label="题型" prop="question_type"
:rules="[{ required: true, message: '请选择题型', trigger: 'change' }]">
<el-select v-model="formData.question_type" placeholder="请选择题型">
<el-option v-for="type in questionTypes" :key="type.item_code" :label="type.item_name"
:value="type.item_code" />
</el-select>
</el-form-item>
<el-form-item label="题干描述" prop="question_description">
<el-input ref="descriptionInputRef" v-model="formData.question_description" type="textarea" placeholder="请输入题干描述" :rows="4" @paste="handleDescriptionPaste" />
</el-form-item>
<el-form-item label="题干配图">
<div class="upload-container">
<el-upload ref="imageUploadRef" class="upload-demo" action="" :before-upload="handleBeforeUploadImage"
:on-change="handleImageChange" list-type="picture-card" :auto-upload="false" accept="image/*" multiple>
<el-icon>
<Plus />
</el-icon>
<div class="el-upload__text">点击上传</div>
</el-upload>
<!-- 粘贴图片预览区 -->
<div v-if="formData.images && formData.images.length > 0" class="pasted-images-container">
<h4>粘贴图片预览 ({{ formData.images.length }})</h4>
<div class="image-preview-list">
<div v-for="(image, index) in formData.images" :key="index" class="image-preview-item">
<el-image :src="image.image_base64" class="preview-image" fit="cover" />
<div class="image-preview-actions">
<el-button type="text" size="small" @click="removePastedImage(index)">删除</el-button>
</div>
</div>
</div>
</div>
</div>
</el-form-item>
<el-form-item label="题干数据集">
<div class="upload-container">
<el-upload ref="datasetUploadRef" class="upload-demo" action="" :before-upload="handleBeforeUploadDataset"
:on-change="handleDatasetChange" accept=".xlsx,.xls" :auto-upload="false" multiple>
<el-button type="primary">上传Excel文件</el-button>
<div class="el-upload__text">支持.xlsx和.xls格式</div>
</el-upload>
<div v-if="formData.datasets && formData.datasets.length > 0" class="upload-tip">
已上传 {{ formData.datasets.length }} 个数据集
</div>
</div>
</el-form-item>
</el-form>
<!-- 添加取消和保存按钮 -->
<div class="form-actions">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import * as XLSX from 'xlsx'
// props
const props = defineProps({
formData: {
type: Object,
required: true
},
questionTypes: {
type: Array,
required: true
}
})
// emits - onCancelonSave
const emit = defineEmits(['update:formData', 'onCancel', 'onSave']);
// ref
const questionFormRef = ref(null)
const imageUploadRef = ref(null)
const datasetUploadRef = ref(null)
const descriptionInputRef = ref(null)
//
const handleDescriptionPaste = (event) => {
const clipboardData = event.clipboardData || event.originalEvent.clipboardData;
const items = clipboardData.items;
//
const text = clipboardData.getData('text/plain');
if (text) {
//
return;
}
//
for (let i = 0; i < items.length; i++) {
if (items[i].kind === 'file') {
const file = items[i].getAsFile();
if (file.type.indexOf('image') !== -1) {
// base64
const reader = new FileReader();
reader.onload = (e) => {
const imageBase64 = e.target.result;
// formData
const newImages = [...props.formData.images, {
file_name: file.name,
image_base64: imageBase64
}];
emit('update:formData', { ...props.formData, images: newImages });
ElMessage.success('图片粘贴成功');
};
reader.readAsDataURL(file);
}
}
}
};
//
const handleBeforeUploadImage = (file) => {
//
return false
}
//
const handleImageChange = (file, fileList) => {
// base64
const reader = new FileReader()
reader.onload = (e) => {
const imageBase64 = e.target.result
// formData
const newImages = [...props.formData.images, {
file_name: file.name,
image_base64: imageBase64
}]
emit('update:formData', { ...props.formData, images: newImages })
}
reader.readAsDataURL(file.raw)
}
//
const removePastedImage = (index) => {
const newImages = props.formData.images.filter((_, i) => i !== index)
emit('update:formData', { ...props.formData, images: newImages })
ElMessage.success('图片已删除')
}
//
const handleBeforeUploadDataset = (file) => {
//
return false
}
//
const handleDatasetChange = (file, fileList) => {
// Excel
const reader = new FileReader()
reader.onload = (e) => {
try {
const data = new Uint8Array(e.target.result)
const workbook = XLSX.read(data, { type: 'array' })
const firstSheetName = workbook.SheetNames[0]
const worksheet = workbook.Sheets[firstSheetName]
//
const jsonData = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
// formData
const newDatasets = [...props.formData.datasets, {
file_name: file.name,
content: jsonData
}]
emit('update:formData', { ...props.formData, datasets: newDatasets })
ElMessage.success('数据集上传成功')
} catch (error) {
console.error('Failed to parse Excel file:', error)
ElMessage.error('数据集解析失败')
}
}
reader.readAsArrayBuffer(file.raw)
}
//
const validateAndSubmit = () => {
return new Promise((resolve, reject) => {
questionFormRef.value.validate((valid) => {
if (valid) {
resolve(props.formData)
} else {
reject(new Error('表单验证失败'))
}
})
})
}
//
defineExpose({
validateAndSubmit
})
//
const handleCancel = () => {
emit('onCancel');
};
//
const handleSave = async () => {
try {
const validatedData = await validateAndSubmit();
if (validatedData) {
emit('onSave', validatedData);
}
} catch (error) {
console.error('表单提交失败:', error);
ElMessage.error('表单提交失败');
}
};
</script>
<style scoped>
.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-tip {
margin-top: 10px;
color: #606266;
}
.preview-image {
width: 100%;
height: 100%;
border-radius: 8px;
border: 1px solid #606266;
}
</style>

View File

@ -0,0 +1,96 @@
<template>
<el-form ref="editDescriptionFormRef" :model="formData" label-width="120px">
<el-form-item label="题干描述" prop="question_description">
<el-input
v-model="formData.question_description"
type="textarea"
placeholder="请输入题干描述"
:rows="6"
/>
</el-form-item>
</el-form>
<!-- 添加取消和保存按钮 -->
<div class="form-actions">
<el-button @click="handleCancel">取消</el-button>
<el-button type="primary" @click="handleSave">保存</el-button>
</div>
</template>
<script setup>
import { ref, defineProps, defineEmits, watch } from 'vue';
// props
const props = defineProps({
modelValue: {
type: Object,
required: true,
default: () => ({})
}
});
// emits - onCancel
const emit = defineEmits(['update:modelValue', 'submit', 'onCancel']);
// ref
const editDescriptionFormRef = ref(null);
//
const formData = ref({ ...props.modelValue });
// modelValue
watch(
() => props.modelValue,
(newValue) => {
formData.value = { ...newValue };
},
{ deep: true }
);
//
const validateAndSubmit = async () => {
try {
//
const valid = await editDescriptionFormRef.value.validate();
if (valid) {
// submit
emit('submit', formData.value);
//
return formData.value;
}
return null;
} catch (error) {
console.error('表单验证失败:', error);
return null;
}
};
//
defineExpose({
validateAndSubmit
});
//
const handleCancel = () => {
emit('onCancel');
};
//
const handleSave = async () => {
try {
//
const validatedData = await validateAndSubmit();
// submit
} catch (error) {
console.error('表单提交失败:', error);
}
};
</script>
<style scoped>
.form-actions {
margin-top: 20px;
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>

View File

@ -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)
}
</script>

View File

@ -36,7 +36,7 @@
</style>
<script setup>
import { ElFooter, ElRow, ElCol } from 'element-plus'
import { ElRow, ElCol } from 'element-plus'
import { ref, onMounted } from 'vue'
// 使ref

View File

@ -6,6 +6,8 @@ import AdminHomeView from '@/views/admin/AdminHomeView.vue'
import QuestionManagementView from '@/views/admin/QuestionManagementView.vue'
// 导入ConfigManagementView
import ConfigManagementView from '@/views/admin/ConfigManagementView.vue'
// 导入DictManagementView
import DictManagementView from '@/views/admin/DictManagementView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@ -32,12 +34,18 @@ const router = createRouter({
name: 'admin-question-management',
component: QuestionManagementView,
},
// 添加ConfigView路由
// 添加ConfigManagementView路由
{
path: '/admin/config-management',
name: 'admin-config-management',
component: ConfigManagementView,
},
// 添加DictManagementView路由
{
path: '/admin/dict-management',
name: 'admin-dict-management',
component: DictManagementView,
},
],
})

View File

@ -0,0 +1,280 @@
<template>
<AdminLayout>
<div class="dict-management-container">
<div class="dict-header">
<h1>字典管理</h1>
<el-button type="primary" @click="handleAddDictItem">+ 添加字典项</el-button>
</div>
<div class="dict-content">
<el-table :data="dictItemsList" style="width: 100%">
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="type_name" label="字典类型" width="180" />
<el-table-column prop="item_code" label="字典编码" width="180" />
<el-table-column prop="item_name" label="字典名称" width="180" />
<el-table-column prop="item_description" label="字典描述" width="180" />
<el-table-column prop="parent_code" label="父级编码" width="180">
<template #default="scope">
<span>{{ scope.row.parent_code || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="is_active" label="状态" width="100">
<template #default="scope">
<span>{{ scope.row.is_active === 1 ? '启用' : '禁用' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleEditDictItem(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDeleteDictItem(scope.row.id, scope.row.itemCode)"
>
删除
</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="type_code" :rules="[{ required: true, message: '请选择字典类型', trigger: 'blur' }]">
<el-select v-model="formData.type_code" placeholder="请选择字典类型">
<el-option v-for="type in dictTypes" :key="type.type_code" :label="type.type_name" :value="type.type_code" />
</el-select>
</el-form-item>
<el-form-item label="字典编码" prop="item_code" :rules="[{ required: true, message: '请输入字典编码', trigger: 'blur' }]">
<el-input v-model="formData.item_code" placeholder="请输入字典编码" :disabled="isEdit" />
</el-form-item>
<el-form-item label="字典名称" prop="item_name" :rules="[{ required: true, message: '请输入字典名称', trigger: 'blur' }]">
<el-input v-model="formData.item_name" placeholder="请输入字典名称" />
</el-form-item>
<el-form-item label="字典描述" prop="item_description" :rules="[{ required: true, message: '请输入字典描述', trigger: 'blur' }]">
<el-input v-model="formData.item_description" placeholder="请输入字典描述" />
</el-form-item>
<el-form-item label="父级编码" prop="parent_code">
<el-input v-model="formData.parent_code" placeholder="请输入父级编码 (可选)" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="formData.is_active" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveDictItem">保存</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 dictItemsList = ref([])
const dictTypes = ref([])
const dialogVisible = ref(false)
const isEdit = ref(false)
const dialogTitle = ref('添加字典项')
const formData = ref({
id: null,
type_code: '',
item_code: '',
item_name: '',
item_description: '',
parent_code: '',
is_active: 1 //
})
//
const loadDictItemsList = async () => {
try {
const data = await window.electronAPI.fetchAllDictItemsWithTypes()
dictItemsList.value = data
} catch (error) {
ElMessage.error('获取字典项列表失败: ' + error.message)
console.error('获取字典项列表失败', error)
}
}
//
const loadDictTypes = async () => {
try {
const data = await window.electronAPI.fetchDictTypes()
dictTypes.value = data
} catch (error) {
ElMessage.error('获取字典类型列表失败: ' + error.message)
console.error('获取字典类型列表失败', error)
}
}
//
const handleAddDictItem = () => {
isEdit.value = false
dialogTitle.value = '添加字典项'
formData.value = {
id: null,
type_code: '',
item_code: '',
item_name: '',
item_description: '',
parent_code: '',
is_active: 1
}
dialogVisible.value = true
}
//
const handleEditDictItem = (row) => {
isEdit.value = true
dialogTitle.value = '编辑字典项'
formData.value = {
id: row.id,
type_code: row.type_code,
item_code: row.item_code,
item_name: row.item_name,
item_description: row.item_description,
parent_code: row.parent_code || '',
is_active: row.is_active
}
dialogVisible.value = true
}
//
const handleSaveDictItem = async () => {
try {
//
const dictItemData = {
type_code: formData.value.type_code,
item_code: formData.value.item_code,
item_name: formData.value.item_name,
item_description: formData.value.item_description,
parent_code: formData.value.parent_code,
is_active: formData.value.is_active
};
// parent_code
if (dictItemData.parent_code) {
const parentExists = await window.electronAPI.checkDictParentCode(dictItemData.parent_code);
if (!parentExists) {
ElMessage.error('父级编码不存在,请检查');
return;
}
}
if (isEdit.value) {
// id
await window.electronAPI.updateDictItem({
id: formData.value.id,
...dictItemData
});
ElMessage.success('字典项更新成功');
} else {
//
const items = await window.electronAPI.fetchDictItemsByType(dictItemData.type_code);
const isDuplicate = items.some(item => item.item_code === dictItemData.item_code);
if (isDuplicate) {
ElMessage.error('该字典类型下已存在相同编码的字典项');
return;
}
await window.electronAPI.createDictItem(dictItemData);
ElMessage.success('字典项添加成功');
}
dialogVisible.value = false;
loadDictItemsList(); //
} catch (error) {
//
if (error.message.includes('SQLITE_CONSTRAINT')) {
ElMessage.error('添加失败:该字典类型下已存在相同编码的字典项');
} else {
ElMessage.error('保存字典项失败: ' + error.message);
}
console.error('保存字典项失败', error);
}
};
//
const handleDeleteDictItem = (id, itemCode) => {
ElMessageBox.confirm(
'确定要删除该字典项吗?',
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
)
.then(async () => {
try {
// item_code
const hasReferences = await window.electronAPI.checkDictChildReferences(itemCode);
if (hasReferences) {
ElMessage.error('该字典项已被其他项引用,不能删除');
return;
}
await window.electronAPI.deleteDictItem(id);
ElMessage.success('字典项删除成功');
loadDictItemsList(); //
} catch (error) {
ElMessage.error('删除字典项失败: ' + error.message);
console.error('删除字典项失败', error);
}
})
.catch(() => {
//
});
};
//
onMounted(() => {
loadDictItemsList()
loadDictTypes()
})
</script>
<style scoped>
.dict-management-container {
width: 100%;
height: 100%;
}
.dict-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.dict-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

@ -1,24 +1,737 @@
<template>
<AdminLayout @menu-click="handleMenuClick">
<!-- 这里是试题管理页面特定的内容 -->
<div class="container mx-auto py-6">
<h1 class="text-2xl font-bold mb-6">试题管理</h1>
<!-- 试题管理相关的内容 -->
<AdminLayout>
<div class="question-management-container">
<div class="question-header">
<h1>试题管理</h1>
<el-button type="primary" @click="openAddQuestionDialog">+ 添加题干</el-button>
</div>
<div class="question-content">
<div v-loading="loading" class="question-list">
<div v-for="question in questionList" :key="question.id" class="question-item">
<div class="expanded-content">
<div class="question-detail">
<p>
<strong>ID: {{ question.id }}</strong>
<el-tag :type="getTypeColor(question.question_type_name)" class="question-type-tag">{{
question.question_type_name }}</el-tag>
<el-button type="primary" size="small" @click="handleEditQuestionDescription(question)">编辑</el-button>
<el-button type="success" size="small" @click="handleAddSubQuestion(question)">添加问题</el-button>
<el-button type="danger" size="small" @click="handleDeleteQuestion(question.id)">删除</el-button>
</p>
<p>
<strong>{{ question.question_description }}</strong>
</p>
</div>
<!-- 显示图片 -->
<div v-if="question.images && question.images.length > 0" class="question-images">
<div class="image-container">
<el-image v-for="(image, index) in question.images" :key="index" :src="image.image_base64"
:preview-src-list="[image.image_base64]" class="preview-image" fit="cover" />
</div>
</div>
<!-- 显示数据集 -->
<div v-if="question.datasets && question.datasets.length > 0" class="question-datasets">
<div v-for="(dataset, index) in question.datasets" :key="index" class="dataset-item">
<!-- 检查dataset_data是否存在且是二维数组 -->
<template
v-if="dataset.dataset_data && Array.isArray(dataset.dataset_data) && dataset.dataset_data.length > 0">
<el-table :data="dataset.dataset_data.slice(1) || []" border size="small" class="dataset-table">
<el-table-column v-for="(header, colIndex) in dataset.dataset_data[0]" :key="colIndex"
:label="header" :prop="colIndex.toString()">
<template #default="scope">
{{ scope.row[colIndex] }}
</template>
</el-table-column>
</el-table>
</template>
<div v-else>无数据</div>
</div>
</div>
<!-- 显示关联的试题 -->
<div class="related-questions" v-if="hasRelatedQuestions(question)">
<!-- 选择题 -->
<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="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>
</div>
</div>
<p><strong>正确答案:</strong> {{ formatCorrectAnswers(JSON.parse(choice.correct_answers), JSON.parse(choice.choice_options).length) }}</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>
</div>
</div>
<!-- 判断题 -->
<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>
</div>
</div>
<!-- 简答题 -->
<div v-else-if="question.shorts && question.shorts.length > 0">
<div v-for="(short, idx) in question.shorts" :key="idx" class="related-question-item">
<p><strong>{{ question.question_type_name }} {{ idx + 1 }}:</strong> {{ short.content }}</p>
<p><strong>参考答案:</strong> {{ short.reference_answer }}</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 其他弹窗代码保持不变 -->
<el-dialog v-model="dialogVisible" :title="dialogTitle" width="70%" :before-close="handleDialogClose">
<!-- 根据表单类型切换显示的组件 -->
<template v-if="dialogFormType === 'addQuestion'">
<QuestionAddForm :formData="formData" :questionTypes="questionTypes"
@update:formData="newData => formData = newData" @onSave="handleSaveQuestion" @onCancel="handleDialogClose" />
</template>
<template v-else-if="dialogFormType === 'editDescription'">
<QuestionDescriptionEditForm
v-model="editDescriptionFormData"
@submit="handleSaveQuestionDescription"
@onCancel="handleDialogClose"
/>
</template>
</el-dialog>
<!-- 选择题弹窗 -->
<el-dialog title="添加选择题" v-model="choiceQuestionDialogVisible" width="800px">
<el-form ref="choiceQuestionFormRef" :model="choiceQuestionFormData" label-width="120px">
<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="choice_type">
<el-radio-group v-model="choiceQuestionFormData.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 choiceQuestionFormData.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="removeOption(index)"
v-if="choiceQuestionFormData.choice_options.length > 1">删除</el-button>
</div>
<el-button type="primary" size="small" @click="addOption">+ 添加选项</el-button>
</el-form-item>
<el-form-item label="正确答案" prop="correct_answers">
<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" />
</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>
</el-checkbox-group>
</template>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="choiceQuestionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveChoiceQuestion">保存</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>
<template #footer>
<el-button @click="fillBlankQuestionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveFillBlankQuestion">保存</el-button>
</template>
</el-dialog>
<!-- 判断题弹窗 -->
<el-dialog title="添加判断题" v-model="judgeQuestionDialogVisible" width="800px">
<el-form ref="judgeQuestionFormRef" :model="judgeQuestionFormData" label-width="120px">
<el-form-item label="判断内容" prop="content">
<el-input v-model="judgeQuestionFormData.content" type="textarea" placeholder="请输入判断内容"
:rows="3" /></el-form-item>
<el-form-item label="正确答案" prop="is_correct">
<el-radio-group v-model="judgeQuestionFormData.is_correct">
<el-radio :label="true">正确</el-radio>
<el-radio :label="false">错误</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="judgeQuestionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveJudgeQuestion">保存</el-button>
</template>
</el-dialog>
<!-- 简答题弹窗 (适用于问答题分析题论述题) -->
<el-dialog :title="shortQuestionDialogTitle" v-model="shortQuestionDialogVisible" width="800px">
<el-form ref="shortQuestionFormRef" :model="shortQuestionFormData" label-width="120px">
<el-form-item label="问题内容" prop="content">
<el-input v-model="shortQuestionFormData.content" type="textarea" placeholder="请输入问题内容"
:rows="3" /></el-form-item>
<el-form-item label="参考答案" prop="reference_answer">
<el-input v-model="shortQuestionFormData.reference_answer" type="textarea" placeholder="请输入参考答案"
:rows="5" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="shortQuestionDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveShortQuestion">保存</el-button>
</template>
</el-dialog>
</AdminLayout>
</template>
<script setup>
import { defineComponent } from 'vue'
import { ref, onMounted, nextTick } from 'vue'
import AdminLayout from '@/components/admin/AdminLayout.vue'
import QuestionAddForm from '@/components/admin/QuestionAddForm.vue'
import QuestionDescriptionEditForm from '@/components/admin/QuestionDescriptionEditForm.vue'
import { ElMessage, ElMessageBox } from 'element-plus'
//
const handleMenuClick = (menuKey) => {
console.log('Menu clicked:', menuKey)
//
//
const questionList = ref([])
const loading = ref(false)
const expandedRows = ref([])
const dialogVisible = ref(false)
const dialogTitle = ref('添加题干')
const dialogFormType = ref('addQuestion')
const questionTypes = ref([])
//
const formData = ref({
id: null,
question_type: '',
question_description: '',
images: [], // base64
datasets: [] //
})
//
const editDescriptionFormData = ref({
id: null,
question_description: ''
})
//
const loadQuestionTypes = async () => {
try {
const data = await window.electronAPI.fetchDictItemsByType('question_type', 1)
questionTypes.value = data
} catch (error) {
ElMessage.error('获取题型列表失败: ' + error.message)
console.error('获取题型列表失败', error)
}
}
//
const fetchQuestions = async () => {
loading.value = true
try {
const questions = await window.electronAPI.fetchAllQuestionsWithRelations();
console.log('questions', questions);
const processedQuestions = questions.map(question => ({
...question,
image_count: question.images ? question.images.length : 0,
dataset_count: question.datasets ? question.datasets.length : 0
}))
questionList.value = processedQuestions
//
nextTick(() => {
expandedRows.value = processedQuestions.map(item => item.id)
})
} catch (error) {
console.error('Failed to fetch questions:', error)
ElMessage.error('获取题干列表失败')
} finally {
loading.value = false
}
}
//
const openAddQuestionDialog = () => {
dialogTitle.value = '添加题干'
dialogFormType.value = 'addQuestion'
resetAddForm()
//
setTimeout(() => {
dialogVisible.value = true
}, 100)
}
// ()
const handleSaveQuestion = async (validatedData) => {
try {
const serializedData = {
...validatedData,
images: validatedData.images.map(image => ({
name: image.file_name || 'image',
base64: image.image_base64
})),
datasets: validatedData.datasets.map(dataset => ({
name: dataset.file_name || 'dataset',
content: typeof dataset.content === 'string' ? dataset.content : JSON.stringify(dataset.content)
}))
}
await window.electronAPI.createQuestion(serializedData)
ElMessage.success('题目添加成功')
dialogVisible.value = false
fetchQuestions()
} catch (error) {
console.error('保存题目失败:', error)
ElMessage.error('保存题目失败')
}
}
const handleEditQuestionDescription = (question) => {
dialogTitle.value = '编辑题干描述'
dialogFormType.value = 'editDescription'
editDescriptionFormData.value = {
id: question.id,
question_description: question.question_description
}
dialogVisible.value = true
}
//
const handleSaveQuestionDescription = async (validatedData) => {
try {
if (!validatedData) return;
await window.electronAPI.updateQuestionDescription(
validatedData.id,
validatedData.question_description
);
dialogVisible.value = false;
ElMessage.success('更新题干描述成功');
fetchQuestions();
} catch (error) {
console.error('Failed to update question description:', error);
ElMessage.error('更新题干描述失败');
}
};
//
const resetAddForm = () => {
formData.value = {
id: null,
question_type: '',
question_description: '',
images: [],
datasets: []
}
}
//
const handleDeleteQuestion = async (id) => {
try {
await ElMessageBox.confirm('确定要删除该题干吗?', '确认删除', {
type: 'warning'
})
await window.electronAPI.deleteQuestion(id)
ElMessage.success('删除题干成功')
fetchQuestions()
} catch (error) {
if (error !== 'cancel') {
console.error('Failed to delete question:', error)
ElMessage.error('删除题干失败')
}
}
}
//
const getTypeColor = (typeName) => {
const colorMap = {
'选择题': 'primary',
'填空题': 'success',
'填表题': 'warning',
'判断题': 'danger',
'问答题': 'info',
'分析题': 'purple',
'论述题': 'blue'
}
return colorMap[typeName] || 'default'
}
//
onMounted(() => {
fetchQuestions()
loadQuestionTypes()
})
//
const choiceQuestionDialogVisible = ref(false)
const fillBlankQuestionDialogVisible = ref(false)
const judgeQuestionDialogVisible = ref(false)
const shortQuestionDialogVisible = ref(false)
const shortQuestionDialogTitle = ref('添加问题')
// ID
const currentQuestionId = ref(null)
//
const choiceQuestionFormData = ref({
choice_description: '', //
choice_type: 'single', //
choice_options: [ //
{ value: '' },
{ value: '' },
{ value: '' },
{ value: '' }
],
correct_answers: [] // ()
})
//
const addOption = () => {
choiceQuestionFormData.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
)
}
const fillBlankQuestionFormData = ref({
blank_count: 1,
answers: ''
})
const judgeQuestionFormData = ref({
content: '',
is_correct: true
})
const shortQuestionFormData = ref({
content: '',
reference_answer: ''
})
// refs
const choiceQuestionFormRef = ref(null)
const fillBlankQuestionFormRef = ref(null)
const judgeQuestionFormRef = ref(null)
const shortQuestionFormRef = ref(null)
//
const handleAddSubQuestion = (question) => {
currentQuestionId.value = question.id
switch (question.question_type_name) {
case '选择题':
choiceQuestionDialogVisible.value = true
break
case '填空题':
fillBlankQuestionDialogVisible.value = true
break
case '判断题':
judgeQuestionDialogVisible.value = true
break
case '问答题':
case '分析题':
case '论述题':
shortQuestionDialogTitle.value = `添加${question.question_type_name}`
shortQuestionDialogVisible.value = true
break
case '填表题':
ElMessage.info('填表题功能暂未实现')
break
default:
ElMessage.error('未知题型')
}
}
//
const handleSaveChoiceQuestion = () => {
//
choiceQuestionFormRef.value.validate(async (valid) => {
if (valid) {
try {
//
const choiceData = {
question_id: Number(currentQuestionId.value), //
choice_description: String(choiceQuestionFormData.value.choice_description),
choice_type: String(choiceQuestionFormData.value.choice_type),
choice_options: choiceQuestionFormData.value.choice_options
.map(option => String(option.value)), //
correct_answers: choiceQuestionFormData.value.correct_answers
.map(answer => Number(answer)) //
}
//
await window.electronAPI.createChoiceQuestion(choiceData)
//
choiceQuestionDialogVisible.value = false
ElMessage.success('选择题添加成功')
//
fetchQuestions()
//
choiceQuestionFormData.value = {
choice_description: '',
choice_type: 'single',
choice_options: [
{ value: '' },
{ value: '' },
{ value: '' },
{ value: '' }
],
correct_answers: []
}
} catch (error) {
console.error('Failed to create choice question:', error)
ElMessage.error('选择题添加失败: ' + (error.message || '未知错误'))
}
}
})
}
const handleSaveFillBlankQuestion = () => {
fillBlankQuestionDialogVisible.value = false
ElMessage.success('填空题添加成功')
}
const handleSaveJudgeQuestion = () => {
judgeQuestionDialogVisible.value = false
ElMessage.success('判断题添加成功')
}
const handleSaveShortQuestion = () => {
shortQuestionDialogVisible.value = false
ElMessage.success(`${shortQuestionDialogTitle.value}添加成功`)
}
//
const handleDialogClose = () => {
dialogVisible.value = false
//
}
//
const hasRelatedQuestions = (question) => {
return (
(question.choices && question.choices.length > 0) ||
(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(', ');
};
//
const isCorrectAnswer = (optionIndex, correctAnswers) => {
if (!correctAnswers || correctAnswers.length === 0) return false;
return correctAnswers.includes(optionIndex);
};
</script>
<style scoped>
/* 可以添加一些页面特定的样式 */
</style>
.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;
}
</style>

View File

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