fix: 🐛 修复系统配置页面报错的问题

This commit is contained in:
Stev_Wang
2025-12-12 20:20:29 +08:00
parent fb51d51215
commit 2ca4cd60f6
3 changed files with 226 additions and 146 deletions

View File

@@ -30,7 +30,7 @@ interface BasicConfigTabProps {
const BasicConfigTab: React.FC<BasicConfigTabProps> = ({ const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
configs, configs,
loading, // loading,
saving, saving,
onSave, onSave,
onReset, onReset,
@@ -38,20 +38,26 @@ const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
onConfigChange onConfigChange
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, any>>({}); const [formData, setFormData] = useState<Record<string, unknown>>(() => {
// 初始化时设置默认数据
// 初始化表单数据 const initialData: Record<string, unknown> = {};
useEffect(() => { configs.forEach(config => {
const initialData: Record<string, any> = {}; initialData[config.config_key] = config.config_value;
});
return initialData;
});
// 初始化表单数据
useEffect(() => {
const initialData: Record<string, unknown> = {};
configs.forEach(config => { configs.forEach(config => {
initialData[config.config_key] = config.config_value; initialData[config.config_key] = config.config_value;
}); });
setFormData(initialData);
form.setFieldsValue(initialData); form.setFieldsValue(initialData);
}, [configs, form]); }, [configs, form]);
// 处理表单值变化 // 处理表单值变化
const handleValuesChange = (changedValues: any, allValues: any) => { const handleValuesChange = (_changedValues: Record<string, unknown>, allValues: Record<string, unknown>) => {
setFormData(allValues); setFormData(allValues);
onConfigChange(); onConfigChange();
}; };
@@ -61,9 +67,10 @@ const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
form.validateFields().then(() => { form.validateFields().then(() => {
const saveRequests: SaveConfigRequest[] = configs.map(config => ({ const saveRequests: SaveConfigRequest[] = configs.map(config => ({
config_key: config.config_key, config_key: config.config_key,
config_value: formData[config.config_key] || '', config_value: String(formData[config.config_key] || ''),
config_label: config.config_label, config_label: config.config_label,
config_group: config.config_group config_group: config.config_group,
config_type: config.config_type
})); }));
onSave(saveRequests); onSave(saveRequests);
}).catch(() => { }).catch(() => {
@@ -155,7 +162,7 @@ const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
style={{ marginBottom: '24px' }} style={{ marginBottom: '24px' }}
> >
<Row gutter={[24, 0]}> <Row gutter={[24, 0]}>
{configItems.map((item, index) => { {configItems.map((item) => {
const config = configs.find(c => c.config_key === item.key); const config = configs.find(c => c.config_key === item.key);
if (!config) return null; if (!config) return null;
@@ -218,11 +225,10 @@ const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
{/* 输入控件 */} {/* 输入控件 */}
<Form.Item <Form.Item
name={item.key}
rules={[ rules={[
{ required: item.required, message: `请输入${item.title}` }, { required: item.required, message: `请输入${item.title}` },
...(item.key === 'admin_email' ? [ ...(item.key === 'admin_email' ? [
{ type: 'email', message: '请输入有效的邮箱地址' } { type: 'email' as const, message: '请输入有效的邮箱地址' }
] : []) ] : [])
]} ]}
> >
@@ -230,16 +236,33 @@ const BasicConfigTab: React.FC<BasicConfigTabProps> = ({
<Input <Input
placeholder={item.placeholder} placeholder={item.placeholder}
disabled={config.config_type === 'boolean'} disabled={config.config_type === 'boolean'}
value={String(formData[item.key] || '')}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/> />
)} )}
{item.type === 'textarea' && ( {item.type === 'textarea' && (
<TextArea <TextArea
placeholder={item.placeholder} placeholder={item.placeholder}
rows={item.rows || 2} rows={item.rows || 2}
value={String(formData[item.key] || '')}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/> />
)} )}
{item.type === 'select' && ( {item.type === 'select' && (
<Select placeholder={item.placeholder}> <Select
placeholder={item.placeholder}
value={String(formData[item.key] || '')}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
>
{item.options?.map(option => ( {item.options?.map(option => (
<Option key={option.value} value={option.value}> <Option key={option.value} value={option.value}>
{option.label} {option.label}

View File

@@ -5,7 +5,7 @@
*/ */
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Form, Input, InputNumber, Switch, Card, Button, Space, Row, Col, Tooltip, Alert } from 'antd'; import { Form, Input, InputNumber, Switch, Select, Card, Button, Space, Row, Col, Tooltip, Alert } from 'antd';
import { import {
InfoCircleOutlined, InfoCircleOutlined,
SaveOutlined, SaveOutlined,
@@ -21,10 +21,10 @@ import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
const { TextArea } = Input; const { TextArea } = Input;
interface GameConfigTabProps { interface GameConfigTabProps {
configs: SystemConfig[]; configs: SystemConfig[];
loading: boolean; saving?: boolean;
saving: boolean;
onSave: (requests: SaveConfigRequest[]) => void; onSave: (requests: SaveConfigRequest[]) => void;
onReset: (configKey: string) => void; onReset: (configKey: string) => void;
onShowHistory: (configKey: string) => void; onShowHistory: (configKey: string) => void;
@@ -33,7 +33,6 @@ interface GameConfigTabProps {
const GameConfigTab: React.FC<GameConfigTabProps> = ({ const GameConfigTab: React.FC<GameConfigTabProps> = ({
configs, configs,
loading,
saving, saving,
onSave, onSave,
onReset, onReset,
@@ -41,11 +40,11 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
onConfigChange onConfigChange
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, any>>({}); const [formData, setFormData] = useState<Record<string, unknown>>({});
// 初始化表单数据 // 初始化表单数据
useEffect(() => { useEffect(() => {
const initialData: Record<string, any> = {}; const initialData: Record<string, unknown> = {};
configs.forEach(config => { configs.forEach(config => {
if (config.config_type === 'boolean') { if (config.config_type === 'boolean') {
initialData[config.config_key] = config.config_value === '1' || config.config_value === 'true'; initialData[config.config_key] = config.config_value === '1' || config.config_value === 'true';
@@ -53,12 +52,17 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
initialData[config.config_key] = config.config_value; initialData[config.config_key] = config.config_value;
} }
}); });
setFormData(initialData); // 延迟设置表单值,避免在渲染期间直接修改
form.setFieldsValue(initialData); const timer = setTimeout(() => {
setFormData(initialData);
form.setFieldsValue(initialData);
}, 0);
return () => clearTimeout(timer);
}, [configs, form]); }, [configs, form]);
// 处理表单值变化 // 处理表单值变化
const handleValuesChange = (changedValues: any, allValues: any) => { const handleValuesChange = (changedValues: Record<string, unknown>, allValues: Record<string, unknown>) => {
setFormData(allValues); setFormData(allValues);
onConfigChange(); onConfigChange();
}; };
@@ -69,8 +73,9 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
const saveRequests: SaveConfigRequest[] = configs.map(config => ({ const saveRequests: SaveConfigRequest[] = configs.map(config => ({
config_key: config.config_key, config_key: config.config_key,
config_value: config.config_type === 'boolean' config_value: config.config_type === 'boolean'
? (formData[config.config_key] ? '1' : '0') ? ((formData[config.config_key] as boolean) ? '1' : '0')
: String(formData[config.config_key] || ''), : String(formData[config.config_key] || ''),
config_type: config.config_type,
config_label: config.config_label, config_label: config.config_label,
config_group: config.config_group config_group: config.config_group
})); }));
@@ -92,8 +97,9 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
// 检查API地址格式 // 检查API地址格式
const validateApiUrl = (url: string) => { const validateApiUrl = (url: string) => {
if (!url) return { valid: false, message: 'API地址不能为空' }; const urlStr = String(url || '');
if (!/^https?:\/\/.+/.test(url)) { if (!urlStr) return { valid: false, message: 'API地址不能为空' };
if (!/^https?:\/\/.+/.test(urlStr)) {
return { valid: false, message: '请输入有效的HTTP/HTTPS地址' }; return { valid: false, message: '请输入有效的HTTP/HTTPS地址' };
} }
return { valid: true, message: 'API地址格式正确' }; return { valid: true, message: 'API地址格式正确' };
@@ -101,10 +107,12 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
// 检查PSK密钥强度 // 检查PSK密钥强度
const checkPskStrength = (psk: string) => { const checkPskStrength = (psk: string) => {
if (!psk) return { level: 'weak', message: 'PSK密钥不能为空' }; const pskStr = String(psk || '');
if (psk.length < 32) return { level: 'weak', message: 'PSK密钥至少需要32位字符' }; if (!pskStr) return { level: 'weak', message: 'PSK密钥不能为空' };
if (psk.length < 64) return { level: 'medium', message: 'PSK密钥长度适中,建议使用更长的密钥' }; if (pskStr.length < 8) return { level: 'weak', message: 'PSK密钥长度不足8位' };
return { level: 'strong', message: 'PSK密钥强度良好' }; if (pskStr.length < 32) return { level: 'medium', message: 'PSK密钥长度不足32位建议使用更强的密钥' };
if (!/[!@#$%^&*(),.?":{}|<>]/.test(pskStr)) return { level: 'medium', message: 'PSK密钥缺少特殊字符建议添加特殊字符' };
return { level: 'strong', message: 'PSK密钥强度符合要求' };
}; };
const apiUrlValidation = validateApiUrl(formData.game_server_api || ''); const apiUrlValidation = validateApiUrl(formData.game_server_api || '');
@@ -223,7 +231,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
style={{ marginBottom: '24px' }} style={{ marginBottom: '24px' }}
> >
<Row gutter={[24, 0]}> <Row gutter={[24, 0]}>
{configItems.map((item, index) => { {configItems.map((item) => {
const config = configs.find(c => c.config_key === item.key); const config = configs.find(c => c.config_key === item.key);
if (!config) return null; if (!config) return null;
@@ -278,74 +286,104 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
</div> </div>
{/* 输入控件 */} {/* 输入控件 */}
<Form.Item {item.type === 'switch' ? (
name={item.key} // Switch组件不需要表单验证直接渲染
rules={[ <div style={{ marginTop: '8px' }}>
{ required: item.required, message: `请输入${item.title}` }, <Switch
...(item.key === 'game_server_api' ? [ checked={Boolean(formData[item.key])}
onChange={(checked) => {
form.setFieldValue(item.key, checked);
onConfigChange();
}}
/>
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
{String(formData[item.key] || '') ? '已启用' : '未启用'}
</span>
</div>
) : (
// 其他输入控件使用Form.Item进行布局但使用value/onChange进行状态管理
<Form.Item
rules={[
{ {
pattern: /^https?:\/\/.+/, required: item.required,
message: '请输入有效的HTTP/HTTPS地址' message: `请输入${item.title}`
} },
] : []), ...(item.key === 'game_server_api' ? [
...(item.key === 'game_server_psk' ? [ {
{ min: 32, message: 'PSK密钥至少需要32位字符' } validator: (_: unknown, value: string) => {
] : []) if (!value || !/^https?:\/\/.+/.test(value)) {
]} return Promise.reject(new Error('请输入有效的API地址'));
> }
{item.type === 'input' && ( return Promise.resolve();
<Input }
placeholder={item.placeholder} }
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }} ] : []),
/> ...(item.key === 'game_server_psk' ? [
)} {
{item.type === 'textarea' && ( min: 32,
<TextArea message: 'PSK密钥至少需要32位字符'
placeholder={item.placeholder} }
rows={item.rows || 3} ] : [])
maxLength={500} ]}
showCount={!item.sensitive} >
autoSize={{ minRows: item.rows || 3, maxRows: 6 }} {item.type === 'input' && (
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 <Input
style={{ width: 'calc(100% - 80px)' }}
placeholder={item.placeholder} placeholder={item.placeholder}
readOnly disabled={config.config_type === 'boolean'}
/> value={formData[item.key]}
<InputNumber onChange={(e) => {
style={{ width: '80px' }} form.setFieldValue(item.key, e.target.value);
placeholder="级别"
/>
</Input.Group>
)}
{item.type === 'switch' && (
<div style={{ marginTop: '8px' }}>
<Switch
checked={formData[item.key]}
onChange={(checked) => {
form.setFieldValue(item.key, checked);
onConfigChange(); onConfigChange();
}} }}
/> />
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}> )}
{formData[item.key] ? '已启用' : '未启用'} {item.type === 'textarea' && (
</span> <TextArea
</div> placeholder={item.placeholder}
)} rows={item.rows || 3}
</Form.Item> maxLength={500}
showCount={!item.sensitive}
autoSize={{ minRows: item.rows || 3, maxRows: 6 }}
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
value={formData[item.key]}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'inputnumber' && (
<InputNumber
style={{ width: '100%' }}
min={item.min}
max={item.max}
placeholder={item.placeholder}
suffix={item.suffix}
value={formData[item.key]}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
/>
)}
{item.type === 'select' && (
<Select
placeholder={item.placeholder}
value={formData[item.key]}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
>
{item.options?.map((option, optionIndex) => (
<Select.Option key={`${item.key}-${option.value}-${optionIndex}`} value={option.value}>
{option.label}
</Select.Option>
))}
</Select>
)}
</Form.Item>
)}
{/* 特殊字段验证信息 */} {/* 特殊字段验证信息 */}
{item.key === 'game_server_api' && formData.game_server_api && ( {item.key === 'game_server_api' && formData.game_server_api && (
@@ -362,7 +400,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
{apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'} {apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}
</div> </div>
<div> <div>
{formData.game_server_api.startsWith('https') ? 'HTTPS' : 'HTTP'} {String(formData.game_server_api || '').startsWith('https') ? 'HTTPS' : 'HTTP'}
</div> </div>
</div> </div>
)} )}
@@ -377,11 +415,11 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
}}> }}>
<strong></strong> <strong></strong>
<div style={{ marginTop: '4px' }}> <div style={{ marginTop: '4px' }}>
: {formData.game_server_psk.length} : {String(formData.game_server_psk || '').length}
{formData.game_server_psk.length >= 32 && ' ✅'} {String(formData.game_server_psk || '').length >= 32 && ' ✅'}
</div> </div>
<div> <div>
: {/[!@#$%^&*(),.?":{}|<>]/.test(formData.game_server_psk) ? '✅' : '❌'} : {/[!@#$%^&*(),.?":{}|<>]/.test(String(formData.game_server_psk || '')) ? '✅' : '❌'}
</div> </div>
</div> </div>
)} )}

View File

@@ -39,7 +39,7 @@ type FormData = Record<string, FormFieldValue>;
key: string; key: string;
title: string; title: string;
description: string; description: string;
type: 'input' | 'textarea' | 'inputnumber' | 'switch'; type: 'input' | 'textarea' | 'inputnumber' | 'switch' | 'select';
required: boolean; required: boolean;
sensitive?: boolean; sensitive?: boolean;
placeholder?: string; placeholder?: string;
@@ -48,10 +48,12 @@ type FormData = Record<string, FormFieldValue>;
rows?: number; rows?: number;
suffix?: string; suffix?: string;
icon?: React.ReactNode; icon?: React.ReactNode;
} options?: Array<{ value: string; label: string }>;
}
const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({ const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
configs, configs,
// loading,
saving, saving,
onSave, onSave,
onReset, onReset,
@@ -76,7 +78,7 @@ const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
}, [configs, form]); }, [configs, form]);
// 处理表单值变化 // 处理表单值变化
const handleValuesChange = (_changedValues: Partial<FormData>, allValues: FormData) => { const handleValuesChange = (changedValues: Partial<FormData>, allValues: FormData) => {
setFormData(allValues); setFormData(allValues);
onConfigChange(); onConfigChange();
}; };
@@ -291,56 +293,73 @@ const SecurityConfigTab: React.FC<SecurityConfigTabProps> = ({
</div> </div>
{/* 输入控件 */} {/* 输入控件 */}
<Form.Item {item.type === 'switch' ? (
name={item.key} // Switch组件不需要表单验证直接渲染
rules={[ <div style={{ marginTop: '8px' }}>
{ required: item.required, message: `请输入${item.title}` }, <Switch
...(item.key === 'jwt_secret' ? [ checked={Boolean(formData[item.key])}
{ min: 32, message: 'JWT密钥至少需要32位字符' } onChange={(checked) => {
] : []) form.setFieldValue(item.key, checked);
]} onConfigChange();
> }}
{item.type === 'input' && (
<Input.Password
placeholder={item.placeholder}
disabled={config.config_type === 'boolean'}
visibilityToggle={!item.sensitive}
/> />
)} <span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
{item.type === 'textarea' && ( {String(formData[item.key] || '') ? '已启用' : '未启用'}
<TextArea </span>
placeholder={item.placeholder} </div>
rows={item.rows || 3} ) : (
maxLength={500} // 其他输入控件使用Form.Item进行布局但使用value/onChange进行状态管理
showCount={!item.sensitive} <Form.Item
autoSize={{ minRows: item.rows || 3, maxRows: 6 }} rules={[
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }} { required: item.required, message: `请输入${item.title}` },
/> ...(item.key === 'jwt_secret' ? [
)} { min: 32 as const, message: 'JWT密钥至少需要32位字符' }
{item.type === 'inputnumber' && ( ] : [])
<InputNumber ]}
style={{ width: '100%' }} >
min={item.min} {item.type === 'input' && (
max={item.max} <Input.Password
placeholder={item.placeholder} placeholder={item.placeholder}
suffix={item.suffix} disabled={config.config_type === 'boolean'}
/> visibilityToggle={!item.sensitive}
)} value={formData[item.key]}
{item.type === 'switch' && ( onChange={(e) => {
<div style={{ marginTop: '8px' }}> form.setFieldValue(item.key, e.target.value);
<Switch
checked={Boolean(formData[item.key])}
onChange={(checked) => {
form.setFieldValue(item.key, checked);
onConfigChange(); onConfigChange();
}} }}
/> />
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}> )}
{formData[item.key] ? '已启用' : '未启用'} {item.type === 'textarea' && (
</span> <TextArea
</div> placeholder={item.placeholder}
)} rows={item.rows || 3}
</Form.Item> maxLength={500}
showCount={!item.sensitive}
autoSize={{ minRows: item.rows || 3, maxRows: 6 }}
style={{ fontFamily: item.sensitive ? 'monospace' : 'inherit' }}
value={formData[item.key]}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
)}
{item.type === 'inputnumber' && (
<InputNumber
style={{ width: '100%' }}
min={item.min}
max={item.max}
placeholder={item.placeholder}
suffix={item.suffix}
value={formData[item.key]}
onChange={(value) => {
form.setFieldValue(item.key, value);
onConfigChange();
}}
/>
)}
</Form.Item>
)}
{/* JWT密钥特殊提示 */} {/* JWT密钥特殊提示 */}
{item.key === 'jwt_secret' && formData.jwt_secret && ( {item.key === 'jwt_secret' && formData.jwt_secret && (