/** * Celestial Bodies Management Page */ import { useState, useEffect } from 'react'; import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; interface CelestialBody { id: string; name: string; name_zh: string; type: string; description: string; is_active: boolean; resources?: { [key: string]: Array<{ id: number; file_path: string; file_size: number; mime_type: string; }>; }; has_resources?: boolean; } export function CelestialBodies() { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [filteredData, setFilteredData] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [form] = Form.useForm(); const [searching, setSearching] = useState(false); const [searchQuery, setSearchQuery] = useState(''); useEffect(() => { loadData(); }, []); const loadData = async () => { setLoading(true); try { const { data: result } = await request.get('/celestial/list'); setData(result.bodies || []); setFilteredData(result.bodies || []); } catch (error) { message.error('加载数据失败'); } finally { setLoading(false); } }; // 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(); setSearchQuery(''); // Default values form.setFieldsValue({ is_active: true, type: 'probe' }); setIsModalOpen(true); }; // Search NASA Horizons by name const handleNASASearch = async () => { if (!searchQuery.trim()) { message.warning('请输入天体名称或ID'); return; } setSearching(true); try { const { data: result } = await request.get('/celestial/search', { params: { name: searchQuery } }); if (result.success) { // Auto-fill form with search results form.setFieldsValue({ id: result.data.id, name: result.data.name, }); // Check if ID looks like it might not be a proper numeric ID const isNumericId = /^-?\d+$/.test(result.data.id); if (isNumericId) { message.success(`找到天体: ${result.data.full_name}`); } else { // Warn user that ID might need manual correction Modal.warning({ title: '找到天体,但请确认 ID', content: (

找到天体: {result.data.full_name}

自动填充的 ID 为: {result.data.id}

⚠️ 建议: 如果知道该天体的数字 ID,请手动修改为数字 ID(如:-48)以便后续查询位置数据。

提示:您可以在 NASA Horizons 网站查询准确的数字 ID

), }); } } else { message.error(result.error || '查询失败'); } } catch (error: any) { message.error(error.response?.data?.detail || '查询失败'); } finally { setSearching(false); } }; // 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 } }; const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', key: 'id', width: 100, sorter: (a, b) => a.id.localeCompare(b.id), }, { title: '英文名', dataIndex: 'name', key: 'name', sorter: (a, b) => a.name.localeCompare(b.name), }, { title: '中文名', dataIndex: 'name_zh', key: 'name_zh', }, { title: '类型', dataIndex: 'type', key: 'type', filters: [ { text: '行星', value: 'planet' }, { text: '恒星', value: 'star' }, { text: '卫星', value: 'satellite' }, { text: '探测器', value: 'probe' }, { text: '矮行星', value: 'dwarf_planet' }, ], onFilter: (value, record) => record.type === value, render: (type: string) => { const typeMap: Record = { star: '恒星', planet: '行星', dwarf_planet: '矮行星', satellite: '卫星', probe: '探测器', }; return typeMap[type] || type; }, }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true, }, { title: '资源配置', key: 'resources', width: 120, render: (_, record) => { if (record.has_resources) { const resourceTypes = Object.keys(record.resources || {}); return ( ); } return ; }, }, ]; return ( <> setIsModalOpen(false)} width={800} >
{!editingRecord && ( <>

推荐使用 JPL Horizons 数字 ID 进行搜索,可获得最准确的结果。

示例:Hubble 的 ID 是 -48,Voyager 1 的 ID 是 -31

不知道 ID?可以先用名称搜索,系统会尽量提取 ID,或提示您手动确认。

} type="info" showIcon style={{ marginBottom: 16 }} /> setSearchQuery(e.target.value)} onPressEnter={handleNASASearch} /> )} {editingRecord && editingRecord.has_resources && ( {Object.entries(editingRecord.resources || {}).map(([type, resources]) => ( {resources.map((res: any, idx: number) => (
{res.file_path} ({(res.file_size / 1024).toFixed(2)} KB)
))}
))}
提示:资源文件管理功能即将推出,敬请期待
)}
); }