import { AppstoreOutlined, AudioOutlined, CalendarOutlined, CheckOutlined, CloudUploadOutlined, DeleteOutlined, EditOutlined, InfoCircleOutlined, PauseCircleOutlined, PlusOutlined, RightOutlined, SearchOutlined, SettingOutlined, SyncOutlined, TeamOutlined, UnorderedListOutlined, UserOutlined, } from "@ant-design/icons"; import { App, Avatar, Button, Card, Dropdown, Empty, Form, Input, List, Modal, Popconfirm, Radio, Select, Skeleton, Space, Table, Tag, Typography, } from "antd"; import dayjs from "dayjs"; import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate, useSearchParams } from "react-router-dom"; import { listUsers } from "../../api"; import { deleteMeeting, getMeetingCreateConfig, getMeetingPage, getMeetingProgress, getRealtimeMeetingSessionStatus, getRealtimeMeetingSessionStatuses, type MeetingCreateConfig, type MeetingProgress, type MeetingVO, type RealtimeMeetingSessionStatus, updateMeetingParticipants, } from "../../api/business/meeting"; import { MeetingCreateDrawer, type MeetingCreateType } from "../../components/business/MeetingCreateDrawer"; import AppPagination from "../../components/shared/AppPagination"; import { usePermission } from "../../hooks/usePermission"; import type { SysUser } from "../../types"; import PageContainer from "../../components/shared/PageContainer"; const { Title, Text } = Typography; const { Option } = Select; const CURRENT_PLATFORM = "WEB" as const; const PAUSED_DISPLAY_STATUS = 5; const REALTIME_ACTIVE_DISPLAY_STATUS = 6; const REALTIME_IDLE_DISPLAY_STATUS = 7; const DEFAULT_CREATE_CONFIG: MeetingCreateConfig = { offlineEnabled: true, realtimeEnabled: true, offlineAudioMaxSizeMb: 1024, }; const isRealtimeMeetingCandidate = (item: MeetingVO) => item.meetingType === "REALTIME" || (!item.meetingType && item.status === 0 && !item.audioUrl); const canControlRealtimeFromCurrentPlatform = (item: MeetingVO) => !item.meetingSource || item.meetingSource === CURRENT_PLATFORM; const getMeetingSourceLabel = (source?: MeetingVO["meetingSource"]) => source === "ANDROID" ? "安卓端" : "Web端"; const getRealtimeSourceLabel = (item: MeetingVO) => getMeetingSourceLabel(item.meetingSource); const isPausedRealtimeSessionStatus = (status?: RealtimeMeetingSessionStatus["status"]) => status === "PAUSED_EMPTY" || status === "PAUSED_RESUMABLE"; const canOpenRealtimeSession = (status?: RealtimeMeetingSessionStatus["status"]) => status === "PAUSED_EMPTY" || status === "PAUSED_RESUMABLE" || status === "ACTIVE" || status === "IDLE"; const shouldPollMeetingCard = (item: MeetingVO) => item.status === 1 || item.status === 2 || item.realtimeSessionStatus === "ACTIVE" || isPausedRealtimeSessionStatus(item.realtimeSessionStatus); const canManageMeeting = (meeting: MeetingVO) => { try { const profileStr = sessionStorage.getItem("userProfile"); if (!profileStr) { return false; } const profile = JSON.parse(profileStr); return profile.isPlatformAdmin === true || profile.isTenantAdmin === true || profile.userId === meeting.creatorId; } catch { return false; } }; const applyRealtimeSessionStatus = (item: MeetingVO, sessionStatus?: RealtimeMeetingSessionStatus): MeetingVO => { if (!sessionStatus) { return item; } if (sessionStatus.status === "PAUSED_EMPTY" || sessionStatus.status === "PAUSED_RESUMABLE") { return { ...item, displayStatus: PAUSED_DISPLAY_STATUS, realtimeSessionStatus: sessionStatus.status }; } if (sessionStatus.status === "ACTIVE") { return { ...item, displayStatus: REALTIME_ACTIVE_DISPLAY_STATUS, realtimeSessionStatus: sessionStatus.status }; } if (sessionStatus.status === "IDLE" && isRealtimeMeetingCandidate(item)) { return { ...item, displayStatus: REALTIME_IDLE_DISPLAY_STATUS, realtimeSessionStatus: sessionStatus.status }; } return { ...item, realtimeSessionStatus: sessionStatus.status }; }; const useMeetingProgress = (meeting: MeetingVO, onComplete?: () => void) => { const [progress, setProgress] = useState(null); useEffect(() => { if (meeting.status !== 1 && meeting.status !== 2) { return; } const fetchProgress = async () => { try { const res = await getMeetingProgress(meeting.id, { suppressErrorToast: true }); const nextProgress = res.data?.data; if (nextProgress) { setProgress(nextProgress); if ((nextProgress.percent === 100 || nextProgress.percent < 0) && onComplete) { onComplete(); } } } catch {} }; void fetchProgress(); const timer = setInterval(fetchProgress, 3000); return () => clearInterval(timer); }, [meeting.id, meeting.status, onComplete]); return progress; }; const IntegratedStatusTag: React.FC<{ meeting: MeetingVO; progress: MeetingProgress | null }> = ({ meeting, progress }) => { const effectiveStatus = meeting.displayStatus ?? meeting.status; const statusConfig: Record = { 0: { text: "排队中", color: "#8c8c8c", bgColor: "rgba(140, 140, 140, 0.1)", icon: }, 1: { text: "识别中", color: "#1890ff", bgColor: "rgba(24, 144, 255, 0.1)", icon: }, 2: { text: "总结中", color: "#faad14", bgColor: "rgba(250, 173, 20, 0.1)", icon: }, 3: { text: "已完成", color: "#52c41a", bgColor: "rgba(82, 196, 26, 0.1)", icon: }, 4: { text: "失败", color: "#ff4d4f", bgColor: "rgba(255, 77, 79, 0.1)", icon: }, 5: { text: "暂停中", color: "#d48806", bgColor: "rgba(212, 136, 6, 0.1)", icon: }, 6: { text: "进行中", color: "#5f51ff", bgColor: "rgba(95, 81, 255, 0.1)", icon: }, 7: { text: "待开始", color: "#595959", bgColor: "rgba(89, 89, 89, 0.1)", icon: }, }; const config = statusConfig[effectiveStatus] || statusConfig[0]; const percent = meeting.status === 1 || meeting.status === 2 ? progress?.percent || 0 : 0; const isProcessing = meeting.status === 1 || meeting.status === 2; return (
{isProcessing && percent > 0 && (
)} {isProcessing ? : config.icon} {config.text} {isProcessing && {percent}%}
); }; const TableStatusCell: React.FC<{ meeting: MeetingVO; fetchData: () => void }> = ({ meeting, fetchData }) => { const progress = useMeetingProgress(meeting, fetchData); return ; }; const MeetingCardItem: React.FC<{ item: MeetingVO; config: { text: string; color: string; bgColor: string }; fetchData: () => void; onOpenMeeting: (meeting: MeetingVO) => void; }> = ({ item, config, fetchData, onOpenMeeting }) => { const progress = useMeetingProgress(item, fetchData); const effectiveStatus = item.displayStatus ?? item.status; const isProcessing = item.status === 1 || item.status === 2; const isPaused = effectiveStatus === PAUSED_DISPLAY_STATUS; const isRealtimeActive = effectiveStatus === REALTIME_ACTIVE_DISPLAY_STATUS; const isRealtimeIdle = effectiveStatus === REALTIME_IDLE_DISPLAY_STATUS; const isCrossPlatformRealtime = (isPaused || isRealtimeActive || isRealtimeIdle) && !canControlRealtimeFromCurrentPlatform(item); const crossPlatformHint = `在${getRealtimeSourceLabel(item)}继续`; const sourceColor = item.meetingSource === "ANDROID" ? "#10b981" : "#3b82f6"; return ( onOpenMeeting(item)} className="meeting-card-v2" style={{ borderRadius: "20px", border: "1px solid rgba(0, 0, 0, 0.04)", background: "#ffffff", overflow: "hidden", transition: "all 0.3s cubic-bezier(0.165, 0.84, 0.44, 1)", boxShadow: "0 2px 8px rgba(0, 0, 0, 0.02), 0 1px 2px rgba(0, 0, 0, 0.02)", height: "100%", display: "flex", flexDirection: "column", minHeight: "200px" }} styles={{ body: { padding: "20px 24px", flex: 1, display: "flex", flexDirection: "column" } }} > {/* Top Section: Status & Source */}
{getMeetingSourceLabel(item.meetingSource)}
{/* Middle Section: Title & Actions */}
{item.title} {canManageMeeting(item) && (
e.stopPropagation()} > deleteMeeting(item.id).then(fetchData)} okText="删除" cancelText="取消" okButtonProps={{ danger: true }} >
)}
{/* Content Section: Progress or Metadata */}
{(isProcessing || isPaused || isRealtimeActive || isRealtimeIdle) ? (
{isProcessing ? : } {isProcessing ? (progress?.message || "深度分析中...") : (isCrossPlatformRealtime ? crossPlatformHint : config.text)}
) : (
{dayjs(item.meetingTime).format("MM-DD HH:mm")}
{item.creatorName || "未知"}
)}
{/* Bottom Section: Tags & Navigate */}
{item.tags?.split(",").filter(Boolean).slice(0, 2).map(tag => ( #{tag} )) || 无标签}
查看详情
); }; const Meetings: React.FC = () => { const { message } = App.useApp(); const { t } = useTranslation(); const navigate = useNavigate(); const { can } = usePermission(); const [searchParams, setSearchParams] = useSearchParams(); const [loading, setLoading] = useState(false); const [data, setData] = useState([]); const [total, setTotal] = useState(0); const [current, setCurrent] = useState(1); const [displayMode, setDisplayMode] = useState<"card" | "list">("card"); const [size, setSize] = useState(8); const [searchTitle, setSearchTitle] = useState(""); const [viewType, setViewType] = useState<"all" | "created" | "involved">("all"); const [createDrawerVisible, setCreateDrawerVisible] = useState(false); const [createDrawerType, setCreateDrawerType] = useState("upload"); const [configLoaded, setConfigLoaded] = useState(false); const [createConfig, setCreateConfig] = useState({ offlineEnabled: false, realtimeEnabled: false, offlineAudioMaxSizeMb: 1024, }); const [userList, setUserList] = useState([]); const hasRunningTasks = data.some(shouldPollMeetingCard); const handleDisplayModeChange = (mode: "card" | "list") => { setDisplayMode(mode); setSize(mode === "card" ? 8 : 10); setCurrent(1); }; useEffect(() => { const action = searchParams.get("action"); const type = searchParams.get("type") as MeetingCreateType; if (action === "create" && (type === "realtime" || type === "upload")) { setCreateDrawerType(type); setCreateDrawerVisible(true); setSearchParams({}); } }, [searchParams, setSearchParams]); useEffect(() => { void fetchData(); }, [current, size, searchTitle, viewType]); useEffect(() => { if (!hasRunningTasks) { return; } const timer = setInterval(() => void fetchData(true), 5000); return () => clearInterval(timer); }, [hasRunningTasks, current, size, searchTitle, viewType]); useEffect(() => { listUsers().then((users) => setUserList(users || [])).catch(() => setUserList([])); getMeetingCreateConfig() .then((res) => { setCreateConfig(res.data.data || DEFAULT_CREATE_CONFIG); setConfigLoaded(true); }) .catch(() => { setCreateConfig(DEFAULT_CREATE_CONFIG); setConfigLoaded(true); }); }, []); const fetchData = async (silent = false) => { if (!silent) { setLoading(true); } try { const res = await getMeetingPage({ current, size, title: searchTitle, viewType }); const records = res.data?.data?.records || []; let statusMap: Record = {}; const realtimeCandidates = records.filter(isRealtimeMeetingCandidate); if (realtimeCandidates.length > 0) { try { const sessionRes = await getRealtimeMeetingSessionStatuses(realtimeCandidates.map((item) => item.id)); statusMap = sessionRes.data?.data || {}; } catch {} } setData(records.map((item) => applyRealtimeSessionStatus(item, statusMap[item.id]))); setTotal(res.data?.data?.total || 0); } finally { if (!silent) { setLoading(false); } } }; const handleOpenMeeting = async (meeting: MeetingVO) => { if (!isRealtimeMeetingCandidate(meeting)) { navigate("/meetings/" + meeting.id); return; } if (!canControlRealtimeFromCurrentPlatform(meeting)) { message.info("该实时会议需在" + getRealtimeSourceLabel(meeting) + "继续,当前仅支持查看详情"); navigate("/meetings/" + meeting.id); return; } if (canOpenRealtimeSession(meeting.realtimeSessionStatus)) { navigate("/meeting-live-session/" + meeting.id); return; } if (!canManageMeeting(meeting)) { navigate("/meetings/" + meeting.id); return; } try { const res = await getRealtimeMeetingSessionStatus(meeting.id); if (canOpenRealtimeSession(res.data?.data?.status)) { navigate("/meeting-live-session/" + meeting.id); return; } } catch {} navigate("/meetings/" + meeting.id); }; const tableColumns = [ { title: "会议标题", dataIndex: "title", key: "title", render: (text: string, record: MeetingVO) => handleOpenMeeting(record)}>{text}, }, { title: "状态", key: "status", width: 150, render: (_: unknown, record: MeetingVO) => void fetchData()} />, }, { title: "会议时间", dataIndex: "meetingTime", key: "meetingTime", width: 160, render: (text: string) => dayjs(text).format("YYYY-MM-DD HH:mm"), }, { title: "创建人", dataIndex: "creatorName", key: "creatorName", width: 100, render: (text: string) => {text || "未知"}, }, { title: "来源", dataIndex: "meetingSource", key: "meetingSource", width: 80, render: (value: MeetingVO["meetingSource"]) => {getMeetingSourceLabel(value)}, }, { title: "参会人", dataIndex: "participants", key: "participants", render: (text: string) => {text || "无参与人员"}, }, { title: "操作", key: "action", width: 160, render: (_: unknown, record: MeetingVO) => ( {canManageMeeting(record) && ( deleteMeeting(record.id).then(() => fetchData())}> )} ), }, ]; const statusConfig: Record = { 0: { text: "排队中", color: "#8c8c8c", bgColor: "#f5f5f5" }, 1: { text: "识别中", color: "#1890ff", bgColor: "#e6f7ff" }, 2: { text: "总结中", color: "#faad14", bgColor: "#fff7e6" }, 3: { text: "已完成", color: "#52c41a", bgColor: "#f6ffed" }, 4: { text: "失败", color: "#ff4d4f", bgColor: "#fff1f0" }, 5: { text: "会议暂停", color: "#d48806", bgColor: "#fff7e6" }, 6: { text: "实时进行中", color: "#1677ff", bgColor: "#e6f4ff" }, 7: { text: "待开始", color: "#595959", bgColor: "#f5f5f5" }, }; return ( handleDisplayModeChange(e.target.value)} buttonStyle="solid"> {configLoaded && ( )} } toolbar={ <> { setViewType(e.target.value); setCurrent(1); }} buttonStyle="solid"> 全部 我发起 我参与 } allowClear onPressEnter={(e) => { setSearchTitle((e.target as HTMLInputElement).value); setCurrent(1); }} style={{ width: 200 }} /> } >
{displayMode === "card" ? ( { const config = statusConfig[item.displayStatus ?? item.status] || statusConfig[0]; return void fetchData()} onOpenMeeting={handleOpenMeeting} />; }} locale={{ emptyText: }} /> ) : ( ({ onClick: () => handleOpenMeeting(record), style: { cursor: "pointer" } })} locale={{ emptyText: }} /> )}
{ setCurrent(p); setSize(s); }} />
setCreateDrawerVisible(false)} onSuccess={() => { setCreateDrawerVisible(false); void fetchData(); }} /> ); }; export default Meetings;