在准备环境前提交次全部更改。
This commit is contained in:
164
apps/admin-web/src/pages/EnvConfig.tsx
Normal file
164
apps/admin-web/src/pages/EnvConfig.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* 环境配置页面。
|
||||
*
|
||||
* - Ant Design Table 展示键值对,支持 inline 编辑
|
||||
* - 敏感值显示为 ****,编辑时可输入新值
|
||||
* - 顶部按钮栏:刷新、保存、导出
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback, useRef } from 'react';
|
||||
import { Table, Button, Input, Tag, Space, message, Card, Typography, Badge } from 'antd';
|
||||
import type { InputRef } from 'antd';
|
||||
import {
|
||||
ReloadOutlined, SaveOutlined, DownloadOutlined, ToolOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { EnvConfigItem } from '../types';
|
||||
import { fetchEnvConfig, updateEnvConfig, exportEnvConfig } from '../api/envConfig';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const MASK = '****';
|
||||
|
||||
const EnvConfig: React.FC = () => {
|
||||
const [items, setItems] = useState<EnvConfigItem[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const [editingKey, setEditingKey] = useState<string | null>(null);
|
||||
const [editValue, setEditValue] = useState('');
|
||||
const [dirtyMap, setDirtyMap] = useState<Record<string, string>>({});
|
||||
const inputRef = useRef<InputRef>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchEnvConfig();
|
||||
setItems(data);
|
||||
setDirtyMap({});
|
||||
setEditingKey(null);
|
||||
} catch { message.error('加载环境配置失败'); }
|
||||
finally { setLoading(false); }
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const startEdit = (key: string, currentValue: string, isSensitive: boolean) => {
|
||||
setEditingKey(key);
|
||||
setEditValue(isSensitive ? '' : (dirtyMap[key] ?? currentValue));
|
||||
setTimeout(() => { inputRef.current?.focus(); }, 0);
|
||||
};
|
||||
|
||||
const confirmEdit = (key: string, originalValue: string, isSensitive: boolean) => {
|
||||
const trimmed = editValue.trim();
|
||||
if (isSensitive && trimmed === '') {
|
||||
setDirtyMap((prev) => { const next = { ...prev }; delete next[key]; return next; });
|
||||
} else if (!isSensitive && trimmed === originalValue) {
|
||||
setDirtyMap((prev) => { const next = { ...prev }; delete next[key]; return next; });
|
||||
} else {
|
||||
setDirtyMap((prev) => ({ ...prev, [key]: trimmed }));
|
||||
}
|
||||
setEditingKey(null);
|
||||
};
|
||||
|
||||
const cancelEdit = () => { setEditingKey(null); };
|
||||
|
||||
const handleSave = async () => {
|
||||
if (Object.keys(dirtyMap).length === 0) { message.info('没有需要保存的修改'); return; }
|
||||
setSaving(true);
|
||||
try {
|
||||
const payload = items.map((item) => ({
|
||||
key: item.key,
|
||||
value: dirtyMap[item.key] ?? item.value,
|
||||
is_sensitive: item.is_sensitive,
|
||||
}));
|
||||
await updateEnvConfig(payload);
|
||||
message.success('保存成功');
|
||||
await load();
|
||||
} catch { message.error('保存失败'); }
|
||||
finally { setSaving(false); }
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
setExporting(true);
|
||||
try { await exportEnvConfig(); message.success('导出成功'); }
|
||||
catch { message.error('导出失败'); }
|
||||
finally { setExporting(false); }
|
||||
};
|
||||
|
||||
const columns: ColumnsType<EnvConfigItem> = [
|
||||
{
|
||||
title: '键名', dataIndex: 'key', key: 'key', width: '35%',
|
||||
render: (text: string) => <code style={{ fontSize: 12 }}>{text}</code>,
|
||||
},
|
||||
{
|
||||
title: '值', dataIndex: 'value', key: 'value', width: '50%',
|
||||
render: (_: string, record: EnvConfigItem) => {
|
||||
if (editingKey === record.key) {
|
||||
return (
|
||||
<Input
|
||||
ref={inputRef} value={editValue} size="small"
|
||||
placeholder={record.is_sensitive ? '输入新值(留空则不修改)' : undefined}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onPressEnter={() => confirmEdit(record.key, record.value, record.is_sensitive)}
|
||||
onBlur={() => confirmEdit(record.key, record.value, record.is_sensitive)}
|
||||
onKeyDown={(e) => { if (e.key === 'Escape') cancelEdit(); }}
|
||||
style={{ fontFamily: 'monospace' }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const isDirty = record.key in dirtyMap;
|
||||
const displayValue = record.is_sensitive
|
||||
? (isDirty ? MASK + ' (已修改)' : MASK)
|
||||
: (isDirty ? dirtyMap[record.key] : record.value);
|
||||
return (
|
||||
<span
|
||||
style={{ cursor: 'pointer', color: isDirty ? '#1677ff' : undefined, fontFamily: 'monospace', fontSize: 12 }}
|
||||
onClick={() => startEdit(record.key, record.value, record.is_sensitive)}
|
||||
title="点击编辑"
|
||||
>
|
||||
{displayValue || <Text type="secondary">(空)</Text>}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '类型', dataIndex: 'is_sensitive', key: 'is_sensitive', width: '15%', align: 'center',
|
||||
render: (v: boolean) => v ? <Tag color="red">敏感</Tag> : <Tag color="green">普通</Tag>,
|
||||
},
|
||||
];
|
||||
|
||||
const hasDirty = Object.keys(dirtyMap).length > 0;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
<ToolOutlined style={{ marginRight: 8 }} />
|
||||
环境配置
|
||||
</Title>
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||||
<Badge count={hasDirty ? Object.keys(dirtyMap).length : 0} size="small">
|
||||
<Button
|
||||
type="primary" icon={<SaveOutlined />}
|
||||
onClick={handleSave} loading={saving} disabled={!hasDirty}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</Badge>
|
||||
<Button icon={<DownloadOutlined />} onClick={handleExport} loading={exporting}>导出</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card size="small">
|
||||
<Table<EnvConfigItem>
|
||||
rowKey="key" columns={columns} dataSource={items}
|
||||
loading={loading} pagination={false} size="small"
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnvConfig;
|
||||
Reference in New Issue
Block a user