修正了前端一些问题
parent
0391dd9cb3
commit
52e5ea553a
|
|
@ -11,7 +11,7 @@ import Dropdown from './Dropdown';
|
|||
import tools from '../utils/tools';
|
||||
import './MeetingTimeline.css';
|
||||
|
||||
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore = false, onLoadMore, loadingMore = false }) => {
|
||||
const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore = false, onLoadMore, loadingMore = false, filterType = 'all', searchQuery = '', selectedTags = [] }) => {
|
||||
const [deleteConfirmInfo, setDeleteConfirmInfo] = useState(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
|
@ -66,7 +66,16 @@ const MeetingTimeline = ({ meetingsByDate, currentUser, onDeleteMeeting, hasMore
|
|||
|
||||
return (
|
||||
<div className="meeting-card-wrapper" key={meeting.meeting_id}>
|
||||
<Link to={`/meetings/${meeting.meeting_id}`}>
|
||||
<Link
|
||||
to={`/meetings/${meeting.meeting_id}`}
|
||||
state={{
|
||||
filterContext: {
|
||||
filterType,
|
||||
searchQuery,
|
||||
selectedTags
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={`meeting-card ${cardClass} meeting-card-link`}>
|
||||
<div className="meeting-content">
|
||||
<div className="meeting-header">
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ const API_CONFIG = {
|
|||
AUDIO: (meetingId) => `/api/meetings/${meetingId}/audio`,
|
||||
UPLOAD_AUDIO: '/api/meetings/upload-audio',
|
||||
UPLOAD_IMAGE: (meetingId) => `/api/meetings/${meetingId}/upload-image`,
|
||||
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`
|
||||
REGENERATE_SUMMARY: (meetingId) => `/api/meetings/${meetingId}/regenerate-summary`,
|
||||
NAVIGATION: (meetingId) => `/api/meetings/${meetingId}/navigation`
|
||||
},
|
||||
ADMIN: {
|
||||
SYSTEM_CONFIG: '/api/admin/system-config'
|
||||
|
|
|
|||
|
|
@ -530,6 +530,9 @@ const Dashboard = ({ user, onLogout }) => {
|
|||
hasMore={pagination.has_more}
|
||||
onLoadMore={handleLoadMore}
|
||||
loadingMore={loadingMore}
|
||||
filterType={filterType}
|
||||
searchQuery={searchQuery}
|
||||
selectedTags={selectedTags}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,132 @@
|
|||
align-items: center;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
/* 融合导航栏样式 */
|
||||
.unified-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%);
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
padding: 0.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
padding: 0.4rem 0.9rem;
|
||||
background: transparent;
|
||||
color: #64748b;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.25s ease;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-btn svg {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* 返回首页按钮样式 */
|
||||
.back-home-btn {
|
||||
color: #475569;
|
||||
margin-right: 0.2rem;
|
||||
border-right: 1.5px solid #e2e8f0;
|
||||
border-radius: 6px 0 0 6px;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.back-home-btn:hover {
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* 上一条/下一条按钮激活状态 */
|
||||
.nav-btn.prev-btn,
|
||||
.nav-btn.next-btn {
|
||||
color: white;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-btn.prev-btn::before,
|
||||
.nav-btn.next-btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.nav-btn.prev-btn:hover::before,
|
||||
.nav-btn.next-btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.nav-btn.prev-btn:hover,
|
||||
.nav-btn.next-btn:hover {
|
||||
background: linear-gradient(135deg, #5a67d8 0%, #6b46c1 100%);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
.nav-btn.prev-btn:active,
|
||||
.nav-btn.next-btn:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
/* 禁用状态 */
|
||||
.nav-btn.disabled {
|
||||
background: linear-gradient(135deg, #cbd5e1 0%, #94a3b8 100%);
|
||||
color: #f1f5f9;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.nav-btn.disabled:hover {
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 位置指示器 */
|
||||
.nav-position {
|
||||
padding: 0.4rem 0.8rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
||||
border-radius: 6px;
|
||||
margin: 0 0.2rem;
|
||||
min-width: 50px;
|
||||
border: 1px solid #cbd5e1;
|
||||
}
|
||||
|
||||
.meeting-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
|
@ -73,20 +199,6 @@
|
|||
min-height: 0;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.back-link:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.details-content-card {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
|
|
@ -326,6 +438,10 @@
|
|||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid #667eea;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.audio-file-name {
|
||||
|
|
@ -334,6 +450,28 @@
|
|||
font-weight: 500;
|
||||
}
|
||||
|
||||
.audio-loading-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: #667eea;
|
||||
padding: 0.2rem 0.6rem;
|
||||
background: rgba(102, 126, 234, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.audio-error-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
font-size: 0.8rem;
|
||||
color: #ef4444;
|
||||
padding: 0.2rem 0.6rem;
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Audio Player Styles */
|
||||
.audio-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
|
|
@ -522,6 +660,11 @@
|
|||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
outline: none; /* 去掉点击后的蓝色边框 */
|
||||
}
|
||||
|
||||
.play-button:focus {
|
||||
outline: white 1px solid; /* 确保聚焦时也没有边框 */
|
||||
}
|
||||
|
||||
.play-button:hover {
|
||||
|
|
@ -529,6 +672,42 @@
|
|||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 播放按钮加载状态 */
|
||||
.play-button.loading,
|
||||
.play-button.buffering {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.play-button.loading:hover,
|
||||
.play-button.buffering:hover {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.play-button.disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.play-button.disabled:hover {
|
||||
transform: none;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.time-info {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { useParams, Link, useNavigate } from 'react-router-dom';
|
||||
import { useParams, Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import apiClient from '../utils/apiClient';
|
||||
import configService from '../utils/configService';
|
||||
import tools from '../utils/tools';
|
||||
import { ArrowLeft, Clock, Users, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff, Image, QrCode, MoreVertical, Upload } from 'lucide-react';
|
||||
import { ArrowLeft, Clock, Users, User, Calendar, Play, Pause, Volume2, MessageCircle, Edit, Trash2, Settings, Save, X, Edit3, Brain, Sparkles, Download, ArrowDown, RefreshCw, RefreshCwOff, Image, QrCode, MoreVertical, Upload, ChevronLeft, ChevronRight, Loader } from 'lucide-react';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import rehypeRaw from 'rehype-raw';
|
||||
|
|
@ -25,6 +25,7 @@ const { TabPane } = Tabs;
|
|||
const MeetingDetails = ({ user }) => {
|
||||
const { meeting_id } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const [meeting, setMeeting] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
|
@ -67,6 +68,26 @@ const MeetingDetails = ({ user }) => {
|
|||
const [showUploadConfirm, setShowUploadConfirm] = useState(false);
|
||||
const [maxFileSize, setMaxFileSize] = useState(100 * 1024 * 1024); // 默认100MB
|
||||
const [uploadError, setUploadError] = useState('');
|
||||
|
||||
// 音频加载状态
|
||||
const [audioLoading, setAudioLoading] = useState(true); // 音频是否正在加载
|
||||
const [audioCanPlay, setAudioCanPlay] = useState(false); // 音频是否可以播放
|
||||
const [audioBuffering, setAudioBuffering] = useState(false); // 音频是否正在缓冲
|
||||
const [audioError, setAudioError] = useState(null); // 音频加载错误
|
||||
|
||||
// 导航相关状态
|
||||
const [navigationInfo, setNavigationInfo] = useState({
|
||||
prev_meeting_id: null,
|
||||
next_meeting_id: null,
|
||||
current_index: null,
|
||||
total_count: null
|
||||
});
|
||||
const [filterContext, setFilterContext] = useState({
|
||||
filterType: 'all',
|
||||
searchQuery: '',
|
||||
selectedTags: []
|
||||
});
|
||||
|
||||
const audioRef = useRef(null);
|
||||
const transcriptRefs = useRef([]);
|
||||
|
||||
|
|
@ -80,6 +101,13 @@ const MeetingDetails = ({ user }) => {
|
|||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
};
|
||||
|
||||
// 获取从Dashboard传递的筛选上下文
|
||||
useEffect(() => {
|
||||
if (location.state?.filterContext) {
|
||||
setFilterContext(location.state.filterContext);
|
||||
}
|
||||
}, [location.state]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMeetingDetails();
|
||||
loadFileSizeConfig();
|
||||
|
|
@ -299,6 +327,35 @@ const MeetingDetails = ({ user }) => {
|
|||
}
|
||||
};
|
||||
|
||||
// 获取导航信息(上一条/下一条会议)
|
||||
const fetchNavigationInfo = async () => {
|
||||
try {
|
||||
const params = {
|
||||
user_id: user.user_id,
|
||||
filter_type: filterContext.filterType,
|
||||
search: filterContext.searchQuery || undefined,
|
||||
tags: filterContext.selectedTags.length > 0 ? filterContext.selectedTags.join(',') : undefined
|
||||
};
|
||||
|
||||
const response = await apiClient.get(
|
||||
buildApiUrl(API_ENDPOINTS.MEETINGS.NAVIGATION(meeting_id)),
|
||||
{ params }
|
||||
);
|
||||
|
||||
setNavigationInfo(response.data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching navigation info:', err);
|
||||
// 不显示错误提示,静默失败
|
||||
}
|
||||
};
|
||||
|
||||
// 当会议详情加载完成后,获取导航信息
|
||||
useEffect(() => {
|
||||
if (meeting && user && filterContext) {
|
||||
fetchNavigationInfo();
|
||||
}
|
||||
}, [meeting, user, filterContext, meeting_id]);
|
||||
|
||||
const handleFileChange = (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
|
|
@ -402,10 +459,19 @@ const MeetingDetails = ({ user }) => {
|
|||
|
||||
const handlePlayPause = () => {
|
||||
if (audioRef.current) {
|
||||
// 如果音频还未加载完成,不允许播放
|
||||
if (!audioCanPlay && !isPlaying) {
|
||||
showToast('音频正在加载中,请稍候...', 'info');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPlaying) {
|
||||
audioRef.current.pause();
|
||||
} else {
|
||||
audioRef.current.play();
|
||||
audioRef.current.play().catch(err => {
|
||||
console.error('播放失败:', err);
|
||||
showToast('播放失败,请重试', 'error');
|
||||
});
|
||||
}
|
||||
setIsPlaying(!isPlaying);
|
||||
}
|
||||
|
|
@ -456,32 +522,99 @@ const MeetingDetails = ({ user }) => {
|
|||
}
|
||||
};
|
||||
|
||||
const handleSeek = (e) => {
|
||||
// 音频可以开始播放
|
||||
const handleCanPlay = () => {
|
||||
setAudioLoading(false);
|
||||
setAudioCanPlay(true);
|
||||
setAudioError(null);
|
||||
};
|
||||
|
||||
// 音频可以完整播放(不需要再缓冲)
|
||||
const handleCanPlayThrough = () => {
|
||||
setAudioLoading(false);
|
||||
setAudioBuffering(false);
|
||||
};
|
||||
|
||||
// 音频开始加载
|
||||
const handleLoadStart = () => {
|
||||
setAudioLoading(true);
|
||||
setAudioCanPlay(false);
|
||||
setAudioError(null);
|
||||
};
|
||||
|
||||
// 音频因缓冲而等待
|
||||
const handleWaiting = () => {
|
||||
setAudioBuffering(true);
|
||||
};
|
||||
|
||||
// 音频开始播放(缓冲结束)
|
||||
const handlePlaying = () => {
|
||||
setAudioBuffering(false);
|
||||
};
|
||||
|
||||
// 音频加载错误
|
||||
const handleAudioError = (e) => {
|
||||
setAudioLoading(false);
|
||||
setAudioCanPlay(false);
|
||||
const error = e.target?.error;
|
||||
let errorMessage = '音频加载失败';
|
||||
if (error) {
|
||||
switch (error.code) {
|
||||
case 1:
|
||||
errorMessage = '音频加载被中止';
|
||||
break;
|
||||
case 2:
|
||||
errorMessage = '网络错误,无法加载音频';
|
||||
break;
|
||||
case 3:
|
||||
errorMessage = '音频解码失败';
|
||||
break;
|
||||
case 4:
|
||||
errorMessage = '不支持的音频格式';
|
||||
break;
|
||||
default:
|
||||
errorMessage = '音频加载失败';
|
||||
}
|
||||
}
|
||||
setAudioError(errorMessage);
|
||||
showToast(errorMessage, 'error');
|
||||
};
|
||||
|
||||
// 音频播放结束
|
||||
const handleEnded = () => {
|
||||
setIsPlaying(false);
|
||||
};
|
||||
|
||||
const handleSeek = (e, progressElement) => {
|
||||
if (!audioRef.current || !duration) return;
|
||||
|
||||
const rect = e.currentTarget.getBoundingClientRect();
|
||||
|
||||
// 使用传入的元素或 currentTarget
|
||||
const element = progressElement || e.currentTarget;
|
||||
const rect = element.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 progressElement = e.currentTarget; // 保存进度条元素引用
|
||||
|
||||
const handleMouseMove = (moveEvent) => {
|
||||
handleSeek(moveEvent);
|
||||
handleSeek(moveEvent, progressElement);
|
||||
};
|
||||
|
||||
|
||||
const handleMouseUp = () => {
|
||||
document.removeEventListener('mousemove', handleMouseMove);
|
||||
document.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
|
||||
|
||||
document.addEventListener('mousemove', handleMouseMove);
|
||||
document.addEventListener('mouseup', handleMouseUp);
|
||||
|
||||
handleSeek(e);
|
||||
|
||||
handleSeek(e, progressElement);
|
||||
};
|
||||
|
||||
const handleVolumeChange = (e) => {
|
||||
|
|
@ -904,12 +1037,46 @@ const MeetingDetails = ({ user }) => {
|
|||
return (
|
||||
<div className="meeting-details-page">
|
||||
<div className="details-header">
|
||||
<Link to="/dashboard">
|
||||
<span className="back-link">
|
||||
<ArrowLeft size={20} />
|
||||
<span>返回首页</span>
|
||||
</span>
|
||||
</Link>
|
||||
<div className="header-left">
|
||||
{/* 融合导航栏:返回首页 + 上一条/下一条 */}
|
||||
<div className="unified-navigation">
|
||||
<Link to="/dashboard" className="nav-btn back-home-btn">
|
||||
<ArrowLeft size={18} />
|
||||
<span>返回首页</span>
|
||||
</Link>
|
||||
|
||||
{navigationInfo.total_count !== null && navigationInfo.total_count > 0 && (
|
||||
<>
|
||||
<Link
|
||||
to={navigationInfo.prev_meeting_id ? `/meetings/${navigationInfo.prev_meeting_id}` : '#'}
|
||||
state={{ filterContext }}
|
||||
className={`nav-btn prev-btn ${!navigationInfo.prev_meeting_id ? 'disabled' : ''}`}
|
||||
onClick={(e) => !navigationInfo.prev_meeting_id && e.preventDefault()}
|
||||
>
|
||||
<ChevronLeft size={18} />
|
||||
<span>上一条</span>
|
||||
</Link>
|
||||
|
||||
{navigationInfo.current_index !== null && (
|
||||
<span className="nav-position">
|
||||
{navigationInfo.current_index + 1} / {navigationInfo.total_count}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<Link
|
||||
to={navigationInfo.next_meeting_id ? `/meetings/${navigationInfo.next_meeting_id}` : '#'}
|
||||
state={{ filterContext }}
|
||||
className={`nav-btn next-btn ${!navigationInfo.next_meeting_id ? 'disabled' : ''}`}
|
||||
onClick={(e) => !navigationInfo.next_meeting_id && e.preventDefault()}
|
||||
>
|
||||
<span>下一条</span>
|
||||
<ChevronRight size={18} />
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isCreator && (
|
||||
<div className="meeting-actions">
|
||||
<Link to={`/meetings/edit/${meeting_id}`} className="action-btn icon-only edit-btn" title="编辑会议">
|
||||
|
|
@ -1026,13 +1193,29 @@ const MeetingDetails = ({ user }) => {
|
|||
{audioFileName && (
|
||||
<div className="audio-file-info">
|
||||
<span className="audio-file-name">{audioFileName}</span>
|
||||
{audioLoading && (
|
||||
<span className="audio-loading-hint">
|
||||
<Loader size={14} className="spin" />
|
||||
加载中...
|
||||
</span>
|
||||
)}
|
||||
{audioError && (
|
||||
<span className="audio-error-hint">{audioError}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<audio
|
||||
ref={audioRef}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
onLoadedMetadata={handleLoadedMetadata}
|
||||
onEnded={() => setIsPlaying(false)}
|
||||
onCanPlay={handleCanPlay}
|
||||
onCanPlayThrough={handleCanPlayThrough}
|
||||
onLoadStart={handleLoadStart}
|
||||
onWaiting={handleWaiting}
|
||||
onPlaying={handlePlaying}
|
||||
onError={handleAudioError}
|
||||
onEnded={handleEnded}
|
||||
preload="auto"
|
||||
>
|
||||
<source src={audioUrl} type="audio/mpeg" />
|
||||
您的浏览器不支持音频播放。
|
||||
|
|
@ -1086,8 +1269,21 @@ const MeetingDetails = ({ user }) => {
|
|||
)}
|
||||
|
||||
<div className="player-controls">
|
||||
<button className="play-button" onClick={handlePlayPause}>
|
||||
{isPlaying ? <Pause size={24} /> : <Play size={24} />}
|
||||
<button
|
||||
className={`play-button ${audioLoading ? 'loading' : ''} ${audioBuffering ? 'buffering' : ''} ${!audioCanPlay ? 'disabled' : ''}`}
|
||||
onClick={handlePlayPause}
|
||||
disabled={audioLoading && !isPlaying}
|
||||
title={audioLoading ? '音频加载中...' : (audioBuffering ? '缓冲中...' : (isPlaying ? '暂停' : '播放'))}
|
||||
>
|
||||
{audioLoading && !isPlaying ? (
|
||||
<Loader size={24} className="spin" />
|
||||
) : audioBuffering ? (
|
||||
<Loader size={24} className="spin" />
|
||||
) : isPlaying ? (
|
||||
<Pause size={24} />
|
||||
) : (
|
||||
<Play size={24} />
|
||||
)}
|
||||
</button>
|
||||
|
||||
<div className="time-info">
|
||||
|
|
|
|||
Loading…
Reference in New Issue