后台QuestionManagement试题管理中实现选择题的添加,其他题型暂未实现,实现字典管理页面,修改了question_fill_table_blanks表结构,修改了dict_items表结构
This commit is contained in:
parent
f8c1321eda
commit
52f8af2b63
@ -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")|
|
||||
|
@ -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
242
electron/db/dict.js
Normal 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
286
electron/db/question.js
Normal 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 // 添加新函数导出
|
||||
};
|
@ -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 }
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
||||
// 添加到连接池
|
||||
|
228
electron/main.js
228
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()
|
||||
|
@ -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) => {
|
||||
|
@ -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;
|
||||
|
200
electron/service/dictService.js
Normal file
200
electron/service/dictService.js
Normal 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;
|
||||
}
|
||||
}
|
142
electron/service/questionService.js
Normal file
142
electron/service/questionService.js
Normal 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
106
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
259
src/components/admin/QuestionAddForm.vue
Normal file
259
src/components/admin/QuestionAddForm.vue
Normal 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 - 添加onCancel和onSave事件
|
||||
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>
|
96
src/components/admin/QuestionDescriptionEditForm.vue
Normal file
96
src/components/admin/QuestionDescriptionEditForm.vue
Normal 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>
|
@ -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>
|
||||
|
||||
|
@ -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定义响应式数据
|
||||
|
@ -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,
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
280
src/views/admin/DictManagementView.vue
Normal file
280
src/views/admin/DictManagementView.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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: {
|
||||
|
Loading…
Reference in New Issue
Block a user