imetting/frontend/src/pages/MeetingCenterPage.jsx

396 lines
14 KiB
JavaScript

import React, { useCallback, useEffect, useState } from 'react';
import {
App,
Avatar,
Button,
Card,
Col,
Divider,
Empty,
Input,
Row,
Segmented,
Space,
Tag,
Typography,
} from 'antd';
import {
ArrowRightOutlined,
ClockCircleOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
TeamOutlined,
UserOutlined,
} from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
import ActionButton from '../components/ActionButton';
import CenterPager from '../components/CenterPager';
import MeetingFormDrawer from '../components/MeetingFormDrawer';
import meetingCacheService from '../services/meetingCacheService';
import tools from '../utils/tools';
import useSystemPageSize from '../hooks/useSystemPageSize';
import './MeetingCenterPage.css';
import '../components/MeetingInfoCard.css';
const { Title, Text, Paragraph } = Typography;
const FILTER_OPTIONS = [
{ label: '全部', value: 'all' },
{ label: '我发起', value: 'created' },
{ label: '我参与', value: 'attended' },
];
const ROLE_META = {
creator: {
label: '我发起',
tagClass: 'console-tag-soft-blue',
},
attendee: {
label: '我参与',
tagClass: 'console-tag-soft-green',
},
};
const STATUS_META = {
completed: {
label: '已完成',
tagClass: 'console-tag-soft-green',
accent: '#52c41a',
},
failed: {
label: '失败',
tagClass: 'console-tag-soft-red',
accent: '#ff4d4f',
},
transcribing: {
label: '转写中',
tagClass: 'console-tag-soft-blue',
accent: '#1677ff',
},
summarizing: {
label: '总结中',
tagClass: 'console-tag-soft-orange',
accent: '#fa8c16',
},
pending: {
label: '待处理',
tagClass: 'console-tag-soft-default',
accent: '#cbd5e1',
},
};
const getStatusMeta = (meeting) => {
const status = meeting?.overall_status || 'pending';
return STATUS_META[status] || STATUS_META.pending;
};
const getSummaryPreview = (summary) => {
if (!summary) return '';
return tools.stripMarkdown(summary).replace(/\s+/g, ' ').trim();
};
const MeetingCenterPage = ({ user }) => {
const navigate = useNavigate();
const location = useLocation();
const { message, modal } = App.useApp();
const [meetings, setMeetings] = useState([]);
const [loading, setLoading] = useState(false);
const [keyword, setKeyword] = useState('');
const [searchValue, setSearchValue] = useState('');
const [filterType, setFilterType] = useState('all');
const [page, setPage] = useState(1);
const { pageSize, isReady: pageSizeReady } = useSystemPageSize(10, { suspendUntilReady: true });
const [total, setTotal] = useState(0);
const [formDrawerOpen, setFormDrawerOpen] = useState(false);
const [editingMeetingId, setEditingMeetingId] = useState(null);
const loadMeetings = useCallback(async (nextPage = page, nextKeyword = keyword, nextFilter = filterType, options = {}) => {
const { forceRefresh = false } = options;
const filterKey = meetingCacheService.generateFilterKey(user.user_id, nextFilter, nextKeyword, []);
if (!forceRefresh) {
const cachedPage = meetingCacheService.getPage(filterKey, nextPage);
if (cachedPage) {
setMeetings(cachedPage.meetings || []);
setTotal(cachedPage.pagination?.total || 0);
setLoading(false);
return;
}
}
setLoading(true);
try {
const res = await apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.LIST), {
params: {
user_id: user.user_id,
page: nextPage,
page_size: pageSize,
filter_type: nextFilter,
search: nextKeyword || undefined,
},
});
const nextMeetings = res.data.meetings || [];
const nextPagination = {
page: res.data.page || nextPage,
total: res.data.total || 0,
has_more: Boolean(res.data.has_more),
};
meetingCacheService.setPage(filterKey, nextPage, nextMeetings, nextPagination);
setMeetings(nextMeetings);
setTotal(nextPagination.total);
} catch (error) {
message.error(error?.response?.data?.message || '加载会议失败');
} finally {
setLoading(false);
}
}, [filterType, keyword, message, page, pageSize, user.user_id]);
useEffect(() => {
meetingCacheService.clearAll();
}, [pageSize]);
useEffect(() => {
if (!pageSizeReady || !pageSize) {
return;
}
loadMeetings(page, keyword, filterType);
}, [filterType, keyword, loadMeetings, page, pageSize, pageSizeReady]);
useEffect(() => {
if (location.state?.openCreate) {
setEditingMeetingId(null);
setFormDrawerOpen(true);
navigate(location.pathname, { replace: true, state: {} });
}
}, [location.pathname, location.state, navigate]);
const handleDeleteMeeting = (meeting) => {
modal.confirm({
title: '删除会议',
content: `确定删除「${meeting.title}」吗?`,
okText: '删除',
okType: 'danger',
onOk: async () => {
try {
await apiClient.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting.meeting_id)));
message.success('会议已删除');
meetingCacheService.clearAll();
const nextPage = page > 1 && meetings.length === 1 ? page - 1 : page;
setPage(nextPage);
loadMeetings(nextPage, keyword, filterType, { forceRefresh: true });
} catch (error) {
message.error(error?.response?.data?.message || '删除失败');
}
},
});
};
return (
<div className="meeting-center-page">
<Card
className="console-surface"
variant="borderless"
style={{ marginBottom: 16 }}
styles={{ body: { padding: '18px 20px' } }}
>
<Space className="meeting-center-header" align="start">
<div>
<Title level={3} style={{ margin: 0 }}>会议中心</Title>
<Text type="secondary">集中查看筛选和管理全部会议记录</Text>
</div>
<Space className="meeting-center-header-actions" wrap>
<Segmented
className="console-segmented"
value={filterType}
onChange={(value) => {
setFilterType(value);
setPage(1);
}}
options={FILTER_OPTIONS}
/>
<Input.Search
className="meeting-center-search-input"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
onSearch={(value) => {
setSearchValue(value);
setKeyword(value.trim());
setPage(1);
}}
placeholder="搜索会议标题"
allowClear
/>
<Button
type="primary"
icon={<PlusOutlined />}
className="btn-pill-primary"
onClick={() => {
setEditingMeetingId(null);
setFormDrawerOpen(true);
}}
>
新建会议
</Button>
</Space>
</Space>
</Card>
<Card
className="console-surface meeting-center-content-panel"
variant="borderless"
styles={{ body: { padding: '20px' } }}
>
{!pageSizeReady ? (
<div className="meeting-center-empty">
<Empty description="加载分页配置中..." />
</div>
) : meetings.length ? (
<>
<Row gutter={[16, 16]}>
{meetings.map((meeting) => {
const statusMeta = getStatusMeta(meeting);
const isCreator = String(meeting.creator_id) === String(user.user_id);
const roleMeta = isCreator ? ROLE_META.creator : ROLE_META.attendee;
const summaryPreview = getSummaryPreview(meeting.summary);
const hasSummary = Boolean(summaryPreview);
return (
<Col key={meeting.meeting_id} xs={24} md={12} xl={8}>
<Card
variant="borderless"
className="meeting-center-card console-surface shared-meeting-card"
hoverable
onClick={() => navigate(`/meetings/${meeting.meeting_id}`)}
>
<div className="shared-meeting-card-shell">
<div className="shared-meeting-card-top">
<Space size={[8, 8]} wrap>
<Tag bordered={false} className={`${roleMeta.tagClass} console-tag-large`}>
{roleMeta.label}
</Tag>
<Tag bordered={false} className={`${statusMeta.tagClass} console-tag-large`}>
{statusMeta.label}
</Tag>
</Space>
{isCreator ? (
<Space size={8}>
<ActionButton
tone="edit"
variant="iconSm"
tooltip="编辑"
icon={<EditOutlined />}
onClick={(event) => {
event.stopPropagation();
setEditingMeetingId(meeting.meeting_id);
setFormDrawerOpen(true);
}}
/>
<ActionButton
tone="delete"
variant="iconSm"
tooltip="删除"
icon={<DeleteOutlined />}
onClick={(event) => {
event.stopPropagation();
handleDeleteMeeting(meeting);
}}
/>
</Space>
) : null}
</div>
<div className="shared-meeting-card-title-block">
<Paragraph
className="shared-meeting-card-title"
ellipsis={{ rows: 2, tooltip: meeting.title }}
>
{meeting.title}
</Paragraph>
</div>
<div className={`shared-meeting-card-summary ${hasSummary ? '' : 'is-empty'}`}>
<Paragraph
className="shared-meeting-card-summary-content"
ellipsis={hasSummary ? { rows: 2, tooltip: summaryPreview } : false}
>
{hasSummary ? summaryPreview : '暂无摘要'}
</Paragraph>
</div>
<div className="shared-meeting-card-meta">
<Space size={10}>
<ClockCircleOutlined className="shared-meeting-card-meta-icon" />
<Text className="shared-meeting-card-meta-text">
{tools.formatTime(meeting.meeting_time || meeting.created_at)}
</Text>
</Space>
<Divider type="vertical" />
<Space size={10}>
<TeamOutlined className="shared-meeting-card-meta-icon" />
<Text className="shared-meeting-card-meta-text">
{meeting.attendees?.length || 0}
</Text>
</Space>
</div>
<div className="shared-meeting-card-footer">
<Space>
<Avatar size="small" src={meeting.creator_avatar_url} icon={<UserOutlined />} />
<Text type="secondary" className="shared-meeting-card-footer-user">
{meeting.creator_username || '未知'}
</Text>
</Space>
<ActionButton
tone="view"
variant="textSm"
icon={<ArrowRightOutlined />}
className="shared-meeting-card-footer-link"
>
查看详情
</ActionButton>
</div>
</div>
</Card>
</Col>
);
})}
</Row>
<CenterPager
current={page}
total={total}
pageSize={pageSize || 10}
onChange={setPage}
/>
</>
) : (
<div className="meeting-center-empty">
<Empty description={loading ? '加载中...' : '暂无会议'} />
</div>
)}
</Card>
<MeetingFormDrawer
open={formDrawerOpen}
onClose={() => setFormDrawerOpen(false)}
meetingId={editingMeetingId}
user={user}
onSuccess={(newMeetingId) => {
meetingCacheService.clearAll();
if (newMeetingId) {
navigate(`/meetings/${newMeetingId}`);
} else {
loadMeetings(page, keyword, filterType, { forceRefresh: true });
}
}}
/>
</div>
);
};
export default MeetingCenterPage;