2026-03-26 06:55:12 +00:00
|
|
|
import React from 'react';
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
|
|
import {
|
|
|
|
|
App,
|
|
|
|
|
Avatar,
|
|
|
|
|
Button,
|
|
|
|
|
Card,
|
|
|
|
|
Divider,
|
|
|
|
|
Dropdown,
|
|
|
|
|
Space,
|
|
|
|
|
Tag,
|
|
|
|
|
Timeline,
|
|
|
|
|
Typography,
|
|
|
|
|
} from 'antd';
|
|
|
|
|
import {
|
|
|
|
|
ArrowRightOutlined,
|
|
|
|
|
CalendarOutlined,
|
|
|
|
|
ClockCircleOutlined,
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
EditOutlined,
|
|
|
|
|
FileTextOutlined,
|
|
|
|
|
MoreOutlined,
|
|
|
|
|
TeamOutlined,
|
|
|
|
|
UserOutlined,
|
|
|
|
|
} from '@ant-design/icons';
|
2026-01-19 11:03:08 +00:00
|
|
|
import MarkdownRenderer from './MarkdownRenderer';
|
|
|
|
|
import tools from '../utils/tools';
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const { Title, Text, Paragraph } = Typography;
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const formatDateMeta = (date) => {
|
|
|
|
|
const parsed = new Date(date);
|
|
|
|
|
if (Number.isNaN(parsed.getTime())) {
|
|
|
|
|
return { main: date, sub: '' };
|
|
|
|
|
}
|
|
|
|
|
return {
|
|
|
|
|
main: tools.formatDateLong(date),
|
|
|
|
|
sub: parsed.toLocaleDateString('zh-CN', { weekday: 'long' }),
|
2026-01-19 11:03:08 +00:00
|
|
|
};
|
2026-03-26 06:55:12 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const MeetingTimeline = ({
|
|
|
|
|
meetingsByDate,
|
|
|
|
|
currentUser,
|
|
|
|
|
onDeleteMeeting,
|
|
|
|
|
hasMore = false,
|
|
|
|
|
onLoadMore,
|
|
|
|
|
loadingMore = false,
|
|
|
|
|
filterType = 'all',
|
|
|
|
|
searchQuery = '',
|
|
|
|
|
selectedTags = [],
|
|
|
|
|
}) => {
|
|
|
|
|
const { modal } = App.useApp();
|
|
|
|
|
const navigate = useNavigate();
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const handleEditClick = (event, meetingId) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
2026-01-19 11:03:08 +00:00
|
|
|
navigate(`/meetings/edit/${meetingId}`);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const handleDeleteClick = (event, meeting) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
modal.confirm({
|
|
|
|
|
title: '删除会议',
|
|
|
|
|
content: `确定要删除会议“${meeting.title}”吗?此操作无法撤销。`,
|
|
|
|
|
okText: '删除',
|
|
|
|
|
okType: 'danger',
|
|
|
|
|
onOk: () => onDeleteMeeting(meeting.meeting_id),
|
2026-01-19 11:03:08 +00:00
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const sortedDates = Object.keys(meetingsByDate).sort((a, b) => new Date(b) - new Date(a));
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
const timelineItems = sortedDates.map((date) => {
|
|
|
|
|
const dateMeta = formatDateMeta(date);
|
|
|
|
|
return {
|
|
|
|
|
label: (
|
|
|
|
|
<div className="timeline-date-label">
|
|
|
|
|
<Text className="timeline-date-main">{dateMeta.main}</Text>
|
|
|
|
|
{dateMeta.sub ? <Text className="timeline-date-sub">{dateMeta.sub}</Text> : null}
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
children: (
|
|
|
|
|
<div className="timeline-date-group">
|
|
|
|
|
{meetingsByDate[date].map((meeting) => {
|
|
|
|
|
const isCreator = String(meeting.creator_id) === String(currentUser.user_id);
|
|
|
|
|
const menuItems = [
|
|
|
|
|
{ key: 'edit', label: '编辑', icon: <EditOutlined />, onClick: ({ domEvent }) => handleEditClick(domEvent, meeting.meeting_id) },
|
|
|
|
|
{ key: 'delete', label: '删除', icon: <DeleteOutlined />, danger: true, onClick: ({ domEvent }) => handleDeleteClick(domEvent, meeting) },
|
|
|
|
|
];
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
return (
|
|
|
|
|
<Card
|
|
|
|
|
key={meeting.meeting_id}
|
|
|
|
|
hoverable
|
|
|
|
|
className="timeline-meeting-card"
|
|
|
|
|
style={{ borderLeft: isCreator ? '4px solid #1677ff' : '4px solid #52c41a' }}
|
|
|
|
|
onClick={() => navigate(`/meetings/${meeting.meeting_id}`, {
|
|
|
|
|
state: { filterContext: { filterType, searchQuery, selectedTags } },
|
|
|
|
|
})}
|
|
|
|
|
>
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 14, gap: 16 }}>
|
|
|
|
|
<Space direction="vertical" size={6} style={{ flex: 1 }}>
|
|
|
|
|
<Title level={4} style={{ margin: 0, fontSize: 20 }}>{meeting.title}</Title>
|
|
|
|
|
<Space size={12} split={<Divider type="vertical" />} wrap>
|
|
|
|
|
<Text type="secondary"><ClockCircleOutlined /> {tools.formatTime(meeting.meeting_time)}</Text>
|
|
|
|
|
<Text type="secondary"><TeamOutlined /> {meeting.attendees?.length || 0} 人</Text>
|
|
|
|
|
<Space size={[6, 6]} wrap>
|
|
|
|
|
{meeting.tags?.slice(0, 4).map((tag) => (
|
|
|
|
|
<Tag key={tag.id} color="blue" bordered={false} style={{ fontSize: 12, borderRadius: 999 }}>
|
|
|
|
|
{tag.name}
|
|
|
|
|
</Tag>
|
|
|
|
|
))}
|
|
|
|
|
</Space>
|
|
|
|
|
</Space>
|
|
|
|
|
</Space>
|
|
|
|
|
{isCreator ? (
|
|
|
|
|
<Dropdown menu={{ items: menuItems }} placement="bottomRight" arrow trigger={['click']}>
|
|
|
|
|
<Button type="text" icon={<MoreOutlined />} className="timeline-action-trigger" onClick={(event) => event.stopPropagation()} />
|
|
|
|
|
</Dropdown>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
{meeting.summary ? (
|
|
|
|
|
<div className="timeline-summary-box">
|
|
|
|
|
<Space size={8} style={{ marginBottom: 8, display: 'flex' }}>
|
|
|
|
|
<FileTextOutlined style={{ color: '#1677ff' }} />
|
|
|
|
|
<Text strong>会议摘要</Text>
|
|
|
|
|
</Space>
|
|
|
|
|
<div className="timeline-summary-content">
|
|
|
|
|
<Paragraph ellipsis={{ rows: 2 }} type="secondary" style={{ margin: 0, fontSize: 13 }}>
|
|
|
|
|
<MarkdownRenderer content={tools.truncateSummary(meeting.summary)} />
|
|
|
|
|
</Paragraph>
|
2026-01-19 11:03:08 +00:00
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 12, flexWrap: 'wrap' }}>
|
|
|
|
|
<Space>
|
|
|
|
|
<Avatar size="small" src={meeting.creator_avatar_url} icon={<UserOutlined />} />
|
|
|
|
|
<Text type="secondary" style={{ fontSize: 12 }}>{meeting.creator_username}</Text>
|
|
|
|
|
</Space>
|
|
|
|
|
<Button type="text" size="small" icon={<ArrowRightOutlined />} className="timeline-footer-link">
|
|
|
|
|
查看详情
|
|
|
|
|
</Button>
|
2026-01-19 11:03:08 +00:00
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
</Card>
|
|
|
|
|
);
|
|
|
|
|
})}
|
2026-01-19 11:03:08 +00:00
|
|
|
</div>
|
2026-03-26 06:55:12 +00:00
|
|
|
),
|
|
|
|
|
};
|
|
|
|
|
});
|
2026-01-19 11:03:08 +00:00
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
return (
|
|
|
|
|
<div className="modern-timeline">
|
|
|
|
|
<Timeline mode="left" items={timelineItems} />
|
|
|
|
|
<div style={{ textAlign: 'center', marginTop: 28 }}>
|
2026-01-19 11:03:08 +00:00
|
|
|
{hasMore ? (
|
2026-03-26 06:55:12 +00:00
|
|
|
<Button onClick={onLoadMore} loading={loadingMore} icon={<CalendarOutlined />}>
|
|
|
|
|
加载更多
|
|
|
|
|
</Button>
|
2026-01-19 11:03:08 +00:00
|
|
|
) : (
|
2026-03-26 06:55:12 +00:00
|
|
|
<Divider plain><Text type="secondary">已加载全部会议</Text></Divider>
|
2026-01-19 11:03:08 +00:00
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
2026-03-26 06:55:12 +00:00
|
|
|
export default MeetingTimeline;
|