/** * Celestial Bodies Management Page */ import { useState, useEffect } from 'react'; import { message, Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import type { UploadFile } from 'antd/es/upload/interface'; 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(''); const [uploading, setUploading] = useState(false); const [refreshResources, setRefreshResources] = useState(0); 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 } }; // Handle resource upload const handleResourceUpload = async (file: File, resourceType: string) => { if (!editingRecord) { message.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', }, } ); message.success(`${response.data.message} (上传到 ${response.data.upload_directory} 目录)`); setRefreshResources(prev => prev + 1); // Trigger reload return false; // Prevent default upload behavior } catch (error: any) { message.error(error.response?.data?.detail || '上传失败'); return false; } finally { setUploading(false); } }; // Handle resource delete const handleResourceDelete = async (resourceId: number) => { try { await request.delete(`/celestial/resources/${resourceId}`); message.success('删除成功'); setRefreshResources(prev => prev + 1); // Trigger reload } catch (error: any) { message.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: '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 ( <> 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 && ( )}
); } // Resource Manager Component function ResourceManager({ bodyId, bodyType, resources, hasResources, onUpload, onDelete, uploading, refreshTrigger, }: { bodyId: string; bodyType: string; resources?: any; hasResources?: boolean; onUpload: (file: File, resourceType: string) => Promise; onDelete: (resourceId: number) => Promise; uploading: boolean; refreshTrigger: number; }) { 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(() => { message.error('加载资源列表失败'); }); }, [refreshTrigger, bodyId]); const resourceTypes = [ { key: 'texture', label: bodyType === 'probe' ? '纹理 (上传到 model 目录)' : '纹理 (上传到 texture 目录)' }, { key: 'model', label: bodyType === 'probe' ? '模型 (上传到 model 目录)' : '模型 (上传到 texture 目录)' }, ]; return ( {resourceTypes.map(({ key, label }) => (
{label}
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(() => { message.success('缩放参数已更新'); }).catch(() => { message.error('更新失败'); }); }} /> (推荐: Webb=0.3, 旅行者=1.5)
)}
))}
)}
))}
); }