feat(ConfigGroup, SystemConfig): add support for secret fields in configuration forms

This commit is contained in:
shiyu
2025-05-25 12:08:15 +08:00
parent 6866580dce
commit a5c7959c4a
3 changed files with 83 additions and 44 deletions
+1
View File
@@ -195,6 +195,7 @@ export interface ConfigResponse {
key: string; key: string;
value: string; value: string;
description: string; description: string;
isSecret: boolean;
createdAt: Date; createdAt: Date;
updatedAt?: Date; updatedAt?: Date;
} }
+62 -44
View File
@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Form, Input, Button, Space, Row, Col, Tooltip } from 'antd'; import { Form, Input, Button, Space, Row, Col, Tooltip } from 'antd';
import { SaveOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { SaveOutlined, QuestionCircleOutlined, LockOutlined } from '@ant-design/icons';
interface ConfigGroupProps { interface ConfigGroupProps {
groupName: string; groupName: string;
@@ -11,6 +11,7 @@ interface ConfigGroupProps {
descriptions: { descriptions: {
[key: string]: string; [key: string]: string;
}; };
secretFields?: string[];
isMobile?: boolean; isMobile?: boolean;
} }
@@ -19,6 +20,7 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
configs, configs,
onSave, onSave,
descriptions, descriptions,
secretFields = [],
isMobile = false isMobile = false
}) => { }) => {
const [form] = Form.useForm(); const [form] = Form.useForm();
@@ -45,6 +47,10 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
} }
}; };
const isSecretField = (key: string): boolean => {
return secretFields.includes(key);
};
return ( return (
<Form <Form
form={form} form={form}
@@ -52,49 +58,61 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
initialValues={configs} initialValues={configs}
size={isMobile ? "middle" : "large"} size={isMobile ? "middle" : "large"}
> >
{Object.keys(configs).map(key => ( {Object.keys(configs).map(key => {
<Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle"> const isSecret = isSecretField(key);
<Col xs={24} lg={16}>
<Form.Item return (
name={key} <Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle">
label={ <Col xs={24} lg={16}>
<Space> <Form.Item
{key} name={key}
{descriptions[key] && ( label={
<Tooltip title={descriptions[key]}> <Space>
<QuestionCircleOutlined /> {key}
</Tooltip> {isSecret && <LockOutlined style={{ color: '#faad14' }} />}
)} {descriptions[key] && (
</Space> <Tooltip title={descriptions[key]}>
} <QuestionCircleOutlined />
> </Tooltip>
{key.toLowerCase().includes('secret') || key.toLowerCase().includes('key') || key.toLowerCase().includes('password') ? ( )}
<Input.Password placeholder={`请输入${key}`} /> </Space>
) : ( }
<Input placeholder={`请输入${key}`} /> extra={isSecret &&
)} <div style={{ fontSize: '12px', color: '#faad14', marginTop: '4px' }}>
</Form.Item>
</Col> </div>
<Col xs={24} lg={8} style={{ }
textAlign: isMobile ? 'left' : 'right', >
marginTop: isMobile ? -10 : 0, {isSecret ? (
marginBottom: isMobile ? 10 : 0 <Input.Password
}}> placeholder={configs[key] === '' ? '请输入新值' : '******(已设置,输入新值以更新)'}
<Button />
type="primary" ) : (
icon={<SaveOutlined />} <Input placeholder={`请输入${key}`} />
onClick={() => handleSaveSingle(key)} )}
style={{ </Form.Item>
marginBottom: isMobile ? 16 : 24, </Col>
width: isMobile ? '100%' : 'auto' <Col xs={24} lg={8} style={{
}} textAlign: isMobile ? 'left' : 'right',
size={isMobile ? "middle" : "large"} marginTop: isMobile ? -10 : 0,
> marginBottom: isMobile ? 10 : 0
}}>
</Button> <Button
</Col> type="primary"
</Row> icon={<SaveOutlined />}
))} onClick={() => handleSaveSingle(key)}
style={{
marginBottom: isMobile ? 16 : 24,
width: isMobile ? '100%' : 'auto'
}}
size={isMobile ? "middle" : "large"}
>
</Button>
</Col>
</Row>
);
})}
<Form.Item> <Form.Item>
<Button <Button
+20
View File
@@ -24,6 +24,7 @@ const SystemConfig: React.FC = () => {
const [restoreLoading, setRestoreLoading] = useState(false); const [restoreLoading, setRestoreLoading] = useState(false);
const [restoreModalVisible, setRestoreModalVisible] = useState(false); const [restoreModalVisible, setRestoreModalVisible] = useState(false);
const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null); const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null);
const [secretFields, setSecretFields] = useState<Record<string, string[]>>({}); // 新增状态管理私密字段
// 获取所有配置项 // 获取所有配置项
const fetchConfigs = async () => { const fetchConfigs = async () => {
@@ -32,15 +33,27 @@ const SystemConfig: React.FC = () => {
const response = await getAllConfigs(); const response = await getAllConfigs();
if (response.success && response.data) { if (response.success && response.data) {
const configGroups: ConfigStructure = {}; const configGroups: ConfigStructure = {};
const secretFieldsMap: Record<string, string[]> = {}; // 记录每个组的私密字段
response.data.forEach(config => { response.data.forEach(config => {
const [group, key] = config.key.split(':'); const [group, key] = config.key.split(':');
if (!configGroups[group]) { if (!configGroups[group]) {
configGroups[group] = {}; configGroups[group] = {};
secretFieldsMap[group] = [];
} }
configGroups[group][key] = config.value; configGroups[group][key] = config.value;
// 记录私密字段
if (config.isSecret) {
if (!secretFieldsMap[group]) {
secretFieldsMap[group] = [];
}
secretFieldsMap[group].push(key);
}
}); });
setConfigs(configGroups); setConfigs(configGroups);
setSecretFields(secretFieldsMap);
// 设置初始存储类型 // 设置初始存储类型
if (configGroups.Storage?.DefaultStorage) { if (configGroups.Storage?.DefaultStorage) {
@@ -245,6 +258,7 @@ const SystemConfig: React.FC = () => {
Model: 'AI 模型名称', Model: 'AI 模型名称',
EmbeddingModel: '嵌入向量模型名称' EmbeddingModel: '嵌入向量模型名称'
}} }}
secretFields={secretFields.AI || []}
isMobile={isMobile} isMobile={isMobile}
/> />
</TabPane> </TabPane>
@@ -265,6 +279,7 @@ const SystemConfig: React.FC = () => {
Issuer: 'JWT 签发者', Issuer: 'JWT 签发者',
Audience: 'JWT 接收者', Audience: 'JWT 接收者',
}} }}
secretFields={secretFields.Jwt || []}
isMobile={isMobile} isMobile={isMobile}
/> />
</TabPane> </TabPane>
@@ -282,6 +297,7 @@ const SystemConfig: React.FC = () => {
"GitHubClientSecret": 'GitHub OAuth 应用客户端密钥', "GitHubClientSecret": 'GitHub OAuth 应用客户端密钥',
"GitHubCallbackUrl": 'GitHub OAuth 认证回调地址' "GitHubCallbackUrl": 'GitHub OAuth 认证回调地址'
}} }}
secretFields={secretFields.Authentication || []}
isMobile={isMobile} isMobile={isMobile}
/> />
</TabPane> </TabPane>
@@ -543,6 +559,7 @@ const SystemConfig: React.FC = () => {
"TelegramStorageBotToken": 'Telegram 机器人令牌', "TelegramStorageBotToken": 'Telegram 机器人令牌',
"TelegramStorageChatId": 'Telegram 聊天ID' "TelegramStorageChatId": 'Telegram 聊天ID'
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}
@@ -569,6 +586,7 @@ const SystemConfig: React.FC = () => {
"S3StorageCdnUrl": 'CDN URL (可选,用于加速文件访问)', "S3StorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
"S3StorageUsePathStyleUrls": '使用路径形式URLs (true/false,兼容非AWS服务)' "S3StorageUsePathStyleUrls": '使用路径形式URLs (true/false,兼容非AWS服务)'
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}
@@ -593,6 +611,7 @@ const SystemConfig: React.FC = () => {
"CosStorageRegion": 'COS区域 (例如:ap-shanghai)', "CosStorageRegion": 'COS区域 (例如:ap-shanghai)',
"CosStorageCdnUrl": 'CDN URL (可选,用于加速文件访问)', "CosStorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}
@@ -615,6 +634,7 @@ const SystemConfig: React.FC = () => {
"WebDAVBasePath": 'WebDAV 基础路径 (例如: files/upload)', "WebDAVBasePath": 'WebDAV 基础路径 (例如: files/upload)',
"WebDAVPublicUrl": 'WebDAV 公共访问 URL (可选,用于文件访问)', "WebDAVPublicUrl": 'WebDAV 公共访问 URL (可选,用于文件访问)',
}} }}
secretFields={secretFields.Storage || []}
isMobile={isMobile} isMobile={isMobile}
/> />
)} )}