215 lines
8.5 KiB
React
215 lines
8.5 KiB
React
|
|
import React, { useState } from 'react';
|
|||
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|||
|
|
import { Clock, Users, FileText, User, Edit, Calendar , Trash2, MoreVertical } from 'lucide-react';
|
|||
|
|
import TagDisplay from './TagDisplay';
|
|||
|
|
import ConfirmDialog from './ConfirmDialog';
|
|||
|
|
import Dropdown from './Dropdown';
|
|||
|
|
import MarkdownRenderer from './MarkdownRenderer';
|
|||
|
|
import tools from '../utils/tools';
|
|||
|
|
import './MeetingTimeline.css';
|
|||
|
|
|
|||
|
|
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore = false, onLoadMore, loadingMore = false, filterType = 'all', searchQuery = '', selectedTags = [] }) => {
|
|||
|
|
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
|
|||
|
|
const navigate = useNavigate();
|
|||
|
|
|
|||
|
|
const shouldShowMoreButton = (summary, maxLines = 3, maxLength = 100) => {
|
|||
|
|
if (!summary) return false;
|
|||
|
|
const lines = summary.split('\n');
|
|||
|
|
return lines.length > maxLines || summary.length > maxLength;
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleEditClick = (meetingId) => {
|
|||
|
|
navigate(`/meetings/edit/${meetingId}`);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleDeleteClick = (meeting) => {
|
|||
|
|
setDeleteConfirmInfo({
|
|||
|
|
id: meeting.meeting_id,
|
|||
|
|
title: meeting.title
|
|||
|
|
});
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const handleConfirmDelete = async () => {
|
|||
|
|
if (onDeleteMeeting && deleteConfirmInfo) {
|
|||
|
|
await onDeleteMeeting(deleteConfirmInfo.id);
|
|||
|
|
}
|
|||
|
|
setDeleteConfirmInfo(null);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
const sortedDates = Object.keys(meetingsByDate).sort((a, b) => new Date(b) - new Date(a));
|
|||
|
|
|
|||
|
|
if (sortedDates.length === 0) {
|
|||
|
|
return (
|
|||
|
|
<div className="timeline-empty">
|
|||
|
|
<Calendar size={48} />
|
|||
|
|
<h3>暂无会议记录</h3>
|
|||
|
|
<p>您还没有参与任何会议</p>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="timeline-container">
|
|||
|
|
<div className="timeline-line"></div>
|
|||
|
|
{sortedDates.map(date => (
|
|||
|
|
<div key={date} className="timeline-date-section">
|
|||
|
|
<div className="timeline-date-node">
|
|||
|
|
<span className="date-text">{tools.formatDateLong(date)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meetings-for-date">
|
|||
|
|
{meetingsByDate[date].map(meeting => {
|
|||
|
|
const isCreator = String(meeting.creator_id) === String(currentUser.user_id);
|
|||
|
|
const cardClass = isCreator ? 'created-by-me' : 'attended-by-me';
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<div className="meeting-card-wrapper" key={meeting.meeting_id}>
|
|||
|
|
<Link
|
|||
|
|
to={`/meetings/${meeting.meeting_id}`}
|
|||
|
|
state={{
|
|||
|
|
filterContext: {
|
|||
|
|
filterType,
|
|||
|
|
searchQuery,
|
|||
|
|
selectedTags
|
|||
|
|
}
|
|||
|
|
}}
|
|||
|
|
>
|
|||
|
|
<div className={`meeting-card ${cardClass} meeting-card-link`}>
|
|||
|
|
<div className="meeting-content">
|
|||
|
|
<div className="meeting-header">
|
|||
|
|
<div className="meeting-title-section">
|
|||
|
|
<div className="title-and-tags">
|
|||
|
|
<h3 className="meeting-title">
|
|||
|
|
{meeting.title}
|
|||
|
|
{meeting.tags && meeting.tags.length > 0 && (
|
|||
|
|
<TagDisplay
|
|||
|
|
tags={meeting.tags.map(tag => tag.name)}
|
|||
|
|
size="small"
|
|||
|
|
maxDisplay={3}
|
|||
|
|
showIcon={true}
|
|||
|
|
className="inline-tags"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</h3>
|
|||
|
|
</div>
|
|||
|
|
{isCreator && (
|
|||
|
|
<Dropdown
|
|||
|
|
trigger={
|
|||
|
|
<button className="dropdown-trigger">
|
|||
|
|
<MoreVertical size={18} />
|
|||
|
|
</button>
|
|||
|
|
}
|
|||
|
|
items={[
|
|||
|
|
{
|
|||
|
|
icon: <Edit size={16} />,
|
|||
|
|
label: '编辑',
|
|||
|
|
onClick: () => handleEditClick(meeting.meeting_id)
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
icon: <Trash2 size={16} />,
|
|||
|
|
label: '删除',
|
|||
|
|
onClick: () => handleDeleteClick(meeting),
|
|||
|
|
danger: true
|
|||
|
|
}
|
|||
|
|
]}
|
|||
|
|
align="right"
|
|||
|
|
/>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
<div className="meeting-meta">
|
|||
|
|
<div className="meta-item">
|
|||
|
|
<Clock size={16} />
|
|||
|
|
<span>{tools.formatTime(meeting.meeting_time)}</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="meta-item">
|
|||
|
|
<Users size={16} />
|
|||
|
|
<span>{meeting.attendees.length} 人参会</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-body">
|
|||
|
|
{meeting.attendees && meeting.attendees.length > 0 && (
|
|||
|
|
<div className="attendees-section">
|
|||
|
|
<span className="attendees-label">参会人:</span>
|
|||
|
|
<div className="attendees-list">
|
|||
|
|
{meeting.attendees.map((attendee, idx) => (
|
|||
|
|
<span key={idx} className="attendee-tag">
|
|||
|
|
{typeof attendee === 'string' ? attendee : attendee.caption}
|
|||
|
|
</span>
|
|||
|
|
))}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
|
|||
|
|
{meeting.summary && (
|
|||
|
|
<div className="summary-section">
|
|||
|
|
<div className="summary-header">
|
|||
|
|
<FileText size={16} />
|
|||
|
|
<span>会议摘要</span>
|
|||
|
|
</div>
|
|||
|
|
<div className="summary-content">
|
|||
|
|
<MarkdownRenderer
|
|||
|
|
content={tools.truncateSummary(meeting.summary)}
|
|||
|
|
className="markdown-content"
|
|||
|
|
/>
|
|||
|
|
{shouldShowMoreButton(meeting.summary) && (
|
|||
|
|
<div className="summary-more-hint">
|
|||
|
|
<span className="more-text">点击查看完整摘要</span>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div className="meeting-footer">
|
|||
|
|
<div className="creator-info">
|
|||
|
|
<User size={14} />
|
|||
|
|
<span>创建人: {meeting.creator_username}</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</Link>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
})}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
))}
|
|||
|
|
|
|||
|
|
{/* 加载更多/加载完毕 UI */}
|
|||
|
|
<div className="timeline-footer">
|
|||
|
|
{hasMore ? (
|
|||
|
|
<button
|
|||
|
|
className="load-more-btn"
|
|||
|
|
onClick={onLoadMore}
|
|||
|
|
disabled={loadingMore}
|
|||
|
|
>
|
|||
|
|
<span>{loadingMore ? '加载中...' : '加载更多'}</span>
|
|||
|
|
</button>
|
|||
|
|
) : (
|
|||
|
|
sortedDates.length > 0 && (
|
|||
|
|
<div className="all-loaded">
|
|||
|
|
<span>已加载全部会议</span>
|
|||
|
|
</div>
|
|||
|
|
)
|
|||
|
|
)}
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
{/* 删除会议确认对话框 */}
|
|||
|
|
<ConfirmDialog
|
|||
|
|
isOpen={!!deleteConfirmInfo}
|
|||
|
|
onClose={() => setDeleteConfirmInfo(null)}
|
|||
|
|
onConfirm={handleConfirmDelete}
|
|||
|
|
title="删除会议"
|
|||
|
|
message={`确定要删除会议"${deleteConfirmInfo?.title}"吗?此操作无法撤销。`}
|
|||
|
|
confirmText="删除"
|
|||
|
|
cancelText="取消"
|
|||
|
|
type="danger"
|
|||
|
|
/>
|
|||
|
|
</div>
|
|||
|
|
);
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
export default MeetingTimeline;
|