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

View File

@@ -195,6 +195,7 @@ export interface ConfigResponse {
key: string;
value: string;
description: string;
isSecret: boolean;
createdAt: Date;
updatedAt?: Date;
}

View File

@@ -1,6 +1,6 @@
import React from 'react';
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 {
groupName: string;
@@ -11,6 +11,7 @@ interface ConfigGroupProps {
descriptions: {
[key: string]: string;
};
secretFields?: string[];
isMobile?: boolean;
}
@@ -19,6 +20,7 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
configs,
onSave,
descriptions,
secretFields = [],
isMobile = false
}) => {
const [form] = Form.useForm();
@@ -45,6 +47,10 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
}
};
const isSecretField = (key: string): boolean => {
return secretFields.includes(key);
};
return (
<Form
form={form}
@@ -52,49 +58,61 @@ const ConfigGroup: React.FC<ConfigGroupProps> = ({
initialValues={configs}
size={isMobile ? "middle" : "large"}
>
{Object.keys(configs).map(key => (
<Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle">
<Col xs={24} lg={16}>
<Form.Item
name={key}
label={
<Space>
{key}
{descriptions[key] && (
<Tooltip title={descriptions[key]}>
<QuestionCircleOutlined />
</Tooltip>
)}
</Space>
}
>
{key.toLowerCase().includes('secret') || key.toLowerCase().includes('key') || key.toLowerCase().includes('password') ? (
<Input.Password placeholder={`请输入${key}`} />
) : (
<Input placeholder={`请输入${key}`} />
)}
</Form.Item>
</Col>
<Col xs={24} lg={8} style={{
textAlign: isMobile ? 'left' : 'right',
marginTop: isMobile ? -10 : 0,
marginBottom: isMobile ? 10 : 0
}}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => handleSaveSingle(key)}
style={{
marginBottom: isMobile ? 16 : 24,
width: isMobile ? '100%' : 'auto'
}}
size={isMobile ? "middle" : "large"}
>
</Button>
</Col>
</Row>
))}
{Object.keys(configs).map(key => {
const isSecret = isSecretField(key);
return (
<Row key={key} gutter={isMobile ? [8, 8] : [16, 16]} align="middle">
<Col xs={24} lg={16}>
<Form.Item
name={key}
label={
<Space>
{key}
{isSecret && <LockOutlined style={{ color: '#faad14' }} />}
{descriptions[key] && (
<Tooltip title={descriptions[key]}>
<QuestionCircleOutlined />
</Tooltip>
)}
</Space>
}
extra={isSecret &&
<div style={{ fontSize: '12px', color: '#faad14', marginTop: '4px' }}>
</div>
}
>
{isSecret ? (
<Input.Password
placeholder={configs[key] === '' ? '请输入新值' : '******(已设置,输入新值以更新)'}
/>
) : (
<Input placeholder={`请输入${key}`} />
)}
</Form.Item>
</Col>
<Col xs={24} lg={8} style={{
textAlign: isMobile ? 'left' : 'right',
marginTop: isMobile ? -10 : 0,
marginBottom: isMobile ? 10 : 0
}}>
<Button
type="primary"
icon={<SaveOutlined />}
onClick={() => handleSaveSingle(key)}
style={{
marginBottom: isMobile ? 16 : 24,
width: isMobile ? '100%' : 'auto'
}}
size={isMobile ? "middle" : "large"}
>
</Button>
</Col>
</Row>
);
})}
<Form.Item>
<Button

View File

@@ -24,6 +24,7 @@ const SystemConfig: React.FC = () => {
const [restoreLoading, setRestoreLoading] = useState(false);
const [restoreModalVisible, setRestoreModalVisible] = useState(false);
const [restoreConfig, setRestoreConfig] = useState<Record<string, string> | null>(null);
const [secretFields, setSecretFields] = useState<Record<string, string[]>>({}); // 新增状态管理私密字段
// 获取所有配置项
const fetchConfigs = async () => {
@@ -32,15 +33,27 @@ const SystemConfig: React.FC = () => {
const response = await getAllConfigs();
if (response.success && response.data) {
const configGroups: ConfigStructure = {};
const secretFieldsMap: Record<string, string[]> = {}; // 记录每个组的私密字段
response.data.forEach(config => {
const [group, key] = config.key.split(':');
if (!configGroups[group]) {
configGroups[group] = {};
secretFieldsMap[group] = [];
}
configGroups[group][key] = config.value;
// 记录私密字段
if (config.isSecret) {
if (!secretFieldsMap[group]) {
secretFieldsMap[group] = [];
}
secretFieldsMap[group].push(key);
}
});
setConfigs(configGroups);
setSecretFields(secretFieldsMap);
// 设置初始存储类型
if (configGroups.Storage?.DefaultStorage) {
@@ -245,6 +258,7 @@ const SystemConfig: React.FC = () => {
Model: 'AI 模型名称',
EmbeddingModel: '嵌入向量模型名称'
}}
secretFields={secretFields.AI || []}
isMobile={isMobile}
/>
</TabPane>
@@ -265,6 +279,7 @@ const SystemConfig: React.FC = () => {
Issuer: 'JWT 签发者',
Audience: 'JWT 接收者',
}}
secretFields={secretFields.Jwt || []}
isMobile={isMobile}
/>
</TabPane>
@@ -282,6 +297,7 @@ const SystemConfig: React.FC = () => {
"GitHubClientSecret": 'GitHub OAuth 应用客户端密钥',
"GitHubCallbackUrl": 'GitHub OAuth 认证回调地址'
}}
secretFields={secretFields.Authentication || []}
isMobile={isMobile}
/>
</TabPane>
@@ -543,6 +559,7 @@ const SystemConfig: React.FC = () => {
"TelegramStorageBotToken": 'Telegram 机器人令牌',
"TelegramStorageChatId": 'Telegram 聊天ID'
}}
secretFields={secretFields.Storage || []}
isMobile={isMobile}
/>
)}
@@ -569,6 +586,7 @@ const SystemConfig: React.FC = () => {
"S3StorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
"S3StorageUsePathStyleUrls": '使用路径形式URLs (true/false,兼容非AWS服务)'
}}
secretFields={secretFields.Storage || []}
isMobile={isMobile}
/>
)}
@@ -593,6 +611,7 @@ const SystemConfig: React.FC = () => {
"CosStorageRegion": 'COS区域 (例如:ap-shanghai)',
"CosStorageCdnUrl": 'CDN URL (可选,用于加速文件访问)',
}}
secretFields={secretFields.Storage || []}
isMobile={isMobile}
/>
)}
@@ -615,6 +634,7 @@ const SystemConfig: React.FC = () => {
"WebDAVBasePath": 'WebDAV 基础路径 (例如: files/upload)',
"WebDAVPublicUrl": 'WebDAV 公共访问 URL (可选,用于文件访问)',
}}
secretFields={secretFields.Storage || []}
isMobile={isMobile}
/>
)}