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

View File

@@ -5,7 +5,7 @@
*/
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 {
InfoCircleOutlined,
SaveOutlined,
@@ -21,10 +21,10 @@ import { SystemConfig, SaveConfigRequest } from '../../types/systemConfig';
const { TextArea } = Input;
interface GameConfigTabProps {
configs: SystemConfig[];
loading: boolean;
saving: boolean;
saving?: boolean;
onSave: (requests: SaveConfigRequest[]) => void;
onReset: (configKey: string) => void;
onShowHistory: (configKey: string) => void;
@@ -33,7 +33,6 @@ interface GameConfigTabProps {
const GameConfigTab: React.FC<GameConfigTabProps> = ({
configs,
loading,
saving,
onSave,
onReset,
@@ -41,11 +40,11 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
onConfigChange
}) => {
const [form] = Form.useForm();
const [formData, setFormData] = useState<Record<string, any>>({});
const [formData, setFormData] = useState<Record<string, unknown>>({});
// 初始化表单数据
useEffect(() => {
const initialData: Record<string, any> = {};
const initialData: Record<string, unknown> = {};
configs.forEach(config => {
if (config.config_type === 'boolean') {
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;
}
});
setFormData(initialData);
form.setFieldsValue(initialData);
// 延迟设置表单值,避免在渲染期间直接修改
const timer = setTimeout(() => {
setFormData(initialData);
form.setFieldsValue(initialData);
}, 0);
return () => clearTimeout(timer);
}, [configs, form]);
// 处理表单值变化
const handleValuesChange = (changedValues: any, allValues: any) => {
const handleValuesChange = (changedValues: Record<string, unknown>, allValues: Record<string, unknown>) => {
setFormData(allValues);
onConfigChange();
};
@@ -69,8 +73,9 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
const saveRequests: SaveConfigRequest[] = configs.map(config => ({
config_key: config.config_key,
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] || ''),
config_type: config.config_type,
config_label: config.config_label,
config_group: config.config_group
}));
@@ -92,8 +97,9 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
// 检查API地址格式
const validateApiUrl = (url: string) => {
if (!url) return { valid: false, message: 'API地址不能为空' };
if (!/^https?:\/\/.+/.test(url)) {
const urlStr = String(url || '');
if (!urlStr) return { valid: false, message: 'API地址不能为空' };
if (!/^https?:\/\/.+/.test(urlStr)) {
return { valid: false, message: '请输入有效的HTTP/HTTPS地址' };
}
return { valid: true, message: 'API地址格式正确' };
@@ -101,10 +107,12 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
// 检查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 pskStr = String(psk || '');
if (!pskStr) return { level: 'weak', message: 'PSK密钥不能为空' };
if (pskStr.length < 8) return { level: 'weak', message: 'PSK密钥长度不足8位' };
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 || '');
@@ -223,7 +231,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
style={{ marginBottom: '24px' }}
>
<Row gutter={[24, 0]}>
{configItems.map((item, index) => {
{configItems.map((item) => {
const config = configs.find(c => c.config_key === item.key);
if (!config) return null;
@@ -278,74 +286,104 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
</div>
{/* 输入控件 */}
<Form.Item
name={item.key}
rules={[
{ required: item.required, message: `请输入${item.title}` },
...(item.key === 'game_server_api' ? [
{item.type === 'switch' ? (
// 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' }}>
{String(formData[item.key] || '') ? '已启用' : '未启用'}
</span>
</div>
) : (
// 其他输入控件使用Form.Item进行布局但使用value/onChange进行状态管理
<Form.Item
rules={[
{
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>
required: item.required,
message: `请输入${item.title}`
},
...(item.key === 'game_server_api' ? [
{
validator: (_: unknown, value: string) => {
if (!value || !/^https?:\/\/.+/.test(value)) {
return Promise.reject(new Error('请输入有效的API地址'));
}
return Promise.resolve();
}
}
] : []),
...(item.key === 'game_server_psk' ? [
{
min: 32,
message: 'PSK密钥至少需要32位字符'
}
] : [])
]}
>
{item.type === 'input' && (
<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);
disabled={config.config_type === 'boolean'}
value={formData[item.key]}
onChange={(e) => {
form.setFieldValue(item.key, e.target.value);
onConfigChange();
}}
/>
<span style={{ marginLeft: '8px', fontSize: '12px', color: '#666' }}>
{formData[item.key] ? '已启用' : '未启用'}
</span>
</div>
)}
</Form.Item>
)}
{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' }}
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 && (
@@ -362,7 +400,7 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
{apiUrlValidation.valid ? '✅ 格式正确' : '❌ 格式错误'}
</div>
<div>
{formData.game_server_api.startsWith('https') ? 'HTTPS' : 'HTTP'}
{String(formData.game_server_api || '').startsWith('https') ? 'HTTPS' : 'HTTP'}
</div>
</div>
)}
@@ -377,11 +415,11 @@ const GameConfigTab: React.FC<GameConfigTabProps> = ({
}}>
<strong></strong>
<div style={{ marginTop: '4px' }}>
: {formData.game_server_psk.length}
{formData.game_server_psk.length >= 32 && ' ✅'}
: {String(formData.game_server_psk || '').length}
{String(formData.game_server_psk || '').length >= 32 && ' ✅'}
</div>
<div>
: {/[!@#$%^&*(),.?":{}|<>]/.test(formData.game_server_psk) ? '✅' : '❌'}
: {/[!@#$%^&*(),.?":{}|<>]/.test(String(formData.game_server_psk || '')) ? '✅' : '❌'}
</div>
</div>
)}

View File

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