414 lines
12 KiB
TypeScript
414 lines
12 KiB
TypeScript
/**
|
||
* 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>
|
||
</>
|
||
);
|
||
}
|