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

255 lines
6.9 KiB
TypeScript
Raw Normal View History

2025-11-29 15:10:00 +00:00
/**
* Celestial Bodies Management Page
*/
import { useState, useEffect } from 'react';
2025-11-30 02:43:47 +00:00
import { message, Modal, Form, Input, Select, Switch, InputNumber } from 'antd';
2025-11-29 15:10:00 +00:00
import type { ColumnsType } from 'antd/es/table';
2025-11-30 02:43:47 +00:00
import { DataTable } from '../../components/admin/DataTable';
2025-11-29 15:10:00 +00:00
import { request } from '../../utils/request';
interface CelestialBody {
id: string;
name: string;
name_zh: string;
type: string;
description: string;
2025-11-30 02:43:47 +00:00
is_active: boolean;
2025-11-29 15:10:00 +00:00
}
export function CelestialBodies() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<CelestialBody[]>([]);
2025-11-30 02:43:47 +00:00
const [filteredData, setFilteredData] = useState<CelestialBody[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<CelestialBody | null>(null);
const [form] = Form.useForm();
2025-11-29 15:10:00 +00:00
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const { data: result } = await request.get('/celestial/list');
setData(result.bodies || []);
2025-11-30 02:43:47 +00:00
setFilteredData(result.bodies || []);
2025-11-29 15:10:00 +00:00
} catch (error) {
message.error('加载数据失败');
} finally {
setLoading(false);
}
};
2025-11-30 02:43:47 +00:00
// Search handler
const handleSearch = (keyword: string) => {
const lowerKeyword = keyword.toLowerCase();
const filtered = data.filter(
(item) =>
item.name.toLowerCase().includes(lowerKeyword) ||
item.name_zh?.toLowerCase().includes(lowerKeyword) ||
item.id.includes(lowerKeyword)
);
setFilteredData(filtered);
};
// Add handler
const handleAdd = () => {
setEditingRecord(null);
form.resetFields();
// Default values
form.setFieldsValue({ is_active: true, type: 'probe' });
setIsModalOpen(true);
};
// Edit handler
const handleEdit = (record: CelestialBody) => {
setEditingRecord(record);
form.setFieldsValue(record);
setIsModalOpen(true);
};
// Delete handler
const handleDelete = async (record: CelestialBody) => {
try {
await request.delete(`/celestial/${record.id}`);
message.success('删除成功');
loadData();
} catch (error) {
message.error('删除失败');
}
};
// Status change handler
const handleStatusChange = async (record: CelestialBody, checked: boolean) => {
try {
await request.put(`/celestial/${record.id}`, { is_active: checked });
message.success(`状态更新成功`);
// Update local state to avoid full reload
const newData = data.map(item =>
item.id === record.id ? { ...item, is_active: checked } : item
);
setData(newData);
setFilteredData(newData); // Should re-filter if needed, but simplistic here
} catch (error) {
message.error('状态更新失败');
}
};
// Form submit
const handleModalOk = async () => {
try {
const values = await form.validateFields();
if (editingRecord) {
// Update
await request.put(`/celestial/${editingRecord.id}`, values);
message.success('更新成功');
} else {
// Create
await request.post('/celestial/', values);
message.success('创建成功');
}
setIsModalOpen(false);
loadData();
} catch (error) {
console.error(error);
// message.error('操作失败'); // request interceptor might already handle this
}
};
2025-11-29 15:10:00 +00:00
const columns: ColumnsType<CelestialBody> = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 100,
2025-11-30 02:43:47 +00:00
sorter: (a, b) => a.id.localeCompare(b.id),
2025-11-29 15:10:00 +00:00
},
{
title: '英文名',
dataIndex: 'name',
key: 'name',
2025-11-30 02:43:47 +00:00
sorter: (a, b) => a.name.localeCompare(b.name),
2025-11-29 15:10:00 +00:00
},
{
title: '中文名',
dataIndex: 'name_zh',
key: 'name_zh',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
2025-11-30 02:43:47 +00:00
filters: [
{ text: '行星', value: 'planet' },
{ text: '恒星', value: 'star' },
{ text: '卫星', value: 'satellite' },
{ text: '探测器', value: 'probe' },
{ text: '矮行星', value: 'dwarf_planet' },
],
onFilter: (value, record) => record.type === value,
2025-11-29 15:10:00 +00:00
render: (type: string) => {
const typeMap: Record<string, string> = {
star: '恒星',
planet: '行星',
dwarf_planet: '矮行星',
2025-11-30 02:43:47 +00:00
satellite: '卫星',
2025-11-29 15:10:00 +00:00
probe: '探测器',
};
return typeMap[type] || type;
},
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
];
return (
2025-11-30 02:43:47 +00:00
<>
<DataTable
title="天体数据管理"
2025-11-29 15:10:00 +00:00
columns={columns}
2025-11-30 02:43:47 +00:00
dataSource={filteredData}
2025-11-29 15:10:00 +00:00
loading={loading}
2025-11-30 02:43:47 +00:00
total={filteredData.length}
onSearch={handleSearch}
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
onStatusChange={handleStatusChange}
statusField="is_active"
rowKey="id"
pageSize={10}
2025-11-29 15:10:00 +00:00
/>
2025-11-30 02:43:47 +00:00
<Modal
title={editingRecord ? '编辑天体' : '新增天体'}
open={isModalOpen}
onOk={handleModalOk}
onCancel={() => setIsModalOpen(false)}
>
<Form
form={form}
layout="vertical"
>
<Form.Item
name="id"
label="JPL ID"
rules={[{ required: true, message: '请输入JPL Horizons ID' }]}
>
<Input disabled={!!editingRecord} placeholder="例如:-31 (Voyager 1) 或 399 (Earth)" />
</Form.Item>
<Form.Item
name="name"
label="英文名"
rules={[{ required: true, message: '请输入英文名' }]}
>
<Input placeholder="例如Voyager 1" />
</Form.Item>
<Form.Item
name="name_zh"
label="中文名"
>
<Input placeholder="例如旅行者1号" />
</Form.Item>
<Form.Item
name="type"
label="类型"
rules={[{ required: true, message: '请选择类型' }]}
>
<Select>
<Select.Option value="planet"></Select.Option>
<Select.Option value="dwarf_planet"></Select.Option>
<Select.Option value="satellite"></Select.Option>
<Select.Option value="probe"></Select.Option>
<Select.Option value="star"></Select.Option>
</Select>
</Form.Item>
<Form.Item
name="is_active"
label="状态"
valuePropName="checked"
>
<Switch checkedChildren="启用" unCheckedChildren="禁用" />
</Form.Item>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
</>
2025-11-29 15:10:00 +00:00
);
2025-11-30 02:43:47 +00:00
}