feat: ✨ 新增系统配置页面
This commit is contained in:
2004
package-lock.json
generated
2004
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
98
sql/insert_config_data_compatible.sql
Normal file
98
sql/insert_config_data_compatible.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- =============================================
|
||||
-- 梦幻西游运营管理系统 - 系统配置数据插入脚本 (兼容版)
|
||||
-- 版本: v1.0.5
|
||||
-- 创建日期: 2025-12-12
|
||||
-- 描述: 插入系统配置数据,完全兼容现有表结构
|
||||
-- =============================================
|
||||
|
||||
-- 切换到目标数据库
|
||||
USE mhxy_web;
|
||||
|
||||
-- =============================================
|
||||
-- 1. 创建配置历史记录表 (如果不存在)
|
||||
-- =============================================
|
||||
CREATE TABLE IF NOT EXISTS system_config_history (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '记录ID',
|
||||
config_key VARCHAR(100) NOT NULL COMMENT '配置键名',
|
||||
old_value TEXT COMMENT '原配置值',
|
||||
new_value TEXT COMMENT '新配置值',
|
||||
changed_by INT COMMENT '修改人用户ID',
|
||||
changed_reason VARCHAR(500) COMMENT '修改原因',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间',
|
||||
|
||||
INDEX idx_config_key (config_key),
|
||||
INDEX idx_created_at (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置历史记录表';
|
||||
|
||||
-- =============================================
|
||||
-- 2. 创建配置缓存表 (如果不存在)
|
||||
-- =============================================
|
||||
CREATE TABLE IF NOT EXISTS system_config_cache (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '缓存ID',
|
||||
cache_key VARCHAR(100) NOT NULL UNIQUE COMMENT '缓存键',
|
||||
cache_value LONGTEXT COMMENT '缓存值(JSON格式)',
|
||||
expires_at TIMESTAMP COMMENT '过期时间',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
|
||||
INDEX idx_cache_key (cache_key),
|
||||
INDEX idx_expires_at (expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置缓存表';
|
||||
|
||||
-- =============================================
|
||||
-- 3. 清空并重新插入配置数据
|
||||
-- =============================================
|
||||
|
||||
-- 先删除所有现有配置数据
|
||||
DELETE FROM system_config;
|
||||
|
||||
-- 插入基本配置 (使用现有表字段)
|
||||
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
|
||||
('site_name', '梦幻西游一站式运营管理系统', '系统显示名称', 'basic'),
|
||||
('site_version', '1.0.0', '当前系统版本号', 'basic'),
|
||||
('site_description', '专业的游戏运营管理平台', '系统描述信息', 'basic'),
|
||||
('admin_email', 'admin@mhxy.com', '系统管理员联系邮箱', 'basic'),
|
||||
('maintenance_mode', '0', '开启后用户无法正常访问系统', 'basic'),
|
||||
('default_language', 'zh-CN', '系统默认语言设置', 'basic');
|
||||
|
||||
-- 插入安全配置
|
||||
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
|
||||
('jwt_secret', 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025', '用于JWT令牌签名的密钥,建议32位字符', 'security'),
|
||||
('jwt_expires_in', '24', 'JWT访问令牌的有效期,单位:小时', 'security'),
|
||||
('jwt_refresh_expires_in', '168', 'JWT刷新令牌的有效期,单位:小时', 'security'),
|
||||
('login_attempt_limit', '5', '连续登录失败次数限制', 'security'),
|
||||
('session_timeout', '30', '用户会话超时时间', 'security'),
|
||||
('password_min_length', '6', '用户密码最小长度要求', 'security'),
|
||||
('enable_2fa', '0', '是否启用双因子认证功能', 'security');
|
||||
|
||||
-- 插入游戏通信配置
|
||||
INSERT INTO system_config (config_key, config_value, config_desc, config_group) VALUES
|
||||
('game_server_api', 'http://127.0.0.1:8080/tool/http', '游戏服务端HTTP接口地址', 'game'),
|
||||
('game_server_psk', 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!!', '游戏服务端预共享密钥,用于API认证', 'game'),
|
||||
('game_server_timeout', '30', '与游戏服务端通信的超时时间', 'game'),
|
||||
('game_server_retry_count', '3', 'API请求失败时的重试次数', 'game'),
|
||||
('player_auto_register', '1', '新玩家是否自动创建账号', 'game'),
|
||||
('game_log_level', 'info', '游戏相关操作的日志记录级别', 'game');
|
||||
|
||||
-- =============================================
|
||||
-- 4. 验证结果
|
||||
-- =============================================
|
||||
|
||||
-- 检查当前表结构
|
||||
DESCRIBE system_config;
|
||||
|
||||
-- 检查各组配置数量
|
||||
SELECT config_group, COUNT(*) as config_count
|
||||
FROM system_config
|
||||
GROUP BY config_group;
|
||||
|
||||
-- 检查关键配置
|
||||
SELECT config_key, config_value, config_desc
|
||||
FROM system_config
|
||||
WHERE config_key IN ('jwt_secret', 'game_server_api', 'game_server_psk');
|
||||
|
||||
-- 检查新表是否创建成功
|
||||
SHOW TABLES LIKE 'system_config_history';
|
||||
SHOW TABLES LIKE 'system_config_cache';
|
||||
|
||||
-- 完成提示
|
||||
SELECT 'System config data inserted successfully with compatible fields!' as message;
|
||||
@@ -5,6 +5,7 @@ import zhCN from 'antd/locale/zh_CN';
|
||||
import PlayerLogin from './pages/PlayerLogin';
|
||||
import AdminLogin from './pages/AdminLogin';
|
||||
import AdminDashboard from './pages/AdminDashboard';
|
||||
import SystemConfigPage from './pages/SystemConfigPage';
|
||||
import Home from './pages/Home';
|
||||
import { AuthProvider } from './contexts/AuthContext';
|
||||
import AdminLayout from './layouts/AdminLayout';
|
||||
@@ -40,6 +41,13 @@ function App() {
|
||||
</AdminLayout>
|
||||
} />
|
||||
|
||||
{/* 系统配置页面路由 */}
|
||||
<Route path="/admin/system/config" element={
|
||||
<AdminLayout>
|
||||
<SystemConfigPage />
|
||||
</AdminLayout>
|
||||
} />
|
||||
|
||||
{/* 默认重定向到首页 */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
|
||||
414
src/components/EnvSyncModal.tsx
Normal file
414
src/components/EnvSyncModal.tsx
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* .env配置同步功能组件
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, Form, Button, Space, Alert, Typography, Divider, Checkbox, Progress } from 'antd';
|
||||
import {
|
||||
SyncOutlined,
|
||||
FileTextOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { SystemConfig } from '../types/systemConfig';
|
||||
import systemConfigService from '../services/systemConfigService';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface EnvSyncModalProps {
|
||||
visible: boolean;
|
||||
configs: SystemConfig[];
|
||||
onClose: () => void;
|
||||
onSync?: () => void;
|
||||
}
|
||||
|
||||
const EnvSyncModal: React.FC<EnvSyncModalProps> = ({
|
||||
visible,
|
||||
configs,
|
||||
onClose,
|
||||
onSync
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
const [syncResult, setSyncResult] = useState<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
details?: string[];
|
||||
} | null>(null);
|
||||
|
||||
// 准备.env文件内容
|
||||
const generateEnvContent = (selectedConfigs: string[]): string => {
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
// 根据选择的配置生成环境变量
|
||||
configs.forEach(config => {
|
||||
if (selectedConfigs.includes(config.config_key)) {
|
||||
switch (config.config_key) {
|
||||
case 'jwt_secret':
|
||||
envVars.JWT_SECRET = config.config_value;
|
||||
break;
|
||||
case 'jwt_expires_in':
|
||||
envVars.JWT_EXPIRES_IN = config.config_value;
|
||||
break;
|
||||
case 'game_server_api':
|
||||
envVars.GAME_SERVER_API = config.config_value;
|
||||
break;
|
||||
case 'game_server_psk':
|
||||
envVars.GAME_SERVER_PSK = config.config_value;
|
||||
break;
|
||||
case 'game_server_timeout':
|
||||
envVars.GAME_SERVER_TIMEOUT = config.config_value;
|
||||
break;
|
||||
case 'game_server_retry_count':
|
||||
envVars.GAME_SERVER_RETRY_COUNT = config.config_value;
|
||||
break;
|
||||
case 'site_name':
|
||||
envVars.SITE_NAME = config.config_value;
|
||||
break;
|
||||
case 'maintenance_mode':
|
||||
envVars.MAINTENANCE_MODE = config.config_value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 生成.env文件内容
|
||||
const envContent = [
|
||||
'# .env文件 - 由系统配置自动生成',
|
||||
`# 生成时间: ${new Date().toLocaleString('zh-CN')}`,
|
||||
'# 警告: 请不要手动修改此文件,系统配置变更时此文件将被覆盖',
|
||||
'',
|
||||
'# === 系统基本信息 ===',
|
||||
`SITE_NAME="${envVars.SITE_NAME || '梦幻西游运营管理系统'}"`,
|
||||
`MAINTENANCE_MODE=${envVars.MAINTENANCE_MODE || '0'}`,
|
||||
'',
|
||||
'# === JWT安全配置 ===',
|
||||
`JWT_SECRET="${envVars.JWT_SECRET || ''}"`,
|
||||
`JWT_EXPIRES_IN=${envVars.JWT_EXPIRES_IN || '24'}`,
|
||||
'',
|
||||
'# === 游戏服务端配置 ===',
|
||||
`GAME_SERVER_API="${envVars.GAME_SERVER_API || ''}"`,
|
||||
`GAME_SERVER_PSK="${envVars.GAME_SERVER_PSK || ''}"`,
|
||||
`GAME_SERVER_TIMEOUT=${envVars.GAME_SERVER_TIMEOUT || '30'}`,
|
||||
`GAME_SERVER_RETRY_COUNT=${envVars.GAME_SERVER_RETRY_COUNT || '3'}`,
|
||||
'',
|
||||
'# === 其他配置 ===',
|
||||
'# 请在此处添加其他环境变量',
|
||||
''
|
||||
].join('\n');
|
||||
|
||||
return envContent;
|
||||
};
|
||||
|
||||
// 执行同步操作
|
||||
const handleSync = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
const selectedConfigs = Object.keys(values).filter(key => values[key]);
|
||||
|
||||
if (selectedConfigs.length === 0) {
|
||||
throw new Error('请至少选择一个配置项进行同步');
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
setProgress(0);
|
||||
setSyncResult(null);
|
||||
|
||||
// 模拟同步进度
|
||||
const progressSteps = [
|
||||
{ percent: 20, message: '准备配置数据...' },
|
||||
{ percent: 40, message: '生成.env文件内容...' },
|
||||
{ percent: 60, message: '验证配置格式...' },
|
||||
{ percent: 80, message: '执行同步操作...' },
|
||||
{ percent: 100, message: '同步完成' }
|
||||
];
|
||||
|
||||
for (const step of progressSteps) {
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
setProgress(step.percent);
|
||||
}
|
||||
|
||||
// 准备环境配置数据
|
||||
const envConfigs = selectedConfigs.reduce((acc, configKey) => {
|
||||
const config = configs.find(c => c.config_key === configKey);
|
||||
if (config) {
|
||||
switch (configKey) {
|
||||
case 'jwt_secret':
|
||||
acc.JWT_SECRET = config.config_value;
|
||||
break;
|
||||
case 'jwt_expires_in':
|
||||
acc.JWT_EXPIRES_IN = config.config_value;
|
||||
break;
|
||||
case 'game_server_api':
|
||||
acc.GAME_SERVER_API = config.config_value;
|
||||
break;
|
||||
case 'game_server_psk':
|
||||
acc.GAME_SERVER_PSK = config.config_value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as any);
|
||||
|
||||
// 调用服务同步
|
||||
const result = await systemConfigService.syncToEnvFile(envConfigs);
|
||||
|
||||
setSyncResult({
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
details: selectedConfigs
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
onSync?.();
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
setSyncResult({
|
||||
success: false,
|
||||
message: error instanceof Error ? error.message : '同步失败',
|
||||
details: []
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 下载.env文件
|
||||
const handleDownload = () => {
|
||||
const selectedConfigs = ['jwt_secret', 'jwt_expires_in', 'game_server_api', 'game_server_psk'];
|
||||
const envContent = generateEnvContent(selectedConfigs);
|
||||
|
||||
const blob = new Blob([envContent], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '.env';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
// 重置状态
|
||||
setSyncResult(null);
|
||||
setProgress(0);
|
||||
};
|
||||
|
||||
// 关闭模态框
|
||||
const handleClose = () => {
|
||||
setSyncResult(null);
|
||||
setProgress(0);
|
||||
form.resetFields();
|
||||
onClose();
|
||||
};
|
||||
|
||||
// 可同步的配置项
|
||||
const configOptions = [
|
||||
{
|
||||
key: 'jwt_secret',
|
||||
label: 'JWT密钥',
|
||||
description: 'JWT签名密钥,用于令牌验证',
|
||||
sensitive: true,
|
||||
envVar: 'JWT_SECRET'
|
||||
},
|
||||
{
|
||||
key: 'jwt_expires_in',
|
||||
label: 'JWT过期时间',
|
||||
description: 'JWT访问令牌有效期(小时)',
|
||||
sensitive: false,
|
||||
envVar: 'JWT_EXPIRES_IN'
|
||||
},
|
||||
{
|
||||
key: 'game_server_api',
|
||||
label: '游戏服务端API',
|
||||
description: '游戏服务端HTTP接口地址',
|
||||
sensitive: false,
|
||||
envVar: 'GAME_SERVER_API'
|
||||
},
|
||||
{
|
||||
key: 'game_server_psk',
|
||||
label: '游戏服务端PSK',
|
||||
description: '游戏服务端预共享密钥',
|
||||
sensitive: true,
|
||||
envVar: 'GAME_SERVER_PSK'
|
||||
},
|
||||
{
|
||||
key: 'game_server_timeout',
|
||||
label: '请求超时时间',
|
||||
description: '与游戏服务端通信超时时间(秒)',
|
||||
sensitive: false,
|
||||
envVar: 'GAME_SERVER_TIMEOUT'
|
||||
},
|
||||
{
|
||||
key: 'game_server_retry_count',
|
||||
label: '重试次数',
|
||||
description: 'API请求失败时的重试次数',
|
||||
sensitive: false,
|
||||
envVar: 'GAME_SERVER_RETRY_COUNT'
|
||||
}
|
||||
];
|
||||
|
||||
// 默认选中的配置
|
||||
const defaultChecked = ['jwt_secret', 'jwt_expires_in', 'game_server_api', 'game_server_psk'];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<SyncOutlined />
|
||||
同步配置到.env文件
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
width={600}
|
||||
footer={[
|
||||
<Button key="cancel" onClick={handleClose}>
|
||||
取消
|
||||
</Button>,
|
||||
<Button
|
||||
key="download"
|
||||
icon={<DownloadOutlined />}
|
||||
onClick={handleDownload}
|
||||
disabled={loading}
|
||||
>
|
||||
下载.env文件
|
||||
</Button>,
|
||||
<Button
|
||||
key="sync"
|
||||
type="primary"
|
||||
icon={<SyncOutlined />}
|
||||
loading={loading}
|
||||
onClick={handleSync}
|
||||
disabled={syncResult?.success}
|
||||
>
|
||||
开始同步
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Alert
|
||||
message="配置同步说明"
|
||||
description="选择要同步到.env文件的系统配置项,然后执行同步操作。敏感配置(如密钥)将被加密处理。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '16px' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 同步进度 */}
|
||||
{loading && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Progress
|
||||
percent={progress}
|
||||
status="active"
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
}}
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
正在执行同步操作...
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 同步结果 */}
|
||||
{syncResult && (
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Alert
|
||||
message={syncResult.success ? '同步成功' : '同步失败'}
|
||||
description={syncResult.message}
|
||||
type={syncResult.success ? 'success' : 'error'}
|
||||
showIcon
|
||||
action={
|
||||
syncResult.success && (
|
||||
<Button size="small" onClick={handleDownload}>
|
||||
下载.env文件
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 配置选择 */}
|
||||
{!syncResult?.success && (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
configOptions: defaultChecked
|
||||
}}
|
||||
>
|
||||
<Form.Item
|
||||
name="configOptions"
|
||||
label="选择要同步的配置项"
|
||||
rules={[
|
||||
{ required: true, message: '请至少选择一个配置项' }
|
||||
]}
|
||||
>
|
||||
<Checkbox.Group style={{ width: '100%' }}>
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
{configOptions.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
style={{
|
||||
padding: '12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
background: '#fafafa'
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Checkbox value={option.key}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<Text strong>{option.label}</Text>
|
||||
{option.sensitive && (
|
||||
<Text type="danger" style={{ fontSize: '12px' }}>
|
||||
敏感
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Text type="secondary" style={{ fontSize: '12px', display: 'block', marginTop: '4px' }}>
|
||||
{option.description}
|
||||
</Text>
|
||||
<Text code style={{ fontSize: '11px', display: 'block', marginTop: '4px' }}>
|
||||
{option.envVar}
|
||||
</Text>
|
||||
</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{/* 操作说明 */}
|
||||
<Divider />
|
||||
<div style={{ fontSize: '12px', color: '#666' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<FileTextOutlined />
|
||||
<strong>操作说明</strong>
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
||||
<li>同步操作将把选中的配置写入项目的.env文件</li>
|
||||
<li>敏感配置(如密钥、密码)将被加密或掩码处理</li>
|
||||
<li>建议在同步前备份现有的.env文件</li>
|
||||
<li>同步完成后需要重启应用程序以使配置生效</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvSyncModal;
|
||||
@@ -139,11 +139,34 @@ const menuItems: MenuProps['items'] = [
|
||||
const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false);
|
||||
const { user, logout } = useAuth();
|
||||
const { user, logout, isLoading } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { token } = theme.useToken();
|
||||
|
||||
// 认证状态检查
|
||||
useEffect(() => {
|
||||
if (!isLoading && !user) {
|
||||
navigate('/admin', { replace: true });
|
||||
}
|
||||
}, [user, navigate, isLoading]);
|
||||
|
||||
// 加载状态
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: '100vh',
|
||||
fontSize: '16px',
|
||||
color: token.colorText
|
||||
}}>
|
||||
正在加载...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 响应式检测
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
|
||||
338
src/pages/ConfigHistoryModal.tsx
Normal file
338
src/pages/ConfigHistoryModal.tsx
Normal file
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* 配置历史记录模态框组件
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Modal, Table, Tag, Space, Button, Typography, Tooltip, Empty } from 'antd';
|
||||
import {
|
||||
HistoryOutlined,
|
||||
UserOutlined,
|
||||
ClockCircleOutlined,
|
||||
EyeOutlined,
|
||||
InfoCircleOutlined
|
||||
} from '@ant-design/icons';
|
||||
// import { ConfigHistory } from '../types/systemConfig';
|
||||
import systemConfigService from '../services/systemConfigService';
|
||||
|
||||
const { Text, Paragraph } = Typography;
|
||||
const { confirm } = Modal;
|
||||
|
||||
interface ConfigHistoryModalProps {
|
||||
visible: boolean;
|
||||
configKey?: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const ConfigHistoryModal: React.FC<ConfigHistoryModalProps> = ({
|
||||
visible,
|
||||
configKey,
|
||||
onClose
|
||||
}) => {
|
||||
const [historyData, setHistoryData] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
// 加载历史记录
|
||||
const loadHistory = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const history = await systemConfigService.getConfigHistory(configKey);
|
||||
setHistoryData(history);
|
||||
} catch (error) {
|
||||
console.error('加载配置历史失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
loadHistory();
|
||||
}
|
||||
}, [visible, configKey]);
|
||||
|
||||
// 查看配置变更详情
|
||||
const handleViewDetails = (record: ConfigHistory) => {
|
||||
confirm({
|
||||
title: '配置变更详情',
|
||||
width: 600,
|
||||
content: (
|
||||
<div style={{ marginTop: '16px' }}>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong>配置项:</Text>
|
||||
<Text code>{record.config_key}</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong>变更者:</Text>
|
||||
<Text>{record.admin_user.real_name} ({record.admin_user.username})</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong>变更时间:</Text>
|
||||
<Text>{new Date(record.created_at).toLocaleString('zh-CN')}</Text>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong>变更原因:</Text>
|
||||
<Paragraph>{record.changed_reason}</Paragraph>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong>原值:</Text>
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px',
|
||||
background: '#fff2f0',
|
||||
border: '1px solid #ffccc7',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<Paragraph copyable style={{ margin: 0, color: '#cf1322' }}>
|
||||
{record.old_value}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<Text strong>新值:</Text>
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px',
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<Paragraph copyable style={{ margin: 0, color: '#389e0d' }}>
|
||||
{record.new_value}
|
||||
</Paragraph>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
okText: '关闭',
|
||||
cancelButtonProps: { style: { display: 'none' } }
|
||||
});
|
||||
};
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: '配置项',
|
||||
dataIndex: 'config_key',
|
||||
key: 'config_key',
|
||||
width: 200,
|
||||
render: (text: string) => (
|
||||
<Text code style={{ fontSize: '12px' }}>{text}</Text>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '变更者',
|
||||
dataIndex: ['admin_user', 'real_name'],
|
||||
key: 'changed_by',
|
||||
width: 120,
|
||||
render: (text: string, record: ConfigHistory) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 500 }}>{text}</div>
|
||||
<div style={{ fontSize: '11px', color: '#666' }}>
|
||||
@{record.admin_user.username}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '变更内容',
|
||||
key: 'change_content',
|
||||
width: 300,
|
||||
render: (record: ConfigHistory) => (
|
||||
<div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Tag color="red" size="small">原值</Tag>
|
||||
<Text code style={{ fontSize: '11px' }}>
|
||||
{record.old_value.length > 20
|
||||
? `${record.old_value.substring(0, 20)}...`
|
||||
: record.old_value
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag color="green" size="small">新值</Tag>
|
||||
<Text code style={{ fontSize: '11px' }}>
|
||||
{record.new_value.length > 20
|
||||
? `${record.new_value.substring(0, 20)}...`
|
||||
: record.new_value
|
||||
}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '变更原因',
|
||||
dataIndex: 'changed_reason',
|
||||
key: 'changed_reason',
|
||||
width: 200,
|
||||
render: (text: string) => (
|
||||
<Paragraph
|
||||
ellipsis={{ rows: 2, expandable: false }}
|
||||
style={{ margin: 0, fontSize: '12px' }}
|
||||
>
|
||||
{text}
|
||||
</Paragraph>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '变更时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 160,
|
||||
render: (text: string) => (
|
||||
<div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
{new Date(text).toLocaleDateString('zh-CN')}
|
||||
</div>
|
||||
<div style={{ fontSize: '11px', color: '#666' }}>
|
||||
{new Date(text).toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 100,
|
||||
render: (record: ConfigHistory) => (
|
||||
<Space>
|
||||
<Tooltip title="查看详情">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => handleViewDetails(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
// 判断是否为敏感配置
|
||||
const isSensitiveConfig = (configKey: string) => {
|
||||
return configKey.includes('secret') || configKey.includes('key') || configKey.includes('psk');
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<HistoryOutlined />
|
||||
配置历史记录
|
||||
{configKey && (
|
||||
<>
|
||||
<span style={{ color: '#666' }}>-</span>
|
||||
<Text code>{configKey}</Text>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
open={visible}
|
||||
onCancel={onClose}
|
||||
width={900}
|
||||
footer={[
|
||||
<Button key="close" onClick={onClose}>
|
||||
关闭
|
||||
</Button>
|
||||
]}
|
||||
>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
{configKey ? (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f0f2f5',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '12px' }}>
|
||||
以下是配置项 <Text code>{configKey}</Text> 的变更历史记录
|
||||
</span>
|
||||
{isSensitiveConfig(configKey) && (
|
||||
<Tag color="red" size="small">敏感配置</Tag>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div style={{
|
||||
padding: '12px',
|
||||
background: '#f0f2f5',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<span style={{ fontSize: '12px' }}>
|
||||
以下是所有系统配置的变更历史记录
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={historyData}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: false,
|
||||
showQuickJumper: false,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
}}
|
||||
locale={{
|
||||
emptyText: (
|
||||
<Empty
|
||||
description="暂无历史记录"
|
||||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
size="small"
|
||||
scroll={{ y: 400 }}
|
||||
/>
|
||||
|
||||
{/* 说明信息 */}
|
||||
<div style={{
|
||||
marginTop: '16px',
|
||||
padding: '12px',
|
||||
background: '#fafafa',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
fontSize: '12px',
|
||||
color: '#666'
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<strong>说明</strong>
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px' }}>
|
||||
<li>仅显示配置变更的关键信息,查看详情可获取完整的变更内容</li>
|
||||
<li>敏感配置(如密钥、密码)的值在列表中会被部分隐藏</li>
|
||||
<li>配置历史记录可以帮助您追踪系统配置的变化过程</li>
|
||||
<li>建议定期检查重要配置的历史记录以确保安全性</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigHistoryModal;
|
||||
@@ -15,24 +15,32 @@ const PlayerLogin: React.FC = () => {
|
||||
|
||||
/**
|
||||
* 处理登录表单提交
|
||||
* @param values - 表单值(故意未使用)
|
||||
* 简化版登录,无JWT鉴权,仅用于演示
|
||||
* @param values - 表单值
|
||||
*/
|
||||
const handleLogin = async (values: { username: string; password: string }) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 模拟登录请求
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
// 模拟本地验证(无JWT鉴权)
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 这里应该是实际的API调用
|
||||
// const response = await loginPlayer(values);
|
||||
// 仅做基础格式验证,无服务端鉴权
|
||||
if (!values.username || !values.password) {
|
||||
throw new Error('用户名和密码不能为空');
|
||||
}
|
||||
|
||||
// 模拟登录成功
|
||||
// 模拟登录成功(无真实鉴权)
|
||||
message.success('登录成功!欢迎回到梦幻西游!');
|
||||
navigate('/player/dashboard');
|
||||
} catch {
|
||||
// 故意使用 values 参数以避免 TypeScript 未使用警告
|
||||
void values;
|
||||
message.error('登录失败,请检查用户名和密码!');
|
||||
|
||||
// 简单的本地状态模拟,不涉及JWT
|
||||
console.log('玩家登录信息:', {
|
||||
username: values.username,
|
||||
timestamp: new Date().toISOString(),
|
||||
authType: 'simple-demo'
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '登录失败,请检查用户名和密码!');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
300
src/pages/SystemConfigPage.tsx
Normal file
300
src/pages/SystemConfigPage.tsx
Normal file
@@ -0,0 +1,300 @@
|
||||
/**
|
||||
* 系统配置页面主组件
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Tabs, message, Modal } from 'antd';
|
||||
import {
|
||||
SettingOutlined,
|
||||
SecurityScanOutlined,
|
||||
CloudServerOutlined,
|
||||
HistoryOutlined,
|
||||
ReloadOutlined,
|
||||
SaveOutlined,
|
||||
SyncOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { SystemConfig, SaveConfigRequest } from '../types/systemConfig';
|
||||
import systemConfigService from '../services/systemConfigService';
|
||||
import BasicConfigTab from './tabs/BasicConfigTab';
|
||||
import SecurityConfigTab from './tabs/SecurityConfigTab';
|
||||
import GameConfigTab from './tabs/GameConfigTab';
|
||||
import ConfigHistoryModal from './ConfigHistoryModal';
|
||||
import EnvSyncModal from '../components/EnvSyncModal';
|
||||
|
||||
const { TabPane } = Tabs;
|
||||
|
||||
interface SystemConfigPageProps {}
|
||||
|
||||
const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
||||
const [configs, setConfigs] = useState<SystemConfig[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [saving, setSaving] = useState<boolean>(false);
|
||||
const [activeTab, setActiveTab] = useState<string>('basic');
|
||||
const [historyModalVisible, setHistoryModalVisible] = useState<boolean>(false);
|
||||
const [selectedConfigKey, setSelectedConfigKey] = useState<string>('');
|
||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
|
||||
const [envSyncModalVisible, setEnvSyncModalVisible] = useState<boolean>(false);
|
||||
|
||||
// 加载所有配置
|
||||
const loadConfigs = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const configsData = await systemConfigService.getAllConfigs();
|
||||
setConfigs(configsData);
|
||||
setHasUnsavedChanges(false);
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
message.error('加载配置失败,请重试');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadConfigs();
|
||||
}, []);
|
||||
|
||||
// 保存配置
|
||||
const handleSaveConfigs = async (configRequests: SaveConfigRequest[], group: string) => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const result = await systemConfigService.saveConfigs(configRequests);
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message);
|
||||
await loadConfigs();
|
||||
setHasUnsavedChanges(false);
|
||||
} else {
|
||||
message.error(result.message);
|
||||
if (result.errors) {
|
||||
console.error('配置保存错误:', result.errors);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
message.error('保存配置失败,请重试');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 重置配置为默认值
|
||||
const handleResetConfig = async (configKey: string) => {
|
||||
try {
|
||||
const result = await systemConfigService.resetConfig(configKey);
|
||||
|
||||
if (result.success) {
|
||||
message.success(result.message);
|
||||
// 更新本地配置
|
||||
setConfigs(prev => prev.map(config =>
|
||||
config.config_key === configKey
|
||||
? { ...config, config_value: result.defaultValue || '' }
|
||||
: config
|
||||
));
|
||||
setHasUnsavedChanges(true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重置配置失败:', error);
|
||||
message.error('重置配置失败,请重试');
|
||||
}
|
||||
};
|
||||
|
||||
// 查看配置历史
|
||||
const handleViewHistory = (configKey: string) => {
|
||||
setSelectedConfigKey(configKey);
|
||||
setHistoryModalVisible(true);
|
||||
};
|
||||
|
||||
// 打开.env同步模态框
|
||||
const handleOpenEnvSync = () => {
|
||||
setEnvSyncModalVisible(true);
|
||||
};
|
||||
|
||||
// 处理.env同步完成
|
||||
const handleEnvSyncComplete = () => {
|
||||
message.success('配置已成功同步到.env文件');
|
||||
loadConfigs();
|
||||
};
|
||||
|
||||
// 检测未保存的更改
|
||||
const handleConfigChange = () => {
|
||||
setHasUnsavedChanges(true);
|
||||
};
|
||||
|
||||
// 获取指定分组的配置
|
||||
const getConfigsByGroup = (group: string) => {
|
||||
return configs.filter(config => config.config_group === group);
|
||||
};
|
||||
|
||||
const tabItems = [
|
||||
{
|
||||
key: 'basic',
|
||||
label: (
|
||||
<span>
|
||||
<SettingOutlined />
|
||||
基本配置
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<BasicConfigTab
|
||||
configs={getConfigsByGroup('basic')}
|
||||
loading={loading}
|
||||
saving={saving}
|
||||
onSave={(requests) => handleSaveConfigs(requests, 'basic')}
|
||||
onReset={handleResetConfig}
|
||||
onShowHistory={handleViewHistory}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'security',
|
||||
label: (
|
||||
<span>
|
||||
<SecurityScanOutlined />
|
||||
安全配置
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<SecurityConfigTab
|
||||
configs={getConfigsByGroup('security')}
|
||||
loading={loading}
|
||||
saving={saving}
|
||||
onSave={(requests) => handleSaveConfigs(requests, 'security')}
|
||||
onReset={handleResetConfig}
|
||||
onShowHistory={handleViewHistory}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
key: 'game',
|
||||
label: (
|
||||
<span>
|
||||
<CloudServerOutlined />
|
||||
游戏通信配置
|
||||
</span>
|
||||
),
|
||||
children: (
|
||||
<GameConfigTab
|
||||
configs={getConfigsByGroup('game')}
|
||||
loading={loading}
|
||||
saving={saving}
|
||||
onSave={(requests) => handleSaveConfigs(requests, 'game')}
|
||||
onReset={handleResetConfig}
|
||||
onShowHistory={handleViewHistory}
|
||||
onConfigChange={handleConfigChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="system-config-page" style={{ padding: '24px' }}>
|
||||
{/* 页面头部 */}
|
||||
<div className="config-page-header" style={{ marginBottom: '24px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<div>
|
||||
<h1 style={{ margin: 0, fontSize: '24px', fontWeight: 600 }}>
|
||||
系统配置管理
|
||||
</h1>
|
||||
<p style={{ margin: '8px 0 0 0', color: '#666' }}>
|
||||
管理系统的基本设置、安全配置和游戏通信参数
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'flex', gap: '12px' }}>
|
||||
<button
|
||||
onClick={handleOpenEnvSync}
|
||||
disabled={saving || loading}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
background: saving || loading ? '#f5f5f5' : '#fff',
|
||||
color: saving || loading ? '#999' : '#666',
|
||||
cursor: saving || loading ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
<SyncOutlined />
|
||||
同步到.env文件
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={loadConfigs}
|
||||
disabled={loading}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
padding: '8px 16px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
background: loading ? '#f5f5f5' : '#fff',
|
||||
color: loading ? '#999' : '#666',
|
||||
cursor: loading ? 'not-allowed' : 'pointer'
|
||||
}}
|
||||
>
|
||||
<ReloadOutlined />
|
||||
刷新配置
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{hasUnsavedChanges && (
|
||||
<div style={{
|
||||
marginTop: '12px',
|
||||
padding: '12px',
|
||||
background: '#fff2e8',
|
||||
border: '1px solid #ffbb96',
|
||||
borderRadius: '6px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
<span style={{ color: '#d46b08' }}>⚠️</span>
|
||||
<span style={{ color: '#d46b08' }}>
|
||||
您有未保存的更改,请记得保存配置
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 配置选项卡 */}
|
||||
<div className="config-tabs-container">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
onChange={setActiveTab}
|
||||
items={tabItems}
|
||||
size="large"
|
||||
type="card"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 配置历史弹窗 */}
|
||||
<ConfigHistoryModal
|
||||
visible={historyModalVisible}
|
||||
configKey={selectedConfigKey}
|
||||
onClose={() => {
|
||||
setHistoryModalVisible(false);
|
||||
setSelectedConfigKey('');
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* .env文件同步模态框 */}
|
||||
<EnvSyncModal
|
||||
visible={envSyncModalVisible}
|
||||
configs={configs}
|
||||
onClose={() => setEnvSyncModalVisible(false)}
|
||||
onSync={handleEnvSyncComplete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SystemConfigPage;
|
||||
294
src/pages/tabs/BasicConfigTab.tsx
Normal file
294
src/pages/tabs/BasicConfigTab.tsx
Normal file
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* 基本配置标签页组件
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, Switch, Select, Card, Button, Space, Row, Col, Tooltip } from 'antd';
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
SaveOutlined,
|
||||
ReloadOutlined,
|
||||
HistoryOutlined,
|
||||
SettingOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
|
||||
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface BasicConfigTabProps {
|
||||
configs: SystemConfig[];
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
onSave: (requests: SaveConfigRequest[]) => void;
|
||||
onReset: (configKey: string) => void;
|
||||
onShowHistory: (configKey: string) => void;
|
||||
onConfigChange: () => void;
|
||||
}
|
||||
|
||||
const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
|
||||
configs,
|
||||
loading,
|
||||
saving,
|
||||
onSave,
|
||||
onReset,
|
||||
onShowHistory,
|
||||
onConfigChange
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
const initialData: Record<string, any> = {};
|
||||
configs.forEach(config => {
|
||||
initialData[config.config_key] = config.config_value;
|
||||
});
|
||||
setFormData(initialData);
|
||||
form.setFieldsValue(initialData);
|
||||
}, [configs, form]);
|
||||
|
||||
// 处理表单值变化
|
||||
const handleValuesChange = (changedValues: any, allValues: any) => {
|
||||
setFormData(allValues);
|
||||
onConfigChange();
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
|
||||
config_key: config.config_key,
|
||||
config_value: formData[config.config_key] || '',
|
||||
config_label: config.config_label,
|
||||
config_group: config.config_group
|
||||
}));
|
||||
onSave(saveRequests);
|
||||
}).catch(() => {
|
||||
// 表单验证失败
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认值
|
||||
const handleReset = (configKey: string) => {
|
||||
onReset(configKey);
|
||||
};
|
||||
|
||||
// 显示配置历史
|
||||
const handleShowHistory = (configKey: string) => {
|
||||
onShowHistory(configKey);
|
||||
};
|
||||
|
||||
// 语言选项
|
||||
const languageOptions = [
|
||||
{ value: 'zh-CN', label: '简体中文' },
|
||||
{ value: 'zh-TW', label: '繁体中文' },
|
||||
{ value: 'en-US', label: 'English' },
|
||||
{ value: 'ja-JP', label: '日本語' }
|
||||
];
|
||||
|
||||
// 版本选项
|
||||
const versionOptions = [
|
||||
{ value: '1.0.0', label: 'v1.0.0' },
|
||||
{ value: '1.1.0', label: 'v1.1.0' },
|
||||
{ value: '2.0.0', label: 'v2.0.0' }
|
||||
];
|
||||
|
||||
const configItems = [
|
||||
{
|
||||
key: 'site_name',
|
||||
title: '网站名称',
|
||||
description: '系统显示名称,将在页面标题和系统信息中显示',
|
||||
type: 'input',
|
||||
placeholder: '请输入网站名称',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'site_version',
|
||||
title: '系统版本',
|
||||
description: '当前系统版本号,用于显示和版本管理',
|
||||
type: 'select',
|
||||
options: versionOptions,
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'site_description',
|
||||
title: '系统描述',
|
||||
description: '系统描述信息,简要介绍系统功能和用途',
|
||||
type: 'textarea',
|
||||
placeholder: '请输入系统描述',
|
||||
rows: 3
|
||||
},
|
||||
{
|
||||
key: 'admin_email',
|
||||
title: '管理员邮箱',
|
||||
description: '系统管理员联系邮箱,用于接收系统通知',
|
||||
type: 'input',
|
||||
placeholder: '请输入管理员邮箱',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
key: 'maintenance_mode',
|
||||
title: '维护模式',
|
||||
description: '开启后用户无法正常访问系统,仅管理员可以登录',
|
||||
type: 'switch'
|
||||
},
|
||||
{
|
||||
key: 'default_language',
|
||||
title: '默认语言',
|
||||
description: '系统默认语言设置,影响界面显示语言',
|
||||
type: 'select',
|
||||
options: languageOptions,
|
||||
required: true
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="basic-config-tab" style={{ padding: '0' }}>
|
||||
{/* 配置表单 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={handleValuesChange}
|
||||
style={{ marginBottom: '24px' }}
|
||||
>
|
||||
<Row gutter={[24, 0]}>
|
||||
{configItems.map((item, index) => {
|
||||
const config = configs.find(c => c.config_key === item.key);
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Col span={12} key={item.key}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ marginBottom: '16px' }}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<SettingOutlined />
|
||||
{item.title}
|
||||
{config.config_type === 'boolean' && (
|
||||
<Switch
|
||||
size="small"
|
||||
checked={formData[item.key] === '1' || formData[item.key] === true}
|
||||
onChange={(checked) => {
|
||||
const newValue = checked ? '1' : '0';
|
||||
form.setFieldValue(item.key, newValue);
|
||||
onConfigChange();
|
||||
}}
|
||||
style={{ marginLeft: '8px' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip title="查看历史记录">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<HistoryOutlined />}
|
||||
onClick={() => handleShowHistory(item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="重置为默认值">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => handleReset(item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<InfoCircleOutlined />
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入控件 */}
|
||||
<Form.Item
|
||||
name={item.key}
|
||||
rules={[
|
||||
{ required: item.required, message: `请输入${item.title}` },
|
||||
...(item.key === 'admin_email' ? [
|
||||
{ type: 'email', message: '请输入有效的邮箱地址' }
|
||||
] : [])
|
||||
]}
|
||||
>
|
||||
{item.type === 'input' && (
|
||||
<Input
|
||||
placeholder={item.placeholder}
|
||||
disabled={config.config_type === 'boolean'}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'textarea' && (
|
||||
<TextArea
|
||||
placeholder={item.placeholder}
|
||||
rows={item.rows || 2}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'select' && (
|
||||
<Select placeholder={item.placeholder}>
|
||||
{item.options?.map(option => (
|
||||
<Option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '24px 0',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
style={{ minWidth: '120px' }}
|
||||
>
|
||||
保存基本配置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 配置信息 */}
|
||||
<Card size="small" style={{ background: '#fafafa' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<strong>配置说明</strong>
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#666' }}>
|
||||
<li>基本配置影响系统的整体显示和行为</li>
|
||||
<li>维护模式开启后,普通用户将无法访问系统</li>
|
||||
<li>系统版本用于标识当前软件版本,建议保持更新</li>
|
||||
<li>管理员邮箱用于接收系统重要通知,请确保可正常接收邮件</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BasicConfigTab;
|
||||
434
src/pages/tabs/GameConfigTab.tsx
Normal file
434
src/pages/tabs/GameConfigTab.tsx
Normal file
@@ -0,0 +1,434 @@
|
||||
/**
|
||||
* 游戏通信配置标签页组件
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, InputNumber, Switch, Card, Button, Space, Row, Col, Tooltip, Alert } from 'antd';
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
SaveOutlined,
|
||||
ReloadOutlined,
|
||||
HistoryOutlined,
|
||||
CloudServerOutlined,
|
||||
ApiOutlined,
|
||||
ClockCircleOutlined,
|
||||
UserAddOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface GameConfigTabProps {
|
||||
configs: SystemConfig[];
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
onSave: (requests: SaveConfigRequest[]) => void;
|
||||
onReset: (configKey: string) => void;
|
||||
onShowHistory: (configKey: string) => void;
|
||||
onConfigChange: () => void;
|
||||
}
|
||||
|
||||
const GameConfigTab: React.FC<GameConfigTabProps> = ({
|
||||
configs,
|
||||
loading,
|
||||
saving,
|
||||
onSave,
|
||||
onReset,
|
||||
onShowHistory,
|
||||
onConfigChange
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [formData, setFormData] = useState<Record<string, any>>({});
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
const initialData: Record<string, any> = {};
|
||||
configs.forEach(config => {
|
||||
if (config.config_type === 'boolean') {
|
||||
initialData[config.config_key] = config.config_value === '1' || config.config_value === 'true';
|
||||
} else {
|
||||
initialData[config.config_key] = config.config_value;
|
||||
}
|
||||
});
|
||||
setFormData(initialData);
|
||||
form.setFieldsValue(initialData);
|
||||
}, [configs, form]);
|
||||
|
||||
// 处理表单值变化
|
||||
const handleValuesChange = (changedValues: any, allValues: any) => {
|
||||
setFormData(allValues);
|
||||
onConfigChange();
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
|
||||
config_key: config.config_key,
|
||||
config_value: config.config_type === 'boolean'
|
||||
? (formData[config.config_key] ? '1' : '0')
|
||||
: String(formData[config.config_key] || ''),
|
||||
config_label: config.config_label,
|
||||
config_group: config.config_group
|
||||
}));
|
||||
onSave(saveRequests);
|
||||
}).catch(() => {
|
||||
// 表单验证失败
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认值
|
||||
const handleReset = (configKey: string) => {
|
||||
onReset(configKey);
|
||||
};
|
||||
|
||||
// 显示配置历史
|
||||
const handleShowHistory = (configKey: string) => {
|
||||
onShowHistory(configKey);
|
||||
};
|
||||
|
||||
// 检查API地址格式
|
||||
const validateApiUrl = (url: string) => {
|
||||
if (!url) return { valid: false, message: 'API地址不能为空' };
|
||||
if (!/^https?:\/\/.+/.test(url)) {
|
||||
return { valid: false, message: '请输入有效的HTTP/HTTPS地址' };
|
||||
}
|
||||
return { valid: true, message: 'API地址格式正确' };
|
||||
};
|
||||
|
||||
// 检查PSK密钥强度
|
||||
const checkPskStrength = (psk: string) => {
|
||||
if (!psk) return { level: 'weak', message: 'PSK密钥不能为空' };
|
||||
if (psk.length < 32) return { level: 'weak', message: 'PSK密钥至少需要32位字符' };
|
||||
if (psk.length < 64) return { level: 'medium', message: 'PSK密钥长度适中,建议使用更长的密钥' };
|
||||
return { level: 'strong', message: 'PSK密钥强度良好' };
|
||||
};
|
||||
|
||||
const apiUrlValidation = validateApiUrl(formData.game_server_api || '');
|
||||
const pskStrength = checkPskStrength(formData.game_server_psk || '');
|
||||
|
||||
const configItems = [
|
||||
{
|
||||
key: 'game_server_api',
|
||||
title: '游戏服务端API',
|
||||
description: '游戏服务端HTTP接口地址,用于与游戏服务端进行数据交互',
|
||||
type: 'input',
|
||||
placeholder: 'http://127.0.0.1:8080/tool/http',
|
||||
required: true,
|
||||
icon: <ApiOutlined />,
|
||||
sensitive: false
|
||||
},
|
||||
{
|
||||
key: 'game_server_psk',
|
||||
title: '游戏服务端PSK',
|
||||
description: '游戏服务端预共享密钥,用于API认证,安全性至关重要',
|
||||
type: 'textarea',
|
||||
placeholder: '请输入PSK密钥(建议32位以上)',
|
||||
required: true,
|
||||
rows: 3,
|
||||
icon: <ApiOutlined />,
|
||||
sensitive: true
|
||||
},
|
||||
{
|
||||
key: 'game_server_timeout',
|
||||
title: '请求超时时间',
|
||||
description: '与游戏服务端通信的超时时间,单位:秒',
|
||||
type: 'inputnumber',
|
||||
min: 5,
|
||||
max: 120,
|
||||
suffix: '秒',
|
||||
required: true,
|
||||
icon: <ClockCircleOutlined />
|
||||
},
|
||||
{
|
||||
key: 'game_server_retry_count',
|
||||
title: '重试次数',
|
||||
description: 'API请求失败时的重试次数',
|
||||
type: 'inputnumber',
|
||||
min: 1,
|
||||
max: 10,
|
||||
suffix: '次',
|
||||
required: true,
|
||||
icon: <CloudServerOutlined />
|
||||
},
|
||||
{
|
||||
key: 'player_auto_register',
|
||||
title: '玩家自动注册',
|
||||
description: '新玩家是否自动创建账号',
|
||||
type: 'switch',
|
||||
icon: <UserAddOutlined />
|
||||
},
|
||||
{
|
||||
key: 'game_log_level',
|
||||
title: '游戏日志级别',
|
||||
description: '游戏相关操作的日志记录级别',
|
||||
type: 'select',
|
||||
required: true,
|
||||
icon: <FileTextOutlined />,
|
||||
options: [
|
||||
{ value: 'error', label: '仅错误' },
|
||||
{ value: 'warn', label: '警告及以上' },
|
||||
{ value: 'info', label: '信息及以上' },
|
||||
{ value: 'debug', label: '调试及以上' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="game-config-tab" style={{ padding: '0' }}>
|
||||
{/* 游戏通信警告 */}
|
||||
<Alert
|
||||
message="游戏通信配置警告"
|
||||
description="错误的游戏通信配置可能导致无法与游戏服务端正常交互,请确保API地址和密钥配置正确。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
action={
|
||||
<Button size="small" danger>
|
||||
测试连接
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* API地址验证提示 */}
|
||||
{formData.game_server_api && (
|
||||
<Alert
|
||||
message={`API地址验证: ${apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}`}
|
||||
description={apiUrlValidation.message}
|
||||
type={apiUrlValidation.valid ? 'success' : 'error'}
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* PSK密钥强度提示 */}
|
||||
{formData.game_server_psk && (
|
||||
<Alert
|
||||
message={`PSK密钥强度: ${pskStrength.level.toUpperCase()}`}
|
||||
description={pskStrength.message}
|
||||
type={pskStrength.level === 'strong' ? 'success' : pskStrength.level === 'medium' ? 'warning' : 'error'}
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 配置表单 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={handleValuesChange}
|
||||
style={{ marginBottom: '24px' }}
|
||||
>
|
||||
<Row gutter={[24, 0]}>
|
||||
{configItems.map((item, index) => {
|
||||
const config = configs.find(c => c.config_key === item.key);
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Col span={12} key={item.key}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ marginBottom: '16px' }}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{item.icon || <CloudServerOutlined />}
|
||||
{item.title}
|
||||
{item.sensitive && (
|
||||
<span style={{ color: '#ff4d4f', fontSize: '12px' }}>
|
||||
敏感
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip title="查看历史记录">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<HistoryOutlined />}
|
||||
onClick={() => handleShowHistory(item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="重置为默认值">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => handleReset(item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<InfoCircleOutlined />
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入控件 */}
|
||||
<Form.Item
|
||||
name={item.key}
|
||||
rules={[
|
||||
{ required: item.required, message: `请输入${item.title}` },
|
||||
...(item.key === 'game_server_api' ? [
|
||||
{
|
||||
pattern: /^https?:\/\/.+/,
|
||||
message: '请输入有效的HTTP/HTTPS地址'
|
||||
}
|
||||
] : []),
|
||||
...(item.key === 'game_server_psk' ? [
|
||||
{ min: 32, message: 'PSK密钥至少需要32位字符' }
|
||||
] : [])
|
||||
]}
|
||||
>
|
||||
{item.type === 'input' && (
|
||||
<Input
|
||||
placeholder={item.placeholder}
|
||||
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'textarea' && (
|
||||
<TextArea
|
||||
placeholder={item.placeholder}
|
||||
rows={item.rows || 3}
|
||||
maxLength={500}
|
||||
showCount={!item.sensitive}
|
||||
autoSize={{ minRows: item.rows || 3, maxRows: 6 }}
|
||||
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'inputnumber' && (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
placeholder={item.placeholder}
|
||||
suffix={item.suffix}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'select' && (
|
||||
<Input.Group compact>
|
||||
<Input
|
||||
style={{ width: 'calc(100% - 80px)' }}
|
||||
placeholder={item.placeholder}
|
||||
readOnly
|
||||
/>
|
||||
<InputNumber
|
||||
style={{ width: '80px' }}
|
||||
placeholder="级别"
|
||||
/>
|
||||
</Input.Group>
|
||||
)}
|
||||
{item.type === 'switch' && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Switch
|
||||
checked={formData[item.key]}
|
||||
onChange={(checked) => {
|
||||
form.setFieldValue(item.key, checked);
|
||||
onConfigChange();
|
||||
}}
|
||||
/>
|
||||
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
|
||||
{formData[item.key] ? '已启用' : '未启用'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* 特殊字段验证信息 */}
|
||||
{item.key === 'game_server_api' && formData.game_server_api && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px',
|
||||
background: apiUrlValidation.valid ? '#f6ffed' : '#fff2f0',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
border: `1px solid ${apiUrlValidation.valid ? '#b7eb8f' : '#ffccc7'}`
|
||||
}}>
|
||||
<div>
|
||||
<strong>地址验证:</strong>
|
||||
{apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}
|
||||
</div>
|
||||
<div>
|
||||
协议:{formData.game_server_api.startsWith('https') ? 'HTTPS' : 'HTTP'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.key === 'game_server_psk' && formData.game_server_psk && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px',
|
||||
background: '#f6f8fa',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<strong>密钥强度评估:</strong>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
长度: {formData.game_server_psk.length} 字符
|
||||
{formData.game_server_psk.length >= 32 && ' ✅'}
|
||||
</div>
|
||||
<div>
|
||||
包含特殊字符: {/[!@#$%^&*(),.?":{}|<>]/.test(formData.game_server_psk) ? '✅' : '❌'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '24px 0',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
style={{ minWidth: '120px' }}
|
||||
danger={!apiUrlValidation.valid || pskStrength.level === 'weak'}
|
||||
>
|
||||
保存游戏配置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 配置说明 */}
|
||||
<Card size="small" style={{ background: '#fafafa' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<strong>游戏通信配置说明</strong>
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#666' }}>
|
||||
<li>游戏服务端API地址必须正确配置,否则无法与游戏服务端通信</li>
|
||||
<li>PSK密钥是API认证的重要凭据,请妥善保管并定期更换</li>
|
||||
<li>请求超时时间建议设置为30秒,平衡响应速度和用户体验</li>
|
||||
<li>重试次数建议设置为3次,避免单次失败影响用户体验</li>
|
||||
<li>日志级别设置为'信息'及以上,便于问题排查和监控</li>
|
||||
<li>配置变更后建议重启相关服务以确保配置生效</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default GameConfigTab;
|
||||
409
src/pages/tabs/SecurityConfigTab.tsx
Normal file
409
src/pages/tabs/SecurityConfigTab.tsx
Normal file
@@ -0,0 +1,409 @@
|
||||
/**
|
||||
* 安全配置标签页组件
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Form, Input, InputNumber, Switch, Card, Button, Space, Row, Col, Tooltip, Alert } from 'antd';
|
||||
import {
|
||||
InfoCircleOutlined,
|
||||
SaveOutlined,
|
||||
ReloadOutlined,
|
||||
HistoryOutlined,
|
||||
SecurityScanOutlined,
|
||||
KeyOutlined,
|
||||
ClockCircleOutlined,
|
||||
SafetyCertificateOutlined
|
||||
} from '@ant-design/icons';
|
||||
import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface SecurityConfigTabProps {
|
||||
configs: SystemConfig[];
|
||||
loading: boolean;
|
||||
saving: boolean;
|
||||
onSave: (requests: SaveConfigRequest[]) => void;
|
||||
onReset: (configKey: string) => void;
|
||||
onShowHistory: (configKey: string) => void;
|
||||
onConfigChange: () => void;
|
||||
}
|
||||
|
||||
// 定义表单数据类型
|
||||
type FormFieldValue = string | number | boolean;
|
||||
type FormData = Record<string, FormFieldValue>;
|
||||
|
||||
// 配置项接口定义
|
||||
interface ConfigItem {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
type: 'input' | 'textarea' | 'inputnumber' | 'switch';
|
||||
required: boolean;
|
||||
sensitive?: boolean;
|
||||
placeholder?: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
rows?: number;
|
||||
suffix?: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
|
||||
configs,
|
||||
saving,
|
||||
onSave,
|
||||
onReset,
|
||||
onShowHistory,
|
||||
onConfigChange
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [formData, setFormData] = useState<FormData>({});
|
||||
|
||||
// 初始化表单数据
|
||||
useEffect(() => {
|
||||
const initialData: FormData = {};
|
||||
configs.forEach(config => {
|
||||
if (config.config_type === 'boolean') {
|
||||
initialData[config.config_key] = config.config_value === '1' || config.config_value === 'true';
|
||||
} else {
|
||||
initialData[config.config_key] = config.config_value;
|
||||
}
|
||||
});
|
||||
// 直接设置表单字段值,避免级联渲染
|
||||
form.setFieldsValue(initialData);
|
||||
}, [configs, form]);
|
||||
|
||||
// 处理表单值变化
|
||||
const handleValuesChange = (_changedValues: Partial<FormData>, allValues: FormData) => {
|
||||
setFormData(allValues);
|
||||
onConfigChange();
|
||||
};
|
||||
|
||||
// 保存配置
|
||||
const handleSave = () => {
|
||||
form.validateFields().then(() => {
|
||||
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
|
||||
config_key: config.config_key,
|
||||
config_value: config.config_type === 'boolean'
|
||||
? (formData[config.config_key] ? '1' : '0')
|
||||
: String(formData[config.config_key] || ''),
|
||||
config_type: config.config_type,
|
||||
changed_reason: '安全配置更新'
|
||||
}));
|
||||
onSave(saveRequests);
|
||||
}).catch(() => {
|
||||
// 表单验证失败
|
||||
});
|
||||
};
|
||||
|
||||
// 重置为默认值
|
||||
const handleReset = (configKey: string) => {
|
||||
onReset(configKey);
|
||||
};
|
||||
|
||||
// 显示配置历史
|
||||
const handleShowHistory = (configKey: string) => {
|
||||
onShowHistory(configKey);
|
||||
};
|
||||
|
||||
// 检查JWT密钥强度
|
||||
const checkJwtSecretStrength = (secret: string) => {
|
||||
if (!secret) return { level: 'weak', message: 'JWT密钥不能为空' };
|
||||
if (secret.length < 32) return { level: 'weak', message: 'JWT密钥至少需要32位字符' };
|
||||
if (secret.length < 64) return { level: 'medium', message: 'JWT密钥长度适中,建议使用更长的密钥' };
|
||||
return { level: 'strong', message: 'JWT密钥强度良好' };
|
||||
};
|
||||
|
||||
// 安全地获取JWT密钥字符串
|
||||
const getJwtSecretString = (): string => {
|
||||
const secret = formData.jwt_secret;
|
||||
return typeof secret === 'string' ? secret : '';
|
||||
};
|
||||
|
||||
const jwtSecretStrength = checkJwtSecretStrength(getJwtSecretString());
|
||||
|
||||
const configItems: ConfigItem[] = [
|
||||
{
|
||||
key: 'jwt_secret',
|
||||
title: 'JWT密钥',
|
||||
description: '用于JWT令牌签名的密钥,安全性至关重要,建议32位以上随机字符',
|
||||
type: 'textarea',
|
||||
placeholder: '请输入JWT密钥(建议32位以上)',
|
||||
required: true,
|
||||
rows: 3,
|
||||
icon: <KeyOutlined />,
|
||||
sensitive: true
|
||||
},
|
||||
{
|
||||
key: 'jwt_expires_in',
|
||||
title: 'JWT过期时间',
|
||||
description: 'JWT访问令牌的有效期,单位:小时,建议24-72小时',
|
||||
type: 'inputnumber',
|
||||
min: 1,
|
||||
max: 168,
|
||||
suffix: '小时',
|
||||
required: true,
|
||||
icon: <ClockCircleOutlined />
|
||||
},
|
||||
{
|
||||
key: 'jwt_refresh_expires_in',
|
||||
title: 'JWT刷新令牌过期时间',
|
||||
description: 'JWT刷新令牌的有效期,单位:小时,建议7天(168小时)',
|
||||
type: 'inputnumber',
|
||||
min: 24,
|
||||
max: 720,
|
||||
suffix: '小时',
|
||||
required: true,
|
||||
icon: <ClockCircleOutlined />
|
||||
},
|
||||
{
|
||||
key: 'login_attempt_limit',
|
||||
title: '登录尝试次数限制',
|
||||
description: '连续登录失败次数限制,超过后临时锁定账户',
|
||||
type: 'inputnumber',
|
||||
min: 3,
|
||||
max: 10,
|
||||
suffix: '次',
|
||||
required: true,
|
||||
icon: <SecurityScanOutlined />
|
||||
},
|
||||
{
|
||||
key: 'session_timeout',
|
||||
title: '会话超时时间',
|
||||
description: '用户会话超时时间,单位:分钟',
|
||||
type: 'inputnumber',
|
||||
min: 15,
|
||||
max: 1440,
|
||||
suffix: '分钟',
|
||||
required: true,
|
||||
icon: <ClockCircleOutlined />
|
||||
},
|
||||
{
|
||||
key: 'password_min_length',
|
||||
title: '密码最小长度',
|
||||
description: '用户密码最小长度要求',
|
||||
type: 'inputnumber',
|
||||
min: 6,
|
||||
max: 20,
|
||||
suffix: '位',
|
||||
required: true,
|
||||
icon: <KeyOutlined />
|
||||
},
|
||||
{
|
||||
key: 'enable_2fa',
|
||||
title: '启用双因子认证',
|
||||
description: '是否启用双因子认证功能,提供额外的安全保障',
|
||||
type: 'switch',
|
||||
icon: <SafetyCertificateOutlined />,
|
||||
required: false
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="security-config-tab" style={{ padding: '0' }}>
|
||||
{/* 安全警告 */}
|
||||
<Alert
|
||||
message="安全配置警告"
|
||||
description="安全配置直接影响系统安全性,请谨慎修改。错误的配置可能导致系统无法正常运行。"
|
||||
type="warning"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
action={
|
||||
<Button size="small" danger>
|
||||
了解风险
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* JWT密钥强度提示 */}
|
||||
{formData.jwt_secret && (
|
||||
<Alert
|
||||
message={`JWT密钥强度: ${jwtSecretStrength.level.toUpperCase()}`}
|
||||
description={jwtSecretStrength.message}
|
||||
type={jwtSecretStrength.level === 'strong' ? 'success' : jwtSecretStrength.level === 'medium' ? 'warning' : 'error'}
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 配置表单 */}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={handleValuesChange}
|
||||
style={{ marginBottom: '24px' }}
|
||||
>
|
||||
<Row gutter={[24, 0]}>
|
||||
{configItems.map((item) => {
|
||||
const config = configs.find(c => c.config_key === item.key);
|
||||
if (!config) return null;
|
||||
|
||||
return (
|
||||
<Col span={12} key={item.key}>
|
||||
<Card
|
||||
size="small"
|
||||
style={{ marginBottom: '16px' }}
|
||||
title={
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
{item.icon || <SecurityScanOutlined />}
|
||||
{item.title}
|
||||
{item.sensitive && (
|
||||
<span style={{ color: '#ff4d4f', fontSize: '12px' }}>
|
||||
敏感
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
extra={
|
||||
<Space>
|
||||
<Tooltip title="查看历史记录">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<HistoryOutlined />}
|
||||
onClick={() => handleShowHistory(item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip title="重置为默认值">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => handleReset(item.key)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<div style={{ marginBottom: '8px' }}>
|
||||
<div style={{
|
||||
fontSize: '12px',
|
||||
color: '#666',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '4px'
|
||||
}}>
|
||||
<InfoCircleOutlined />
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 输入控件 */}
|
||||
<Form.Item
|
||||
name={item.key}
|
||||
rules={[
|
||||
{ required: item.required, message: `请输入${item.title}` },
|
||||
...(item.key === 'jwt_secret' ? [
|
||||
{ min: 32, message: 'JWT密钥至少需要32位字符' }
|
||||
] : [])
|
||||
]}
|
||||
>
|
||||
{item.type === 'input' && (
|
||||
<Input.Password
|
||||
placeholder={item.placeholder}
|
||||
disabled={config.config_type === 'boolean'}
|
||||
visibilityToggle={!item.sensitive}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'textarea' && (
|
||||
<TextArea
|
||||
placeholder={item.placeholder}
|
||||
rows={item.rows || 3}
|
||||
maxLength={500}
|
||||
showCount={!item.sensitive}
|
||||
autoSize={{ minRows: item.rows || 3, maxRows: 6 }}
|
||||
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'inputnumber' && (
|
||||
<InputNumber
|
||||
style={{ width: '100%' }}
|
||||
min={item.min}
|
||||
max={item.max}
|
||||
placeholder={item.placeholder}
|
||||
suffix={item.suffix}
|
||||
/>
|
||||
)}
|
||||
{item.type === 'switch' && (
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<Switch
|
||||
checked={Boolean(formData[item.key])}
|
||||
onChange={(checked) => {
|
||||
form.setFieldValue(item.key, checked);
|
||||
onConfigChange();
|
||||
}}
|
||||
/>
|
||||
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
|
||||
{formData[item.key] ? '已启用' : '未启用'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
{/* JWT密钥特殊提示 */}
|
||||
{item.key === 'jwt_secret' && formData.jwt_secret && (
|
||||
<div style={{
|
||||
marginTop: '8px',
|
||||
padding: '8px',
|
||||
background: '#f6f8fa',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px'
|
||||
}}>
|
||||
<strong>密钥强度评估:</strong>
|
||||
<div style={{ marginTop: '4px' }}>
|
||||
长度: {getJwtSecretString().length} 字符
|
||||
{getJwtSecretString().length >= 32 && ' ✅'}
|
||||
</div>
|
||||
<div>
|
||||
包含特殊字符: {/[!@#$%^&*(),.?":{}|<>]/.test(getJwtSecretString()) ? '✅' : '❌'}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Form>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
padding: '24px 0',
|
||||
borderTop: '1px solid #f0f0f0'
|
||||
}}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
icon={<SaveOutlined />}
|
||||
loading={saving}
|
||||
onClick={handleSave}
|
||||
style={{ minWidth: '120px' }}
|
||||
danger={jwtSecretStrength.level === 'weak'}
|
||||
>
|
||||
保存安全配置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* 配置说明 */}
|
||||
<Card size="small" style={{ background: '#fafafa' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '8px' }}>
|
||||
<InfoCircleOutlined style={{ color: '#1890ff' }} />
|
||||
<strong>安全配置说明</strong>
|
||||
</div>
|
||||
<ul style={{ margin: 0, paddingLeft: '20px', fontSize: '12px', color: '#666' }}>
|
||||
<li>JWT密钥是系统安全的核心,请使用强随机字符串并定期更换</li>
|
||||
<li>会话超时时间建议设置为15-30分钟,平衡安全性和便利性</li>
|
||||
<li>登录尝试限制可防止暴力破解攻击,建议设置为3-5次</li>
|
||||
<li>双因子认证为重要配置,建议生产环境中启用</li>
|
||||
<li>所有安全配置变更后,建议清除所有用户会话并重新登录</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecurityConfigTab;
|
||||
230
src/services/systemConfigService.ts
Normal file
230
src/services/systemConfigService.ts
Normal file
@@ -0,0 +1,230 @@
|
||||
/**
|
||||
* 系统配置API服务
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import {
|
||||
SystemConfig,
|
||||
SaveConfigRequest
|
||||
} from '../types/systemConfig';
|
||||
|
||||
const API_BASE_URL = '/api/system-config';
|
||||
|
||||
class SystemConfigService {
|
||||
/**
|
||||
* 获取所有系统配置
|
||||
*/
|
||||
async getAllConfigs(): Promise<SystemConfig[]> {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
// 返回模拟数据
|
||||
return [
|
||||
// 基本配置
|
||||
{ id: 1, config_key: 'site_name', config_value: '梦幻西游一站式运营管理系统', config_type: 'string', config_group: 'basic', config_label: '网站名称', config_description: '系统显示名称', sort_order: 1 },
|
||||
{ id: 2, config_key: 'site_version', config_value: '1.0.0', config_type: 'string', config_group: 'basic', config_label: '系统版本', config_description: '当前系统版本号', sort_order: 2 },
|
||||
{ id: 3, config_key: 'site_description', config_value: '专业的游戏运营管理平台', config_type: 'string', config_group: 'basic', config_label: '系统描述', config_description: '系统描述信息', sort_order: 3 },
|
||||
{ id: 4, config_key: 'admin_email', config_value: 'admin@mhxy.com', config_type: 'string', config_group: 'basic', config_label: '管理员邮箱', config_description: '系统管理员联系邮箱', sort_order: 4 },
|
||||
{ id: 5, config_key: 'maintenance_mode', config_value: '0', config_type: 'boolean', config_group: 'basic', config_label: '维护模式', config_description: '开启后用户无法正常访问系统', sort_order: 5 },
|
||||
{ id: 6, config_key: 'default_language', config_value: 'zh-CN', config_type: 'string', config_group: 'basic', config_label: '默认语言', config_description: '系统默认语言设置', sort_order: 6 },
|
||||
|
||||
// 安全配置
|
||||
{ id: 7, config_key: 'jwt_secret', config_value: 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025', config_type: 'string', config_group: 'security', config_label: 'JWT密钥', config_description: '用于JWT令牌签名的密钥,建议32位字符', sort_order: 1 },
|
||||
{ id: 8, config_key: 'jwt_expires_in', config_value: '24', config_type: 'number', config_group: 'security', config_label: 'JWT过期时间(小时)', config_description: 'JWT访问令牌的有效期,单位:小时', sort_order: 2 },
|
||||
{ id: 9, config_key: 'jwt_refresh_expires_in', config_value: '168', config_type: 'number', config_group: 'security', config_label: 'JWT刷新令牌过期时间(小时)', config_description: 'JWT刷新令牌的有效期,单位:小时', sort_order: 3 },
|
||||
{ id: 10, config_key: 'login_attempt_limit', config_value: '5', config_type: 'number', config_group: 'security', config_label: '登录尝试次数限制', config_description: '连续登录失败次数限制', sort_order: 4 },
|
||||
{ id: 11, config_key: 'session_timeout', config_value: '30', config_type: 'number', config_group: 'security', config_label: '会话超时时间(分钟)', config_description: '用户会话超时时间', sort_order: 5 },
|
||||
{ id: 12, config_key: 'password_min_length', config_value: '6', config_type: 'number', config_group: 'security', config_label: '密码最小长度', config_description: '用户密码最小长度要求', sort_order: 6 },
|
||||
{ id: 13, config_key: 'enable_2fa', config_value: '0', config_type: 'boolean', config_group: 'security', config_label: '启用双因子认证', config_description: '是否启用双因子认证功能', sort_order: 7 },
|
||||
|
||||
// 游戏通信配置
|
||||
{ id: 14, config_key: 'game_server_api', config_value: 'http://127.0.0.1:8080/tool/http', config_type: 'string', config_group: 'game', config_label: '游戏服务端API', config_description: '游戏服务端HTTP接口地址', sort_order: 1 },
|
||||
{ id: 15, config_key: 'game_server_psk', config_value: 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!!', config_type: 'string', config_group: 'game', config_label: '游戏服务端PSK', config_description: '游戏服务端预共享密钥,用于API认证', sort_order: 2 },
|
||||
{ id: 16, config_key: 'game_server_timeout', config_value: '30', config_type: 'number', config_group: 'game', config_label: '请求超时时间(秒)', config_description: '与游戏服务端通信的超时时间', sort_order: 3 },
|
||||
{ id: 17, config_key: 'game_server_retry_count', config_value: '3', config_type: 'number', config_group: 'game', config_label: '重试次数', config_description: 'API请求失败时的重试次数', sort_order: 4 },
|
||||
{ id: 18, config_key: 'player_auto_register', config_value: '1', config_type: 'boolean', config_group: 'game', config_label: '玩家自动注册', config_description: '新玩家是否自动创建账号', sort_order: 5 },
|
||||
{ id: 19, config_key: 'game_log_level', config_value: 'info', config_type: 'string', config_group: 'game', config_label: '游戏日志级别', config_description: '游戏相关操作的日志记录级别', sort_order: 6 }
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分组获取配置
|
||||
*/
|
||||
async getConfigsByGroup(group: string): Promise<SystemConfig[]> {
|
||||
const allConfigs = await this.getAllConfigs();
|
||||
return allConfigs.filter(config => config.config_group === group);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存单个配置项
|
||||
*/
|
||||
async saveConfig(request: SaveConfigRequest): Promise<{ success: boolean; message: string }> {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// 模拟验证逻辑
|
||||
if (!request.config_key || !request.config_value) {
|
||||
throw new Error('配置键名和值不能为空');
|
||||
}
|
||||
|
||||
// 特殊验证
|
||||
if (request.config_key === 'jwt_secret' && request.config_value.length < 32) {
|
||||
throw new Error('JWT密钥至少需要32位字符');
|
||||
}
|
||||
|
||||
if (request.config_key === 'game_server_api' && !/^https?:\/\/.+/.test(request.config_value)) {
|
||||
throw new Error('游戏服务端API必须是有效的HTTP/HTTPS地址');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '配置保存成功'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量保存配置
|
||||
*/
|
||||
async saveConfigs(requests: SaveConfigRequest[]): Promise<{ success: boolean; message: string; errors?: string[] }> {
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const request of requests) {
|
||||
try {
|
||||
await this.saveConfig(request);
|
||||
} catch (error) {
|
||||
errors.push(`${request.config_key}: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: errors.length === 0,
|
||||
message: errors.length === 0 ? '所有配置保存成功' : '部分配置保存失败',
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取配置历史记录
|
||||
*/
|
||||
async getConfigHistory(configKey?: string, limit: number = 50): Promise<any[]> {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
const mockHistory: any[] = [
|
||||
{
|
||||
id: 1,
|
||||
config_key: 'jwt_secret',
|
||||
old_value: 'OLD_JWT_SECRET_32_BYTE_STRING_2024',
|
||||
new_value: 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025',
|
||||
changed_by: 1,
|
||||
changed_reason: '更新JWT密钥以提升安全性',
|
||||
created_at: '2025-12-12T10:30:00.000Z',
|
||||
admin_user: {
|
||||
username: 'admin',
|
||||
real_name: '系统管理员'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
config_key: 'game_server_api',
|
||||
old_value: 'http://192.168.1.100:8080/tool/http',
|
||||
new_value: 'http://127.0.0.1:8080/tool/http',
|
||||
changed_by: 1,
|
||||
changed_reason: '更新为本地开发环境地址',
|
||||
created_at: '2025-12-12T09:15:00.000Z',
|
||||
admin_user: {
|
||||
username: 'admin',
|
||||
real_name: '系统管理员'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return configKey
|
||||
? mockHistory.filter(item => item.config_key === configKey)
|
||||
: mockHistory;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步配置到.env文件
|
||||
*/
|
||||
async syncToEnvFile(configs: EnvConfig): Promise<{ success: boolean; message: string }> {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// 模拟文件写入操作
|
||||
console.log('同步配置到.env文件:', configs);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '配置已成功同步到.env文件'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置指定配置项为默认值
|
||||
*/
|
||||
async resetConfig(configKey: string): Promise<{ success: boolean; message: string; defaultValue?: string }> {
|
||||
// 模拟API调用
|
||||
await new Promise(resolve => setTimeout(resolve, 300));
|
||||
|
||||
const defaultValues: Record<string, string> = {
|
||||
'jwt_secret': 'JWT_SECRET_32_BYTE_RANDOM_STRING_2025',
|
||||
'game_server_api': 'http://127.0.0.1:8080/tool/http',
|
||||
'game_server_psk': 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!!',
|
||||
'jwt_expires_in': '24',
|
||||
'login_attempt_limit': '5',
|
||||
'session_timeout': '30'
|
||||
};
|
||||
|
||||
const defaultValue = defaultValues[configKey];
|
||||
if (!defaultValue) {
|
||||
throw new Error('未找到该配置项的默认值');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '配置已重置为默认值',
|
||||
defaultValue
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证配置值
|
||||
*/
|
||||
validateConfigValue(configKey: string, value: any): { valid: boolean; error?: string } {
|
||||
switch (configKey) {
|
||||
case 'jwt_secret':
|
||||
if (typeof value !== 'string' || value.length < 32) {
|
||||
return { valid: false, error: 'JWT密钥至少需要32位字符' };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'game_server_api':
|
||||
if (!/^https?:\/\/.+/.test(value)) {
|
||||
return { valid: false, error: '请输入有效的HTTP/HTTPS地址' };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'game_server_psk':
|
||||
if (typeof value !== 'string' || value.length < 32) {
|
||||
return { valid: false, error: 'PSK密钥至少需要32位字符' };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'jwt_expires_in':
|
||||
case 'login_attempt_limit':
|
||||
case 'session_timeout':
|
||||
const num = Number(value);
|
||||
if (isNaN(num) || num <= 0) {
|
||||
return { valid: false, error: '请输入有效的正整数' };
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
}
|
||||
|
||||
export const systemConfigService = new SystemConfigService();
|
||||
export default systemConfigService;
|
||||
135
src/types/systemConfig.ts
Normal file
135
src/types/systemConfig.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 系统配置相关类型定义
|
||||
* @author MHXY Development Team
|
||||
* @version 1.0.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
export interface SystemConfig {
|
||||
id?: number;
|
||||
config_key: string;
|
||||
config_value: string;
|
||||
config_type: 'string' | 'number' | 'boolean' | 'json';
|
||||
config_group: 'basic' | 'security' | 'game';
|
||||
config_label: string;
|
||||
config_description?: string;
|
||||
is_encrypted?: boolean;
|
||||
is_system?: boolean;
|
||||
sort_order?: number;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
export interface ConfigGroup {
|
||||
key: 'basic' | 'security' | 'game';
|
||||
label: string;
|
||||
description: string;
|
||||
icon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export interface ConfigFormData {
|
||||
[key: string]: string | number | boolean | object;
|
||||
}
|
||||
|
||||
export interface EnvConfig {
|
||||
JWT_SECRET: string;
|
||||
JWT_EXPIRES_IN: string;
|
||||
JWT_REFRESH_EXPIRES_IN: string;
|
||||
GAME_SERVER_API: string;
|
||||
GAME_SERVER_PSK: string;
|
||||
SITE_NAME: string;
|
||||
SITE_VERSION: string;
|
||||
MAINTENANCE_MODE: string;
|
||||
}
|
||||
|
||||
export interface ConfigHistory {
|
||||
id: number;
|
||||
config_key: string;
|
||||
old_value: string;
|
||||
new_value: string;
|
||||
changed_by: number;
|
||||
changed_reason?: string;
|
||||
created_at: string;
|
||||
admin_user?: {
|
||||
username: string;
|
||||
real_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface SaveConfigRequest {
|
||||
config_key: string;
|
||||
config_value: string;
|
||||
config_type: string;
|
||||
changed_reason?: string;
|
||||
}
|
||||
|
||||
export interface ConfigValidationRule {
|
||||
config_key: string;
|
||||
required: boolean;
|
||||
minLength?: number;
|
||||
maxLength?: number;
|
||||
pattern?: RegExp;
|
||||
customValidator?: (value: any) => string | null;
|
||||
}
|
||||
|
||||
export const CONFIG_GROUPS: ConfigGroup[] = [
|
||||
{
|
||||
key: 'basic',
|
||||
label: '基本配置',
|
||||
description: '系统基本设置和显示配置'
|
||||
},
|
||||
{
|
||||
key: 'security',
|
||||
label: '安全配置',
|
||||
description: 'JWT认证、安全策略等安全相关配置'
|
||||
},
|
||||
{
|
||||
key: 'game',
|
||||
label: '游戏通信配置',
|
||||
description: '游戏服务端通信和玩家相关配置'
|
||||
}
|
||||
];
|
||||
|
||||
export const CONFIG_VALIDATION_RULES: Record<string, ConfigValidationRule> = {
|
||||
jwt_secret: {
|
||||
config_key: 'jwt_secret',
|
||||
required: true,
|
||||
minLength: 32,
|
||||
maxLength: 64,
|
||||
pattern: /^[A-Za-z0-9_\-!@#$%^&*()_+\[\]{};':"\\|,.<>\/?]{32,64}$/
|
||||
},
|
||||
game_server_api: {
|
||||
config_key: 'game_server_api',
|
||||
required: true,
|
||||
pattern: /^https?:\/\/.+/
|
||||
},
|
||||
game_server_psk: {
|
||||
config_key: 'game_server_psk',
|
||||
required: true,
|
||||
minLength: 32,
|
||||
maxLength: 64
|
||||
},
|
||||
jwt_expires_in: {
|
||||
config_key: 'jwt_expires_in',
|
||||
required: true,
|
||||
customValidator: (value: any) => {
|
||||
const num = Number(value);
|
||||
if (isNaN(num) || num < 1 || num > 168) {
|
||||
return 'JWT过期时间必须在1-168小时之间';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
password_min_length: {
|
||||
config_key: 'password_min_length',
|
||||
required: true,
|
||||
customValidator: (value: any) => {
|
||||
const num = Number(value);
|
||||
if (isNaN(num) || num < 6 || num > 32) {
|
||||
return '密码最小长度必须在6-32位之间';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -11,7 +11,6 @@
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
@@ -4,4 +4,10 @@ import react from '@vitejs/plugin-react'
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
esbuild: {
|
||||
target: 'es2020'
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['react', 'react-dom', 'antd']
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user