import { useEffect, useMemo, useState } from 'react'; import { Badge, Button, Form, Modal, Popconfirm, Select, Space, Tag, Tooltip } from 'antd'; import { GlobalOutlined, StarOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { DataTable } from '../../components/admin/DataTable'; import { AdminPage } from '../../components/admin/AdminPage'; import { useListPageSize } from './useListPageSize'; import { useToast } from '../../contexts/ToastContext'; import { request } from '../../utils/request'; import { CelestialBodyModal } from './celestial-bodies/CelestialBodyModal'; import type { CelestialBody, StarSystem } from './celestial-bodies/types'; const bodyTypeLabels: Record = { star: '恒星', planet: '行星', dwarf_planet: '矮行星', satellite: '卫星', comet: '彗星', probe: '探测器', }; function getErrorDetail(error: unknown, fallback: string) { if (typeof error !== 'object' || error === null || !('response' in error)) return fallback; const response = (error as { response?: { data?: { detail?: string } } }).response; return response?.data?.detail || fallback; } function filterBodies(items: CelestialBody[], searchTerm: string) { const normalized = searchTerm.toLowerCase(); return items.filter((item) => item.name.toLowerCase().includes(normalized) || item.name_zh?.toLowerCase().includes(normalized) || item.id.toLowerCase().includes(normalized) ); } export function CelestialBodies() { // 每页数量由系统参数 page_size 控制 const systemPageSize = useListPageSize(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [keyword, setKeyword] = useState(''); const [starSystems, setStarSystems] = useState([]); const [selectedSystemId, setSelectedSystemId] = useState(1); const [isModalOpen, setIsModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [searching, setSearching] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [uploading, setUploading] = useState(false); const [refreshResources, setRefreshResources] = useState(0); const [form] = Form.useForm(); const toast = useToast(); const filteredData = useMemo(() => filterBodies(data, keyword), [data, keyword]); useEffect(() => { request.get('/star-systems', { params: { limit: 1000 } }) .then(({ data: result }) => setStarSystems(result.systems || [])) .catch(() => toast.error('加载恒星系统列表失败')); }, [toast]); useEffect(() => { if (selectedSystemId === null) { setData([]); return; } setLoading(true); request.get('/celestial/list', { params: { system_id: selectedSystemId } }) .then(({ data: result }) => { const bodies = result.bodies || []; setData(bodies); }) .catch(() => toast.error('加载数据失败')) .finally(() => setLoading(false)); }, [selectedSystemId, toast]); const reloadData = async () => { if (selectedSystemId === null) return; setLoading(true); try { const { data: result } = await request.get('/celestial/list', { params: { system_id: selectedSystemId } }); const bodies = result.bodies || []; setData(bodies); } catch { toast.error('加载数据失败'); } finally { setLoading(false); } }; const handleSearch = (searchTerm: string) => { setKeyword(searchTerm); }; const handleAdd = () => { setEditingRecord(null); setSearchQuery(''); form.resetFields(); form.setFieldsValue({ is_active: true, type: 'probe', system_id: selectedSystemId }); setIsModalOpen(true); }; const handleNasaSearch = async () => { if (!searchQuery.trim()) { toast.warning('请输入天体名称或ID'); return; } setSearching(true); try { const { data: result } = await request.get('/celestial/search', { params: { name: searchQuery } }); if (!result.success) { toast.error(result.error || '查询失败'); return; } const existingBody = data.find((body) => body.id === result.data.id); if (existingBody) { Modal.warning({ title: '天体已存在', content: (

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

ID: {result.data.id}

该天体已在数据库中,名称为: {existingBody.name}

如需修改,请在列表中直接编辑该天体。

), }); return; } form.setFieldsValue({ id: result.data.id, name: result.data.name }); if (/^-?\d+$/.test(result.data.id)) { toast.success(`找到天体: ${result.data.full_name}`); } else { Modal.warning({ title: '找到天体,但请确认 ID', content: (

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

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

建议手动确认数字 ID,以便后续查询位置数据。

可在 NASA Horizons 查询准确 ID。

), }); } } catch (error) { toast.error(getErrorDetail(error, '查询失败')); } finally { setSearching(false); } }; const handleEdit = async (record: CelestialBody) => { form.resetFields(); setEditingRecord(record); let extraData = record.extra_data; if (typeof extraData === 'string') { try { extraData = JSON.parse(extraData); } catch { extraData = {}; } } let orbitInfo = null; try { const { data: orbitData } = await request.get(`/celestial/orbits/${record.id}`); if (orbitData) orbitInfo = { num_points: orbitData.num_points, period_days: orbitData.period_days }; } catch { orbitInfo = null; } form.setFieldsValue({ ...record, extra_data: extraData || {}, orbit_info: orbitInfo }); setIsModalOpen(true); }; const handleDelete = async (record: CelestialBody) => { try { await request.delete(`/celestial/${record.id}`); toast.success('删除成功'); await reloadData(); } catch { toast.error('删除失败'); } }; const handleStatusChange = async (record: CelestialBody, checked: boolean) => { try { await request.put(`/celestial/${record.id}`, { is_active: checked }); const updated = data.map((item) => item.id === record.id ? { ...item, is_active: checked } : item); setData(updated); toast.success('状态更新成功'); } catch { toast.error('状态更新失败'); } }; const handleModalOk = async () => { try { const values = await form.validateFields(); if (editingRecord) { await request.put(`/celestial/${editingRecord.id}`, values); toast.success('更新成功'); } else { await request.post('/celestial/', values); toast.success('创建成功'); } setIsModalOpen(false); await reloadData(); } catch (error) { if (typeof error === 'object' && error !== null && 'errorFields' in error) { toast.error('请填写所有必填字段'); } else { toast.error(getErrorDetail(error, '操作失败')); } } }; const handleResourceUpload = async (file: File, resourceType: string) => { if (!editingRecord) return false; setUploading(true); const formData = new FormData(); formData.append('file', file); try { const response = await request.post( `/celestial/resources/upload?body_id=${editingRecord.id}&resource_type=${resourceType}`, formData, { headers: { 'Content-Type': 'multipart/form-data' } }, ); toast.success(response.data.message); setRefreshResources((value) => value + 1); } catch (error) { toast.error(getErrorDetail(error, '上传失败')); } finally { setUploading(false); } return false; }; const handleResourceDelete = async (resourceId: number) => { try { await request.delete(`/celestial/resources/${resourceId}`); toast.success('删除成功'); setRefreshResources((value) => value + 1); } catch (error) { toast.error(getErrorDetail(error, '删除失败')); } }; const handleGenerateOrbit = async (record: CelestialBody) => { if (!['planet', 'dwarf_planet'].includes(record.type)) return; setLoading(true); try { await request.post(`/celestial/admin/orbits/generate?body_ids=${record.id}`); toast.success('轨道生成任务已启动,请前往"系统任务"查看进度', 5000); } catch (error) { toast.error(getErrorDetail(error, '轨道生成任务启动失败')); } finally { setLoading(false); } }; 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: 'system_id', key: 'system_id', width: 120, render: (systemId: number) => { const system = starSystems.find((item) => item.id === systemId); return system ? }>{system.name_zh || system.name} : '-'; }, }, { title: '类型', dataIndex: 'type', key: 'type', filters: Object.entries(bodyTypeLabels).map(([value, text]) => ({ text, value })), onFilter: (value, record) => record.type === value, render: (type: string) => bodyTypeLabels[type] || type, }, { title: '描述', dataIndex: 'description', key: 'description', ellipsis: true }, { title: '资源配置', key: 'resources', width: 120, render: (_, record) => record.has_resources ? : , }, ]; const selectedSystem = starSystems.find((system) => system.id === selectedSystemId); return ( } title="天体数据管理" description="按恒星系统维护天体基础信息、显示资源与轨道数据"> void reloadData()} onSearch={handleSearch} searchPlaceholder="搜索 ID / 英文名 / 中文名" onAdd={handleAdd} addText="新增天体" onEdit={handleEdit} onDelete={handleDelete} deleteConfirmTitle="确认删除该天体?" deleteConfirmDescription="删除后该天体的位置与资源数据将不可用" onStatusChange={handleStatusChange} statusField="is_active" rowKey="id" pageSize={systemPageSize} toolbar={