cosmo/frontend/src/pages/admin/SystemSettings.tsx

422 lines
12 KiB
TypeScript
Raw Normal View History

2025-11-30 05:25:41 +00:00
/**
* System Settings Management Page
*/
import { useState, useEffect } from 'react';
import { message, Modal, Form, Input, InputNumber, Switch, Select, Button, Card, Descriptions, Badge, Space, Popconfirm, Alert, Divider } from 'antd';
import { ReloadOutlined, ClearOutlined, WarningOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { DataTable } from '../../components/admin/DataTable';
import { request } from '../../utils/request';
interface SystemSetting {
id: number;
key: string;
value: any;
raw_value: string;
value_type: 'string' | 'int' | 'float' | 'bool' | 'json';
category: string;
label: string;
description?: string;
is_public: boolean;
created_at?: string;
updated_at?: string;
}
const CATEGORY_MAP: Record<string, string> = {
visualization: '可视化',
cache: '缓存',
ui: '界面',
api: 'API',
general: '常规',
};
export function SystemSettings() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<SystemSetting[]>([]);
const [filteredData, setFilteredData] = useState<SystemSetting[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<SystemSetting | null>(null);
const [form] = Form.useForm();
const [clearingCache, setClearingCache] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const { data: result } = await request.get('/system/settings');
setData(result.settings || []);
setFilteredData(result.settings || []);
} catch (error) {
message.error('加载数据失败');
} finally {
setLoading(false);
}
};
// Search handler
const handleSearch = (keyword: string) => {
const lowerKeyword = keyword.toLowerCase();
const filtered = data.filter(
(item) =>
item.key.toLowerCase().includes(lowerKeyword) ||
item.label?.toLowerCase().includes(lowerKeyword) ||
item.category?.toLowerCase().includes(lowerKeyword)
);
setFilteredData(filtered);
};
// Add handler
const handleAdd = () => {
setEditingRecord(null);
form.resetFields();
form.setFieldsValue({ value_type: 'string', category: 'general', is_public: false });
setIsModalOpen(true);
};
// Edit handler
const handleEdit = (record: SystemSetting) => {
setEditingRecord(record);
form.setFieldsValue({
key: record.key,
value: record.value,
value_type: record.value_type,
category: record.category,
label: record.label,
description: record.description,
is_public: record.is_public,
});
setIsModalOpen(true);
};
// Delete handler
const handleDelete = async (record: SystemSetting) => {
try {
await request.delete(`/system/settings/${record.key}`);
message.success('删除成功');
loadData();
} catch (error) {
message.error('删除失败');
}
};
// Form submit
const handleModalOk = async () => {
try {
const values = await form.validateFields();
if (editingRecord) {
// Update
await request.put(`/system/settings/${editingRecord.key}`, values);
message.success('更新成功');
} else {
// Create
await request.post('/system/settings', values);
message.success('创建成功');
}
setIsModalOpen(false);
loadData();
} catch (error) {
console.error(error);
}
};
// Clear all caches
const handleClearCache = async () => {
setClearingCache(true);
try {
const { data } = await request.post('/system/cache/clear');
message.success(
<>
<div>{data.message}</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
: {data.redis_cache.positions_keys} | NASA: {data.redis_cache.nasa_keys}
</div>
</>,
5
);
loadData();
} catch (error) {
message.error('清除缓存失败');
} finally {
setClearingCache(false);
}
};
const columns: ColumnsType<SystemSetting> = [
{
title: '参数键',
dataIndex: 'key',
key: 'key',
width: 220,
fixed: 'left',
render: (key: string, record) => (
<div>
<div style={{ fontFamily: 'monospace', fontWeight: 500 }}>{key}</div>
{record.is_public && (
<Badge status="success" text="前端可访问" style={{ fontSize: 11 }} />
)}
</div>
),
},
{
title: '名称',
dataIndex: 'label',
key: 'label',
width: 180,
},
{
title: '当前值',
dataIndex: 'value',
key: 'value',
width: 150,
render: (value: any, record) => {
if (record.value_type === 'bool') {
return <Badge status={value ? 'success' : 'default'} text={value ? '是' : '否'} />;
}
return <span style={{ fontWeight: 500 }}>{String(value)}</span>;
},
},
{
title: '类型',
dataIndex: 'value_type',
key: 'value_type',
width: 90,
filters: [
{ text: '字符串', value: 'string' },
{ text: '整数', value: 'int' },
{ text: '浮点数', value: 'float' },
{ text: '布尔值', value: 'bool' },
{ text: 'JSON', value: 'json' },
],
onFilter: (value, record) => record.value_type === value,
render: (type: string) => {
const typeMap: Record<string, string> = {
string: '字符串',
int: '整数',
float: '浮点数',
bool: '布尔值',
json: 'JSON',
};
return typeMap[type] || type;
},
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
width: 100,
filters: Object.keys(CATEGORY_MAP).map((key) => ({
text: CATEGORY_MAP[key],
value: key,
})),
onFilter: (value, record) => record.category === value,
render: (category: string) => CATEGORY_MAP[category] || category,
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
];
return (
<>
{/* Cache Management Card */}
<Card
title={
<Space>
<ClearOutlined />
<span></span>
</Space>
}
style={{ marginBottom: 16 }}
styles={{ body: { padding: 16 } }}
>
<Alert
title="重要操作说明"
description={
<div>
<p style={{ marginBottom: 8 }}>
Redis
</p>
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
<li></li>
<li>NASA API </li>
<li></li>
</ul>
<p style={{ marginTop: 8, marginBottom: 0, color: '#fa8c16' }}>
<WarningOutlined /> NASA API
</p>
</div>
}
type="warning"
showIcon
style={{ marginBottom: 16 }}
/>
<Space>
<Popconfirm
title="确认清除所有缓存?"
description="此操作会清空所有缓存数据,下次查询可能会较慢"
onConfirm={handleClearCache}
okText="确认清除"
cancelText="取消"
okButtonProps={{ danger: true }}
>
<Button
danger
type="primary"
icon={<ClearOutlined />}
loading={clearingCache}
>
</Button>
</Popconfirm>
<Button icon={<ReloadOutlined />} onClick={loadData}>
</Button>
</Space>
</Card>
<Divider />
{/* Settings Table */}
<DataTable
title="系统参数"
columns={columns}
dataSource={filteredData}
loading={loading}
total={filteredData.length}
onSearch={handleSearch}
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
rowKey="id"
pageSize={15}
scroll={{ x: 1200 }}
/>
<Modal
title={editingRecord ? '编辑参数' : '新增参数'}
open={isModalOpen}
onOk={handleModalOk}
onCancel={() => setIsModalOpen(false)}
width={700}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="key"
label="参数键"
rules={[{ required: true, message: '请输入参数键' }]}
>
<Input disabled={!!editingRecord} placeholder="例如timeline_interval_days" />
</Form.Item>
<Form.Item
name="label"
label="参数名称"
rules={[{ required: true, message: '请输入参数名称' }]}
>
<Input placeholder="例如:时间轴播放间隔(天)" />
</Form.Item>
<Form.Item
name="value_type"
label="数据类型"
rules={[{ required: true, message: '请选择数据类型' }]}
>
<Select>
<Select.Option value="string"></Select.Option>
<Select.Option value="int"></Select.Option>
<Select.Option value="float"></Select.Option>
<Select.Option value="bool"></Select.Option>
<Select.Option value="json">JSON</Select.Option>
</Select>
</Form.Item>
<Form.Item
noStyle
shouldUpdate={(prevValues, currentValues) =>
prevValues.value_type !== currentValues.value_type
}
>
{({ getFieldValue }) => {
const valueType = getFieldValue('value_type');
if (valueType === 'bool') {
return (
<Form.Item
name="value"
label="参数值"
valuePropName="checked"
rules={[{ required: true, message: '请设置参数值' }]}
>
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
);
} else if (valueType === 'int' || valueType === 'float') {
return (
<Form.Item
name="value"
label="参数值"
rules={[{ required: true, message: '请输入参数值' }]}
>
<InputNumber style={{ width: '100%' }} step={valueType === 'float' ? 0.1 : 1} />
</Form.Item>
);
} else {
return (
<Form.Item
name="value"
label="参数值"
rules={[{ required: true, message: '请输入参数值' }]}
>
<Input.TextArea rows={3} placeholder={valueType === 'json' ? 'JSON 格式数据' : '参数值'} />
</Form.Item>
);
}
}}
</Form.Item>
<Form.Item
name="category"
label="分类"
rules={[{ required: true, message: '请选择分类' }]}
>
<Select>
<Select.Option value="visualization"></Select.Option>
<Select.Option value="cache"></Select.Option>
<Select.Option value="ui"></Select.Option>
<Select.Option value="api">API</Select.Option>
<Select.Option value="general"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="is_public"
label="前端可访问"
valuePropName="checked"
>
<Switch checkedChildren="是" unCheckedChildren="否" />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={3} placeholder="参数说明" />
</Form.Item>
</Form>
</Modal>
</>
);
}