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

414 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* System Settings Management Page
*/
import { useState, useEffect } from 'react';
import { 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';
import { useToast } from '../../contexts/ToastContext';
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);
const toast = useToast();
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const { data: result } = await request.get('/system/settings');
setData(result.settings || []);
setFilteredData(result.settings || []);
} catch (error) {
toast.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}`);
toast.success('删除成功');
loadData();
} catch (error) {
toast.error('删除失败');
}
};
// Form submit
const handleModalOk = async () => {
try {
const values = await form.validateFields();
if (editingRecord) {
// Update
await request.put(`/system/settings/${editingRecord.key}`, values);
toast.success('更新成功');
} else {
// Create
await request.post('/system/settings', values);
toast.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');
toast.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) {
toast.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="清除缓存会清空所有内存缓存和 Redis 缓存,包括:"
description={
<div>
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
<li>* </li>
<li>* NASA API </li>
<li>* </li>
</ul>
</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>
</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>
</>
);
}