feat: 新增系统配置页面

This commit is contained in:
Stev_Wang
2025-12-12 20:01:39 +08:00
parent 5a3d3918ba
commit fb51d51215
15 changed files with 4633 additions and 93 deletions

2004
package-lock.json generated

File diff suppressed because it is too large Load Diff

View 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;

View File

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

View 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;

View File

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

View 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;

View File

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

View 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;

View 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;

View 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;

View 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;

View 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
View 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;
}
}
};

View File

@@ -11,7 +11,6 @@
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",

View File

@@ -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']
}
})