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

443 lines
13 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.

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, SyncOutlined } 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 [reloading, setReloading] = 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);
}
};
// Reload system settings
const handleReloadSettings = async () => {
setReloading(true);
try {
const { data } = await request.post('/system/settings/reload');
toast.success(data.message);
} catch (error) {
toast.error('重载配置失败');
} finally {
setReloading(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>
<ul style={{ marginBottom: 0, paddingLeft: 20 }}>
<li><strong></strong> Redis </li>
<li><strong></strong>使</li>
</ul>
</div>
}
type="info"
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
type="default"
icon={<SyncOutlined />}
onClick={handleReloadSettings}
loading={reloading}
>
</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 if (valueType === 'json') {
return (
<Form.Item
name="value"
label="参数值"
rules={[{ required: true, message: '请输入参数值' }]}
>
<Input.TextArea rows={3} placeholder="JSON 格式数据" />
</Form.Item>
);
} else {
return (
<Form.Item
name="value"
label="参数值"
rules={[{ required: true, message: '请输入参数值' }]}
>
<Input placeholder="参数值" />
</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>
</>
);
}