提取AdminLayout模板页,整理db/config.js和service/configService.js,实现ConfigManagementView页的功能
This commit is contained in:
parent
0bf9ff662d
commit
f8c1321eda
@ -1,11 +1,7 @@
|
|||||||
// 删除或注释掉原来的路径相关代码
|
|
||||||
// import path from 'path';
|
|
||||||
// import fs from 'fs';
|
|
||||||
// import { app } from 'electron';
|
|
||||||
|
|
||||||
// 导入统一的路径工具函数
|
// 导入统一的路径工具函数
|
||||||
import { getSystemDbPath } from './path.js';
|
import { getSystemDbPath } from './path.js';
|
||||||
import { openDatabase, executeWithRetry } from './utils.js';
|
import { executeWithRetry } from './utils.js';
|
||||||
|
import { getDbConnection } from './index.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从config表获取配置项
|
* 从config表获取配置项
|
||||||
@ -14,10 +10,7 @@ import { openDatabase, executeWithRetry } from './utils.js';
|
|||||||
*/
|
*/
|
||||||
async function getConfig(key) {
|
async function getConfig(key) {
|
||||||
try {
|
try {
|
||||||
// 使用统一的数据库路径
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
const dbPath = getSystemDbPath();
|
|
||||||
|
|
||||||
const db = await openDatabase(dbPath);
|
|
||||||
|
|
||||||
const result = await executeWithRetry(db, async () => {
|
const result = await executeWithRetry(db, async () => {
|
||||||
return await db.getAsync('SELECT * FROM config WHERE key = ?', [key]);
|
return await db.getAsync('SELECT * FROM config WHERE key = ?', [key]);
|
||||||
@ -30,6 +23,45 @@ async function getConfig(key) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有配置项列表
|
||||||
|
* @returns {Promise<Array<{id: number, key: string, value: string, protected: number}>>} 配置项列表
|
||||||
|
*/
|
||||||
|
async function getAllConfigs() {
|
||||||
|
try {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
|
||||||
|
const result = await executeWithRetry(db, async () => {
|
||||||
|
return await db.allAsync('SELECT * FROM config');
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取配置项列表失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过ID获取配置项
|
||||||
|
* @param {number} id - 配置项ID
|
||||||
|
* @returns {Promise<{id: number, key: string, value: string, protected: number} | null>} 配置项对象,如果不存在返回null
|
||||||
|
*/
|
||||||
|
async function getConfigById(id) {
|
||||||
|
try {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
|
||||||
|
const result = await executeWithRetry(db, async () => {
|
||||||
|
return await db.getAsync('SELECT * FROM config WHERE id = ?', [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`通过ID获取配置项${id}失败:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新或插入配置项
|
* 更新或插入配置项
|
||||||
* @param {string} key - 配置项键名
|
* @param {string} key - 配置项键名
|
||||||
@ -38,10 +70,7 @@ async function getConfig(key) {
|
|||||||
*/
|
*/
|
||||||
async function setConfig(key, value) {
|
async function setConfig(key, value) {
|
||||||
try {
|
try {
|
||||||
// 使用统一的数据库路径
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
const dbPath = getSystemDbPath();
|
|
||||||
|
|
||||||
const db = await openDatabase(dbPath);
|
|
||||||
|
|
||||||
// 先检查是否存在
|
// 先检查是否存在
|
||||||
const existing = await executeWithRetry(db, async () => {
|
const existing = await executeWithRetry(db, async () => {
|
||||||
@ -49,15 +78,19 @@ async function setConfig(key, value) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
// 检查是否受保护
|
||||||
|
if (existing.protected === 1) {
|
||||||
|
throw new Error(`配置项${key}是受保护的,无法修改`);
|
||||||
|
}
|
||||||
// 更新
|
// 更新
|
||||||
await executeWithRetry(db, async () => {
|
await executeWithRetry(db, async () => {
|
||||||
await db.runAsync('UPDATE config SET value = ? WHERE key = ?', [value, key]);
|
await db.runAsync('UPDATE config SET value = ? WHERE key = ?', [value, key]);
|
||||||
console.log(`成功更新配置项: ${key}`);
|
console.log(`成功更新配置项: ${key}`);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// 插入
|
// 插入 (默认不保护)
|
||||||
await executeWithRetry(db, async () => {
|
await executeWithRetry(db, async () => {
|
||||||
await db.runAsync('INSERT INTO config (key, value) VALUES (?, ?)', [key, value]);
|
await db.runAsync('INSERT INTO config (key, value, protected) VALUES (?, ?, 0)', [key, value]);
|
||||||
console.log(`成功插入配置项: ${key}`);
|
console.log(`成功插入配置项: ${key}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -67,7 +100,46 @@ async function setConfig(key, value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除配置项
|
||||||
|
* @param {number} id - 配置项ID
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function deleteConfig(id) {
|
||||||
|
try {
|
||||||
|
const db = await getDbConnection(getSystemDbPath());
|
||||||
|
|
||||||
|
// 先检查是否存在
|
||||||
|
const existing = await executeWithRetry(db, async () => {
|
||||||
|
return await db.getAsync('SELECT * FROM config WHERE id = ?', [id]);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing) {
|
||||||
|
throw new Error(`配置项ID ${id} 不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否受保护
|
||||||
|
if (existing.protected === 1) {
|
||||||
|
throw new Error(`配置项 ${existing.key} 是受保护的,无法删除`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除
|
||||||
|
await executeWithRetry(db, async () => {
|
||||||
|
await db.runAsync('DELETE FROM config WHERE id = ?', [id]);
|
||||||
|
console.log(`成功删除配置项ID: ${id}`);
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`删除配置项ID ${id} 失败:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出保持不变
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getConfig,
|
getConfig,
|
||||||
setConfig
|
setConfig,
|
||||||
|
getAllConfigs,
|
||||||
|
getConfigById,
|
||||||
|
deleteConfig
|
||||||
};
|
};
|
@ -260,4 +260,5 @@ process.on('exit', closeAllConnections);
|
|||||||
export {
|
export {
|
||||||
initializeDatabase,
|
initializeDatabase,
|
||||||
checkDatabaseInitialized,
|
checkDatabaseInitialized,
|
||||||
|
getDbConnection,
|
||||||
};
|
};
|
@ -3,15 +3,22 @@ import { app, BrowserWindow, ipcMain } from 'electron';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { checkDatabaseInitialized, initializeDatabase } from './db/index.js';
|
import { checkDatabaseInitialized, initializeDatabase } from './db/index.js';
|
||||||
import { getSystemConfig } from './service/system.js';
|
// 导入配置项服务
|
||||||
|
import {
|
||||||
|
fetchAllConfigs,
|
||||||
|
fetchConfigById,
|
||||||
|
saveConfig,
|
||||||
|
removeConfig,
|
||||||
|
getSystemConfig,
|
||||||
|
updateSystemConfig,
|
||||||
|
increaseQuestionBandVersion,
|
||||||
|
initAuthIpc
|
||||||
|
} from './service/configService.js';
|
||||||
|
|
||||||
// 定义 __dirname 和 __filename
|
// 定义 __dirname 和 __filename
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
// 引入认证服务
|
|
||||||
import { initAuthIpc } from './service/auth.service.js';
|
|
||||||
|
|
||||||
// 确保在应用最开始处添加单实例锁检查
|
// 确保在应用最开始处添加单实例锁检查
|
||||||
// 尝试获取单实例锁
|
// 尝试获取单实例锁
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
@ -84,18 +91,12 @@ async function setupApp() {
|
|||||||
console.log('数据库初始化状态:', isInitialized);
|
console.log('数据库初始化状态:', isInitialized);
|
||||||
|
|
||||||
// 只检查状态,不自动初始化
|
// 只检查状态,不自动初始化
|
||||||
// if (!isInitialized) {
|
|
||||||
// console.log('数据库未初始化,开始初始化...');
|
|
||||||
// const initResult = await initializeDatabase();
|
|
||||||
// console.log('数据库初始化结果:', initResult ? '成功' : '失败');
|
|
||||||
// }
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('数据库检查过程中出错:', error);
|
console.error('数据库检查过程中出错:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Setup IPC communication
|
// Setup IPC communication
|
||||||
// 在文件适当位置添加以下代码
|
|
||||||
function setupIpcMain() {
|
function setupIpcMain() {
|
||||||
// 数据库相关
|
// 数据库相关
|
||||||
ipcMain.handle('check-database-initialized', async () => {
|
ipcMain.handle('check-database-initialized', async () => {
|
||||||
@ -116,16 +117,6 @@ function setupIpcMain() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 认证相关
|
|
||||||
ipcMain.handle('auth-admin-login', async (event, credentials) => {
|
|
||||||
try {
|
|
||||||
return await adminLogin(credentials);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Login failed:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// 系统相关
|
// 系统相关
|
||||||
ipcMain.handle('system-get-config', async () => {
|
ipcMain.handle('system-get-config', async () => {
|
||||||
try {
|
try {
|
||||||
@ -135,14 +126,69 @@ function setupIpcMain() {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('system-update-config', async (event, config) => {
|
||||||
|
try {
|
||||||
|
return await updateSystemConfig(config);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update system config:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('system-increase-question-band-version', async () => {
|
||||||
|
try {
|
||||||
|
return await increaseQuestionBandVersion();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to increase question band version:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 初始化认证相关IPC
|
// 初始化认证相关IPC
|
||||||
initAuthIpc();
|
initAuthIpc(ipcMain);
|
||||||
|
|
||||||
|
// 配置项管理相关IPC
|
||||||
|
ipcMain.handle('config-fetch-all', async () => {
|
||||||
|
try {
|
||||||
|
return await fetchAllConfigs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch all configs:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('config-fetch-by-id', async (event, id) => {
|
||||||
|
try {
|
||||||
|
return await fetchConfigById(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to fetch config by id ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('config-save', async (event, { key, value }) => {
|
||||||
|
try {
|
||||||
|
await saveConfig(key, value);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to save config ${key}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('config-delete', async (event, id) => {
|
||||||
|
try {
|
||||||
|
await removeConfig(id);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete config ${id}:`, error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保在 app.whenReady() 中调用 setupIpcMain()
|
// 确保在 app.whenReady() 中调用 setupIpcMain()
|
||||||
// 删除重复的app.whenReady()调用,只保留一个
|
|
||||||
// 保留一个正确的初始化代码块
|
|
||||||
// 保留一个app.whenReady()调用
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
setupApp()
|
setupApp()
|
||||||
createWindow()
|
createWindow()
|
||||||
|
@ -10,7 +10,15 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
|||||||
adminLogin: (credentials) => ipcRenderer.invoke('admin-login', credentials),
|
adminLogin: (credentials) => ipcRenderer.invoke('admin-login', credentials),
|
||||||
|
|
||||||
// 系统相关
|
// 系统相关
|
||||||
getSystemConfig: () => ipcRenderer.invoke('system-get-config')
|
getSystemConfig: () => ipcRenderer.invoke('system-get-config'),
|
||||||
|
updateSystemConfig: (config) => ipcRenderer.invoke('system-update-config', config),
|
||||||
|
increaseQuestionBandVersion: () => ipcRenderer.invoke('system-increase-question-band-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)
|
||||||
});
|
});
|
||||||
|
|
||||||
// 这里可以添加预加载脚本
|
// 这里可以添加预加载脚本
|
||||||
|
@ -1,43 +0,0 @@
|
|||||||
import argon2 from 'argon2';
|
|
||||||
import { getConfig } from '../db/config.js';
|
|
||||||
import { ipcMain } from 'electron';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 管理员登录验证
|
|
||||||
* @param {string} password - 用户输入的密码
|
|
||||||
* @returns {Promise<{success: boolean, message: string}>}
|
|
||||||
*/
|
|
||||||
async function verifyAdminPassword(password) {
|
|
||||||
try {
|
|
||||||
const config = await getConfig('admin_password');
|
|
||||||
if (!config || !config.value) {
|
|
||||||
return { success: false, message: '管理员密码未设置' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const isMatch = await argon2.verify(config.value, password);
|
|
||||||
if (isMatch) {
|
|
||||||
return { success: true, message: '登录成功' };
|
|
||||||
} else {
|
|
||||||
return { success: false, message: '密码错误' };
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证管理员密码失败:', error);
|
|
||||||
return { success: false, message: '验证过程发生错误' };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 初始化认证相关的IPC处理程序
|
|
||||||
*/
|
|
||||||
function initAuthIpc() {
|
|
||||||
// 管理员登录验证 - 使用正确的通道名称
|
|
||||||
ipcMain.handle('admin-login', async (event, credentials) => {
|
|
||||||
// 从credentials对象中获取密码
|
|
||||||
return await verifyAdminPassword(credentials.password);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
verifyAdminPassword,
|
|
||||||
initAuthIpc
|
|
||||||
};
|
|
162
electron/service/configService.js
Normal file
162
electron/service/configService.js
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
// 导入数据库操作方法
|
||||||
|
import { getConfig, setConfig, getAllConfigs, getConfigById, deleteConfig } from '../db/config.js';
|
||||||
|
import argon2 from 'argon2';
|
||||||
|
|
||||||
|
// 原有接口保持不变
|
||||||
|
/**
|
||||||
|
* 服务层:获取配置项
|
||||||
|
* @param {string} key - 配置项键名
|
||||||
|
* @returns {Promise<{key: string, value: string} | null>}
|
||||||
|
*/
|
||||||
|
export async function fetchConfig(key) {
|
||||||
|
try {
|
||||||
|
return await getConfig(key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 获取配置项失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:获取所有配置项
|
||||||
|
* @returns {Promise<Array<{id: number, key: string, value: string, protected: number}>>}
|
||||||
|
*/
|
||||||
|
export async function fetchAllConfigs() {
|
||||||
|
try {
|
||||||
|
return await getAllConfigs();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 获取所有配置项失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:通过ID获取配置项
|
||||||
|
* @param {number} id - 配置项ID
|
||||||
|
* @returns {Promise<{id: number, key: string, value: string, protected: number} | null>}
|
||||||
|
*/
|
||||||
|
export async function fetchConfigById(id) {
|
||||||
|
try {
|
||||||
|
return await getConfigById(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 通过ID获取配置项失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:保存配置项
|
||||||
|
* @param {string} key - 配置项键名
|
||||||
|
* @param {string} value - 配置项值
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function saveConfig(key, value) {
|
||||||
|
try {
|
||||||
|
await setConfig(key, value);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 保存配置项失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 服务层:删除配置项
|
||||||
|
* @param {number} id - 配置项ID
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function removeConfig(id) {
|
||||||
|
try {
|
||||||
|
await deleteConfig(id);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('服务层: 删除配置项失败', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 system.js 整合的接口
|
||||||
|
/**
|
||||||
|
* 获取系统配置并转为Map
|
||||||
|
* @returns {Promise<{[key: string]: string}>}
|
||||||
|
*/
|
||||||
|
export async function getSystemConfig() {
|
||||||
|
try {
|
||||||
|
const configs = await getAllConfigs();
|
||||||
|
const configMap = {};
|
||||||
|
configs.forEach(config => {
|
||||||
|
configMap[config.key] = config.value;
|
||||||
|
});
|
||||||
|
return configMap;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取系统配置失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量更新系统配置
|
||||||
|
* @param {{[key: string]: string}} config - 配置对象
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function updateSystemConfig(config) {
|
||||||
|
try {
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
await setConfig(key, value);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新系统配置失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增加题库版本号
|
||||||
|
* @returns {Promise<boolean>}
|
||||||
|
*/
|
||||||
|
export async function increaseQuestionBandVersion() {
|
||||||
|
try {
|
||||||
|
const currentVersion = await getConfig('question_bank_version');
|
||||||
|
const newVersion = currentVersion ? parseInt(currentVersion.value) + 1 : 1;
|
||||||
|
await setConfig('question_bank_version', newVersion.toString());
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('增加题库版本号失败:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 auth.service.js 整合的接口
|
||||||
|
/**
|
||||||
|
* 管理员登录验证
|
||||||
|
* @param {string} password - 用户输入的密码
|
||||||
|
* @returns {Promise<{success: boolean, message: string}>}
|
||||||
|
*/
|
||||||
|
export async function verifyAdminPassword(password) {
|
||||||
|
try {
|
||||||
|
const config = await getConfig('admin_password');
|
||||||
|
if (!config || !config.value) {
|
||||||
|
return { success: false, message: '管理员密码未设置' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await argon2.verify(config.value, password);
|
||||||
|
if (isMatch) {
|
||||||
|
return { success: true, message: '登录成功' };
|
||||||
|
} else {
|
||||||
|
return { success: false, message: '密码错误' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('验证管理员密码失败:', error);
|
||||||
|
return { success: false, message: '验证过程发生错误' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化认证相关的IPC处理程序
|
||||||
|
* @param {import('electron').IpcMain} ipcMain - IPC主进程实例
|
||||||
|
*/
|
||||||
|
export function initAuthIpc(ipcMain) {
|
||||||
|
// 管理员登录验证
|
||||||
|
ipcMain.handle('admin-login', async (event, credentials) => {
|
||||||
|
return await verifyAdminPassword(credentials.password);
|
||||||
|
});
|
||||||
|
}
|
@ -1,61 +0,0 @@
|
|||||||
// 将 CommonJS 导入改为 ES 模块导入
|
|
||||||
import { getSystemDbPath } from '../db/path.js';
|
|
||||||
import { openDatabase } from '../db/utils.js';
|
|
||||||
|
|
||||||
// 获取系统配置
|
|
||||||
async function getSystemConfig() {
|
|
||||||
try {
|
|
||||||
const systemDb = await openDatabase(getSystemDbPath());
|
|
||||||
const configs = await systemDb.allAsync('SELECT key, value FROM config');
|
|
||||||
// 不要关闭连接,由连接池管理
|
|
||||||
|
|
||||||
const configMap = {};
|
|
||||||
configs.forEach(config => {
|
|
||||||
configMap[config.key] = config.value;
|
|
||||||
});
|
|
||||||
return configMap;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Get system config failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新系统配置
|
|
||||||
async function updateSystemConfig(config) {
|
|
||||||
try {
|
|
||||||
const systemDb = await openDatabase(getSystemDbPath());
|
|
||||||
await systemDb.runAsync('BEGIN TRANSACTION');
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(config)) {
|
|
||||||
await systemDb.runAsync('UPDATE config SET value = ? WHERE key = ?', [value, key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
await systemDb.runAsync('COMMIT');
|
|
||||||
// 不要关闭连接,由连接池管理
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
await systemDb.runAsync('ROLLBACK');
|
|
||||||
// 不要关闭连接,由连接池管理
|
|
||||||
console.error('Update system config failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 增加题库版本号
|
|
||||||
async function increaseQuestionBandVersion() {
|
|
||||||
try {
|
|
||||||
const systemDb = await openDatabase(getSystemDbPath());
|
|
||||||
await systemDb.runAsync('UPDATE config SET value = value + 1 WHERE key = ?', ['question_bank_version']);
|
|
||||||
// 不要关闭连接,由连接池管理
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Increase question bank version failed:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
getSystemConfig,
|
|
||||||
updateSystemConfig,
|
|
||||||
increaseQuestionBandVersion
|
|
||||||
};
|
|
52
src/components/admin/AdminLayout.vue
Normal file
52
src/components/admin/AdminLayout.vue
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<template>
|
||||||
|
<div class="d-flex flex-column min-vh-100">
|
||||||
|
<!-- 顶部标题栏 - 水平占满 -->
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<!-- 中间内容区 -->
|
||||||
|
<div class="flex-grow-1 d-flex">
|
||||||
|
<!-- 左侧导航 - 宽度200px,占满标题栏到页脚高度,允许垂直滚动 -->
|
||||||
|
<aside class="overflow-y-auto aside-container">
|
||||||
|
<AdminSider @menu-click="handleMenuClick" />
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- 右侧主内容区 - 占满剩余宽度和高度,与导航栏间隔10px -->
|
||||||
|
<main class="flex-grow-1 overflow-y-auto main-container">
|
||||||
|
<!-- 插槽 - 用于插入每个页面特定的内容 -->
|
||||||
|
<slot></slot>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<!-- 底部页脚 - 水平占满,置底 -->
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineProps, defineEmits } from 'vue'
|
||||||
|
import Header from '../common/Header.vue'
|
||||||
|
import AdminSider from './Sider.vue'
|
||||||
|
import Footer from '../common/Footer.vue'
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['menu-click'])
|
||||||
|
|
||||||
|
// 处理菜单点击事件
|
||||||
|
const handleMenuClick = (menuKey) => {
|
||||||
|
emit('menu-click', menuKey)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 可以添加一些布局相关的样式 */
|
||||||
|
.aside-container {
|
||||||
|
width: 200px;
|
||||||
|
height: calc(100vh - 80px - 60px);
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-right: 1px solid #e8e9ea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-container {
|
||||||
|
height: calc(100vh - 80px - 60px);
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
</style>
|
187
src/components/admin/Sider.vue
Normal file
187
src/components/admin/Sider.vue
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
<template>
|
||||||
|
<el-menu
|
||||||
|
background-color="#f8f9fa"
|
||||||
|
class="el-menu-vertical w-full menu-container"
|
||||||
|
:default-active="currentMenuIndex"
|
||||||
|
text-color="#000"
|
||||||
|
@open="handleOpen"
|
||||||
|
@close="handleClose"
|
||||||
|
>
|
||||||
|
<template v-for="menuItem in menuData" :key="menuItem.index">
|
||||||
|
<el-menu-item
|
||||||
|
v-if="!menuItem.children"
|
||||||
|
:index="menuItem.index"
|
||||||
|
@click="handleMenuClick(menuItem)"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="menuItem.icon" class="me-3" />
|
||||||
|
<span>{{ menuItem.label }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
|
||||||
|
<!-- 如果有子菜单,可以在这里添加 el-sub-menu 的逻辑 -->
|
||||||
|
<!--
|
||||||
|
<el-sub-menu v-else :index="menuItem.index">
|
||||||
|
<template #title>
|
||||||
|
<font-awesome-icon :icon="menuItem.icon" class="me-3" />
|
||||||
|
<span>{{ menuItem.label }}</span>
|
||||||
|
</template>
|
||||||
|
<el-menu-item
|
||||||
|
v-for="child in menuItem.children"
|
||||||
|
:key="child.index"
|
||||||
|
:index="child.index"
|
||||||
|
@click="handleMenuClick(child)"
|
||||||
|
>
|
||||||
|
<span>{{ child.label }}</span>
|
||||||
|
</el-menu-item>
|
||||||
|
</el-sub-menu>
|
||||||
|
-->
|
||||||
|
</template>
|
||||||
|
</el-menu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineEmits, ref, watch, onMounted } from 'vue'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
// 分开导入router和useRoute
|
||||||
|
import router from '@/router/index.js'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { ElMessageBox } from 'element-plus'
|
||||||
|
|
||||||
|
// 定义菜单数据
|
||||||
|
const menuData = [
|
||||||
|
{
|
||||||
|
index: '1',
|
||||||
|
label: '管理员首页',
|
||||||
|
icon: 'home',
|
||||||
|
route: '/admin/home'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '2',
|
||||||
|
label: '配置项管理',
|
||||||
|
icon: 'sliders',
|
||||||
|
route: '/admin/config-management'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '3',
|
||||||
|
label: '字典管理',
|
||||||
|
icon: 'book',
|
||||||
|
route: '/admin/dict-management'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '4',
|
||||||
|
label: '试题管理',
|
||||||
|
icon: 'question-circle',
|
||||||
|
route: '/admin/question-management'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '5',
|
||||||
|
label: '考试管理',
|
||||||
|
icon: 'file-alt',
|
||||||
|
route: '/admin/exam-management'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '6',
|
||||||
|
label: '考生管理',
|
||||||
|
icon: 'users',
|
||||||
|
route: '/admin/student-management'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
index: '7',
|
||||||
|
label: '退出',
|
||||||
|
icon: 'sign-out',
|
||||||
|
action: 'logout'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const emit = defineEmits(['menu-click'])
|
||||||
|
const route = useRoute()
|
||||||
|
// 修改默认值为管理员首页的index
|
||||||
|
const currentMenuIndex = ref('1')
|
||||||
|
|
||||||
|
// 监听路由变化更新当前菜单
|
||||||
|
watch(() => route.path, (newPath) => {
|
||||||
|
const menuItem = menuData.find(item => item.route === newPath)
|
||||||
|
if (menuItem) {
|
||||||
|
currentMenuIndex.value = menuItem.index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 组件挂载时检查当前路由
|
||||||
|
onMounted(() => {
|
||||||
|
const menuItem = menuData.find(item => item.route === route.path)
|
||||||
|
if (menuItem) {
|
||||||
|
currentMenuIndex.value = menuItem.index
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理菜单点击
|
||||||
|
const handleMenuClick = (menuItem) => {
|
||||||
|
if (menuItem.action === 'logout') {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要退出登录吗?',
|
||||||
|
'确认退出',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
).then(() => {
|
||||||
|
router.push('/')
|
||||||
|
}).catch(() => {
|
||||||
|
console.log('取消退出')
|
||||||
|
})
|
||||||
|
} else if (menuItem.route) {
|
||||||
|
router.push(menuItem.route)
|
||||||
|
} else {
|
||||||
|
emit('menu-click', menuItem.index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOpen = (key, keyPath) => {
|
||||||
|
console.log('展开菜单:', key, keyPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = (key, keyPath) => {
|
||||||
|
console.log('关闭菜单:', key, keyPath)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.menu-container {
|
||||||
|
margin-top: 3rem;
|
||||||
|
}
|
||||||
|
/* 确保菜单占满侧边栏宽度 */
|
||||||
|
.el-menu-vertical {
|
||||||
|
width: 100%;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义子菜单样式 */
|
||||||
|
.el-sub-menu .el-menu {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整菜单项样式 */
|
||||||
|
.el-menu-item {
|
||||||
|
height: 50px;
|
||||||
|
line-height: 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item),
|
||||||
|
:deep(.el-sub-menu__title),
|
||||||
|
:deep(.el-sub-menu),
|
||||||
|
:deep(.el-sub-menu__item),
|
||||||
|
:deep(.el-menu-item-group__title) {
|
||||||
|
font-size: 16px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active),
|
||||||
|
:deep(.el-sub-menu__title.is-active) {
|
||||||
|
font-weight: bolder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 调整图标位置 */
|
||||||
|
.el-menu-item i,
|
||||||
|
.el-sub-menu__title i {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-footer class="footer-container">
|
<footer class="w-100 py-3 mt-auto footer-container">
|
||||||
<el-row type="flex" justify="space-between" align="middle" height="100%">
|
<el-row type="flex" justify="space-between" align="middle" height="100%">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<p>© {{ thisYear }} 统计技能考试系统 - 版权所有</p>
|
<p>© {{ thisYear }} 统计技能考试系统 - 版权所有</p>
|
||||||
@ -8,7 +8,7 @@
|
|||||||
<p style="text-align: right !important;">题库版本:{{ questionBankVersion || '未知' }}</p>
|
<p style="text-align: right !important;">题库版本:{{ questionBankVersion || '未知' }}</p>
|
||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
</el-footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@ -18,11 +18,13 @@
|
|||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
border-top: 1px solid #e9ecef;
|
border-top: 1px solid #e9ecef;
|
||||||
height: 60px; /* 固定高度 */
|
height: 60px;
|
||||||
|
/* 固定高度 */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-left, .text-right {
|
.text-left,
|
||||||
|
.text-right {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
@ -46,7 +48,7 @@ onMounted(async () => {
|
|||||||
try {
|
try {
|
||||||
// 调用electronAPI获取系统配置
|
// 调用electronAPI获取系统配置
|
||||||
const config = await window.electronAPI.getSystemConfig()
|
const config = await window.electronAPI.getSystemConfig()
|
||||||
console.log(config)
|
// console.log(config)
|
||||||
// 设置题库版本
|
// 设置题库版本
|
||||||
questionBankVersion.value = config.question_bank_version || '未知'
|
questionBankVersion.value = config.question_bank_version || '未知'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -1,41 +1,29 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-header class="header-container">
|
<header class="header-container text-white shadow-md">
|
||||||
<el-row type="flex" justify="center" height="100%">
|
<div class="h-100">
|
||||||
<el-col>
|
<div class="row h-100 align-items-center justify-content-center">
|
||||||
<h1 class="display-5">
|
<div class="col-auto">
|
||||||
<font-awesome-icon icon="graduation-cap" class="me-3" />
|
<h2 class="display-6 m-0 d-flex align-items-center">
|
||||||
统计技能考试系统
|
<font-awesome-icon icon="graduation-cap" class="me-3" />
|
||||||
</h1>
|
统计技能考试系统
|
||||||
</el-col>
|
</h2>
|
||||||
</el-row>
|
</div>
|
||||||
</el-header>
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.header-container {
|
.header-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #1e40af; /* 深蓝色背景 */
|
background-color: #1e40af; /* 深蓝色背景 */
|
||||||
color: white;
|
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
height: 80px; /* 设置固定高度 */
|
height: 80px; /* 设置固定高度 */
|
||||||
padding: 1.5rem 0;
|
padding: 0 2rem;
|
||||||
}
|
box-sizing: border-box;
|
||||||
|
|
||||||
h1.display-5 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.me-3 {
|
|
||||||
margin-right: 0.75rem;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ElHeader, ElRow, ElCol } from 'element-plus'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
</script>
|
</script>
|
@ -1,7 +1,12 @@
|
|||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import WelcomeView from '../views/WelcomeView.vue'
|
import WelcomeView from '@/views/WelcomeView.vue'
|
||||||
import StudentHomeView from '../views/StudentHomeView.vue'
|
import StudentHomeView from '@/views/user/StudentHomeView.vue'
|
||||||
import AdminHomeView from '../views/AdminHomeView.vue'
|
import AdminHomeView from '@/views/admin/AdminHomeView.vue'
|
||||||
|
// 导入QuestionManagementView
|
||||||
|
import QuestionManagementView from '@/views/admin/QuestionManagementView.vue'
|
||||||
|
// 导入ConfigManagementView
|
||||||
|
import ConfigManagementView from '@/views/admin/ConfigManagementView.vue'
|
||||||
|
|
||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
@ -15,11 +20,24 @@ const router = createRouter({
|
|||||||
name: 'student-home',
|
name: 'student-home',
|
||||||
component: StudentHomeView,
|
component: StudentHomeView,
|
||||||
},
|
},
|
||||||
|
// admin/AdminHomeView路由
|
||||||
{
|
{
|
||||||
path: '/admin-home',
|
path: '/admin/home',
|
||||||
name: 'admin-home',
|
name: 'admin-home',
|
||||||
component: AdminHomeView,
|
component: AdminHomeView,
|
||||||
},
|
},
|
||||||
|
// admin/QuestionManagementView
|
||||||
|
{
|
||||||
|
path: '/admin/question-management',
|
||||||
|
name: 'admin-question-management',
|
||||||
|
component: QuestionManagementView,
|
||||||
|
},
|
||||||
|
// 添加ConfigView路由
|
||||||
|
{
|
||||||
|
path: '/admin/config-management',
|
||||||
|
name: 'admin-config-management',
|
||||||
|
component: ConfigManagementView,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="min-h-screen flex flex-col bg-gray-50">
|
|
||||||
<!-- 顶部标题栏 -->
|
|
||||||
<header class="bg-primary text-white py-4 px-6 shadow-md flex justify-between items-center">
|
|
||||||
<div class="container mx-auto flex justify-between items-center">
|
|
||||||
<h1 class="text-2xl font-bold flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-cog" /> 考试系统 - 管理中心
|
|
||||||
</h1>
|
|
||||||
<el-button type="danger" size="small" @click="logout" class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-sign-out-alt" /> 退出
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- 中间内容区 -->
|
|
||||||
<main class="flex-grow flex h-[calc(100vh-120px)]">
|
|
||||||
<!-- 左侧导航菜单 -->
|
|
||||||
<aside class="w-64 bg-gray-800 text-white p-4 overflow-y-auto flex-shrink-0">
|
|
||||||
<nav class="space-y-2">
|
|
||||||
<el-menu :default-openeds="['1']" class="bg-transparent border-0 text-white" active-text-color="#4096ff">
|
|
||||||
<el-menu-item index="1-1" class="text-white">
|
|
||||||
<template #title>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-home" /> 管理员首页
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-sub-menu index="2">
|
|
||||||
<template #title>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-cog" /> 系统管理
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
<el-menu-item index="2-1" class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-sliders" /> 配置项管理
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="2-2" class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-book" /> 字典管理
|
|
||||||
</el-menu-item>
|
|
||||||
</el-sub-menu>
|
|
||||||
<el-menu-item index="3" class="text-white">
|
|
||||||
<template #title>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-file-question" /> 试题管理
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="4" class="text-white">
|
|
||||||
<template #title>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-calendar-check" /> 考试管理
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item index="5" class="text-white">
|
|
||||||
<template #title>
|
|
||||||
<span class="flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-users" /> 考生管理
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<!-- 右侧内容区域 -->
|
|
||||||
<div class="flex-grow p-6 overflow-auto bg-gray-50">
|
|
||||||
<div class="bg-white rounded-lg shadow-md p-6 min-h-full">
|
|
||||||
<h2 class="text-xl font-semibold text-gray-800 mb-6 flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-home" /> 管理员首页
|
|
||||||
</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
|
||||||
<div class="bg-blue-50 p-4 rounded-lg border border-blue-100 hover:shadow-md transition-shadow duration-300">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h3 class="text-lg font-medium text-blue-700">总试题数</h3>
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-file-question" class="text-blue-500" />
|
|
||||||
</div>
|
|
||||||
<p class="text-3xl font-bold text-blue-900 mt-2">120</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-green-50 p-4 rounded-lg border border-green-100 hover:shadow-md transition-shadow duration-300">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h3 class="text-lg font-medium text-green-700">总考试数</h3>
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-calendar-check" class="text-green-500" />
|
|
||||||
</div>
|
|
||||||
<p class="text-3xl font-bold text-green-900 mt-2">8</p>
|
|
||||||
</div>
|
|
||||||
<div class="bg-purple-50 p-4 rounded-lg border border-purple-100 hover:shadow-md transition-shadow duration-300">
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<h3 class="text-lg font-medium text-purple-700">总考生数</h3>
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-users" class="text-purple-500" />
|
|
||||||
</div>
|
|
||||||
<p class="text-3xl font-bold text-purple-900 mt-2">156</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="bg-gray-50 p-4 rounded-lg border border-gray-100">
|
|
||||||
<h3 class="text-lg font-medium text-gray-700 mb-4 flex items-center gap-2">
|
|
||||||
<FontAwesomeIcon icon="fa-solid fa-history" /> 最近考试
|
|
||||||
</h3>
|
|
||||||
<div class="overflow-x-auto">
|
|
||||||
<el-table :data="recentExams" style="width: 100%">
|
|
||||||
<el-table-column prop="name" label="考试名称" width="200" />
|
|
||||||
<el-table-column prop="startTime" label="开始时间" width="180" />
|
|
||||||
<el-table-column prop="endTime" label="结束时间" width="180" />
|
|
||||||
<el-table-column prop="status" label="状态" width="100">
|
|
||||||
<template #default="scope">
|
|
||||||
<span :class="{
|
|
||||||
'text-green-600': scope.row.status === '已完成',
|
|
||||||
'text-blue-600': scope.row.status === '进行中',
|
|
||||||
'text-gray-600': scope.row.status === '未开始'
|
|
||||||
}">
|
|
||||||
{{ scope.row.status }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- 底部页脚 -->
|
|
||||||
<footer class="bg-gray-800 text-white py-4 px-6 text-center text-sm">
|
|
||||||
<div class="container mx-auto">
|
|
||||||
<p>© 2023 考试系统. 版权所有. 版本 v1.0.0</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElButton, ElMenu, ElMenuItem, ElSubMenu, ElTable, ElTableColumn } from 'element-plus'
|
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const logout = () => {
|
|
||||||
router.push('/')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 模拟最近考试数据
|
|
||||||
const recentExams = [
|
|
||||||
{
|
|
||||||
name: '期末考试',
|
|
||||||
startTime: '2023-06-20 09:00',
|
|
||||||
endTime: '2023-06-20 11:00',
|
|
||||||
status: '已完成'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '期中考试',
|
|
||||||
startTime: '2023-04-15 14:00',
|
|
||||||
endTime: '2023-04-15 16:00',
|
|
||||||
status: '已完成'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '月考',
|
|
||||||
startTime: '2023-05-10 10:00',
|
|
||||||
endTime: '2023-05-10 11:30',
|
|
||||||
status: '已完成'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* 使用Bootstrap样式并自定义 */
|
|
||||||
@import 'bootstrap/dist/css/bootstrap.min.css';
|
|
||||||
|
|
||||||
/* 自定义样式 */
|
|
||||||
.bg-primary {
|
|
||||||
background-color: var(--primary-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu {
|
|
||||||
--el-menu-bg-color: transparent !important;
|
|
||||||
--el-menu-text-color: #ffffff !important;
|
|
||||||
--el-menu-hover-bg-color: rgba(255, 255, 255, 0.1) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-menu-item {
|
|
||||||
--el-menu-item-height: 40px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-sub-menu__title {
|
|
||||||
--el-submenu-title-height: 40px !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
@ -210,7 +210,7 @@ const handleAdminLogin = async () => {
|
|||||||
if (result && result.success) {
|
if (result && result.success) {
|
||||||
console.log('管理员登录 - 成功,跳转到管理首页');
|
console.log('管理员登录 - 成功,跳转到管理首页');
|
||||||
ElMessage.success('登录成功');
|
ElMessage.success('登录成功');
|
||||||
router.push('/admin-home');
|
router.push('/admin/home');
|
||||||
} else {
|
} else {
|
||||||
const errorMessage = result && result.message ? result.message : '登录失败';
|
const errorMessage = result && result.message ? result.message : '登录失败';
|
||||||
console.warn('管理员登录 - 失败:', errorMessage);
|
console.warn('管理员登录 - 失败:', errorMessage);
|
||||||
|
50
src/views/admin/AdminHomeView.vue
Normal file
50
src/views/admin/AdminHomeView.vue
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<AdminLayout @menu-click="handleMenuClick">
|
||||||
|
<!-- 这里是首页特定的内容 -->
|
||||||
|
<div class="container mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">管理员首页</h1>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||||
|
<div class="bg-blue-50 p-4 rounded-lg border border-blue-100 hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h3 class="text-lg font-medium text-blue-700">总试题数</h3>
|
||||||
|
<font-awesome-icon icon="newspaper" class="text-blue-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-3xl font-bold text-blue-900 mt-2">120</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-green-50 p-4 rounded-lg border border-green-100 hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h3 class="text-lg font-medium text-green-700">总考试数</h3>
|
||||||
|
<font-awesome-icon icon="calendar-check" class="text-green-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-3xl font-bold text-green-900 mt-2">8</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-purple-50 p-4 rounded-lg border border-purple-100 hover:shadow-md transition-shadow duration-300">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<h3 class="text-lg font-medium text-purple-700">总考生数</h3>
|
||||||
|
<font-awesome-icon icon="users" class="text-purple-500" />
|
||||||
|
</div>
|
||||||
|
<p class="text-3xl font-bold text-purple-900 mt-2">156</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineComponent, ref } from 'vue'
|
||||||
|
import AdminLayout from '@/components/admin/AdminLayout.vue'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'
|
||||||
|
|
||||||
|
// 示例数据
|
||||||
|
const recentExams = ref([])
|
||||||
|
|
||||||
|
// 处理菜单点击事件
|
||||||
|
const handleMenuClick = (menuKey) => {
|
||||||
|
console.log('Menu clicked:', menuKey)
|
||||||
|
// 这里可以添加路由跳转逻辑
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 可以添加一些页面特定的样式 */
|
||||||
|
</style>
|
205
src/views/admin/ConfigManagementView.vue
Normal file
205
src/views/admin/ConfigManagementView.vue
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
<template>
|
||||||
|
<AdminLayout>
|
||||||
|
<div class="config-management-container">
|
||||||
|
<div class="config-header">
|
||||||
|
<h1>配置项管理</h1>
|
||||||
|
<el-button type="primary" @click="handleAddConfig">+ 添加配置项</el-button>
|
||||||
|
</div>
|
||||||
|
<div class="config-content">
|
||||||
|
<el-table :data="configList" style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column prop="key" label="配置键" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="ellipsis-cell" :title="scope.row.key">{{ scope.row.key }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="value" label="配置值">
|
||||||
|
<template #default="scope">
|
||||||
|
<div class="ellipsis-cell" :title="scope.row.value">{{ scope.row.value }}</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="protected" label="受保护的" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<span>{{ scope.row.protected === 1 ? '是' : '否' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="180" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
@click="handleEditConfig(scope.row)"
|
||||||
|
:disabled="scope.row.protected === 1"
|
||||||
|
>
|
||||||
|
编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
size="small"
|
||||||
|
@click="handleDeleteConfig(scope.row.id)"
|
||||||
|
:disabled="scope.row.protected === 1"
|
||||||
|
>
|
||||||
|
删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 添加/编辑配置项弹窗 -->
|
||||||
|
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="500px">
|
||||||
|
<el-form :model="formData" label-width="100px">
|
||||||
|
<el-form-item label="配置键" prop="key" :rules="[{ required: true, message: '请输入配置键', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="formData.key" :disabled="isEdit" placeholder="请输入配置键" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="配置值" prop="value" :rules="[{ required: true, message: '请输入配置值', trigger: 'blur' }]">
|
||||||
|
<el-input v-model="formData.value" type="textarea" placeholder="请输入配置值" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSaveConfig">保存</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 configList = ref([])
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const dialogTitle = ref('添加配置项')
|
||||||
|
const formData = ref({
|
||||||
|
id: null,
|
||||||
|
key: '',
|
||||||
|
value: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 加载配置项列表
|
||||||
|
const loadConfigList = async () => {
|
||||||
|
try {
|
||||||
|
const data = await window.electronAPI.fetchAllConfigs()
|
||||||
|
configList.value = data
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取配置项失败: ' + error.message)
|
||||||
|
console.error('获取配置项失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加配置项
|
||||||
|
const handleAddConfig = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
dialogTitle.value = '添加配置项'
|
||||||
|
formData.value = {
|
||||||
|
id: null,
|
||||||
|
key: '',
|
||||||
|
value: ''
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑配置项
|
||||||
|
const handleEditConfig = async (row) => {
|
||||||
|
try {
|
||||||
|
const data = await window.electronAPI.fetchConfigById(row.id)
|
||||||
|
if (data) {
|
||||||
|
isEdit.value = true
|
||||||
|
dialogTitle.value = '编辑配置项'
|
||||||
|
formData.value = {
|
||||||
|
id: data.id,
|
||||||
|
key: data.key,
|
||||||
|
value: data.value
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
} else {
|
||||||
|
ElMessage.error('未找到该配置项')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取配置项详情失败: ' + error.message)
|
||||||
|
console.error('获取配置项详情失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存配置项
|
||||||
|
const handleSaveConfig = async () => {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.saveConfig(formData.value.key, formData.value.value)
|
||||||
|
ElMessage.success(isEdit.value ? '配置项更新成功' : '配置项添加成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
loadConfigList() // 重新加载列表
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('保存配置项失败: ' + error.message)
|
||||||
|
console.error('保存配置项失败', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除配置项
|
||||||
|
const handleDeleteConfig = (id) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
'确定要删除该配置项吗?',
|
||||||
|
'确认删除',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
try {
|
||||||
|
await window.electronAPI.deleteConfig(id)
|
||||||
|
ElMessage.success('配置项删除成功')
|
||||||
|
loadConfigList() // 重新加载列表
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除配置项失败: ' + error.message)
|
||||||
|
console.error('删除配置项失败', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消删除
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件挂载时加载数据
|
||||||
|
onMounted(() => {
|
||||||
|
loadConfigList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.config-management-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-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>
|
24
src/views/admin/QuestionManagementView.vue
Normal file
24
src/views/admin/QuestionManagementView.vue
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<AdminLayout @menu-click="handleMenuClick">
|
||||||
|
<!-- 这里是试题管理页面特定的内容 -->
|
||||||
|
<div class="container mx-auto py-6">
|
||||||
|
<h1 class="text-2xl font-bold mb-6">试题管理</h1>
|
||||||
|
<!-- 试题管理相关的内容 -->
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import AdminLayout from '@/components/admin/AdminLayout.vue'
|
||||||
|
|
||||||
|
// 处理菜单点击事件
|
||||||
|
const handleMenuClick = (menuKey) => {
|
||||||
|
console.log('Menu clicked:', menuKey)
|
||||||
|
// 这里可以添加路由跳转逻辑
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* 可以添加一些页面特定的样式 */
|
||||||
|
</style>
|
Loading…
Reference in New Issue
Block a user