Files
Neo-ZQYY/apps/admin-web/src/pages/EnvConfig.tsx

165 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 环境配置页面。
*
* - 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;