2025-11-30 05:25:41 +00:00
|
|
|
|
/**
|
|
|
|
|
|
* System Settings Management Page
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { useState, useEffect } from 'react';
|
2025-12-01 08:52:04 +00:00
|
|
|
|
import { Modal, Form, Input, InputNumber, Switch, Select, Button, Card, Descriptions, Badge, Space, Popconfirm, Alert, Divider } from 'antd';
|
2025-11-30 05:25:41 +00:00
|
|
|
|
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';
|
2025-12-01 08:52:04 +00:00
|
|
|
|
import { useToast } from '../../contexts/ToastContext';
|
2025-11-30 05:25:41 +00:00
|
|
|
|
|
|
|
|
|
|
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);
|
2025-12-01 08:52:04 +00:00
|
|
|
|
const toast = useToast();
|
2025-11-30 05:25:41 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
loadData();
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
|
|
const loadData = async () => {
|
|
|
|
|
|
setLoading(true);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data: result } = await request.get('/system/settings');
|
|
|
|
|
|
setData(result.settings || []);
|
|
|
|
|
|
setFilteredData(result.settings || []);
|
|
|
|
|
|
} catch (error) {
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.error('加载数据失败');
|
2025-11-30 05:25:41 +00:00
|
|
|
|
} 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}`);
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.success('删除成功');
|
2025-11-30 05:25:41 +00:00
|
|
|
|
loadData();
|
|
|
|
|
|
} catch (error) {
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.error('删除失败');
|
2025-11-30 05:25:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Form submit
|
|
|
|
|
|
const handleModalOk = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const values = await form.validateFields();
|
|
|
|
|
|
|
|
|
|
|
|
if (editingRecord) {
|
|
|
|
|
|
// Update
|
|
|
|
|
|
await request.put(`/system/settings/${editingRecord.key}`, values);
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.success('更新成功');
|
2025-11-30 05:25:41 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// Create
|
|
|
|
|
|
await request.post('/system/settings', values);
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.success('创建成功');
|
2025-11-30 05:25:41 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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');
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.success(
|
2025-11-30 05:25:41 +00:00
|
|
|
|
<>
|
|
|
|
|
|
<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) {
|
2025-12-01 08:52:04 +00:00
|
|
|
|
toast.error('清除缓存失败');
|
2025-11-30 05:25:41 +00:00
|
|
|
|
} 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
|
2025-12-04 13:43:43 +00:00
|
|
|
|
title="清除缓存会清空所有内存缓存和 Redis 缓存,包括:"
|
2025-11-30 05:25:41 +00:00
|
|
|
|
description={
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
|
2025-12-04 13:43:43 +00:00
|
|
|
|
<li>* 位置数据缓存(当前位置和历史位置)</li>
|
|
|
|
|
|
<li>* NASA API 响应缓存</li>
|
|
|
|
|
|
<li>* 所有其他临时缓存数据</li>
|
2025-11-30 05:25:41 +00:00
|
|
|
|
</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>
|
|
|
|
|
|
</>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|