cosmo/frontend/src/pages/admin/CelestialBodies.tsx

1061 lines
37 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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<CelestialBody[]>([]);
const [filteredData, setFilteredData] = useState<CelestialBody[]>([]);
const [starSystems, setStarSystems] = useState<StarSystem[]>([]);
const [selectedSystemId, setSelectedSystemId] = useState<number | null>(1); // 默认选择太阳系
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<CelestialBody | null>(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: (
<div>
<p>: <strong>{result.data.full_name}</strong></p>
<p>ID: <strong>{result.data.id}</strong></p>
<p style={{ color: '#faad14', marginTop: '10px' }}>
: <strong>{existingBody.name}</strong>
</p>
<p style={{ fontSize: '12px', color: '#888' }}>
</p>
</div>
),
});
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: (
<div>
<p>: <strong>{result.data.full_name}</strong></p>
<p> ID : <strong>{result.data.id}</strong></p>
<p style={{ color: '#faad14' }}>
: ID ID-48便
</p>
<p style={{ fontSize: '12px', color: '#888' }}>
<a href="https://ssd.jpl.nasa.gov/horizons/" target="_blank" rel="noopener noreferrer">NASA Horizons</a> ID
</p>
</div>
),
});
}
} 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<CelestialBody> = [
{
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 ? (
<Tag color="blue" icon={<StarOutlined />}>
{system.name_zh || system.name}
</Tag>
) : '-';
},
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
filters: [
{ text: '恒星', value: 'star' },
{ text: '行星', value: 'planet' },
{ text: '矮行星', value: 'dwarf_planet' },
{ text: '卫星', value: 'satellite' },
{ text: '彗星', value: 'comet' },
{ text: '探测器', value: 'probe' },
],
onFilter: (value, record) => record.type === value,
render: (type: string) => {
const typeMap: Record<string, string> = {
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 (
<Badge status="success" text={`${resourceTypes.length}`} />
);
}
return <Badge status="default" text="未配置" />;
},
},
];
return (
<>
{/* Star System Selector */}
<Card className="mb-4" style={{ marginBottom: 16 }}>
<Space>
<StarOutlined style={{ color: '#1890ff', fontSize: 18 }} />
<span style={{ fontWeight: 500 }}></span>
<Select
showSearch
style={{ width: 400 }}
value={selectedSystemId}
onChange={setSelectedSystemId}
placeholder="请选择恒星系统"
loading={starSystems.length === 0}
optionFilterProp="children"
filterOption={(input, option) => {
const system = starSystems.find(s => s.id === option?.value);
if (!system) return false;
const searchText = input.toLowerCase();
return (
system.name.toLowerCase().includes(searchText) ||
system.name_zh?.toLowerCase().includes(searchText) ||
system.id.toString().includes(searchText)
);
}}
>
{starSystems.map(system => (
<Select.Option key={system.id} value={system.id}>
{system.name_zh || system.name}
</Select.Option>
))}
</Select>
{selectedSystemId && (
<span style={{ color: '#888', fontSize: 12 }}>
{starSystems.find(s => s.id === selectedSystemId)?.name_zh || starSystems.find(s => s.id === selectedSystemId)?.name}
</span>
)}
</Space>
</Card>
<DataTable
title="天体数据管理"
columns={columns}
dataSource={filteredData}
loading={loading}
total={filteredData.length}
onSearch={handleSearch}
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
onStatusChange={handleStatusChange}
statusField="is_active"
rowKey="id"
pageSize={10}
customActions={(record) => {
// Show "Generate Orbit" button for all types, but disable for non-planets
const canGenerateOrbit = ['planet', 'dwarf_planet'].includes(record.type);
return (
<Popconfirm
title="确认生成轨道"
description={`确定要为 ${record.name_zh || record.name} 生成轨道吗?`}
onConfirm={() => handleGenerateOrbit(record)}
okText="确认"
cancelText="取消"
disabled={!canGenerateOrbit}
>
<Tooltip title={canGenerateOrbit ? "生成轨道" : "仅行星和矮行星可生成轨道"}>
<Button
type="text"
size="small"
className={canGenerateOrbit ? "text-green-600 hover:text-green-500" : ""}
loading={loading}
disabled={!canGenerateOrbit}
>
</Button>
</Tooltip>
</Popconfirm>
);
}}
/>
<Modal
title={editingRecord ? '编辑天体' : '新增天体'}
open={isModalOpen}
onOk={handleModalOk}
onCancel={() => setIsModalOpen(false)}
width={1000}
>
<Form
form={form}
layout="vertical"
>
{editingRecord ? (
// 编辑模式显示双tab
<Tabs activeKey={activeTabKey} onChange={setActiveTabKey}>
<Tabs.TabPane tab="基础信息" key="basic">
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="id"
label="JPL Horizons ID"
rules={[{ required: true, message: '请输入JPL Horizons ID' }]}
>
<Input disabled={!!editingRecord} placeholder="例如:-31 (Voyager 1) 或 399 (Earth)" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="type"
label="类型"
rules={[{ required: true, message: '请选择类型' }]}
>
<Select>
<Select.Option value="star"></Select.Option>
<Select.Option value="planet"></Select.Option>
<Select.Option value="dwarf_planet"></Select.Option>
<Select.Option value="satellite"></Select.Option>
<Select.Option value="comet"></Select.Option>
<Select.Option value="probe"></Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
{/* Hidden system_id field - auto-set from selector */}
<Form.Item name="system_id" hidden>
<InputNumber />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="name"
label="英文名"
rules={[{ required: true, message: '请输入英文名' }]}
>
<Input placeholder="例如Voyager 1" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="name_zh"
label="中文名"
>
<Input placeholder="例如旅行者1号" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="short_name"
label="简称"
tooltip="NASA SBDB API使用的短名称如Jupiter的简称是Juptr"
>
<Input placeholder="例如Juptr" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={2} />
</Form.Item>
{/* Orbit parameters and info for planets and dwarf planets */}
<Form.Item noStyle shouldUpdate={(prevValues, currentValues) =>
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 (
<Alert
message="轨道参数与信息"
description={
<div>
{/* Editable orbit parameters */}
<Row gutter={16}>
<Col span={12}>
<Form.Item
name={['extra_data', 'orbit_period_days']}
label="轨道周期(天)"
tooltip="完整公转一周所需的天数"
>
<InputNumber
style={{ width: '100%' }}
min={0}
step={1}
placeholder="例如365.25(地球)"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name={['extra_data', 'orbit_color']}
label="轨道颜色"
tooltip="轨道线的显示颜色HEX格式"
>
<Input
type="color"
placeholder="#4A90E2"
/>
</Form.Item>
</Col>
</Row>
{/* Display orbit info if exists (read-only) */}
{editingRecord && orbitInfo && orbitInfo.num_points && (
<div style={{
marginTop: 16,
paddingTop: 16,
borderTop: '1px solid #d9d9d9',
background: '#f0f9ff',
padding: '12px',
borderRadius: '4px'
}}>
<div style={{ fontWeight: 600, marginBottom: 8, color: '#1890ff' }}>
</div>
<div style={{ display: 'flex', gap: '24px' }}>
<div>
<strong>:</strong> {orbitInfo.num_points?.toLocaleString()}
</div>
{orbitInfo.period_days && (
<div>
<strong>:</strong> {orbitInfo.period_days.toFixed(2)}
</div>
)}
</div>
</div>
)}
</div>
}
type="info"
style={{ marginBottom: 16 }}
/>
);
}}
</Form.Item>
{editingRecord && (
<ResourceManager
bodyId={editingRecord.id}
bodyType={editingRecord.type}
resources={editingRecord.resources}
hasResources={editingRecord.has_resources}
onUpload={handleResourceUpload}
onDelete={handleResourceDelete}
uploading={uploading}
refreshTrigger={refreshResources}
toast={toast}
/>
)}
</Tabs.TabPane>
<Tabs.TabPane tab="详细信息" key="details">
<Form.Item name="details" style={{ marginBottom: 0 }}>
<MdEditor
value={form.getFieldValue('details')}
style={{ height: '500px' }}
renderHTML={(text) => MdEditorParser.render(text)}
onChange={({ text }) => form.setFieldsValue({ details: text })}
/>
</Form.Item>
</Tabs.TabPane>
</Tabs>
) : (
// 新增模式:只显示基础信息
<>
<Alert
title="智能搜索提示"
description={
<div>
<p>使 <strong>JPL Horizons ID</strong> </p>
<p style={{ marginTop: 4 }}>
Hubble ID <code>-48</code>Voyager 1 ID <code>-31</code>
</p>
<p style={{ marginTop: 4, fontSize: '12px', color: '#666' }}>
ID ID
</p>
</div>
}
type="info"
showIcon
style={{ marginBottom: 16 }}
/>
<Form.Item label="从 NASA 数据库搜索">
<Space.Compact style={{ width: '100%' }}>
<Input
placeholder="输入数字 ID (推荐, 如: -48) 或名称 (如: Hubble)"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onPressEnter={handleNASASearch}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={handleNASASearch}
loading={searching}
>
</Button>
</Space.Compact>
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="id"
label="JPL Horizons ID"
rules={[{ required: true, message: '请输入JPL Horizons ID' }]}
>
<Input placeholder="例如:-31 (Voyager 1) 或 399 (Earth)" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="type"
label="类型"
rules={[{ required: true, message: '请选择类型' }]}
>
<Select>
<Select.Option value="star"></Select.Option>
<Select.Option value="planet"></Select.Option>
<Select.Option value="dwarf_planet"></Select.Option>
<Select.Option value="satellite"></Select.Option>
<Select.Option value="comet"></Select.Option>
<Select.Option value="probe"></Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
{/* Hidden system_id field - auto-set from selector */}
<Form.Item name="system_id" hidden>
<InputNumber />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="name"
label="英文名"
rules={[{ required: true, message: '请输入英文名' }]}
>
<Input placeholder="例如Voyager 1" />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="name_zh"
label="中文名"
>
<Input placeholder="例如旅行者1号" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Form.Item
name="short_name"
label="简称"
tooltip="NASA SBDB API使用的短名称如Jupiter的简称是Juptr"
>
<Input placeholder="例如Juptr" />
</Form.Item>
</Col>
</Row>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={2} />
</Form.Item>
</>
)}
</Form>
</Modal>
</>
);
}
// 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<boolean>;
onDelete: (resourceId: number) => Promise<void>;
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 (
<Form.Item label="资源配置">
<Space direction="vertical" style={{ width: '100%' }} size="middle">
{resourceTypes.map(({ key, label, type }) => (
<div key={key}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{label}</div>
{type === 'image' && currentResources?.[key] && currentResources[key].length > 0 ? (
// Image preview for icon
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<img
src={`/upload/${currentResources[key][0].file_path}`}
alt="Icon preview"
style={{
width: 80,
height: 80,
objectFit: 'contain',
border: '1px solid #d9d9d9',
borderRadius: 4,
padding: 8,
backgroundColor: '#fafafa'
}}
/>
<div>
<Upload
beforeUpload={(file) => onUpload(file, key)}
showUploadList={false}
disabled={uploading}
accept="image/*"
>
<Button icon={<UploadOutlined />} loading={uploading} size="small">
</Button>
</Upload>
<div style={{ marginTop: 8 }}>
<Popconfirm
title="确认删除图标?"
onConfirm={() => onDelete(currentResources[key][0].id)}
okText="删除"
cancelText="取消"
>
<Button
type="link"
danger
size="small"
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>
</div>
<div style={{ fontSize: 12, color: '#888', marginTop: 4 }}>
({(currentResources[key][0].file_size / 1024).toFixed(2)} KB)
</div>
</div>
</div>
) : type === 'image' ? (
// No icon uploaded yet
<Upload
beforeUpload={(file) => onUpload(file, key)}
showUploadList={false}
disabled={uploading}
accept="image/*"
>
<Button icon={<UploadOutlined />} loading={uploading} size="small">
</Button>
</Upload>
) : (
// File upload for texture/model
<>
<Upload
beforeUpload={(file) => onUpload(file, key)}
showUploadList={false}
disabled={uploading}
>
<Button icon={<UploadOutlined />} loading={uploading} size="small">
{label.split(' ')[0]}
</Button>
</Upload>
{currentResources?.[key] && currentResources[key].length > 0 && (
<div style={{ marginTop: 8 }}>
{currentResources[key].map((res: any) => (
<div key={res.id} style={{ marginBottom: 8 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<Tag color="blue">{res.file_path}</Tag>
<span style={{ fontSize: 12, color: '#888' }}>
({(res.file_size / 1024).toFixed(2)} KB)
</span>
<Popconfirm
title="确认删除?"
onConfirm={() => onDelete(res.id)}
okText="删除"
cancelText="取消"
>
<Button
type="link"
danger
size="small"
icon={<DeleteOutlined />}
>
</Button>
</Popconfirm>
</div>
{key === 'model' && (
<div style={{ marginLeft: 8 }}>
<Space size="small">
<span style={{ fontSize: 12, color: '#666' }}>:</span>
<InputNumber
size="small"
min={0.1}
max={5}
step={0.1}
defaultValue={res.extra_data?.scale || 1.0}
style={{ width: 80 }}
placeholder="1.0"
onChange={(value) => {
// 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('更新失败');
});
}}
/>
<span style={{ fontSize: 11, color: '#999' }}>
(: Webb=0.3, =1.5)
</span>
</Space>
</div>
)}
</div>
))}
</div>
)}
</>
)}
</div>
))}
</Space>
</Form.Item>
);
}