import { useState, useEffect } from 'react'; import { Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col, Tabs, Card, Tooltip } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined, StarOutlined } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload/interface'; import type { ColumnsType } from 'antd/es/table'; import MdEditor from 'react-markdown-editor-lite'; import MarkdownIt from 'markdown-it'; import 'react-markdown-editor-lite/lib/index.css'; import { DataTable } from '../../components/admin/DataTable'; import { request } from '../../utils/request'; import { useToast } from '../../contexts/ToastContext'; const MdEditorParser = new MarkdownIt(); interface CelestialBody { id: string; name: string; name_zh: string; short_name?: string; // NASA SBDB API short name type: string; system_id?: number; description: string; details?: string; // Added details field is_active: boolean; extra_data?: { orbit_period_days?: number; orbit_color?: string; [key: string]: any; // Allow other extra data }; resources?: { [key: string]: Array<{ id: number; file_path: string; file_size: number; mime_type: string; }>; }; has_resources?: boolean; orbit_info?: { num_points: number; period_days?: number; }; } interface StarSystem { id: number; name: string; name_zh: string; planet_count: number; } export function CelestialBodies() { const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [filteredData, setFilteredData] = useState([]); const [starSystems, setStarSystems] = useState([]); const [selectedSystemId, setSelectedSystemId] = useState(1); // 默认选择太阳系 const [isModalOpen, setIsModalOpen] = useState(false); const [editingRecord, setEditingRecord] = useState(null); const [form] = Form.useForm(); const [searching, setSearching] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const [uploading, setUploading] = useState(false); const [refreshResources, setRefreshResources] = useState(0); const [activeTabKey, setActiveTabKey] = useState('basic'); // State for active tab const toast = useToast(); // 加载恒星系统列表 useEffect(() => { loadStarSystems(); }, []); // 当选择的恒星系改变时,重新加载天体数据 useEffect(() => { if (selectedSystemId !== null) { loadData(); } }, [selectedSystemId]); const loadStarSystems = async () => { try { const { data: result } = await request.get('/star-systems', { params: { limit: 1000 } }); setStarSystems(result.systems || []); } catch (error) { toast.error('加载恒星系统列表失败'); } }; const loadData = async () => { if (selectedSystemId === null) { setData([]); setFilteredData([]); return; } setLoading(true); try { const { data: result } = await request.get('/celestial/list', { params: { system_id: selectedSystemId } }); setData(result.bodies || []); setFilteredData(result.bodies || []); } catch (error) { toast.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(''); setActiveTabKey('basic'); // Reset to basic tab // Default values form.setFieldsValue({ is_active: true, type: 'probe', system_id: selectedSystemId // Auto-set to current selected system }); setIsModalOpen(true); }; // Search NASA Horizons by name 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) { // Check if this body already exists in our database const existingBody = data.find(b => b.id === result.data.id); if (existingBody) { Modal.warning({ title: '天体已存在', content: (

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

