imetting/frontend/src/pages/MeetingCenterPage.jsx

397 lines
13 KiB
React
Raw Normal View History

2026-03-26 06:55:12 +00:00
import React, { useEffect, useMemo, useState } from 'react';
import {
App,
Button,
Card,
Empty,
Input,
Segmented,
Space,
Tag,
Tooltip,
Typography,
} from 'antd';
import {
CalendarOutlined,
DeleteOutlined,
EditOutlined,
PlusOutlined,
RightOutlined,
SearchOutlined,
TeamOutlined,
} from '@ant-design/icons';
import { useLocation, useNavigate } from 'react-router-dom';
import apiClient from '../utils/apiClient';
import { API_ENDPOINTS, buildApiUrl } from '../config/api';
import CenterPager from '../components/CenterPager';
import MeetingFormDrawer from '../components/MeetingFormDrawer';
const { Title, Text, Paragraph } = Typography;
const FILTER_OPTIONS = [
{ label: '全部', value: 'all' },
{ label: '我发起', value: 'created' },
{ label: '我参与', value: 'attended' },
];
const STATUS_META = {
completed: {
label: '已完成',
tagColor: '#52c41a',
tagBg: '#f6ffed',
tagBorder: '#b7eb8f',
accent: '#52c41a',
},
failed: {
label: '失败',
tagColor: '#ff4d4f',
tagBg: '#fff1f0',
tagBorder: '#ffccc7',
accent: '#ff4d4f',
},
transcribing: {
label: '转写中',
tagColor: '#1677ff',
tagBg: '#eff6ff',
tagBorder: '#bfdbfe',
accent: '#1677ff',
},
summarizing: {
label: '总结中',
tagColor: '#fa8c16',
tagBg: '#fff7e6',
tagBorder: '#ffd591',
accent: '#fa8c16',
},
pending: {
label: '待处理',
tagColor: '#8c8c8c',
tagBg: '#fafafa',
tagBorder: '#d9d9d9',
accent: '#d9d9d9',
},
};
const getStatusMeta = (meeting) => {
const status = meeting?.overall_status || 'pending';
return STATUS_META[status] || STATUS_META.pending;
};
const formatMeetingTime = (value) => {
if (!value) return '-';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '-';
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
};
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] = useState(8);
const [total, setTotal] = useState(0);
const [formDrawerOpen, setFormDrawerOpen] = useState(false);
const [editingMeetingId, setEditingMeetingId] = useState(null);
const loadMeetings = async (nextPage = page, nextKeyword = keyword, nextFilter = filterType) => {
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,
},
});
setMeetings(res.data.meetings || []);
setTotal(res.data.total || 0);
} catch (error) {
message.error(error?.response?.data?.message || '加载会议失败');
} finally {
setLoading(false);
}
};
useEffect(() => {
loadMeetings(page, keyword, filterType);
}, [page, keyword, filterType]);
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('会议已删除');
const nextPage = page > 1 && meetings.length === 1 ? page - 1 : page;
setPage(nextPage);
loadMeetings(nextPage, keyword, filterType);
} catch (error) {
message.error(error?.response?.data?.message || '删除失败');
}
},
});
};
const footerText = useMemo(() => {
if (!total) return '当前没有会议';
return `为您找到 ${total} 场会议`;
}, [total]);
return (
<div
style={{
minHeight: 'calc(100vh - 128px)',
padding: '10px 0 28px',
}}
>
<div
style={{
background: '#f3f6fa',
borderRadius: 28,
padding: 24,
boxShadow: 'inset 0 1px 0 rgba(255,255,255,0.9)',
}}
>
<Card
variant="borderless"
2026-03-26 06:55:12 +00:00
style={{
borderRadius: 20,
marginBottom: 22,
boxShadow: '0 8px 30px rgba(40, 72, 120, 0.08)',
}}
styles={{ body: { padding: '16px 20px' } }}
2026-03-26 06:55:12 +00:00
>
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 20, alignItems: 'center', flexWrap: 'wrap' }}>
<Space size={14} align="center">
<span
style={{
width: 6,
height: 28,
borderRadius: 999,
background: 'linear-gradient(180deg, #4ea1ff 0%, #1677ff 100%)',
display: 'inline-block',
}}
/>
<Title level={3} style={{ margin: 0, fontSize: 32 }}>会议中心</Title>
</Space>
<Space size={16} wrap>
<Segmented
className="console-segmented"
value={filterType}
onChange={(value) => {
setFilterType(value);
setPage(1);
}}
options={FILTER_OPTIONS}
/>
<Input
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
onPressEnter={() => {
setKeyword(searchValue.trim());
setPage(1);
}}
placeholder="搜索会议标题"
prefix={<SearchOutlined style={{ color: '#9aa8b6' }} />}
allowClear
style={{ width: 220, borderRadius: 12 }}
/>
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => { setEditingMeetingId(null); setFormDrawerOpen(true); }}
style={{ borderRadius: 12, height: 40, boxShadow: '0 10px 18px rgba(22, 119, 255, 0.2)' }}
>
新建会议
</Button>
</Space>
</div>
</Card>
<div
style={{
minHeight: 520,
background: '#f7fafc',
borderRadius: 24,
padding: 4,
}}
>
{meetings.length ? (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))',
gap: 22,
alignItems: 'start',
}}
>
{meetings.map((meeting) => {
const statusMeta = getStatusMeta(meeting);
const isCreator = String(meeting.creator_id) === String(user.user_id);
return (
<Card
key={meeting.meeting_id}
variant="borderless"
2026-03-26 06:55:12 +00:00
hoverable
onClick={() => navigate(`/meetings/${meeting.meeting_id}`)}
style={{
borderRadius: 20,
background: '#fff',
height: 240,
boxShadow: '0 18px 34px rgba(70, 92, 120, 0.08)',
position: 'relative',
overflow: 'hidden',
}}
styles={{ body: { padding: 0 } }}
2026-03-26 06:55:12 +00:00
>
<div
style={{
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 5,
background: statusMeta.accent,
}}
/>
<div style={{ padding: '20px 20px 18px 28px', display: 'flex', flexDirection: 'column', height: 240 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, marginBottom: 14 }}>
<Tag
style={{
marginInlineEnd: 0,
color: statusMeta.tagColor,
background: statusMeta.tagBg,
border: `1px solid ${statusMeta.tagBorder}`,
borderRadius: 8,
paddingInline: 14,
lineHeight: '24px',
fontWeight: 600,
}}
>
{statusMeta.label}
</Tag>
{isCreator ? (
<Space size={10}>
<Tooltip title="编辑">
<Button
shape="circle"
className="btn-icon-soft-blue"
icon={<EditOutlined />}
onClick={(event) => {
event.stopPropagation();
setEditingMeetingId(meeting.meeting_id);
setFormDrawerOpen(true);
}}
/>
</Tooltip>
<Tooltip title="删除">
<Button
shape="circle"
className="btn-icon-soft-red"
icon={<DeleteOutlined />}
onClick={(event) => {
event.stopPropagation();
handleDeleteMeeting(meeting);
}}
/>
</Tooltip>
</Space>
) : null}
</div>
<div style={{ minHeight: 78, marginBottom: 22 }}>
<Paragraph
style={{ margin: 0, fontSize: 30, lineHeight: 1.3, fontWeight: 700, color: '#0f172a' }}
ellipsis={{ rows: 2, tooltip: meeting.title }}
>
{meeting.title}
</Paragraph>
</div>
<Space direction="vertical" size={14} style={{ color: '#6b7a90' }}>
<Space size={10}>
<CalendarOutlined />
<Text style={{ color: '#6b7a90' }}>{formatMeetingTime(meeting.meeting_time || meeting.created_at)}</Text>
</Space>
<Space size={10} style={{ width: '100%', alignItems: 'flex-start' }}>
<TeamOutlined />
<Paragraph
style={{ margin: 0, color: '#6b7a90', flex: 1 }}
ellipsis={{ rows: 1, tooltip: meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '无参与人员' }}
>
{meeting.attendees?.length ? `${meeting.attendees.map((item) => item.caption).join('、')}` : '无参与人员'}
</Paragraph>
</Space>
</Space>
<div style={{ marginTop: 'auto', display: 'flex', justifyContent: 'flex-end' }}>
<Button
type="text"
icon={<RightOutlined />}
onClick={(event) => {
event.stopPropagation();
navigate(`/meetings/${meeting.meeting_id}`);
}}
style={{ color: '#b7c0cd' }}
/>
</div>
</div>
</Card>
);
})}
</div>
) : (
<div style={{ minHeight: 460, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Empty description={loading ? '加载中...' : '暂无会议'} />
</div>
)}
<CenterPager
current={page}
total={total}
pageSize={pageSize}
onChange={setPage}
/>
</div>
</div>
<MeetingFormDrawer
open={formDrawerOpen}
onClose={() => setFormDrawerOpen(false)}
meetingId={editingMeetingId}
user={user}
onSuccess={(newMeetingId) => {
if (newMeetingId) {
navigate(`/meetings/${newMeetingId}`);
} else {
loadMeetings(page, keyword, filterType);
}
}}
/>
</div>
);
};
export default MeetingCenterPage;