diff --git a/src/config/api.js b/src/config/api.js
index e1be57f..9bed12c 100644
--- a/src/config/api.js
+++ b/src/config/api.js
@@ -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'
diff --git a/src/pages/Dashboard.jsx b/src/pages/Dashboard.jsx
index 0a726ed..c89233a 100644
--- a/src/pages/Dashboard.jsx
+++ b/src/pages/Dashboard.jsx
@@ -530,6 +530,9 @@ const Dashboard = ({ user, onLogout }) => {
hasMore={pagination.has_more}
onLoadMore={handleLoadMore}
loadingMore={loadingMore}
+ filterType={filterType}
+ searchQuery={searchQuery}
+ selectedTags={selectedTags}
/>
diff --git a/src/pages/MeetingDetails.css b/src/pages/MeetingDetails.css
index 8ba07dc..c913949 100644
--- a/src/pages/MeetingDetails.css
+++ b/src/pages/MeetingDetails.css
@@ -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;
diff --git a/src/pages/MeetingDetails.jsx b/src/pages/MeetingDetails.jsx
index 35dd37a..1244fc4 100644
--- a/src/pages/MeetingDetails.jsx
+++ b/src/pages/MeetingDetails.jsx
@@ -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 (
-
-
-
- 返回首页
-
-
+
+ {/* 融合导航栏:返回首页 + 上一条/下一条 */}
+
+
+
+
返回首页
+
+
+ {navigationInfo.total_count !== null && navigationInfo.total_count > 0 && (
+ <>
+
!navigationInfo.prev_meeting_id && e.preventDefault()}
+ >
+
+
上一条
+
+
+ {navigationInfo.current_index !== null && (
+
+ {navigationInfo.current_index + 1} / {navigationInfo.total_count}
+
+ )}
+
+
!navigationInfo.next_meeting_id && e.preventDefault()}
+ >
+
下一条
+
+
+ >
+ )}
+
+
+
{isCreator && (
@@ -1026,13 +1193,29 @@ const MeetingDetails = ({ user }) => {
{audioFileName && (
{audioFileName}
+ {audioLoading && (
+
+
+ 加载中...
+
+ )}
+ {audioError && (
+ {audioError}
+ )}
)}