fix: 🐛 优化系统配置页部分问题
This commit is contained in:
@@ -1,414 +0,0 @@
|
|||||||
/**
|
|
||||||
* .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;
|
|
||||||
@@ -22,7 +22,7 @@ import { useAuth } from '../contexts/AuthContext';
|
|||||||
import { useNavigate, useLocation } from 'react-router-dom';
|
import { useNavigate, useLocation } from 'react-router-dom';
|
||||||
import TabNavigation from './TabNavigation';
|
import TabNavigation from './TabNavigation';
|
||||||
|
|
||||||
const { Header, Sider, Content } = Layout;
|
const { Header, Sider } = Layout;
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
// 定义菜单项类型
|
// 定义菜单项类型
|
||||||
@@ -139,6 +139,7 @@ const menuItems: MenuProps['items'] = [
|
|||||||
const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false);
|
const [mobileDrawerVisible, setMobileDrawerVisible] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const { user, logout, isLoading } = useAuth();
|
const { user, logout, isLoading } = useAuth();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@@ -151,6 +152,20 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
}
|
}
|
||||||
}, [user, navigate, isLoading]);
|
}, [user, navigate, isLoading]);
|
||||||
|
|
||||||
|
// 响应式检测
|
||||||
|
useEffect(() => {
|
||||||
|
const checkIsMobile = () => {
|
||||||
|
setIsMobile(window.innerWidth < 768);
|
||||||
|
if (window.innerWidth >= 768) {
|
||||||
|
setMobileDrawerVisible(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
checkIsMobile();
|
||||||
|
window.addEventListener('resize', checkIsMobile);
|
||||||
|
return () => window.removeEventListener('resize', checkIsMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 加载状态
|
// 加载状态
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
@@ -167,22 +182,6 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 响应式检测
|
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const checkIsMobile = () => {
|
|
||||||
setIsMobile(window.innerWidth < 768);
|
|
||||||
if (window.innerWidth >= 768) {
|
|
||||||
setMobileDrawerVisible(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
checkIsMobile();
|
|
||||||
window.addEventListener('resize', checkIsMobile);
|
|
||||||
return () => window.removeEventListener('resize', checkIsMobile);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理用户下拉菜单点击
|
* 处理用户下拉菜单点击
|
||||||
*/
|
*/
|
||||||
@@ -331,7 +330,11 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 菜单区域 */}
|
{/* 菜单区域 */}
|
||||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
<div style={{
|
||||||
|
flex: 1,
|
||||||
|
overflow: 'auto',
|
||||||
|
height: '100%'
|
||||||
|
}}>
|
||||||
<Menu
|
<Menu
|
||||||
mode="inline"
|
mode="inline"
|
||||||
selectedKeys={getSelectedKeys()}
|
selectedKeys={getSelectedKeys()}
|
||||||
@@ -349,7 +352,7 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout style={{ minHeight: '100vh' }}>
|
<div style={{ minHeight: '100vh', position: 'relative' }}>
|
||||||
{/* 侧边栏 - 桌面端 */}
|
{/* 侧边栏 - 桌面端 */}
|
||||||
{!isMobile && (
|
{!isMobile && (
|
||||||
<Sider
|
<Sider
|
||||||
@@ -359,8 +362,15 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
width={240}
|
width={240}
|
||||||
style={{
|
style={{
|
||||||
background: token.colorBgContainer,
|
background: token.colorBgContainer,
|
||||||
borderRight: `1px solid ${token.colorBorderSecondary}`
|
borderRight: `1px solid ${token.colorBorderSecondary}`,
|
||||||
|
position: 'fixed',
|
||||||
|
left: 0,
|
||||||
|
top: 0, // 从页面顶部开始
|
||||||
|
bottom: 0,
|
||||||
|
zIndex: 99,
|
||||||
|
overflow: 'hidden'
|
||||||
}}
|
}}
|
||||||
|
className="admin-sider"
|
||||||
>
|
>
|
||||||
{siderContent}
|
{siderContent}
|
||||||
</Sider>
|
</Sider>
|
||||||
@@ -378,19 +388,22 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
{siderContent}
|
{siderContent}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
|
|
||||||
<Layout>
|
{/* 头部导航栏 - 固定定位 */}
|
||||||
{/* 头部 */}
|
<Header style={{
|
||||||
<Header style={{
|
padding: '0 24px',
|
||||||
padding: '0 24px',
|
background: token.colorBgContainer,
|
||||||
background: token.colorBgContainer,
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
display: 'flex',
|
||||||
display: 'flex',
|
alignItems: 'center',
|
||||||
alignItems: 'center',
|
justifyContent: 'space-between',
|
||||||
justifyContent: 'space-between',
|
position: 'fixed',
|
||||||
position: 'sticky',
|
top: 0,
|
||||||
top: 0,
|
left: !isMobile ? (collapsed ? 80 : 240) : 0,
|
||||||
zIndex: 100
|
right: 0,
|
||||||
}}>
|
height: '64px',
|
||||||
|
zIndex: 100,
|
||||||
|
transition: 'left 0.2s'
|
||||||
|
}}>
|
||||||
{/* 左侧控制区 */}
|
{/* 左侧控制区 */}
|
||||||
<Space>
|
<Space>
|
||||||
{/* 折叠按钮 */}
|
{/* 折叠按钮 */}
|
||||||
@@ -441,22 +454,40 @@ const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Header>
|
</Header>
|
||||||
|
|
||||||
{/* 标签导航 */}
|
{/* 标签导航 - 固定在头部下方 */}
|
||||||
<TabNavigation />
|
<div style={{
|
||||||
|
position: 'fixed',
|
||||||
|
top: '64px',
|
||||||
|
left: !isMobile ? (collapsed ? 80 : 240) : 0,
|
||||||
|
right: 0,
|
||||||
|
zIndex: 90,
|
||||||
|
background: token.colorBgContainer,
|
||||||
|
borderBottom: `1px solid ${token.colorBorderSecondary}`,
|
||||||
|
transition: 'left 0.2s'
|
||||||
|
}}>
|
||||||
|
<TabNavigation />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* 内容区域 */}
|
{/* 内容区域 */}
|
||||||
<Content style={{
|
<div style={{
|
||||||
margin: '16px',
|
|
||||||
padding: '24px',
|
padding: '24px',
|
||||||
background: token.colorBgContainer,
|
background: token.colorBgContainer,
|
||||||
borderRadius: token.borderRadiusLG,
|
minHeight: 'calc(100vh - 112px)',
|
||||||
minHeight: 'calc(100vh - 64px - 48px - 32px)',
|
overflow: 'auto',
|
||||||
overflow: 'auto'
|
marginTop: '112px', // 头部64px + 标签导航48px
|
||||||
|
marginLeft: !isMobile ? (collapsed ? 80 : 240) : 0,
|
||||||
|
transition: 'margin-left 0.2s'
|
||||||
}}>
|
}}>
|
||||||
{children}
|
<div style={{
|
||||||
</Content>
|
background: token.colorBgContainer,
|
||||||
</Layout>
|
borderRadius: token.borderRadiusLG,
|
||||||
</Layout>
|
minHeight: 'calc(100vh - 112px)',
|
||||||
|
padding: '24px'
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,12 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Tabs, message, Modal } from 'antd';
|
import { Tabs, message } from 'antd';
|
||||||
import {
|
import {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
SecurityScanOutlined,
|
SecurityScanOutlined,
|
||||||
CloudServerOutlined,
|
CloudServerOutlined,
|
||||||
HistoryOutlined,
|
ReloadOutlined
|
||||||
ReloadOutlined,
|
|
||||||
SaveOutlined,
|
|
||||||
SyncOutlined
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { SystemConfig, SaveConfigRequest } from '../types/systemConfig';
|
import { SystemConfig, SaveConfigRequest } from '../types/systemConfig';
|
||||||
import systemConfigService from '../services/systemConfigService';
|
import systemConfigService from '../services/systemConfigService';
|
||||||
@@ -21,13 +18,8 @@ import BasicConfigTab from './tabs/BasicConfigTab';
|
|||||||
import SecurityConfigTab from './tabs/SecurityConfigTab';
|
import SecurityConfigTab from './tabs/SecurityConfigTab';
|
||||||
import GameConfigTab from './tabs/GameConfigTab';
|
import GameConfigTab from './tabs/GameConfigTab';
|
||||||
import ConfigHistoryModal from './ConfigHistoryModal';
|
import ConfigHistoryModal from './ConfigHistoryModal';
|
||||||
import EnvSyncModal from '../components/EnvSyncModal';
|
|
||||||
|
|
||||||
const { TabPane } = Tabs;
|
const SystemConfigPage: React.FC = () => {
|
||||||
|
|
||||||
interface SystemConfigPageProps {}
|
|
||||||
|
|
||||||
const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|
||||||
const [configs, setConfigs] = useState<SystemConfig[]>([]);
|
const [configs, setConfigs] = useState<SystemConfig[]>([]);
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [saving, setSaving] = useState<boolean>(false);
|
const [saving, setSaving] = useState<boolean>(false);
|
||||||
@@ -35,7 +27,6 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
const [historyModalVisible, setHistoryModalVisible] = useState<boolean>(false);
|
const [historyModalVisible, setHistoryModalVisible] = useState<boolean>(false);
|
||||||
const [selectedConfigKey, setSelectedConfigKey] = useState<string>('');
|
const [selectedConfigKey, setSelectedConfigKey] = useState<string>('');
|
||||||
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
|
const [hasUnsavedChanges, setHasUnsavedChanges] = useState<boolean>(false);
|
||||||
const [envSyncModalVisible, setEnvSyncModalVisible] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// 加载所有配置
|
// 加载所有配置
|
||||||
const loadConfigs = async () => {
|
const loadConfigs = async () => {
|
||||||
@@ -57,7 +48,7 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 保存配置
|
// 保存配置
|
||||||
const handleSaveConfigs = async (configRequests: SaveConfigRequest[], group: string) => {
|
const handleSaveConfigs = async (configRequests: SaveConfigRequest[]) => {
|
||||||
try {
|
try {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const result = await systemConfigService.saveConfigs(configRequests);
|
const result = await systemConfigService.saveConfigs(configRequests);
|
||||||
@@ -107,25 +98,14 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
setHistoryModalVisible(true);
|
setHistoryModalVisible(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开.env同步模态框
|
|
||||||
const handleOpenEnvSync = () => {
|
|
||||||
setEnvSyncModalVisible(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理.env同步完成
|
|
||||||
const handleEnvSyncComplete = () => {
|
|
||||||
message.success('配置已成功同步到.env文件');
|
|
||||||
loadConfigs();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检测未保存的更改
|
// 检测未保存的更改
|
||||||
const handleConfigChange = () => {
|
const handleConfigChange = () => {
|
||||||
setHasUnsavedChanges(true);
|
setHasUnsavedChanges(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取指定分组的配置
|
// 获取指定分组的配置
|
||||||
const getConfigsByGroup = (group: string) => {
|
const getConfigsByGroup = (_group: string) => {
|
||||||
return configs.filter(config => config.config_group === group);
|
return configs.filter(config => config.config_group === _group);
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabItems = [
|
const tabItems = [
|
||||||
@@ -142,7 +122,7 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
configs={getConfigsByGroup('basic')}
|
configs={getConfigsByGroup('basic')}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onSave={(requests) => handleSaveConfigs(requests, 'basic')}
|
onSave={handleSaveConfigs}
|
||||||
onReset={handleResetConfig}
|
onReset={handleResetConfig}
|
||||||
onShowHistory={handleViewHistory}
|
onShowHistory={handleViewHistory}
|
||||||
onConfigChange={handleConfigChange}
|
onConfigChange={handleConfigChange}
|
||||||
@@ -162,7 +142,7 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
configs={getConfigsByGroup('security')}
|
configs={getConfigsByGroup('security')}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onSave={(requests) => handleSaveConfigs(requests, 'security')}
|
onSave={handleSaveConfigs}
|
||||||
onReset={handleResetConfig}
|
onReset={handleResetConfig}
|
||||||
onShowHistory={handleViewHistory}
|
onShowHistory={handleViewHistory}
|
||||||
onConfigChange={handleConfigChange}
|
onConfigChange={handleConfigChange}
|
||||||
@@ -182,7 +162,7 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
configs={getConfigsByGroup('game')}
|
configs={getConfigsByGroup('game')}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
onSave={(requests) => handleSaveConfigs(requests, 'game')}
|
onSave={handleSaveConfigs}
|
||||||
onReset={handleResetConfig}
|
onReset={handleResetConfig}
|
||||||
onShowHistory={handleViewHistory}
|
onShowHistory={handleViewHistory}
|
||||||
onConfigChange={handleConfigChange}
|
onConfigChange={handleConfigChange}
|
||||||
@@ -206,25 +186,6 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ display: 'flex', gap: '12px' }}>
|
<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
|
<button
|
||||||
onClick={loadConfigs}
|
onClick={loadConfigs}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
@@ -285,14 +246,6 @@ const SystemConfigPage: React.FC<SystemConfigPageProps> = () => {
|
|||||||
setSelectedConfigKey('');
|
setSelectedConfigKey('');
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* .env文件同步模态框 */}
|
|
||||||
<EnvSyncModal
|
|
||||||
visible={envSyncModalVisible}
|
|
||||||
configs={configs}
|
|
||||||
onClose={() => setEnvSyncModalVisible(false)}
|
|
||||||
onSync={handleEnvSyncComplete}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
|
|||||||
<div className="game-config-tab" style={{ padding: '0' }}>
|
<div className="game-config-tab" style={{ padding: '0' }}>
|
||||||
{/* 游戏通信警告 */}
|
{/* 游戏通信警告 */}
|
||||||
<Alert
|
<Alert
|
||||||
message="游戏通信配置警告"
|
title="游戏通信配置警告"
|
||||||
description="错误的游戏通信配置可能导致无法与游戏服务端正常交互,请确保API地址和密钥配置正确。"
|
description="错误的游戏通信配置可能导致无法与游戏服务端正常交互,请确保API地址和密钥配置正确。"
|
||||||
type="warning"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
@@ -204,7 +204,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
|
|||||||
{/* API地址验证提示 */}
|
{/* API地址验证提示 */}
|
||||||
{formData.game_server_api && (
|
{formData.game_server_api && (
|
||||||
<Alert
|
<Alert
|
||||||
message={`API地址验证: ${apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}`}
|
title={`API地址验证: ${apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}`}
|
||||||
description={apiUrlValidation.message}
|
description={apiUrlValidation.message}
|
||||||
type={apiUrlValidation.valid ? 'success' : 'error'}
|
type={apiUrlValidation.valid ? 'success' : 'error'}
|
||||||
showIcon
|
showIcon
|
||||||
@@ -215,7 +215,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
|
|||||||
{/* PSK密钥强度提示 */}
|
{/* PSK密钥强度提示 */}
|
||||||
{formData.game_server_psk && (
|
{formData.game_server_psk && (
|
||||||
<Alert
|
<Alert
|
||||||
message={`PSK密钥强度: ${pskStrength.level.toUpperCase()}`}
|
title={`PSK密钥强度: ${pskStrength.level.toUpperCase()}`}
|
||||||
description={pskStrength.message}
|
description={pskStrength.message}
|
||||||
type={pskStrength.level === 'strong' ? 'success' : pskStrength.level === 'medium' ? 'warning' : 'error'}
|
type={pskStrength.level === 'strong' ? 'success' : pskStrength.level === 'medium' ? 'warning' : 'error'}
|
||||||
showIcon
|
showIcon
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
|
|||||||
<div className="security-config-tab" style={{ padding: '0' }}>
|
<div className="security-config-tab" style={{ padding: '0' }}>
|
||||||
{/* 安全警告 */}
|
{/* 安全警告 */}
|
||||||
<Alert
|
<Alert
|
||||||
message="安全配置警告"
|
title="安全配置警告"
|
||||||
description="安全配置直接影响系统安全性,请谨慎修改。错误的配置可能导致系统无法正常运行。"
|
description="安全配置直接影响系统安全性,请谨慎修改。错误的配置可能导致系统无法正常运行。"
|
||||||
type="warning"
|
type="warning"
|
||||||
showIcon
|
showIcon
|
||||||
@@ -222,7 +222,7 @@ const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
|
|||||||
{/* JWT密钥强度提示 */}
|
{/* JWT密钥强度提示 */}
|
||||||
{formData.jwt_secret && (
|
{formData.jwt_secret && (
|
||||||
<Alert
|
<Alert
|
||||||
message={`JWT密钥强度: ${jwtSecretStrength.level.toUpperCase()}`}
|
title={`JWT密钥强度: ${jwtSecretStrength.level.toUpperCase()}`}
|
||||||
description={jwtSecretStrength.message}
|
description={jwtSecretStrength.message}
|
||||||
type={jwtSecretStrength.level === 'strong' ? 'success' : jwtSecretStrength.level === 'medium' ? 'warning' : 'error'}
|
type={jwtSecretStrength.level === 'strong' ? 'success' : jwtSecretStrength.level === 'medium' ? 'warning' : 'error'}
|
||||||
showIcon
|
showIcon
|
||||||
|
|||||||
Reference in New Issue
Block a user