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

793 lines
26 KiB
TypeScript
Raw Normal View History

2025-11-29 15:10:00 +00:00
import { useState, useEffect } from 'react';
2025-12-06 09:06:39 +00:00
import { Modal, Form, Input, Select, Switch, InputNumber, Tag, Badge, Descriptions, Button, Space, Alert, Upload, Popconfirm, Row, Col, Tabs, Card } from 'antd';
import { CheckCircleOutlined, CloseCircleOutlined, SearchOutlined, UploadOutlined, DeleteOutlined, StarOutlined } from '@ant-design/icons';
2025-11-30 15:04:04 +00:00
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';
2025-11-30 02:43:47 +00:00
import { DataTable } from '../../components/admin/DataTable';
2025-11-29 15:10:00 +00:00
import { request } from '../../utils/request';
import { useToast } from '../../contexts/ToastContext';
2025-11-29 15:10:00 +00:00
const MdEditorParser = new MarkdownIt();
2025-11-29 15:10:00 +00:00
interface CelestialBody {
id: string;
name: string;
name_zh: string;
type: string;
2025-12-06 09:06:39 +00:00
system_id?: number;
2025-11-29 15:10:00 +00:00
description: string;
details?: string; // Added details field
2025-11-30 02:43:47 +00:00
is_active: boolean;
2025-11-30 05:25:41 +00:00
resources?: {
[key: string]: Array<{
id: number;
file_path: string;
file_size: number;
mime_type: string;
}>;
};
has_resources?: boolean;
2025-11-29 15:10:00 +00:00
}
2025-12-06 09:06:39 +00:00
interface StarSystem {
id: number;
name: string;
name_zh: string;
planet_count: number;
}
2025-11-29 15:10:00 +00:00
export function CelestialBodies() {
const [loading, setLoading] = useState(false);
const [data, setData] = useState<CelestialBody[]>([]);
2025-11-30 02:43:47 +00:00
const [filteredData, setFilteredData] = useState<CelestialBody[]>([]);
2025-12-06 09:06:39 +00:00
const [starSystems, setStarSystems] = useState<StarSystem[]>([]);
const [selectedSystemId, setSelectedSystemId] = useState<number | null>(1); // 默认选择太阳系
2025-11-30 02:43:47 +00:00
const [isModalOpen, setIsModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<CelestialBody | null>(null);
const [form] = Form.useForm();
2025-11-30 05:25:41 +00:00
const [searching, setSearching] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
2025-11-30 15:04:04 +00:00
const [uploading, setUploading] = useState(false);
const [refreshResources, setRefreshResources] = useState(0);
const [activeTabKey, setActiveTabKey] = useState('basic'); // State for active tab
const toast = useToast();
2025-11-29 15:10:00 +00:00
2025-12-06 09:06:39 +00:00
// 加载恒星系统列表
2025-11-29 15:10:00 +00:00
useEffect(() => {
2025-12-06 09:06:39 +00:00
loadStarSystems();
2025-11-29 15:10:00 +00:00
}, []);
2025-12-06 09:06:39 +00:00
// 当选择的恒星系改变时,重新加载天体数据
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('加载恒星系统列表失败');
}
};
2025-11-29 15:10:00 +00:00
const loadData = async () => {
2025-12-06 09:06:39 +00:00
if (selectedSystemId === null) {
setData([]);
setFilteredData([]);
return;
}
2025-11-29 15:10:00 +00:00
setLoading(true);
try {
2025-12-06 09:06:39 +00:00
const { data: result } = await request.get('/celestial/list', {
params: { system_id: selectedSystemId }
});
2025-11-29 15:10:00 +00:00
setData(result.bodies || []);
2025-11-30 02:43:47 +00:00
setFilteredData(result.bodies || []);
2025-11-29 15:10:00 +00:00
} catch (error) {
toast.error('加载数据失败');
2025-11-29 15:10:00 +00:00
} finally {
setLoading(false);
}
};
2025-11-30 02:43:47 +00:00
// 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();
2025-11-30 05:25:41 +00:00
setSearchQuery('');
setActiveTabKey('basic'); // Reset to basic tab
2025-11-30 02:43:47 +00:00
// Default values
2025-12-06 09:06:39 +00:00
form.setFieldsValue({
is_active: true,
type: 'probe',
system_id: selectedSystemId // Auto-set to current selected system
});
2025-11-30 02:43:47 +00:00
setIsModalOpen(true);
};
2025-11-30 05:25:41 +00:00
// Search NASA Horizons by name
const handleNASASearch = async () => {
if (!searchQuery.trim()) {
toast.warning('请输入天体名称或ID');
2025-11-30 05:25:41 +00:00
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;
}
2025-11-30 05:25:41 +00:00
// 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}`);
2025-11-30 05:25:41 +00:00
} 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 || '查询失败');
2025-11-30 05:25:41 +00:00
}
} catch (error: any) {
toast.error(error.response?.data?.detail || '查询失败');
2025-11-30 05:25:41 +00:00
} finally {
setSearching(false);
}
};
2025-11-30 02:43:47 +00:00
// Edit handler
const handleEdit = (record: CelestialBody) => {
setEditingRecord(record);
form.setFieldsValue(record);
setActiveTabKey('basic'); // Reset to basic tab
2025-11-30 02:43:47 +00:00
setIsModalOpen(true);
};
// Delete handler
const handleDelete = async (record: CelestialBody) => {
try {
await request.delete(`/celestial/${record.id}`);
toast.success('删除成功');
2025-11-30 02:43:47 +00:00
loadData();
} catch (error) {
toast.error('删除失败');
2025-11-30 02:43:47 +00:00
}
};
// Status change handler
const handleStatusChange = async (record: CelestialBody, checked: boolean) => {
try {
await request.put(`/celestial/${record.id}`, { is_active: checked });
toast.success(`状态更新成功`);
2025-11-30 02:43:47 +00:00
// 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('状态更新失败');
2025-11-30 02:43:47 +00:00
}
};
// Form submit
const handleModalOk = async () => {
2025-12-06 09:06:39 +00:00
let values: any;
2025-11-30 02:43:47 +00:00
try {
2025-12-06 09:06:39 +00:00
values = await form.validateFields();
2025-11-30 15:04:04 +00:00
2025-11-30 02:43:47 +00:00
if (editingRecord) {
// Update
await request.put(`/celestial/${editingRecord.id}`, values);
toast.success('更新成功');
2025-11-30 02:43:47 +00:00
} else {
// Create
await request.post('/celestial/', values);
toast.success('创建成功');
2025-11-30 02:43:47 +00:00
}
2025-11-30 15:04:04 +00:00
2025-11-30 02:43:47 +00:00
setIsModalOpen(false);
loadData();
} catch (error: any) {
2025-11-30 02:43:47 +00:00
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')) {
2025-12-06 09:06:39 +00:00
toast.error(`天体已存在: ${values?.id || ''}`);
} else {
toast.error(detail || '请检查表单数据是否完整');
}
} else if (error.errorFields) {
// Validation error
toast.error('请填写所有必填字段');
} else {
toast.error(error.response?.data?.detail || '操作失败');
}
2025-11-30 02:43:47 +00:00
}
};
2025-11-30 15:04:04 +00:00
// Handle resource upload
const handleResourceUpload = async (file: File, resourceType: string) => {
if (!editingRecord) {
toast.error('请先选择要编辑的天体');
2025-11-30 15:04:04 +00:00
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} 目录)`);
2025-11-30 15:04:04 +00:00
setRefreshResources(prev => prev + 1); // Trigger reload
return false; // Prevent default upload behavior
} catch (error: any) {
toast.error(error.response?.data?.detail || '上传失败');
2025-11-30 15:04:04 +00:00
return false;
} finally {
setUploading(false);
}
};
// Handle resource delete
const handleResourceDelete = async (resourceId: number) => {
try {
await request.delete(`/celestial/resources/${resourceId}`);
toast.success('删除成功');
2025-11-30 15:04:04 +00:00
setRefreshResources(prev => prev + 1); // Trigger reload
} catch (error: any) {
toast.error(error.response?.data?.detail || '删除失败');
2025-11-30 15:04:04 +00:00
}
};
2025-11-29 15:10:00 +00:00
const columns: ColumnsType<CelestialBody> = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 100,
2025-11-30 02:43:47 +00:00
sorter: (a, b) => a.id.localeCompare(b.id),
2025-11-29 15:10:00 +00:00
},
{
title: '英文名',
dataIndex: 'name',
key: 'name',
2025-11-30 02:43:47 +00:00
sorter: (a, b) => a.name.localeCompare(b.name),
2025-11-29 15:10:00 +00:00
},
{
title: '中文名',
dataIndex: 'name_zh',
key: 'name_zh',
},
2025-12-06 09:06:39 +00:00
{
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>
) : '-';
},
},
2025-11-29 15:10:00 +00:00
{
title: '类型',
dataIndex: 'type',
key: 'type',
2025-11-30 02:43:47 +00:00
filters: [
{ text: '行星', value: 'planet' },
{ text: '恒星', value: 'star' },
{ text: '卫星', value: 'satellite' },
{ text: '探测器', value: 'probe' },
{ text: '矮行星', value: 'dwarf_planet' },
2025-11-30 15:04:04 +00:00
{ text: '彗星', value: 'comet' },
2025-11-30 02:43:47 +00:00
],
onFilter: (value, record) => record.type === value,
2025-11-29 15:10:00 +00:00
render: (type: string) => {
const typeMap: Record<string, string> = {
star: '恒星',
planet: '行星',
dwarf_planet: '矮行星',
2025-11-30 02:43:47 +00:00
satellite: '卫星',
2025-11-29 15:10:00 +00:00
probe: '探测器',
2025-11-30 15:04:04 +00:00
comet: '彗星',
2025-11-29 15:10:00 +00:00
};
return typeMap[type] || type;
},
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
2025-11-30 05:25:41 +00:00
{
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="未配置" />;
},
},
2025-11-29 15:10:00 +00:00
];
return (
2025-11-30 02:43:47 +00:00
<>
2025-12-06 09:06:39 +00:00
{/* 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} ({system.planet_count} )
</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>
2025-11-30 02:43:47 +00:00
<DataTable
title="天体数据管理"
2025-11-29 15:10:00 +00:00
columns={columns}
2025-11-30 02:43:47 +00:00
dataSource={filteredData}
2025-11-29 15:10:00 +00:00
loading={loading}
2025-11-30 02:43:47 +00:00
total={filteredData.length}
onSearch={handleSearch}
onAdd={handleAdd}
onEdit={handleEdit}
onDelete={handleDelete}
onStatusChange={handleStatusChange}
statusField="is_active"
rowKey="id"
pageSize={10}
2025-11-29 15:10:00 +00:00
/>
2025-11-30 02:43:47 +00:00
<Modal
title={editingRecord ? '编辑天体' : '新增天体'}
open={isModalOpen}
onOk={handleModalOk}
onCancel={() => setIsModalOpen(false)}
width={1000}
2025-11-30 02:43:47 +00:00
>
<Form
form={form}
layout="vertical"
>
2025-12-06 09:06:39 +00:00
{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' }]}
2025-11-30 05:25:41 +00:00
>
<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="planet"></Select.Option>
<Select.Option value="dwarf_planet"></Select.Option>
<Select.Option value="satellite"></Select.Option>
<Select.Option value="probe"></Select.Option>
<Select.Option value="star"></Select.Option>
<Select.Option value="comet"></Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
2025-12-06 09:06:39 +00:00
{/* 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>
2025-11-30 05:25:41 +00:00
2025-11-30 15:04:04 +00:00
<Form.Item
name="description"
label="描述"
2025-11-30 15:04:04 +00:00
>
<Input.TextArea rows={2} />
2025-11-30 15:04:04 +00:00
</Form.Item>
2025-11-30 02:43:47 +00:00
{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 })}
/>
2025-11-30 15:04:04 +00:00
</Form.Item>
</Tabs.TabPane>
</Tabs>
2025-12-06 09:06:39 +00:00
) : (
// 新增模式:只显示基础信息
<>
<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="planet"></Select.Option>
<Select.Option value="dwarf_planet"></Select.Option>
<Select.Option value="satellite"></Select.Option>
<Select.Option value="probe"></Select.Option>
<Select.Option value="star"></Select.Option>
<Select.Option value="comet"></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>
<Form.Item
name="description"
label="描述"
>
<Input.TextArea rows={2} />
</Form.Item>
</>
)}
2025-11-30 02:43:47 +00:00
</Form>
</Modal>
</>
2025-11-29 15:10:00 +00:00
);
2025-11-30 15:04:04 +00:00
}
// Resource Manager Component
function ResourceManager({
bodyId,
bodyType,
resources,
hasResources,
onUpload,
onDelete,
uploading,
refreshTrigger,
toast,
2025-11-30 15:04:04 +00:00
}: {
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;
2025-11-30 15:04:04 +00:00
}) {
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('加载资源列表失败');
2025-11-30 15:04:04 +00:00
});
}, [refreshTrigger, bodyId]);
const resourceTypes = [
{ key: 'texture', label: bodyType === 'probe' ? '纹理 (上传到 model 目录)' : '纹理 (上传到 texture 目录)' },
{ key: 'model', label: bodyType === 'probe' ? '模型 (上传到 model 目录)' : '模型 (上传到 texture 目录)' },
];
return (
<Form.Item label="资源配置">
<Space direction="vertical" style={{ width: '100%' }} size="middle">
{resourceTypes.map(({ key, label }) => (
<div key={key}>
<div style={{ marginBottom: 8, fontWeight: 500 }}>{label}</div>
<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('缩放参数已更新');
2025-11-30 15:04:04 +00:00
}).catch(() => {
toast.error('更新失败');
2025-11-30 15:04:04 +00:00
});
}}
/>
<span style={{ fontSize: 11, color: '#999' }}>
(: Webb=0.3, =1.5)
</span>
</Space>
</div>
)}
</div>
))}
</div>
)}
</div>
))}
</Space>
</Form.Item>
);
2025-11-30 02:43:47 +00:00
}