imetting_client/src/pages/Meetings/Meetings.jsx

411 lines
14 KiB
React
Raw Normal View History

2025-12-02 10:54:36 +00:00
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Clock, User, QrCode, ChevronUp, ChevronDown, Trash2, Play, Pause, X } from 'lucide-react';
import { meetingService } from '../../services/meeting';
import { authService } from '../../services/auth';
import Toast from '../../components/Toast';
import './Meetings.css';
function Meetings() {
const navigate = useNavigate();
const [meetings, setMeetings] = useState([]);
const [loading, setLoading] = useState(true);
const [pagination, setPagination] = useState({
page: 1,
page_size: 5,
total: 0,
total_pages: 0
});
const [toast, setToast] = useState(null);
const [showQRModal, setShowQRModal] = useState(false);
const [selectedMeetingUrl, setSelectedMeetingUrl] = useState('');
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [deletingMeeting, setDeletingMeeting] = useState(null);
const [isDeleting, setIsDeleting] = useState(false);
const currentUser = authService.getLocalUser();
// 音频播放相关状态
const [playingMeetingId, setPlayingMeetingId] = useState(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const audioRef = useRef(null);
useEffect(() => {
fetchMeetings(1);
}, []);
const fetchMeetings = async (page) => {
setLoading(true);
try {
const response = await meetingService.getMeetings({
page,
page_size: pagination.page_size
});
// API响应格式: { code, message, data: { meetings, page, page_size, total, total_pages } }
const data = response.data;
setMeetings(data.meetings || []);
setPagination({
page: data.page,
page_size: data.page_size,
total: data.total,
total_pages: data.total_pages
});
} catch (error) {
console.error('Failed to fetch meetings:', error);
setToast({ message: '加载会议列表失败', type: 'error' });
} finally {
setLoading(false);
}
};
const handlePageChange = (newPage) => {
if (newPage >= 1 && newPage <= pagination.total_pages) {
fetchMeetings(newPage);
}
};
const handleShowQR = (meetingId) => {
const url = `${window.location.origin}/meetings/preview/${meetingId}`;
setSelectedMeetingUrl(url);
setShowQRModal(true);
};
const handleDeleteClick = (meeting, e) => {
e.stopPropagation();
setDeletingMeeting(meeting);
setShowDeleteModal(true);
};
const handleConfirmDelete = async () => {
if (!deletingMeeting) return;
setIsDeleting(true);
try {
await meetingService.deleteMeeting(deletingMeeting.meeting_id);
setToast({ message: '会议删除成功', type: 'success' });
setShowDeleteModal(false);
setDeletingMeeting(null);
// 重新加载当前页
// 如果当前页只有一条记录且不是第一页,则返回上一页
const isLastItemOnPage = meetings.length === 1 && pagination.page > 1;
const targetPage = isLastItemOnPage ? pagination.page - 1 : pagination.page;
fetchMeetings(targetPage);
} catch (error) {
console.error('Failed to delete meeting:', error);
setToast({
message: error.response?.data?.message || '删除会议失败',
type: 'error'
});
} finally {
setIsDeleting(false);
}
};
const handleCancelDelete = () => {
setShowDeleteModal(false);
setDeletingMeeting(null);
};
// 音频播放相关函数
const handlePlayAudio = (meetingId, e) => {
e.stopPropagation();
// 如果点击的是同一个会议,且正在播放,则暂停
if (playingMeetingId === meetingId && isPlaying) {
audioRef.current?.pause();
setIsPlaying(false);
return;
}
// 如果点击的是不同的会议,或者同一个会议但暂停状态
if (playingMeetingId !== meetingId) {
// 停止之前的播放
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
// 创建新的音频元素
const audio = new Audio(`/api/meetings/${meetingId}/audio/stream`);
audioRef.current = audio;
// 监听音频事件
audio.addEventListener('loadedmetadata', () => {
setDuration(audio.duration);
});
audio.addEventListener('timeupdate', () => {
setCurrentTime(audio.currentTime);
});
audio.addEventListener('ended', () => {
setIsPlaying(false);
setCurrentTime(0);
});
audio.addEventListener('error', (e) => {
console.error('Audio error:', e);
setToast({ message: '音频加载失败', type: 'error' });
setPlayingMeetingId(null);
setIsPlaying(false);
});
setPlayingMeetingId(meetingId);
setCurrentTime(0);
audio.play();
setIsPlaying(true);
} else {
// 同一个会议,从暂停恢复播放
audioRef.current?.play();
setIsPlaying(true);
}
};
const handlePlayPause = (e) => {
e.stopPropagation();
if (isPlaying) {
audioRef.current?.pause();
setIsPlaying(false);
} else {
audioRef.current?.play();
setIsPlaying(true);
}
};
const handleSeek = (e) => {
const progressBar = e.currentTarget;
const rect = progressBar.getBoundingClientRect();
const percent = (e.clientX - rect.left) / rect.width;
const newTime = percent * duration;
if (audioRef.current) {
audioRef.current.currentTime = newTime;
setCurrentTime(newTime);
}
};
const handleClosePlayer = (e) => {
e.stopPropagation();
if (audioRef.current) {
audioRef.current.pause();
audioRef.current = null;
}
setPlayingMeetingId(null);
setIsPlaying(false);
setCurrentTime(0);
setDuration(0);
};
const formatTime = (seconds) => {
if (isNaN(seconds)) return '0:00';
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const formatDateTime = (dateString) => {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
};
return (
<div className="meetings-page">
{toast && (
<Toast
message={toast.message}
type={toast.type}
duration={3000}
onClose={() => setToast(null)}
/>
)}
<div className="page-header">
<button className="back-button" onClick={() => navigate('/')} title="返回首页">
<ArrowLeft size={24} />
</button>
<h1 className="page-title">会议记录</h1>
</div>
<div className="meetings-container">
{loading ? (
<div className="loading-state">
<p>加载中...</p>
</div>
) : meetings.length === 0 ? (
<div className="empty-state">
<p>暂无会议记录</p>
</div>
) : (
<>
<div className="meetings-content-wrapper">
<div className="meetings-list">
{meetings.map((meeting) => {
const isCreator = String(meeting.creator_id) === String(currentUser?.user_id);
const cardClass = isCreator ? 'created-by-me' : 'attended-by-me';
const isPlaying = playingMeetingId === meeting.meeting_id;
return (
<div key={meeting.meeting_id} className={`meeting-item ${cardClass} ${isPlaying ? 'playing' : ''}`}>
<div
className="meeting-main"
onClick={(e) => {
if (!isPlaying) {
handlePlayAudio(meeting.meeting_id, e);
}
}}
>
{isPlaying ? (
/* 播放器模式 */
<div className="audio-player">
<button className="play-btn" onClick={handlePlayPause} title={isPlaying ? '暂停' : '播放'}>
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
</button>
<div className="player-info">
<div className="player-title">{meeting.title}</div>
<div className="progress-container">
<span className="time-current">{formatTime(currentTime)}</span>
<div className="progress-bar" onClick={handleSeek}>
<div
className="progress-fill"
style={{ width: `${duration ? (currentTime / duration) * 100 : 0}%` }}
></div>
</div>
<span className="time-duration">{formatTime(duration)}</span>
</div>
</div>
<button className="close-player-btn" onClick={handleClosePlayer} title="关闭播放器">
<X size={20} />
</button>
</div>
) : (
/* 正常信息显示 */
<div className="meeting-info">
<h3 className="meeting-title">{meeting.title}</h3>
<div className="meeting-meta">
<div className="meta-item">
<Clock size={14} />
<span>{formatDateTime(meeting.meeting_time)}</span>
</div>
<div className="meta-item">
<User size={14} />
<span>创建人: {meeting.creator_username}</span>
</div>
</div>
</div>
)}
</div>
{!isPlaying && (
<div className="meeting-actions">
<button
className="qr-btn"
onClick={(e) => {
e.stopPropagation();
handleShowQR(meeting.meeting_id);
}}
title="查看二维码"
>
<QrCode size={18} />
</button>
{isCreator && (
<button
className="delete-btn"
onClick={(e) => handleDeleteClick(meeting, e)}
title="删除会议"
>
<Trash2 size={18} />
</button>
)}
</div>
)}
</div>
);
})}
</div>
{/* 竖直分页器 */}
{pagination.total_pages > 1 && (
<div className="vertical-pagination">
<button
className="pagination-arrow-btn"
onClick={() => handlePageChange(pagination.page - 1)}
disabled={pagination.page === 1}
title="上一页"
>
<ChevronUp size={20} />
</button>
<div className="pagination-page-info">
{pagination.page}/{pagination.total_pages}
</div>
<button
className="pagination-arrow-btn"
onClick={() => handlePageChange(pagination.page + 1)}
disabled={pagination.page === pagination.total_pages}
title="下一页"
>
<ChevronDown size={20} />
</button>
</div>
)}
</div>
</>
)}
</div>
{/* QR码模态框 */}
{showQRModal && (
<div className="modal-overlay" onClick={() => setShowQRModal(false)}>
<div className="qr-modal" onClick={(e) => e.stopPropagation()}>
<h3>扫码查看会议</h3>
<div className="qr-code-container">
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(selectedMeetingUrl)}`}
alt="QR Code"
/>
</div>
<p className="qr-url">{selectedMeetingUrl}</p>
<button className="close-btn" onClick={() => setShowQRModal(false)}>
关闭
</button>
</div>
</div>
)}
{/* 删除确认模态框 */}
{showDeleteModal && (
<div className="modal-overlay" onClick={handleCancelDelete}>
<div className="delete-modal" onClick={(e) => e.stopPropagation()}>
<div className="delete-icon">
<Trash2 size={48} />
</div>
<h3>确认删除会议</h3>
<p className="delete-warning">
确定要删除会议 "<strong>{deletingMeeting?.title}</strong>"
</p>
<div className="modal-actions">
<button className="cancel-btn" onClick={handleCancelDelete} disabled={isDeleting}>
取消
</button>
<button className="confirm-delete-btn" onClick={handleConfirmDelete} disabled={isDeleting}>
{isDeleting ? '删除中...' : '确认删除'}
</button>
</div>
</div>
</div>
)}
</div>
);
}
export default Meetings;