在准备环境前提交次全部更改。

This commit is contained in:
Neo
2026-02-19 08:35:13 +08:00
parent ded6dfb9d8
commit 4eac07da47
1387 changed files with 6107191 additions and 33002 deletions

View 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;