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
|
2026-03-26 11:51:00 +00:00
|
|
|
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)',
|
|
|
|
|
}}
|
2026-03-26 12:03:01 +00:00
|
|
|
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}
|
2026-03-26 11:51:00 +00:00
|
|
|
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',
|
|
|
|
|
}}
|
2026-03-26 12:03:01 +00:00
|
|
|
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;
|