import React, { useEffect, useRef, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd'; import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import MarkdownRenderer from '../components/MarkdownRenderer'; import MindMap from '../components/MindMap'; import AudioPlayerBar from '../components/AudioPlayerBar'; import TranscriptTimeline from '../components/TranscriptTimeline'; import tools from '../utils/tools'; import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService'; import './MeetingPreview.css'; const { Content } = Layout; const MeetingPreview = () => { const { meeting_id } = useParams(); const { message } = App.useApp(); const audioRef = useRef(null); const transcriptRefs = useRef([]); const [meeting, setMeeting] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [password, setPassword] = useState(''); const [passwordError, setPasswordError] = useState(''); const [passwordRequired, setPasswordRequired] = useState(false); const [isAuthorized, setIsAuthorized] = useState(false); const [branding, setBranding] = useState(DEFAULT_BRANDING_CONFIG); const [transcript, setTranscript] = useState([]); const [audioUrl, setAudioUrl] = useState(''); const [activeSegmentIndex, setActiveSegmentIndex] = useState(-1); const [playbackRate, setPlaybackRate] = useState(1); useEffect(() => { configService.getBrandingConfig().then(setBranding).catch(() => {}); }, []); useEffect(() => { setMeeting(null); setTranscript([]); transcriptRefs.current = []; setAudioUrl(''); setActiveSegmentIndex(-1); setPlaybackRate(1); setError(null); setPassword(''); setPasswordError(''); setPasswordRequired(false); setIsAuthorized(false); fetchPreview(); }, [meeting_id]); const fetchTranscriptAndAudio = async () => { const [transcriptRes, audioRes] = await Promise.allSettled([ apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.TRANSCRIPT(meeting_id))), apiClient.get(buildApiUrl(API_ENDPOINTS.MEETINGS.AUDIO(meeting_id))), ]); if (transcriptRes.status === 'fulfilled') { setTranscript(Array.isArray(transcriptRes.value.data) ? transcriptRes.value.data : []); } else { setTranscript([]); } if (audioRes.status === 'fulfilled') { setAudioUrl(buildApiUrl(`${API_ENDPOINTS.MEETINGS.AUDIO(meeting_id)}/stream`)); } else { setAudioUrl(''); } }; const fetchPreview = async (pwd = '') => { setLoading(true); try { const endpoint = API_ENDPOINTS.MEETINGS.PREVIEW_DATA(meeting_id); const url = buildApiUrl(`${endpoint}${pwd ? `?password=${encodeURIComponent(pwd)}` : ''}`); const res = await apiClient.get(url); setMeeting(res.data); setIsAuthorized(true); setPasswordRequired(false); setPasswordError(''); setError(null); await fetchTranscriptAndAudio(); } catch (err) { const responseCode = String(err?.response?.data?.code || ''); if (responseCode === '401') { setMeeting(null); setIsAuthorized(false); setPasswordRequired(true); setPasswordError(err?.response?.data?.message || ''); setError(null); } else { setMeeting(null); setIsAuthorized(false); setPasswordRequired(false); setPasswordError(''); setError(err?.response?.data?.message || '无法加载会议预览'); } } finally { setLoading(false); } }; const handleCopyLink = async () => { await navigator.clipboard.writeText(window.location.href); message.success('分享链接已复制'); }; const handleShare = async () => { if (navigator.share) { try { await navigator.share({ title: meeting?.title || branding.preview_title, url: window.location.href }); return; } catch { // Fallback to copying the link when native share is cancelled or unavailable. } } await handleCopyLink(); }; const handleTimeUpdate = () => { if (!audioRef.current || !transcript.length) { return; } const currentMs = audioRef.current.currentTime * 1000; const index = transcript.findIndex( (item) => currentMs >= item.start_time_ms && currentMs <= item.end_time_ms, ); setActiveSegmentIndex(index); }; const jumpToSegment = (segment) => { if (!audioRef.current || !segment) { return; } audioRef.current.currentTime = (segment.start_time_ms || 0) / 1000; audioRef.current.play().catch(() => {}); }; useEffect(() => { if (activeSegmentIndex < 0) { return; } transcriptRefs.current[activeSegmentIndex]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); }, [activeSegmentIndex]); const handleVerify = () => { if (!password) { message.warning('请输入访问密码'); return; } fetchPreview(password); }; if (loading && !meeting) { return (

正在加载会议预览...

); } if (error) { return (
⚠️

加载失败

{error}

); } if (passwordRequired && !isAuthorized) { return (

此会议受密码保护

请输入访问密码以查看会议纪要

{ setPassword(e.target.value); if (passwordError) { setPasswordError(''); } }} onPressEnter={handleVerify} />
{passwordError ?
{passwordError}
: null}
); } return (

{meeting.title}

会议信息

创建人:{meeting.creator_username || '未知'}
会议时间:{tools.formatDateTime(meeting.meeting_time)}
参会人员:{meeting.attendees?.length ? meeting.attendees.map((item) => item.caption).join('、') : '未设置'}
计算人数:{meeting.attendees_count || meeting.attendees?.length || 0}人
总结 ), children: (
{meeting.summary ? : }
), }, { key: 'mindmap', label: ( 思维导图 ), children: (
), }, { key: 'transcript', label: ( 会议转录 ), children: (
{audioUrl ? ( ) : null} jumpToSegment({ start_time_ms: timeMs })} transcriptRefs={transcriptRefs} maxHeight="520px" getSpeakerColor={(speakerId) => { const palette = ['#1677ff', '#52c41a', '#fa8c16', '#eb2f96', '#722ed1', '#13c2c2', '#2f54eb', '#faad14']; return palette[(speakerId ?? 0) % palette.length]; }} emptyDescription="暂无转录内容" />
), } ]} />
{branding.footer_text}
); }; export default MeetingPreview;