ID: {result.data.id}

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

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

), }); return; } // 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) { toast.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 { toast.error(result.error || '查询失败'); } } catch (error: any) { toast.error(error.response?.data?.detail || '查询失败'); } finally { setSearching(false); } }; // Edit handler const handleEdit = async (record: CelestialBody) => { setEditingRecord(record); // Parse extra_data if it's a string (from backend JSON field) let extraData = record.extra_data; if (typeof extraData === 'string') { try { extraData = JSON.parse(extraData); } catch (e) { console.error('Failed to parse extra_data:', e); extraData = {}; } } // Fetch orbit information if exists 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 }; console.log('Loaded orbit info for', record.id, orbitInfo); } } catch (e) { // Orbit not found or error - this is fine, not all bodies have orbits console.log('No orbit data for', record.id); } // Properly set form values including nested extra_data and orbit info const formValues = { ...record, extra_data: extraData || {}, // Ensure extra_data is an object orbit_info: orbitInfo }; form.setFieldsValue(formValues); setActiveTabKey('basic'); // Reset to basic tab setIsModalOpen(true); }; // Delete handler const handleDelete = async (record: CelestialBody) => { try { await request.delete(`/celestial/${record.id}`); toast.success('删除成功'); loadData(); } catch (error) { toast.error('删除失败'); } }; // Status change handler const handleStatusChange = async (record: CelestialBody, checked: boolean) => { try { await request.put(`/celestial/${record.id}`, { is_active: checked }); toast.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) { toast.error('状态更新失败'); } }; // Form submit const handleModalOk = async () => { let values: any; try { values = await form.validateFields(); if (editingRecord) { // Update await request.put(`/celestial/${editingRecord.id}`, values); toast.success('更新成功'); } else { // Create await request.post('/celestial/', values); toast.success('创建成功'); } setIsModalOpen(false); loadData(); } catch (error: any) { console.error(error); // Check for specific error messages if (error.response?.status === 400) { const detail = error.response?.data?.detail; if (detail && detail.includes('already exists')) { toast.error(`天体已存在: ${values?.id || ''}`); } else { toast.error(detail || '请检查表单数据是否完整'); } } else if (error.errorFields) { // Validation error toast.error('请填写所有必填字段'); } else { toast.error(error.response?.data?.detail || '操作失败'); } } }; // Handle resource upload const handleResourceUpload = async (file: File, resourceType: string) => { if (!editingRecord) { toast.error('请先选择要编辑的天体'); 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} (上传到 ${response.data.upload_directory} 目录)`); // Trigger a refresh of resources setRefreshResources(prev => prev + 1); return false; // Prevent default upload behavior } catch (error: any) { toast.error(error.response?.data?.detail || '上传失败'); return false; } finally { setUploading(false); } }; // Generate orbit for a celestial body const handleGenerateOrbit = async (record: CelestialBody) => { if (!['planet', 'dwarf_planet'].includes(record.type)) { toast.warning('只有行星和矮行星可以生成轨道'); return; } setLoading(true); try { await request.post( `/celestial/admin/orbits/generate?body_ids=${record.id}` ); // 提示用户任务已启动 toast.success('轨道生成任务已启动,请前往"系统任务"查看进度', 5000); } catch (error: any) { toast.error(error.response?.data?.detail || '轨道生成任务启动失败'); } finally { setLoading(false); } }; // Handle resource delete const handleResourceDelete = async (resourceId: number) => { try { await request.delete(`/celestial/resources/${resourceId}`); toast.success('删除成功'); setRefreshResources(prev => prev + 1); // Trigger reload } catch (error: any) { toast.error(error.response?.data?.detail || '删除失败'); } }; 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(s => s.id === systemId); return system ? ( }> {system.name_zh || system.name} ) : '-'; }, }, { title: '类型', dataIndex: 'type', key: 'type', filters: [ { text: '行星', value: 'planet' }, { text: '恒星', value: 'star' }, { text: '卫星', value: 'satellite' }, { text: '探测器', value: 'probe' }, { text: '矮行星', value: 'dwarf_planet' }, { text: '彗星', value: 'comet' }, ], onFilter: (value, record) => record.type === value, render: (type: string) => { const typeMap: Record = { star: '恒星', planet: '行星', dwarf_planet: '矮行星', satellite: '卫星', probe: '探测器', comet: '彗星', }; 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 ( <> {/* Star System Selector */} 选择恒星系统: {selectedSystemId && ( 已选择:{starSystems.find(s => s.id === selectedSystemId)?.name_zh || starSystems.find(s => s.id === selectedSystemId)?.name} )} { // Show "Generate Orbit" button for all types, but disable for non-planets const canGenerateOrbit = ['planet', 'dwarf_planet'].includes(record.type); return ( handleGenerateOrbit(record)} okText="确认" cancelText="取消" disabled={!canGenerateOrbit} > ); }} /> setIsModalOpen(false)} width={1000} >
{editingRecord ? ( // 编辑模式:显示双tab {/* Hidden system_id field - auto-set from selector */} {/* Orbit parameters and info for planets and dwarf planets */} prevValues.type !== currentValues.type || prevValues.orbit_info !== currentValues.orbit_info }> {({ getFieldValue }) => { const bodyType = getFieldValue('type'); const orbitInfo = getFieldValue('orbit_info'); if (!['planet', 'dwarf_planet'].includes(bodyType)) { return null; } return ( {/* Editable orbit parameters */} {/* Display orbit info if exists (read-only) */} {editingRecord && orbitInfo && orbitInfo.num_points && (
已生成的轨道数据:
轨道点数: {orbitInfo.num_points?.toLocaleString()} 个点
{orbitInfo.period_days && (
实际周期: {orbitInfo.period_days.toFixed(2)} 天
)}
)} } type="info" style={{ marginBottom: 16 }} /> ); }}
{editingRecord && ( )}
MdEditorParser.render(text)} onChange={({ text }) => form.setFieldsValue({ details: text })} />
) : ( // 新增模式:只显示基础信息 <>

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

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

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

} type="info" showIcon style={{ marginBottom: 16 }} /> setSearchQuery(e.target.value)} onPressEnter={handleNASASearch} /> {/* Hidden system_id field - auto-set from selector */} )}
); } // Resource Manager Component function ResourceManager({ bodyId, bodyType, resources, hasResources, onUpload, onDelete, uploading, refreshTrigger, toast, }: { bodyId: string; bodyType: string; resources?: any; hasResources?: boolean; onUpload: (file: File, resourceType: string) => Promise; onDelete: (resourceId: number) => Promise; uploading: boolean; refreshTrigger: number; toast: any; }) { const [currentResources, setCurrentResources] = useState(resources); // Load resources when modal opens useEffect(() => { request.get(`/celestial/resources/${bodyId}`) .then(({ data }) => { const grouped: any = {}; data.resources.forEach((res: any) => { if (!grouped[res.resource_type]) { grouped[res.resource_type] = []; } grouped[res.resource_type].push(res); }); setCurrentResources(grouped); }) .catch(() => { toast.error('加载资源列表失败'); }); }, [refreshTrigger, bodyId]); const resourceTypes = [ { key: 'icon', label: '图标 (上传到 icon 目录)', type: 'image' }, { key: 'texture', label: bodyType === 'probe' ? '纹理 (上传到 model 目录)' : '纹理 (上传到 texture 目录)', type: 'file' }, { key: 'model', label: bodyType === 'probe' ? '模型 (上传到 model 目录)' : '模型 (上传到 texture 目录)', type: 'file' }, ]; return ( {resourceTypes.map(({ key, label, type }) => (
{label}
{type === 'image' && currentResources?.[key] && currentResources[key].length > 0 ? ( // Image preview for icon
Icon preview
onUpload(file, key)} showUploadList={false} disabled={uploading} accept="image/*" >
onDelete(currentResources[key][0].id)} okText="删除" cancelText="取消" >
({(currentResources[key][0].file_size / 1024).toFixed(2)} KB)
) : type === 'image' ? ( // No icon uploaded yet onUpload(file, key)} showUploadList={false} disabled={uploading} accept="image/*" > ) : ( // File upload for texture/model <> onUpload(file, key)} showUploadList={false} disabled={uploading} > {currentResources?.[key] && currentResources[key].length > 0 && (
{currentResources[key].map((res: any) => (
{res.file_path} ({(res.file_size / 1024).toFixed(2)} KB) onDelete(res.id)} okText="删除" cancelText="取消" >
{key === 'model' && (
显示缩放: { // Update scale in resource const newScale = value || 1.0; request.put(`/celestial/resources/${res.id}`, { extra_data: { ...res.extra_data, scale: newScale } }).then(() => { toast.success('缩放参数已更新'); }).catch(() => { toast.error('更新失败'); }); }} /> (推荐: Webb=0.3, 旅行者=1.5)
)}
))}
)} )}
))}
); }