368 lines
12 KiB
TypeScript
368 lines
12 KiB
TypeScript
/**
|
||
* My Celestial Bodies Page (User Follow)
|
||
* 我的天体页面 - 左侧是关注的天体列表,右侧是天体详情和事件
|
||
*/
|
||
import { useState, useEffect } from 'react';
|
||
import { Row, Col, Card, List, Tag, Button, Empty, Descriptions, Table, Space } from 'antd';
|
||
import { StarFilled, RocketOutlined } from '@ant-design/icons';
|
||
import type { ColumnsType } from 'antd/es/table';
|
||
import { request } from '../../utils/request';
|
||
import { useToast } from '../../contexts/ToastContext';
|
||
|
||
interface CelestialBody {
|
||
id: string;
|
||
name: string;
|
||
name_zh: string;
|
||
type: string;
|
||
is_active: boolean;
|
||
followed_at?: string;
|
||
}
|
||
|
||
interface CelestialEvent {
|
||
id: number;
|
||
title: string;
|
||
event_type: string;
|
||
event_time: string;
|
||
description: string;
|
||
details: any;
|
||
source: string;
|
||
}
|
||
|
||
export function MyCelestialBodies() {
|
||
const [loading, setLoading] = useState(false);
|
||
const [followedBodies, setFollowedBodies] = useState<CelestialBody[]>([]);
|
||
const [selectedBody, setSelectedBody] = useState<CelestialBody | null>(null);
|
||
const [bodyEvents, setBodyEvents] = useState<CelestialEvent[]>([]);
|
||
const [eventsLoading, setEventsLoading] = useState(false);
|
||
const toast = useToast();
|
||
|
||
useEffect(() => {
|
||
loadFollowedBodies();
|
||
}, []);
|
||
|
||
const loadFollowedBodies = async () => {
|
||
setLoading(true);
|
||
try {
|
||
const { data } = await request.get('/social/follows');
|
||
setFollowedBodies(data || []);
|
||
// 如果有数据,默认选中第一个
|
||
if (data && data.length > 0) {
|
||
handleSelectBody(data[0]);
|
||
}
|
||
} catch (error) {
|
||
toast.error('加载关注列表失败');
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleSelectBody = async (body: CelestialBody) => {
|
||
setSelectedBody(body);
|
||
setEventsLoading(true);
|
||
try {
|
||
const { data } = await request.get(`/events`, {
|
||
params: {
|
||
body_id: body.id,
|
||
limit: 100
|
||
}
|
||
});
|
||
setBodyEvents(data || []);
|
||
} catch (error) {
|
||
toast.error('加载天体事件失败');
|
||
setBodyEvents([]);
|
||
} finally {
|
||
setEventsLoading(false);
|
||
}
|
||
};
|
||
|
||
const handleUnfollow = async (bodyId: string) => {
|
||
try {
|
||
await request.delete(`/social/follow/${bodyId}`);
|
||
toast.success('已取消关注');
|
||
|
||
// 重新加载列表
|
||
await loadFollowedBodies();
|
||
|
||
// 如果取消关注的是当前选中的天体,清空右侧显示
|
||
if (selectedBody?.id === bodyId) {
|
||
setSelectedBody(null);
|
||
setBodyEvents([]);
|
||
}
|
||
} catch (error) {
|
||
toast.error('取消关注失败');
|
||
}
|
||
};
|
||
|
||
const getBodyTypeLabel = (type: string) => {
|
||
const labelMap: Record<string, string> = {
|
||
'star': '恒星',
|
||
'planet': '行星',
|
||
'dwarf_planet': '矮行星',
|
||
'satellite': '卫星',
|
||
'comet': '彗星',
|
||
'asteroid': '小行星',
|
||
'probe': '探测器',
|
||
};
|
||
return labelMap[type] || type;
|
||
};
|
||
|
||
const getBodyTypeColor = (type: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
'star': 'gold',
|
||
'planet': 'blue',
|
||
'dwarf_planet': 'cyan',
|
||
'satellite': 'geekblue',
|
||
'comet': 'purple',
|
||
'asteroid': 'volcano',
|
||
'probe': 'magenta',
|
||
};
|
||
return colorMap[type] || 'default';
|
||
};
|
||
|
||
const getEventTypeLabel = (type: string) => {
|
||
const labelMap: Record<string, string> = {
|
||
'approach': '接近',
|
||
'close_approach': '近距离接近',
|
||
'eclipse': '食',
|
||
'conjunction': '合',
|
||
'opposition': '冲',
|
||
'transit': '凌',
|
||
};
|
||
return labelMap[type] || type;
|
||
};
|
||
|
||
const getEventTypeColor = (type: string) => {
|
||
const colorMap: Record<string, string> = {
|
||
'approach': 'blue',
|
||
'close_approach': 'magenta',
|
||
'eclipse': 'purple',
|
||
'conjunction': 'cyan',
|
||
'opposition': 'orange',
|
||
'transit': 'green',
|
||
};
|
||
return colorMap[type] || 'default';
|
||
};
|
||
|
||
const eventColumns: ColumnsType<CelestialEvent> = [
|
||
{
|
||
title: '事件',
|
||
dataIndex: 'title',
|
||
key: 'title',
|
||
ellipsis: true,
|
||
width: '40%',
|
||
},
|
||
{
|
||
title: '类型',
|
||
dataIndex: 'event_type',
|
||
key: 'event_type',
|
||
width: 200,
|
||
render: (type) => (
|
||
<Tag color={getEventTypeColor(type)}>
|
||
{getEventTypeLabel(type)}
|
||
</Tag>
|
||
),
|
||
filters: [
|
||
{ text: '接近', value: 'approach' },
|
||
{ text: '近距离接近', value: 'close_approach' },
|
||
{ text: '食', value: 'eclipse' },
|
||
{ text: '合', value: 'conjunction' },
|
||
{ text: '冲', value: 'opposition' },
|
||
{ text: '凌', value: 'transit' },
|
||
],
|
||
onFilter: (value, record) => record.event_type === value,
|
||
},
|
||
{
|
||
title: '时间',
|
||
dataIndex: 'event_time',
|
||
key: 'event_time',
|
||
width: 180,
|
||
render: (time) => new Date(time).toLocaleString('zh-CN'),
|
||
sorter: (a, b) => new Date(a.event_time).getTime() - new Date(b.event_time).getTime(),
|
||
},
|
||
];
|
||
|
||
return (
|
||
<Row gutter={16} style={{ height: 'calc(100vh - 150px)' }}>
|
||
{/* 左侧:关注的天体列表 */}
|
||
<Col span={8}>
|
||
<Card
|
||
title={
|
||
<Space>
|
||
<StarFilled style={{ color: '#faad14' }} />
|
||
<span>我的天体</span>
|
||
<Tag color="blue">{followedBodies.length}</Tag>
|
||
</Space>
|
||
}
|
||
extra={
|
||
<Button size="small" onClick={loadFollowedBodies} loading={loading}>
|
||
刷新
|
||
</Button>
|
||
}
|
||
bordered={false}
|
||
style={{ height: '100%', overflow: 'hidden' }}
|
||
bodyStyle={{ height: 'calc(100% - 57px)', overflowY: 'auto', padding: 0 }}
|
||
>
|
||
{followedBodies.length === 0 && !loading ? (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="还没有关注任何天体"
|
||
style={{ marginTop: 60 }}
|
||
>
|
||
<p style={{ color: '#999', margin: '8px 0' }}>
|
||
在主页面点击天体,查看详情后可以关注
|
||
</p>
|
||
</Empty>
|
||
) : (
|
||
<List
|
||
dataSource={followedBodies}
|
||
loading={loading}
|
||
renderItem={(body) => (
|
||
<List.Item
|
||
key={body.id}
|
||
onClick={() => handleSelectBody(body)}
|
||
style={{
|
||
cursor: 'pointer',
|
||
backgroundColor: selectedBody?.id === body.id ? '#f0f5ff' : 'transparent',
|
||
padding: '12px 16px',
|
||
transition: 'background-color 0.3s',
|
||
}}
|
||
actions={[
|
||
<Button
|
||
key="unfollow"
|
||
type="link"
|
||
danger
|
||
size="small"
|
||
icon={<StarFilled />}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
handleUnfollow(body.id);
|
||
}}
|
||
>
|
||
取消
|
||
</Button>,
|
||
]}
|
||
>
|
||
<List.Item.Meta
|
||
avatar={<StarFilled style={{ color: '#faad14', fontSize: 20 }} />}
|
||
title={
|
||
<Space>
|
||
<span>{body.name_zh || body.name}</span>
|
||
<Tag color={getBodyTypeColor(body.type)} style={{ marginLeft: 4 }}>
|
||
{getBodyTypeLabel(body.type)}
|
||
</Tag>
|
||
</Space>
|
||
}
|
||
description={
|
||
body.followed_at
|
||
? `关注于 ${new Date(body.followed_at).toLocaleDateString('zh-CN')}`
|
||
: body.name_zh ? body.name : undefined
|
||
}
|
||
/>
|
||
</List.Item>
|
||
)}
|
||
/>
|
||
)}
|
||
</Card>
|
||
</Col>
|
||
|
||
{/* 右侧:天体详情和事件 */}
|
||
<Col span={16}>
|
||
{selectedBody ? (
|
||
<Space direction="vertical" size="middle" style={{ width: '100%', height: '100%' }}>
|
||
{/* 天体资料 */}
|
||
<Card
|
||
title={
|
||
<Space>
|
||
<RocketOutlined />
|
||
<span>{selectedBody.name_zh || selectedBody.name}</span>
|
||
<Tag color={getBodyTypeColor(selectedBody.type)}>
|
||
{getBodyTypeLabel(selectedBody.type)}
|
||
</Tag>
|
||
</Space>
|
||
}
|
||
bordered={false}
|
||
>
|
||
<Descriptions column={2} bordered size="small">
|
||
<Descriptions.Item label="ID">{selectedBody.id}</Descriptions.Item>
|
||
<Descriptions.Item label="类型">
|
||
{getBodyTypeLabel(selectedBody.type)}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="中文名">
|
||
{selectedBody.name_zh || '-'}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="英文名">
|
||
{selectedBody.name}
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="状态">
|
||
<Tag color={selectedBody.is_active ? 'success' : 'default'}>
|
||
{selectedBody.is_active ? '活跃' : '已归档'}
|
||
</Tag>
|
||
</Descriptions.Item>
|
||
<Descriptions.Item label="关注时间">
|
||
{selectedBody.followed_at
|
||
? new Date(selectedBody.followed_at).toLocaleString('zh-CN')
|
||
: '-'}
|
||
</Descriptions.Item>
|
||
</Descriptions>
|
||
</Card>
|
||
|
||
{/* 天体事件列表 */}
|
||
<Card
|
||
title="相关天体事件"
|
||
bordered={false}
|
||
style={{ marginTop: 16 }}
|
||
>
|
||
<Table
|
||
columns={eventColumns}
|
||
dataSource={bodyEvents}
|
||
rowKey="id"
|
||
loading={eventsLoading}
|
||
size="small"
|
||
pagination={{
|
||
pageSize: 10,
|
||
showSizeChanger: false,
|
||
showTotal: (total) => `共 ${total} 条`,
|
||
}}
|
||
locale={{
|
||
emptyText: (
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="暂无相关事件"
|
||
/>
|
||
),
|
||
}}
|
||
expandable={{
|
||
expandedRowRender: (record) => (
|
||
<div style={{ padding: '8px 16px' }}>
|
||
<p style={{ margin: 0 }}>
|
||
<strong>描述:</strong>
|
||
{record.description}
|
||
</p>
|
||
{record.details && (
|
||
<p style={{ margin: '8px 0 0', color: '#666' }}>
|
||
<strong>详情:</strong>
|
||
{JSON.stringify(record.details, null, 2)}
|
||
</p>
|
||
)}
|
||
</div>
|
||
),
|
||
}}
|
||
/>
|
||
</Card>
|
||
</Space>
|
||
) : (
|
||
<Card
|
||
bordered={false}
|
||
style={{ height: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
|
||
>
|
||
<Empty
|
||
image={Empty.PRESENTED_IMAGE_SIMPLE}
|
||
description="请从左侧选择一个天体查看详情"
|
||
/>
|
||
</Card>
|
||
)}
|
||
</Col>
|
||
</Row>
|
||
);
|
||
}
|