imetting_client/src/pages/Meetings/Meetings.jsx

411 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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;