import React, { useState, useEffect, useRef } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import axios from 'axios'; import { ArrowLeft, Clock, Users, FileText, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2 } from 'lucide-react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; import rehypeSanitize from 'rehype-sanitize'; import { buildApiUrl, API_ENDPOINTS, API_BASE_URL } from '../config/api'; import './MeetingDetails.css'; const MeetingDetails = ({ user }) => { const { meeting_id } = useParams(); const navigate = useNavigate(); const [meeting, setMeeting] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [isPlaying, setIsPlaying] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [duration, setDuration] = useState(0); const [volume, setVolume] = useState(1); const [transcript, setTranscript] = useState([]); const [showTranscript, setShowTranscript] = useState(true); const [audioUrl, setAudioUrl] = useState(null); const [audioFileName, setAudioFileName] = useState(null); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const audioRef = useRef(null); useEffect(() => { fetchMeetingDetails(); }, [meeting_id]); const fetchMeetingDetails = async () => { try { setLoading(true); // Fallback URL construction in case config fails const baseUrl = "" const detailEndpoint = API_ENDPOINTS?.MEETINGS?.DETAIL?.(meeting_id) || `/api/meetings/${meeting_id}`; const audioEndpoint = API_ENDPOINTS?.MEETINGS?.AUDIO?.(meeting_id) || `/api/meetings/${meeting_id}/audio`; const transcriptEndpoint = API_ENDPOINTS?.MEETINGS?.TRANSCRIPT?.(meeting_id) || `/api/meetings/${meeting_id}/transcript`; const response = await axios.get(`${baseUrl}${detailEndpoint}`); setMeeting(response.data); // Fetch audio file if available try { const audioResponse = await axios.get(`${baseUrl}${audioEndpoint}`); // Construct URL using uploads path and relative path from database setAudioUrl(`${baseUrl}${audioResponse.data.file_path}`); setAudioFileName(audioResponse.data.file_name); } catch (audioError) { console.warn('No audio file available:', audioError); setAudioUrl(null); setAudioFileName(null); } // Fetch transcript segments from database try { const transcriptResponse = await axios.get(`${baseUrl}${transcriptEndpoint}`); setTranscript(transcriptResponse.data); } catch (transcriptError) { console.warn('No transcript data available:', transcriptError); setTranscript([]); } } catch (err) { console.error('Error fetching meeting details:', err); setError('无法加载会议详情,请稍后重试。'); } finally { setLoading(false); } }; const formatDateTime = (dateTimeString) => { if (!dateTimeString) return '时间待定'; const date = new Date(dateTimeString); return date.toLocaleString('zh-CN', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; const formatTime = (seconds) => { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; }; const handlePlayPause = () => { if (audioRef.current) { if (isPlaying) { audioRef.current.pause(); } else { audioRef.current.play(); } setIsPlaying(!isPlaying); } }; const handleTimeUpdate = () => { if (audioRef.current) { setCurrentTime(audioRef.current.currentTime); } }; const handleLoadedMetadata = () => { if (audioRef.current) { setDuration(audioRef.current.duration); } }; const handleSeek = (e) => { if (!audioRef.current || !duration) return; const rect = e.currentTarget.getBoundingClientRect(); const percent = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width)); const seekTime = percent * duration; audioRef.current.currentTime = seekTime; setCurrentTime(seekTime); }; const handleProgressMouseDown = (e) => { e.preventDefault(); const handleMouseMove = (moveEvent) => { handleSeek(moveEvent); }; const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mouseup', handleMouseUp); }; document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mouseup', handleMouseUp); handleSeek(e); }; const handleVolumeChange = (e) => { const newVolume = parseFloat(e.target.value); setVolume(newVolume); if (audioRef.current) { audioRef.current.volume = newVolume; } }; const jumpToTime = (timestamp) => { if (audioRef.current) { audioRef.current.currentTime = timestamp; setCurrentTime(timestamp); audioRef.current.play(); setIsPlaying(true); } }; const handleDeleteMeeting = async () => { try { await axios.delete(buildApiUrl(API_ENDPOINTS.MEETINGS.DELETE(meeting_id))); navigate('/dashboard'); } catch (err) { console.error('Error deleting meeting:', err); setError('删除会议失败,请重试'); } }; const isCreator = meeting && user && String(meeting.creator_id) === String(user.user_id); if (loading) { return
加载中...
{error}
返回首页未找到会议信息。
返回首页该会议没有录音文件
录音功能可能未开启或录音文件丢失
确定要删除会议 "{meeting.title}" 吗?此操作无法撤销。