diff --git a/frontend/src/components/AudioPlayerBar.css b/frontend/src/components/AudioPlayerBar.css new file mode 100644 index 0000000..89d4218 --- /dev/null +++ b/frontend/src/components/AudioPlayerBar.css @@ -0,0 +1,179 @@ +.audio-player-bar { + display: flex; + align-items: center; + gap: 10px; + min-height: 42px; + padding: 4px 12px; + border-radius: 999px; + border: 1px solid #e3ebf6; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #f7faff 100%); + box-shadow: 0 8px 18px rgba(40, 72, 120, 0.06); +} + +.audio-player-bar.is-empty { + justify-content: center; +} + +.audio-player-bar-audio { + display: none; +} + +.audio-player-bar-play { + width: 24px; + min-width: 24px; + height: 24px; + padding: 0; + border: 0; + border-radius: 999px; + background: transparent; + color: #5f7392; + box-shadow: none; +} + +.audio-player-bar-play.ant-btn:hover, +.audio-player-bar-play.ant-btn:focus { + background: rgba(233, 241, 251, 0.7) !important; + color: #355171 !important; +} + +.audio-player-bar-play.ant-btn:disabled { + background: transparent !important; + color: #a8b7ca !important; +} + +.audio-player-bar-time { + flex: 0 0 auto; + min-width: 102px; + font-size: 12px; + font-weight: 700; + color: #5f7392; + letter-spacing: 0.01em; + white-space: nowrap; +} + +.audio-player-bar-progress { + position: relative; + flex: 1; + height: 5px; + border: 0; + border-radius: 999px; + background: #edf2fa; + cursor: pointer; + appearance: none; + outline: none; +} + +.audio-player-bar-progress::-webkit-slider-runnable-track { + height: 5px; + border-radius: 999px; + background: linear-gradient( + 90deg, + #bfd1e8 0%, + #bfd1e8 var(--progress, 0%), + #edf2fa var(--progress, 0%), + #edf2fa 100% + ); +} + +.audio-player-bar-progress::-webkit-slider-thumb { + appearance: none; + width: 10px; + height: 10px; + margin-top: -2.5px; + border: 2px solid #ffffff; + border-radius: 50%; + background: #b9cde7; + box-shadow: 0 2px 5px rgba(79, 111, 157, 0.12); +} + +.audio-player-bar-progress::-moz-range-track { + height: 5px; + border-radius: 999px; + background: #edf2fa; +} + +.audio-player-bar-progress::-moz-range-progress { + height: 5px; + border-radius: 999px; + background: #bfd1e8; +} + +.audio-player-bar-progress::-moz-range-thumb { + width: 10px; + height: 10px; + border: 2px solid #ffffff; + border-radius: 50%; + background: #b9cde7; + box-shadow: 0 2px 5px rgba(79, 111, 157, 0.12); +} + +.audio-player-bar-volume { + display: inline-flex; + align-items: center; + justify-content: center; + color: #5f7392; + font-size: 14px; +} + +.audio-player-bar-divider { + width: 1px; + height: 16px; + background: #e4ebf5; +} + +.audio-player-bar-control.ant-btn { + height: 24px; + min-height: 24px; + padding: 0 6px; + border: 0; + border-radius: 999px; + background: transparent; + color: #5f7392; + box-shadow: none; + font-size: 11px; +} + +.audio-player-bar-control.ant-btn:hover, +.audio-player-bar-control.ant-btn:focus { + background: rgba(240, 245, 252, 0.75) !important; + color: #355171 !important; +} + +.audio-player-bar-control.ant-btn:disabled { + background: #f8fafc !important; + color: #a8b7ca !important; +} + +.audio-player-bar-rate.ant-btn { + min-width: 46px; + font-weight: 700; +} + +.audio-player-bar-more.ant-btn { + width: 24px; + min-width: 24px; + padding: 0; + border-radius: 999px; +} + +.audio-player-bar-empty-text { + color: #5f7392; + font-size: 12px; +} + +@media (max-width: 768px) { + .audio-player-bar { + gap: 8px; + padding: 4px 9px; + } + + .audio-player-bar-time { + min-width: 78px; + font-size: 12px; + } + + .audio-player-bar-volume, + .audio-player-bar-divider { + display: none; + } +} diff --git a/frontend/src/components/AudioPlayerBar.jsx b/frontend/src/components/AudioPlayerBar.jsx new file mode 100644 index 0000000..8938f30 --- /dev/null +++ b/frontend/src/components/AudioPlayerBar.jsx @@ -0,0 +1,185 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { Button, Dropdown } from 'antd'; +import { + CaretRightFilled, + DownOutlined, + MoreOutlined, + PauseOutlined, + SoundOutlined, +} from '@ant-design/icons'; +import tools from '../utils/tools'; +import './AudioPlayerBar.css'; + +const DEFAULT_RATE_OPTIONS = [0.75, 1, 1.25, 1.5, 2]; + +const AudioPlayerBar = ({ + audioRef, + src, + playbackRate = 1, + onPlaybackRateChange, + onTimeUpdate, + onLoadedMetadata, + moreMenuItems = [], + emptyText = '暂无音频', + showMoreButton = true, + rateOptions = DEFAULT_RATE_OPTIONS, +}) => { + const [isPlaying, setIsPlaying] = useState(false); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + + useEffect(() => { + const audio = audioRef?.current; + if (!audio) { + return undefined; + } + + const syncState = () => { + setCurrentTime(audio.currentTime || 0); + setDuration(audio.duration || 0); + setIsPlaying(!audio.paused && !audio.ended); + }; + + const handleMeta = (event) => { + syncState(); + onLoadedMetadata?.(event); + }; + + const handleTime = (event) => { + syncState(); + onTimeUpdate?.(event); + }; + + const handlePlay = () => setIsPlaying(true); + const handlePause = () => setIsPlaying(false); + const handleEnded = () => { + setIsPlaying(false); + setCurrentTime(audio.duration || 0); + }; + + syncState(); + audio.addEventListener('loadedmetadata', handleMeta); + audio.addEventListener('durationchange', syncState); + audio.addEventListener('timeupdate', handleTime); + audio.addEventListener('play', handlePlay); + audio.addEventListener('pause', handlePause); + audio.addEventListener('ended', handleEnded); + + return () => { + audio.removeEventListener('loadedmetadata', handleMeta); + audio.removeEventListener('durationchange', syncState); + audio.removeEventListener('timeupdate', handleTime); + audio.removeEventListener('play', handlePlay); + audio.removeEventListener('pause', handlePause); + audio.removeEventListener('ended', handleEnded); + }; + }, [audioRef, onLoadedMetadata, onTimeUpdate, src]); + + useEffect(() => { + if (audioRef?.current) { + audioRef.current.playbackRate = playbackRate; + } + }, [audioRef, playbackRate]); + + const rateMenuItems = useMemo( + () => rateOptions.map((rate) => ({ + key: String(rate), + label: `${rate.toFixed(rate % 1 === 0 ? 1 : 2)}x`, + onClick: () => onPlaybackRateChange?.(rate), + })), + [onPlaybackRateChange, rateOptions], + ); + + const progress = duration > 0 ? Math.min((currentTime / duration) * 100, 100) : 0; + + const togglePlay = async () => { + const audio = audioRef?.current; + if (!audio || !src) { + return; + } + + if (audio.paused || audio.ended) { + try { + await audio.play(); + } catch { + setIsPlaying(false); + } + } else { + audio.pause(); + } + }; + + const handleSeek = (event) => { + const audio = audioRef?.current; + if (!audio || !src) { + return; + } + + const nextTime = Number(event.target.value); + audio.currentTime = nextTime; + setCurrentTime(nextTime); + }; + + if (!src) { + return ( +
+ {emptyText} +
+ ); + } + + return ( +
+
+ ); +}; + +export default AudioPlayerBar; diff --git a/frontend/src/components/MindMap.jsx b/frontend/src/components/MindMap.jsx index 14770b6..3d8dc25 100644 --- a/frontend/src/components/MindMap.jsx +++ b/frontend/src/components/MindMap.jsx @@ -6,9 +6,16 @@ import { FullscreenOutlined, ZoomInOutlined, ZoomOutOutlined, SyncOutlined } fro const transformer = new Transformer(); +const hasRenderableSize = (element) => { + if (!element) return false; + const rect = element.getBoundingClientRect(); + return Number.isFinite(rect.width) && Number.isFinite(rect.height) && rect.width > 0 && rect.height > 0; +}; + const MindMap = ({ content, title }) => { const svgRef = useRef(null); const markmapRef = useRef(null); + const latestRootRef = useRef(null); const [loading, setLoading] = useState(true); useEffect(() => { @@ -17,16 +24,22 @@ const MindMap = ({ content, title }) => { setLoading(true); try { const { root } = transformer.transform(content); - + latestRootRef.current = root; + if (markmapRef.current) { markmapRef.current.setData(root); - markmapRef.current.fit(); } else { markmapRef.current = Markmap.create(svgRef.current, { - autoFit: true, + autoFit: false, duration: 500, }, root); } + + requestAnimationFrame(() => { + if (svgRef.current && hasRenderableSize(svgRef.current)) { + markmapRef.current?.fit(); + } + }); } catch (error) { console.error('Markmap error:', error); } finally { @@ -34,6 +47,33 @@ const MindMap = ({ content, title }) => { } }, [content]); + useEffect(() => { + const svgElement = svgRef.current; + if (!svgElement || typeof ResizeObserver === 'undefined') { + return undefined; + } + + const observer = new ResizeObserver(() => { + if (!hasRenderableSize(svgElement)) { + return; + } + + if (!markmapRef.current && latestRootRef.current) { + markmapRef.current = Markmap.create(svgElement, { + autoFit: false, + duration: 500, + }, latestRootRef.current); + } + + requestAnimationFrame(() => { + markmapRef.current?.fit(); + }); + }); + + observer.observe(svgElement); + return () => observer.disconnect(); + }, []); + const handleFit = () => markmapRef.current?.fit(); const handleZoomIn = () => markmapRef.current?.rescale(1.2); const handleZoomOut = () => markmapRef.current?.rescale(0.8); diff --git a/frontend/src/components/TranscriptTimeline.css b/frontend/src/components/TranscriptTimeline.css new file mode 100644 index 0000000..6186bcd --- /dev/null +++ b/frontend/src/components/TranscriptTimeline.css @@ -0,0 +1,118 @@ +.transcript-scroll-panel { + overflow-y: auto; + padding: 12px 12px 12px 4px; +} + +.transcript-scroll-panel-fill { + flex: 1; + min-height: 0; + height: 100%; +} + +.transcript-loading { + display: flex; + justify-content: center; + padding-top: 120px; +} + +.transcript-entry { + margin-left: -4px; + padding: 10px 12px; + border: 1px solid transparent; + border-radius: 14px; + background: rgba(255, 255, 255, 0.82); + cursor: pointer; + transition: background 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} + +.transcript-entry:hover { + background: #f8fbff; + border-color: #d9e8fb; +} + +.transcript-entry.is-active { + background: linear-gradient(180deg, #eef6ff 0%, #e2f0ff 100%); + border-color: #bfd8fb; + box-shadow: 0 10px 22px rgba(29, 78, 216, 0.12); +} + +.transcript-entry-dot { + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--speaker-color, #1677ff); + box-shadow: none; + transition: box-shadow 0.2s ease; +} + +.transcript-entry-dot.is-active { + box-shadow: 0 0 0 3px rgba(22, 119, 255, 0.16); +} + +.transcript-entry-time.ant-typography { + font-size: 12px; + white-space: nowrap; +} + +.transcript-entry-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; +} + +.transcript-entry-speaker { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.transcript-entry-avatar.ant-avatar { + flex-shrink: 0; +} + +.transcript-entry-speaker-label.ant-typography { + margin: 0 !important; + font-size: 13px; + font-weight: 700; + color: #1677ff; +} + +.transcript-entry-speaker-label.is-editable { + cursor: pointer; +} + +.transcript-entry-speaker-edit { + font-size: 11px; + margin-left: 3px; +} + +.transcript-entry-content { + color: #334155; + font-size: 14px; + line-height: 1.72; +} + +.transcript-entry-content.ant-typography { + margin: 0 !important; +} + +.transcript-render-hint { + text-align: center; + padding: 8px 0 20px; +} + +@media (max-width: 768px) { + .transcript-scroll-panel { + padding-right: 6px; + } + + .transcript-entry { + padding: 9px 10px; + } + + .transcript-entry-content { + font-size: 13px; + } +} diff --git a/frontend/src/components/TranscriptTimeline.jsx b/frontend/src/components/TranscriptTimeline.jsx new file mode 100644 index 0000000..77a002b --- /dev/null +++ b/frontend/src/components/TranscriptTimeline.jsx @@ -0,0 +1,219 @@ +import React from 'react'; +import { + Avatar, + Button, + Empty, + Input, + Space, + Spin, + Timeline, + Typography, +} from 'antd'; +import { + CheckOutlined, + CloseOutlined, + EditOutlined, + UserOutlined, +} from '@ant-design/icons'; +import tools from '../utils/tools'; +import './TranscriptTimeline.css'; + +const { Text } = Typography; + +const TranscriptTimeline = ({ + transcript = [], + loading = false, + visibleCount, + currentHighlightIndex = -1, + onJumpToTime, + onScroll, + transcriptRefs, + getSpeakerColor, + emptyDescription = '暂无对话数据', + loadingTip = '正在加载转录内容...', + showRenderHint = false, + fillHeight = false, + maxHeight = null, + editable = false, + isMeetingOwner = false, + editing = {}, +}) => { + const { + inlineSpeakerEdit = null, + inlineSpeakerEditSegmentId = null, + inlineSpeakerValue = '', + setInlineSpeakerValue, + startInlineSpeakerEdit, + saveInlineSpeakerEdit, + cancelInlineSpeakerEdit, + inlineSegmentEditId = null, + inlineSegmentValue = '', + setInlineSegmentValue, + startInlineSegmentEdit, + saveInlineSegmentEdit, + cancelInlineSegmentEdit, + savingInlineEdit = false, + } = editing; + + const renderCount = Math.min(visibleCount ?? transcript.length, transcript.length); + + if (loading) { + return ( +
+
+ +
+
+ ); + } + + if (!transcript.length) { + return ( +
+ +
+ ); + } + + return ( +
+ { + const isActive = currentHighlightIndex === index; + const speakerColor = getSpeakerColor(item.speaker_id); + const speakerEditKey = `speaker-${item.speaker_id}-${item.segment_id}`; + + return { + label: ( + + {tools.formatDuration(item.start_time_ms / 1000)} + + ), + dot: ( + + ), + children: ( +
{ + if (transcriptRefs?.current) { + transcriptRefs.current[index] = el; + } + }} + className={`transcript-entry${isActive ? ' is-active' : ''}`} + onClick={() => onJumpToTime?.(item.start_time_ms)} + > +
+
+ } + className="transcript-entry-avatar" + style={{ backgroundColor: speakerColor }} + /> + {editable && inlineSpeakerEdit === item.speaker_id && inlineSpeakerEditSegmentId === speakerEditKey ? ( + event.stopPropagation()}> + setInlineSpeakerValue?.(event.target.value)} + onPressEnter={saveInlineSpeakerEdit} + style={{ width: 180 }} + /> +
+
+ + {editable && inlineSegmentEditId === item.segment_id ? ( +
event.stopPropagation()}> + setInlineSegmentValue?.(event.target.value)} + onPressEnter={(event) => { + if (event.ctrlKey || event.metaKey) { + saveInlineSegmentEdit?.(); + } + }} + /> + + + + +
+ ) : ( + { + event.stopPropagation(); + startInlineSegmentEdit?.(item); + } : undefined} + > + {item.text_content} + + )} +
+ ), + }; + })} + /> + {showRenderHint && renderCount < transcript.length ? ( +
+ + 已渲染 {renderCount} / {transcript.length} 条转录,继续向下滚动将自动加载更多 + +
+ ) : null} +
+ ); +}; + +export default TranscriptTimeline; diff --git a/frontend/src/pages/MeetingDetails.jsx b/frontend/src/pages/MeetingDetails.jsx index ede8d6a..5018933 100644 --- a/frontend/src/pages/MeetingDetails.jsx +++ b/frontend/src/pages/MeetingDetails.jsx @@ -3,7 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'; import { Card, Row, Col, Button, Space, Typography, Tag, Avatar, Tooltip, Progress, Spin, App, Dropdown, - Divider, List, Timeline, Tabs, Input, Upload, Empty, Drawer, Select, Switch + Divider, List, Tabs, Input, Upload, Empty, Drawer, Select, Switch } from 'antd'; import { ClockCircleOutlined, UserOutlined, TeamOutlined, @@ -13,13 +13,15 @@ import { EyeOutlined, FileTextOutlined, PartitionOutlined, SaveOutlined, CloseOutlined, StarFilled, RobotOutlined, DownloadOutlined, - DownOutlined, CheckOutlined, + CheckOutlined, MoreOutlined, AudioOutlined, CopyOutlined } from '@ant-design/icons'; import MarkdownRenderer from '../components/MarkdownRenderer'; import MarkdownEditor from '../components/MarkdownEditor'; import MindMap from '../components/MindMap'; import ActionButton from '../components/ActionButton'; +import AudioPlayerBar from '../components/AudioPlayerBar'; +import TranscriptTimeline from '../components/TranscriptTimeline'; import apiClient from '../utils/apiClient'; import tools from '../utils/tools'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; @@ -731,14 +733,6 @@ const MeetingDetails = ({ user }) => { { key: 'upload', icon: , label: isUploading ? '上传中...' : '上传音频', disabled: isUploading, onClick: openAudioUploadPicker }, ]; - const playbackRateMenuItems = [ - { key: '0.75', label: '0.75x', onClick: () => changePlaybackRate(0.75) }, - { key: '1', label: '1.0x', onClick: () => changePlaybackRate(1) }, - { key: '1.25', label: '1.25x', onClick: () => changePlaybackRate(1.25) }, - { key: '1.5', label: '1.5x', onClick: () => changePlaybackRate(1.5) }, - { key: '2', label: '2.0x', onClick: () => changePlaybackRate(2) }, - ]; - /* ══════════════════ 渲染 ══════════════════ */ if (loading) return
; @@ -842,188 +836,54 @@ const MeetingDetails = ({ user }) => { {/* 音频播放器 */}
-
-
- {audioUrl ? ( -
-
- - - - -
-
+ { + if (audioRef.current) { + audioRef.current.playbackRate = playbackRate; + } + }} + moreMenuItems={audioMoreMenuItems} + emptyText="暂无音频,可通过右侧更多操作上传音频" + />
{/* 转录时间轴 */} -
- {transcriptLoading ? ( -
- -
- ) : transcript.length > 0 ? ( - <> - { - const isActive = currentHighlightIndex === index; - return { - label: ( - - {tools.formatDuration(item.start_time_ms / 1000)} - - ), - dot: ( -
- ), - children: ( -
transcriptRefs.current[index] = el} - style={{ - padding: '6px 10px', - borderRadius: 8, - background: isActive ? '#e6f4ff' : 'transparent', - cursor: 'pointer', - transition: 'background 0.2s', - marginLeft: -4, - }} - onClick={() => jumpToTime(item.start_time_ms)} - > -
- } - style={{ backgroundColor: getSpeakerColor(item.speaker_id), flexShrink: 0 }} - /> - {inlineSpeakerEdit === item.speaker_id && inlineSpeakerEditSegmentId === `speaker-${item.speaker_id}-${item.segment_id}` ? ( - e.stopPropagation()}> - setInlineSpeakerValue(e.target.value)} - onPressEnter={saveInlineSpeakerEdit} - style={{ width: 180 }} - /> -
- {inlineSegmentEditId === item.segment_id ? ( -
e.stopPropagation()}> - setInlineSegmentValue(e.target.value)} - onPressEnter={(e) => { - if (e.ctrlKey || e.metaKey) { - saveInlineSegmentEdit(); - } - }} - /> - - - - -
- ) : ( - { - e.stopPropagation(); - startInlineSegmentEdit(item); - } : undefined} - > - {item.text_content} - - )} -
- ), - }; - })} - /> - {transcriptVisibleCount < transcript.length ? ( -
- - 已渲染 {transcriptVisibleCount} / {transcript.length} 条转录,继续向下滚动将自动加载更多 - -
- ) : null} - - ) : ( - - )} +
+
diff --git a/frontend/src/pages/MeetingPreview.css b/frontend/src/pages/MeetingPreview.css index 67a6ebe..0a8a0b1 100644 --- a/frontend/src/pages/MeetingPreview.css +++ b/frontend/src/pages/MeetingPreview.css @@ -591,9 +591,14 @@ ======================================== */ .transcript-wrapper { + display: block; padding: 20px 0; } +.preview-audio-toolbar { + padding: 0; +} + .preview-audio-player { background: linear-gradient(135deg, #667eea, #764ba2); border-radius: 12px; @@ -823,34 +828,4 @@ font-size: 15px; } - /* 预览页面播放器移动端优化 */ - .preview-audio-player { - padding: 16px; - } - - .preview-player-controls { - gap: 12px; - } - - .preview-progress-wrapper { - width: 100%; - } - - .preview-current-time { - font-size: 11px; - padding: 4px 8px; - } - - .preview-current-time::after { - border-left: 4px solid transparent; - border-right: 4px solid transparent; - border-top: 4px solid white; - bottom: -3px; - } - - .preview-slider-thumb::after { - width: 14px; - height: 14px; - border-width: 2px; - } } diff --git a/frontend/src/pages/MeetingPreview.jsx b/frontend/src/pages/MeetingPreview.jsx index f262b57..3c72e80 100644 --- a/frontend/src/pages/MeetingPreview.jsx +++ b/frontend/src/pages/MeetingPreview.jsx @@ -1,11 +1,13 @@ import React, { useEffect, useRef, useState } from 'react'; import { useParams, Link } from 'react-router-dom'; import { Layout, Space, Button, App, Empty, Input, Tabs } from 'antd'; -import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, UserOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons'; +import { LockOutlined, EyeOutlined, CopyOutlined, ShareAltOutlined, HomeOutlined, FileTextOutlined, PartitionOutlined, AudioOutlined } from '@ant-design/icons'; import apiClient from '../utils/apiClient'; import { buildApiUrl, API_ENDPOINTS } from '../config/api'; import MarkdownRenderer from '../components/MarkdownRenderer'; import MindMap from '../components/MindMap'; +import AudioPlayerBar from '../components/AudioPlayerBar'; +import TranscriptTimeline from '../components/TranscriptTimeline'; import tools from '../utils/tools'; import configService, { DEFAULT_BRANDING_CONFIG } from '../utils/configService'; import './MeetingPreview.css'; @@ -16,6 +18,7 @@ const MeetingPreview = () => { const { meeting_id } = useParams(); const { message } = App.useApp(); const audioRef = useRef(null); + const transcriptRefs = useRef([]); const [meeting, setMeeting] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -27,6 +30,7 @@ const MeetingPreview = () => { const [transcript, setTranscript] = useState([]); const [audioUrl, setAudioUrl] = useState(''); const [activeSegmentIndex, setActiveSegmentIndex] = useState(-1); + const [playbackRate, setPlaybackRate] = useState(1); useEffect(() => { configService.getBrandingConfig().then(setBranding).catch(() => {}); @@ -35,8 +39,10 @@ const MeetingPreview = () => { useEffect(() => { setMeeting(null); setTranscript([]); + transcriptRefs.current = []; setAudioUrl(''); setActiveSegmentIndex(-1); + setPlaybackRate(1); setError(null); setPassword(''); setPasswordError(''); @@ -132,6 +138,14 @@ const MeetingPreview = () => { audioRef.current.play().catch(() => {}); }; + useEffect(() => { + if (activeSegmentIndex < 0) { + return; + } + + transcriptRefs.current[activeSegmentIndex]?.scrollIntoView({ behavior: 'smooth', block: 'center' }); + }, [activeSegmentIndex]); + const handleVerify = () => { if (!password) { message.warning('请输入访问密码'); @@ -274,39 +288,28 @@ const MeetingPreview = () => { children: (
{audioUrl ? ( -
-
+ ) : null} - {transcript.length ? ( -
- {transcript.map((segment, index) => ( -
jumpToSegment(segment)} - > -
- - - {segment.speaker_tag || `发言人 ${segment.speaker_id}`} - - {tools.formatDuration((segment.start_time_ms || 0) / 1000)} -
-
{segment.text_content}
-
- ))} -
- ) : ( -
暂无转录内容
- )} + jumpToSegment({ start_time_ms: timeMs })} + transcriptRefs={transcriptRefs} + maxHeight="520px" + getSpeakerColor={(speakerId) => { + const palette = ['#1677ff', '#52c41a', '#fa8c16', '#eb2f96', '#722ed1', '#13c2c2', '#2f54eb', '#faad14']; + return palette[(speakerId ?? 0) % palette.length]; + }} + emptyDescription="暂无转录内容" + />
), } diff --git a/frontend/src/styles/console-theme.css b/frontend/src/styles/console-theme.css index 47b72dc..b31cdff 100644 --- a/frontend/src/styles/console-theme.css +++ b/frontend/src/styles/console-theme.css @@ -439,110 +439,6 @@ body { width: calc(100% - 96px) !important; } -.meeting-audio-toolbar { - display: flex; - align-items: center; - gap: 12px; - padding: 0; -} - -.meeting-audio-toolbar-player { - flex: 1; - min-width: 0; -} - -.meeting-audio-toolbar-native { - width: 100%; - height: 40px; - border-radius: 999px; - background: #f5f7fb; -} - -.meeting-audio-toolbar-native::-webkit-media-controls-panel { - background: #f5f7fb; -} - -.meeting-audio-toolbar-empty { - height: 40px; - display: flex; - align-items: center; - padding: 0 14px; - border-radius: 999px; - background: #f5f7fb; -} - -.meeting-audio-toolbar-actions { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 0; -} - -.meeting-audio-toolbar-button.ant-btn { - height: 32px; - padding: 0 10px; - border: 1px solid #dbe3ef; - border-radius: 999px; - background: #fff; - color: #526581; - font-size: 12px; - font-weight: 600; - box-shadow: none; -} - -.meeting-audio-toolbar-button.ant-btn:hover, -.meeting-audio-toolbar-button.ant-btn:focus { - color: #355171; - border-color: #c7d4e4; - background: #f8fafc !important; - box-shadow: none; -} - -.meeting-audio-toolbar-button.ant-btn:disabled { - color: #a1b1c4; - border-color: #e3e8f0; - background: #f8fafc !important; - box-shadow: none; -} - -.meeting-audio-toolbar-rate.ant-btn { - display: inline-flex; - align-items: center; - gap: 5px; - min-width: 68px; - justify-content: center; - font-weight: 700; -} - -.meeting-audio-toolbar-button .anticon { - font-size: 11px; -} - -.meeting-audio-toolbar-rate .anticon { - font-size: 9px; -} - -.meeting-audio-toolbar-more.ant-btn { - width: 32px; - min-width: 32px; - padding-inline: 0; - padding: 0; - border-color: #c9ddfb; - background: linear-gradient(180deg, #f8fbff 0%, #eef5ff 100%); - color: #1d4ed8; -} - -.meeting-audio-toolbar-more.ant-btn:hover, -.meeting-audio-toolbar-more.ant-btn:focus { - border-color: #a9c9fa; - background: linear-gradient(180deg, #f2f8ff 0%, #e7f0ff 100%) !important; - color: #1d4ed8; -} - -.meeting-audio-toolbar-more.ant-btn .anticon { - font-size: 12px; -} - .console-tab-toolbar { display: flex; justify-content: space-between;