From 9480d0e4bf4ddf8b3af93a7ccc8c52c66b703792 Mon Sep 17 00:00:00 2001 From: alanpaine Date: Fri, 17 Apr 2026 11:00:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BC=9A=E8=AE=AE=E8=AF=A6=E6=83=85):=20?= =?UTF-8?q?=E6=94=B9=E8=BF=9B=E9=9F=B3=E9=A2=91=E6=92=AD=E6=94=BE=E5=92=8C?= =?UTF-8?q?=E8=BD=AC=E5=86=99=E4=BA=A4=E4=BA=92=E4=BD=93=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将会议预览页面的基本信息卡片合并到会议概览卡片,优化布局 - 为转写列表添加当前播放高亮和自动滚动功能 - 实现浮动音频播放器,随滚动保持可见 - 调整转写列表的响应式网格布局 --- frontend/src/pages/business/MeetingDetail.tsx | 242 +++++++++++++----- .../src/pages/business/MeetingPreview.css | 8 +- .../src/pages/business/MeetingPreview.tsx | 105 +++----- 3 files changed, 221 insertions(+), 134 deletions(-) diff --git a/frontend/src/pages/business/MeetingDetail.tsx b/frontend/src/pages/business/MeetingDetail.tsx index a22ddfc..715342a 100644 --- a/frontend/src/pages/business/MeetingDetail.tsx +++ b/frontend/src/pages/business/MeetingDetail.tsx @@ -452,6 +452,8 @@ type ActiveTranscriptRowProps = { onDraftBlur: (item: MeetingTranscriptVO, value: string) => void; onDraftKeyDown: (item: MeetingTranscriptVO, value: string, event: React.KeyboardEvent) => void; onSpeakerUpdated: () => void; + isActive: boolean; + audioPlaying: boolean; }; const ActiveTranscriptRow = React.memo(({ @@ -466,19 +468,29 @@ const ActiveTranscriptRow = React.memo(({ onDraftBlur, onDraftKeyDown, onSpeakerUpdated, + isActive, + audioPlaying, }) => { - const [draftValue, setDraftValue] = useState(item.content || ''); - const speakerTagLabel = item.speakerLabel ? (speakerLabelMap.get(item.speakerLabel) || item.speakerLabel) : ''; + const [draftValue, setDraftValue] = useState(item.content); + const rowRef = useRef(null); useEffect(() => { if (isEditing) { - setDraftValue(item.content || ''); + setDraftValue(item.content); } }, [isEditing, item.content]); + useEffect(() => { + if (isActive && audioPlaying && rowRef.current) { + rowRef.current.scrollIntoView({ behavior: 'smooth', block: 'center' }); + } + }, [isActive, audioPlaying]); + + const speakerTagLabel = item.speakerLabel ? (speakerLabelMap.get(item.speakerLabel) || item.speakerLabel) : ''; + return ( - onSeek(item.startTime)}> -
+ onSeek(item.startTime)}> +
} className="transcript-avatar" />
@@ -550,6 +562,8 @@ const ActiveTranscriptRow = React.memo(({ && prevProps.onDraftBlur === nextProps.onDraftBlur && prevProps.onDraftKeyDown === nextProps.onDraftKeyDown && prevProps.onSpeakerUpdated === nextProps.onSpeakerUpdated + && prevProps.isActive === nextProps.isActive + && prevProps.audioPlaying === nextProps.audioPlaying )); const MeetingDetail: React.FC = () => { @@ -591,6 +605,11 @@ const MeetingDetail: React.FC = () => { const audioRef = useRef(null); const summaryPdfRef = useRef(null); + const transcriptItemRefs = useRef>({}); + const leftColumnRef = useRef(null); + const transcriptSectionRef = useRef(null); + const [showFloatingTranscriptPlayer, setShowFloatingTranscriptPlayer] = useState(false); + const [floatingTranscriptPlayerLayout, setFloatingTranscriptPlayerLayout] = useState<{ left: number; width: number } | null>(null); const analysis = useMemo( () => buildMeetingAnalysis(meeting?.analysis, meeting?.summaryContent, meeting?.tags || ''), @@ -632,6 +651,60 @@ const MeetingDetail: React.FC = () => { const canRetrySummary = isOwner && transcripts.length > 0 && meeting?.status !== 1 && meeting?.status !== 2; const canRetryTranscription = isOwner && meeting?.status === 4 && transcripts.length === 0 && !!meeting?.audioUrl; + useEffect(() => { + if (!meeting?.audioUrl) { + setShowFloatingTranscriptPlayer(false); + setFloatingTranscriptPlayerLayout(null); + return undefined; + } + + const updateFloatingPlayerState = () => { + const target = transcriptSectionRef.current; + if (!target) { + setShowFloatingTranscriptPlayer(false); + setFloatingTranscriptPlayerLayout(null); + return; + } + + const rect = target.getBoundingClientRect(); + const rootRect = leftColumnRef.current?.getBoundingClientRect(); + + setFloatingTranscriptPlayerLayout({ + left: rect.left, + width: rect.width, + }); + + if (rootRect) { + const isVisible = rect.bottom > rootRect.top + 80 && rect.top < rootRect.bottom - 40; + setShowFloatingTranscriptPlayer(isVisible); + return; + } + + const isVisible = rect.bottom > 120 && rect.top < window.innerHeight - 80; + setShowFloatingTranscriptPlayer(isVisible); + }; + + updateFloatingPlayerState(); + + const target = transcriptSectionRef.current; + const root = leftColumnRef.current; + const resizeObserver = target ? new ResizeObserver(() => updateFloatingPlayerState()) : null; + if (target && resizeObserver) { + resizeObserver.observe(target); + } + + window.addEventListener('resize', updateFloatingPlayerState); + window.addEventListener('scroll', updateFloatingPlayerState, { passive: true }); + root?.addEventListener('scroll', updateFloatingPlayerState, { passive: true }); + + return () => { + window.removeEventListener('resize', updateFloatingPlayerState); + window.removeEventListener('scroll', updateFloatingPlayerState); + root?.removeEventListener('scroll', updateFloatingPlayerState); + resizeObserver?.disconnect(); + }; + }, [meeting?.audioUrl]); + useEffect(() => { if (!id) return; fetchData(Number(id)); @@ -668,7 +741,9 @@ const MeetingDetail: React.FC = () => { setAudioCurrentTime(audio.currentTime || 0); audio.playbackRate = audioPlaybackRate; }; - const handleTimeUpdate = () => setAudioCurrentTime(audio.currentTime || 0); + const handleTimeUpdate = () => { + setAudioCurrentTime(audio.currentTime || 0); + }; const handlePlay = () => setAudioPlaying(true); const handlePause = () => setAudioPlaying(false); const handleEnded = () => setAudioPlaying(false); @@ -1284,7 +1359,7 @@ const MeetingDetail: React.FC = () => { ) : ( -
+
@@ -1465,63 +1540,47 @@ const MeetingDetail: React.FC = () => {
- 原文}> - {meeting.audioUrl && +
@@ -1591,6 +1650,36 @@ const MeetingDetail: React.FC = () => { )}
+ {meeting?.audioUrl && showFloatingTranscriptPlayer && floatingTranscriptPlayerLayout && ( +
+ +
+
+ {formatPlayerTime(audioCurrentTime)} + {formatPlayerTime(audioDuration)} +
+ +
+ +
+ )} +