396 lines
14 KiB
JavaScript
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;
|