diff --git a/frontend/src/Router.tsx b/frontend/src/Router.tsx index 1cf9479..7e5666a 100644 --- a/frontend/src/Router.tsx +++ b/frontend/src/Router.tsx @@ -8,6 +8,8 @@ import { Dashboard } from './pages/admin/Dashboard'; import { CelestialBodies } from './pages/admin/CelestialBodies'; import { StaticData } from './pages/admin/StaticData'; import { Users } from './pages/admin/Users'; +import { NASADownload } from './pages/admin/NASADownload'; +import { SystemSettings } from './pages/admin/SystemSettings'; import { auth } from './utils/auth'; import { ToastProvider } from './contexts/ToastContext'; import App from './App'; @@ -45,7 +47,8 @@ export function Router() { } /> } /> } /> - NASA数据下载管理开发中...} /> + } /> + } /> {/* Fallback */} diff --git a/frontend/src/components/ProbeList.tsx b/frontend/src/components/ProbeList.tsx index 5bb6046..3dd3c89 100644 --- a/frontend/src/components/ProbeList.tsx +++ b/frontend/src/components/ProbeList.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { ChevronLeft, ChevronRight, Search, Globe, Rocket } from 'lucide-react'; +import { ChevronLeft, ChevronRight, ChevronDown, ChevronUp, Search, Globe, Rocket, Moon, Asterisk } from 'lucide-react'; import type { CelestialBody } from '../types'; interface ProbeListProps { @@ -13,6 +13,7 @@ interface ProbeListProps { export function ProbeList({ probes, planets, onBodySelect, selectedBody, onResetCamera }: ProbeListProps) { const [isCollapsed, setIsCollapsed] = useState(false); const [searchTerm, setSearchTerm] = useState(''); + const [expandedGroup, setExpandedGroup] = useState(null); // 只允许一个分组展开 // Calculate distance for sorting const calculateDistance = (body: CelestialBody) => { @@ -22,7 +23,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset const processBodies = (list: CelestialBody[]) => { return list - .filter(b => + .filter(b => (b.name_zh || b.name).toLowerCase().includes(searchTerm.toLowerCase()) && b.type !== 'star' // Exclude Sun from list ) @@ -33,8 +34,19 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset .sort((a, b) => a.distance - b.distance); }; - const planetList = processBodies(planets); - const probeList = processBodies(probes); + // Group bodies by type + const allBodies = [...planets, ...probes]; + const processedBodies = processBodies(allBodies); + + const planetList = processedBodies.filter(({ body }) => body.type === 'planet'); + const dwarfPlanetList = processedBodies.filter(({ body }) => body.type === 'dwarf_planet'); + const satelliteList = processedBodies.filter(({ body }) => body.type === 'satellite'); + const probeList = processedBodies.filter(({ body }) => body.type === 'probe'); + + const toggleGroup = (groupName: string) => { + // 如果点击的是当前展开的分组,则收起;否则切换到新分组 + setExpandedGroup(prev => prev === groupName ? null : groupName); + }; return ( {/* List Content */} - + {/* Planets Group */} - - {/* text-xs -> text-[10px] */} - - 行星 ({planetList.length}) - - - {planetList.map(({ body, distance }) => ( - onBodySelect(body)} - /> - ))} - - + {planetList.length > 0 && ( + } + count={planetList.length} + bodies={planetList} + isExpanded={expandedGroup === 'planet'} + onToggle={() => toggleGroup('planet')} + selectedBody={selectedBody} + onBodySelect={onBodySelect} + /> + )} + + {/* Dwarf Planets Group */} + {dwarfPlanetList.length > 0 && ( + } + count={dwarfPlanetList.length} + bodies={dwarfPlanetList} + isExpanded={expandedGroup === 'dwarf_planet'} + onToggle={() => toggleGroup('dwarf_planet')} + selectedBody={selectedBody} + onBodySelect={onBodySelect} + /> + )} + + {/* Satellites Group */} + {satelliteList.length > 0 && ( + } + count={satelliteList.length} + bodies={satelliteList} + isExpanded={expandedGroup === 'satellite'} + onToggle={() => toggleGroup('satellite')} + selectedBody={selectedBody} + onBodySelect={onBodySelect} + /> + )} {/* Probes Group */} - - {/* text-xs -> text-[10px] */} - - 探测器 ({probeList.length}) + {probeList.length > 0 && ( + } + count={probeList.length} + bodies={probeList} + isExpanded={expandedGroup === 'probe'} + onToggle={() => toggleGroup('probe')} + selectedBody={selectedBody} + onBodySelect={onBodySelect} + /> + )} + + {/* No results message */} + {processedBodies.length === 0 && ( + + 未找到匹配的天体 - - {probeList.map(({ body, distance }) => ( - onBodySelect(body)} - /> - ))} - - + )} @@ -123,7 +160,7 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset setIsCollapsed(!isCollapsed)} className={` - absolute top-0 ${isCollapsed ? 'left-0' : '-right-4'} + absolute top-0 ${isCollapsed ? 'left-0' : '-right-4'} w-8 h-8 flex items-center justify-center bg-black/80 backdrop-blur-md border border-white/10 rounded-full text-white hover:bg-blue-600 transition-all shadow-lg z-50 @@ -137,6 +174,59 @@ export function ProbeList({ probes, planets, onBodySelect, selectedBody, onReset ); } +// Group component for collapsible body lists +function BodyGroup({ + title, + icon, + count, + bodies, + isExpanded, + onToggle, + selectedBody, + onBodySelect +}: { + title: string; + icon: React.ReactNode; + count: number; + bodies: Array<{ body: CelestialBody; distance: number }>; + isExpanded: boolean; + onToggle: () => void; + selectedBody: CelestialBody | null; + onBodySelect: (body: CelestialBody) => void; +}) { + return ( + + {/* Group Header */} + + + {icon} + {title} + ({count}) + + {isExpanded ? : } + + + {/* Group Content */} + {isExpanded && ( + + {bodies.map(({ body, distance }) => ( + onBodySelect(body)} + /> + ))} + + )} + + ); +} + function BodyItem({ body, distance, isSelected, onClick }: { body: CelestialBody, distance: number, diff --git a/frontend/src/components/admin/DataTable.tsx b/frontend/src/components/admin/DataTable.tsx index 2e630e7..6a1d934 100644 --- a/frontend/src/components/admin/DataTable.tsx +++ b/frontend/src/components/admin/DataTable.tsx @@ -130,8 +130,7 @@ export function DataTable({ )} } - bodyStyle={{ padding: 0 }} - bordered={false} + styles={{ body: { padding: 0 } }} > diff --git a/frontend/src/pages/admin/CelestialBodies.tsx b/frontend/src/pages/admin/CelestialBodies.tsx index 4de540d..61dd974 100644 --- a/frontend/src/pages/admin/CelestialBodies.tsx +++ b/frontend/src/pages/admin/CelestialBodies.tsx @@ -2,7 +2,8 @@ * Celestial Bodies Management Page */ import { useState, useEffect } from 'react'; -import { message, Modal, Form, Input, Select, Switch, InputNumber } from 'antd'; +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'; @@ -14,6 +15,15 @@ interface CelestialBody { 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() { @@ -23,6 +33,8 @@ export function CelestialBodies() { 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(); @@ -57,11 +69,65 @@ export function CelestialBodies() { 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); @@ -167,6 +233,20 @@ export function CelestialBodies() { key: 'description', ellipsis: true, }, + { + title: '资源配置', + key: 'resources', + width: 120, + render: (_, record) => { + if (record.has_resources) { + const resourceTypes = Object.keys(record.resources || {}); + return ( + + ); + } + return ; + }, + }, ]; return ( @@ -192,19 +272,61 @@ export function CelestialBodies() { open={isModalOpen} onOk={handleModalOk} onCancel={() => 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} + /> + } + onClick={handleNASASearch} + loading={searching} + > + 搜索 + + + + > + )} + - + + + {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) + + + ))} + + ))} + + + 提示:资源文件管理功能即将推出,敬请期待 + + + )} > diff --git a/frontend/src/pages/admin/NASADownload.tsx b/frontend/src/pages/admin/NASADownload.tsx new file mode 100644 index 0000000..b3a27d3 --- /dev/null +++ b/frontend/src/pages/admin/NASADownload.tsx @@ -0,0 +1,373 @@ +/** + * NASA Data Download Page + * Downloads position data for celestial bodies from NASA Horizons API + */ +import { useState, useEffect } from 'react'; +import { + Row, + Col, + Card, + Checkbox, + DatePicker, + Button, + message, + Badge, + Spin, + Typography, + Collapse, + Space, + Progress, + Calendar, + Alert +} from 'antd'; +import { + DownloadOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + LoadingOutlined +} from '@ant-design/icons'; +import type { CheckboxChangeEvent } from 'antd/es/checkbox'; +import type { Dayjs } from 'dayjs'; +import dayjs from 'dayjs'; +import isBetween from 'dayjs/plugin/isBetween'; +import { request } from '../../utils/request'; + +// Extend dayjs with isBetween plugin +dayjs.extend(isBetween); + +const { Title, Text } = Typography; +const { RangePicker } = DatePicker; + +interface CelestialBody { + id: string; + name: string; + name_zh: string; + type: string; + is_active: boolean; + description: string; +} + +interface GroupedBodies { + [type: string]: CelestialBody[]; +} + +export function NASADownload() { + const [loading, setLoading] = useState(false); + const [bodies, setBodies] = useState({}); + const [selectedBodies, setSelectedBodies] = useState([]); + const [dateRange, setDateRange] = useState<[Dayjs, Dayjs]>([ + dayjs().subtract(1, 'month').startOf('month'), + dayjs().subtract(1, 'month').endOf('month') + ]); + const [availableDates, setAvailableDates] = useState>(new Set()); + const [loadingDates, setLoadingDates] = useState(false); + const [downloading, setDownloading] = useState(false); + const [downloadProgress, setDownloadProgress] = useState({ current: 0, total: 0 }); + + // Type name mapping + const typeNames: Record = { + star: '恒星', + planet: '行星', + dwarf_planet: '矮行星', + satellite: '卫星', + probe: '探测器', + }; + + useEffect(() => { + loadBodies(); + }, []); + + useEffect(() => { + if (selectedBodies.length > 0) { + loadAvailableDates(); + } else { + setAvailableDates(new Set()); + } + }, [selectedBodies, dateRange]); + + const loadBodies = async () => { + setLoading(true); + try { + const { data } = await request.get('/celestial/positions/download/bodies'); + setBodies(data.bodies || {}); + } catch (error) { + message.error('加载天体列表失败'); + } finally { + setLoading(false); + } + }; + + const loadAvailableDates = async () => { + if (selectedBodies.length === 0) return; + + setLoadingDates(true); + try { + const allDates = new Set(); + + // Load available dates for the first selected body + const bodyId = selectedBodies[0]; + const startDate = dateRange[0].format('YYYY-MM-DD'); + const endDate = dateRange[1].format('YYYY-MM-DD'); + + const { data } = await request.get('/celestial/positions/download/status', { + params: { + body_id: bodyId, + start_date: startDate, + end_date: endDate + } + }); + + data.available_dates.forEach((date: string) => { + allDates.add(date); + }); + + setAvailableDates(allDates); + } catch (error) { + message.error('加载数据状态失败'); + } finally { + setLoadingDates(false); + } + }; + + const handleBodySelect = (bodyId: string, checked: boolean) => { + setSelectedBodies(prev => + checked ? [...prev, bodyId] : prev.filter(id => id !== bodyId) + ); + }; + + const handleTypeSelectAll = (type: string, checked: boolean) => { + const typeBodyIds = bodies[type].map(b => b.id); + setSelectedBodies(prev => { + if (checked) { + return [...new Set([...prev, ...typeBodyIds])]; + } else { + return prev.filter(id => !typeBodyIds.includes(id)); + } + }); + }; + + const handleDateRangeChange = (dates: any) => { + if (dates && dates[0] && dates[1]) { + setDateRange([dates[0], dates[1]]); + } + }; + + const handleDownload = async (selectedDate?: Dayjs) => { + if (selectedBodies.length === 0) { + message.warning('请先选择至少一个天体'); + return; + } + + let datesToDownload: string[] = []; + + if (selectedDate) { + // Download single date + datesToDownload = [selectedDate.format('YYYY-MM-DD')]; + } else { + // Download all dates in range + const start = dateRange[0]; + const end = dateRange[1]; + let current = start; + + while (current.isBefore(end) || current.isSame(end, 'day')) { + datesToDownload.push(current.format('YYYY-MM-DD')); + current = current.add(1, 'day'); + } + } + + setDownloading(true); + setDownloadProgress({ current: 0, total: datesToDownload.length }); + + try { + const { data } = await request.post('/celestial/positions/download', { + body_ids: selectedBodies, + dates: datesToDownload + }); + + setDownloadProgress({ current: datesToDownload.length, total: datesToDownload.length }); + + if (data.total_success > 0) { + message.success(`成功下载 ${data.total_success} 条数据${data.total_failed > 0 ? `,${data.total_failed} 条失败` : ''}`); + // Reload available dates to show newly downloaded data + loadAvailableDates(); + } else { + message.error('下载失败'); + } + } catch (error) { + message.error('下载请求失败'); + } finally { + setDownloading(false); + setDownloadProgress({ current: 0, total: 0 }); + } + }; + + // Custom calendar cell renderer + const dateCellRender = (value: Dayjs) => { + const dateStr = value.format('YYYY-MM-DD'); + const hasData = availableDates.has(dateStr); + const inRange = value.isBetween(dateRange[0], dateRange[1], 'day', '[]'); + + if (!inRange) return null; + + return ( + + {hasData ? ( + + ) : ( + + )} + + ); + }; + + const disabledDate = (current: Dayjs) => { + // Cannot select dates in the future + return current && current.isAfter(dayjs(), 'day'); + }; + + const handleCalendarDateClick = (date: Dayjs) => { + const dateStr = date.format('YYYY-MM-DD'); + const hasData = availableDates.has(dateStr); + const inRange = date.isBetween(dateRange[0], dateRange[1], 'day', '[]'); + + if (!inRange) { + message.warning('请选择在日期范围内的日期'); + return; + } + + if (hasData) { + message.info('该日期已有数据'); + return; + } + + if (selectedBodies.length === 0) { + message.warning('请先选择天体'); + return; + } + + handleDownload(date); + }; + + return ( + + + + + {/* Left: Body Selection */} + + + 已选择: {selectedBodies.length} + + } + > + ({ + key: type, + label: ( + + {typeNames[type] || type} + selectedBodies.includes(b.id))} + indeterminate={ + typeBodies.some(b => selectedBodies.includes(b.id)) && + !typeBodies.every(b => selectedBodies.includes(b.id)) + } + onChange={(e) => { + e.stopPropagation(); + handleTypeSelectAll(type, e.target.checked); + }} + > + 全选 + + + ), + children: ( + + {typeBodies.map((body) => ( + handleBodySelect(body.id, e.target.checked)} + > + {body.name_zh || body.name} + {!body.is_active && } + + ))} + + ), + }))} + /> + + + + {/* Right: Date Selection and Calendar */} + + + + } + onClick={() => handleDownload()} + disabled={selectedBodies.length === 0} + loading={downloading} + > + 下载范围内所有日期 + + + } + > + }> + + + + + + + + {downloading && ( + + + + 正在下载: {downloadProgress.current} / {downloadProgress.total} + + + )} + + + + + + + + ); +} diff --git a/frontend/src/pages/admin/SystemSettings.tsx b/frontend/src/pages/admin/SystemSettings.tsx new file mode 100644 index 0000000..9d63a3d --- /dev/null +++ b/frontend/src/pages/admin/SystemSettings.tsx @@ -0,0 +1,421 @@ +/** + * System Settings Management Page + */ +import { useState, useEffect } from 'react'; +import { message, Modal, Form, Input, InputNumber, Switch, Select, Button, Card, Descriptions, Badge, Space, Popconfirm, Alert, Divider } from 'antd'; +import { ReloadOutlined, ClearOutlined, WarningOutlined } from '@ant-design/icons'; +import type { ColumnsType } from 'antd/es/table'; +import { DataTable } from '../../components/admin/DataTable'; +import { request } from '../../utils/request'; + +interface SystemSetting { + id: number; + key: string; + value: any; + raw_value: string; + value_type: 'string' | 'int' | 'float' | 'bool' | 'json'; + category: string; + label: string; + description?: string; + is_public: boolean; + created_at?: string; + updated_at?: string; +} + +const CATEGORY_MAP: Record = { + visualization: '可视化', + cache: '缓存', + ui: '界面', + api: 'API', + general: '常规', +}; + +export function SystemSettings() { + 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 [clearingCache, setClearingCache] = useState(false); + + useEffect(() => { + loadData(); + }, []); + + const loadData = async () => { + setLoading(true); + try { + const { data: result } = await request.get('/system/settings'); + setData(result.settings || []); + setFilteredData(result.settings || []); + } catch (error) { + message.error('加载数据失败'); + } finally { + setLoading(false); + } + }; + + // Search handler + const handleSearch = (keyword: string) => { + const lowerKeyword = keyword.toLowerCase(); + const filtered = data.filter( + (item) => + item.key.toLowerCase().includes(lowerKeyword) || + item.label?.toLowerCase().includes(lowerKeyword) || + item.category?.toLowerCase().includes(lowerKeyword) + ); + setFilteredData(filtered); + }; + + // Add handler + const handleAdd = () => { + setEditingRecord(null); + form.resetFields(); + form.setFieldsValue({ value_type: 'string', category: 'general', is_public: false }); + setIsModalOpen(true); + }; + + // Edit handler + const handleEdit = (record: SystemSetting) => { + setEditingRecord(record); + form.setFieldsValue({ + key: record.key, + value: record.value, + value_type: record.value_type, + category: record.category, + label: record.label, + description: record.description, + is_public: record.is_public, + }); + setIsModalOpen(true); + }; + + // Delete handler + const handleDelete = async (record: SystemSetting) => { + try { + await request.delete(`/system/settings/${record.key}`); + message.success('删除成功'); + loadData(); + } catch (error) { + message.error('删除失败'); + } + }; + + // Form submit + const handleModalOk = async () => { + try { + const values = await form.validateFields(); + + if (editingRecord) { + // Update + await request.put(`/system/settings/${editingRecord.key}`, values); + message.success('更新成功'); + } else { + // Create + await request.post('/system/settings', values); + message.success('创建成功'); + } + + setIsModalOpen(false); + loadData(); + } catch (error) { + console.error(error); + } + }; + + // Clear all caches + const handleClearCache = async () => { + setClearingCache(true); + try { + const { data } = await request.post('/system/cache/clear'); + message.success( + <> + {data.message} + + 位置缓存: {data.redis_cache.positions_keys} 个键 | NASA缓存: {data.redis_cache.nasa_keys} 个键 + + >, + 5 + ); + loadData(); + } catch (error) { + message.error('清除缓存失败'); + } finally { + setClearingCache(false); + } + }; + + const columns: ColumnsType = [ + { + title: '参数键', + dataIndex: 'key', + key: 'key', + width: 220, + fixed: 'left', + render: (key: string, record) => ( + + {key} + {record.is_public && ( + + )} + + ), + }, + { + title: '名称', + dataIndex: 'label', + key: 'label', + width: 180, + }, + { + title: '当前值', + dataIndex: 'value', + key: 'value', + width: 150, + render: (value: any, record) => { + if (record.value_type === 'bool') { + return ; + } + return {String(value)}; + }, + }, + { + title: '类型', + dataIndex: 'value_type', + key: 'value_type', + width: 90, + filters: [ + { text: '字符串', value: 'string' }, + { text: '整数', value: 'int' }, + { text: '浮点数', value: 'float' }, + { text: '布尔值', value: 'bool' }, + { text: 'JSON', value: 'json' }, + ], + onFilter: (value, record) => record.value_type === value, + render: (type: string) => { + const typeMap: Record = { + string: '字符串', + int: '整数', + float: '浮点数', + bool: '布尔值', + json: 'JSON', + }; + return typeMap[type] || type; + }, + }, + { + title: '分类', + dataIndex: 'category', + key: 'category', + width: 100, + filters: Object.keys(CATEGORY_MAP).map((key) => ({ + text: CATEGORY_MAP[key], + value: key, + })), + onFilter: (value, record) => record.category === value, + render: (category: string) => CATEGORY_MAP[category] || category, + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + ellipsis: true, + }, + ]; + + return ( + <> + {/* Cache Management Card */} + + + 缓存管理 + + } + style={{ marginBottom: 16 }} + styles={{ body: { padding: 16 } }} + > + + + 清除缓存会清空所有内存缓存和 Redis 缓存,包括: + + + 位置数据缓存(当前位置和历史位置) + NASA API 响应缓存 + 所有其他临时缓存数据 + + + 清除后下次查询会重新从数据库或 NASA API 获取数据,可能会较慢 + + + } + type="warning" + showIcon + style={{ marginBottom: 16 }} + /> + + + + } + loading={clearingCache} + > + 清除所有缓存 + + + + } onClick={loadData}> + 刷新数据 + + + + + + + {/* Settings Table */} + + + setIsModalOpen(false)} + width={700} + > + + + + + + + + + + + + 字符串 + 整数 + 浮点数 + 布尔值 + JSON + + + + + prevValues.value_type !== currentValues.value_type + } + > + {({ getFieldValue }) => { + const valueType = getFieldValue('value_type'); + if (valueType === 'bool') { + return ( + + + + ); + } else if (valueType === 'int' || valueType === 'float') { + return ( + + + + ); + } else { + return ( + + + + ); + } + }} + + + + + 可视化 + 缓存 + 界面 + API + 常规 + + + + + + + + + + + + + > + ); +}
开发中...
找到天体: {result.data.full_name}
自动填充的 ID 为: {result.data.id}
+ ⚠️ 建议: 如果知道该天体的数字 ID,请手动修改为数字 ID(如:-48)以便后续查询位置数据。 +
+ 提示:您可以在 NASA Horizons 网站查询准确的数字 ID +
推荐使用 JPL Horizons 数字 ID 进行搜索,可获得最准确的结果。
+ 示例:Hubble 的 ID 是 -48,Voyager 1 的 ID 是 -31 +
-48
-31
+ 不知道 ID?可以先用名称搜索,系统会尽量提取 ID,或提示您手动确认。 +
+ 清除缓存会清空所有内存缓存和 Redis 缓存,包括: +
+ 清除后下次查询会重新从数据库或 NASA API 获取数据,可能会较慢 +