From 4499e6265b5e96277a8363693e2db28d57c839e6 Mon Sep 17 00:00:00 2001 From: puz <13060209078@163.com> Date: Mon, 22 Jun 2026 17:07:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BC=9A=E8=AE=AE=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E7=9A=84=E5=88=97=E8=A1=A8=E5=88=86=E9=A1=B5=E5=92=8C?= =?UTF-8?q?=E5=A3=B0=E7=BA=B9=E6=B3=A8=E5=86=8C=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/pages/business/Meetings.tsx | 78 +++++++++++------ frontend/src/pages/business/SpeakerReg.tsx | 97 ++++++++++++++++++---- 2 files changed, 132 insertions(+), 43 deletions(-) diff --git a/frontend/src/pages/business/Meetings.tsx b/frontend/src/pages/business/Meetings.tsx index e8d923e..2e4ad4e 100644 --- a/frontend/src/pages/business/Meetings.tsx +++ b/frontend/src/pages/business/Meetings.tsx @@ -499,6 +499,11 @@ const Meetings: React.FC = () => { const activeFilterCount = (statusFilter !== ALL_STATUS_FILTER ? 1 : 0) + (searchTitle ? 1 : 0); + const handlePaginationChange = (page: number, pageSize: number) => { + setCurrent(pageSize !== size ? 1 : page); + setSize(pageSize); + }; + const handleDisplayModeChange = (mode: "card" | "list") => { setDisplayMode(mode); setSize(mode === "card" ? 8 : 10); @@ -557,6 +562,13 @@ const Meetings: React.FC = () => { void fetchData(); }, [current, size, searchTitle, viewType, statusFilter]); + useEffect(() => { + const maxPage = Math.max(1, Math.ceil(total / size)); + if (current > maxPage) { + setCurrent(maxPage); + } + }, [current, size, total]); + useEffect(() => { const trackedMeetings = data.filter(shouldTrackGenerationProgress); if (trackedMeetings.length === 0) { @@ -930,39 +942,42 @@ const Meetings: React.FC = () => { style={{ flex: 1, minHeight: 0, overflow: "hidden", display: "flex", flexDirection: "column", border: 'none', background: 'transparent' }} styles={{ body: { padding: 0, flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" } }} > -
+
{displayMode === "card" ? ( - - { - const progress = progressMap[item.id] || null; - const visualStatus = getEffectiveStatus(item, progress); - const config = statusConfig[visualStatus] || statusConfig[0]; - return ( - { void handleRetrySchedule(meeting); }} - onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }} - retrying={!!retryingMeetingIds[item.id]} - /> - ); - }} - locale={{ emptyText: }} - /> - +
+ + { + const progress = progressMap[item.id] || null; + const visualStatus = getEffectiveStatus(item, progress); + const config = statusConfig[visualStatus] || statusConfig[0]; + return ( + { void handleRetrySchedule(meeting); }} + onDelete={(id) => { deleteMeeting(id).then(() => { message.success('删除成功'); fetchData(); }) }} + retrying={!!retryingMeetingIds[item.id]} + /> + ); + }} + locale={{ emptyText: }} + /> + +
) : ( ({ onClick: () => handleOpenMeeting(record), style: { cursor: "pointer" } })} locale={{ emptyText: }} /> @@ -970,7 +985,7 @@ const Meetings: React.FC = () => {
- { setCurrent(p); setSize(s); }} /> +
@@ -990,6 +1005,17 @@ const Meetings: React.FC = () => { .meeting-card-v2:hover .view-detail-text { max-width: 60px; opacity: 1; margin-left: 6px; } .status-bar-active { animation: statusBreathing 2s infinite ease-in-out; } @keyframes statusBreathing { 0% { opacity: 1; } 50% { opacity: 0.4; } 100% { opacity: 1; } } + .meetings-list-table.ant-table-wrapper, + .meetings-list-table.ant-table-wrapper .ant-spin-nested-loading, + .meetings-list-table.ant-table-wrapper .ant-spin-container, + .meetings-list-table.ant-table-wrapper .ant-table, + .meetings-list-table.ant-table-wrapper .ant-table-container { + height: 100%; + min-height: 0; + } + .meetings-list-table.ant-table-wrapper .ant-table-body { + max-height: calc(100vh - 430px) !important; + } /* Premium Button Styles */ .ant-btn-primary:not(.ant-btn-dangerous) { diff --git a/frontend/src/pages/business/SpeakerReg.tsx b/frontend/src/pages/business/SpeakerReg.tsx index 7da6f31..61138a8 100644 --- a/frontend/src/pages/business/SpeakerReg.tsx +++ b/frontend/src/pages/business/SpeakerReg.tsx @@ -30,6 +30,31 @@ const REG_CONTENT = 'iMeeting 智能会议系统,助力高效办公,让每一场讨论都有据可查。我正在进行声纹注册,以确保会议识别的准确性。'; const DEFAULT_DURATION = 15; const DEFAULT_PAGE_SIZE = 8; +const AUDIO_EXT_PATTERN = /\.(mp3|wav|m4a|aac|ogg|flac|webm)$/i; + +const SPEAKER_STATUS_META: Record = { + 1: { label: '已保存', color: 'default' }, + 2: { label: '注册中', color: 'processing' }, + 3: { label: '已注册', color: 'success' }, + 4: { label: '本地已保存,声纹同步失败', color: 'error' } +}; + +const getSpeakerStatusMeta = (status?: number) => { + return SPEAKER_STATUS_META[Number(status)] || SPEAKER_STATUS_META[1]; +}; + +const isAudioFile = (file: File) => { + return file.type.startsWith('audio/') || AUDIO_EXT_PATTERN.test(file.name); +}; + +const buildResourceUrl = (prefix: string, resourcePath?: string) => { + if (!resourcePath) { + return ''; + } + const normalizedPrefix = prefix.endsWith('/') ? prefix : `${prefix}/`; + const normalizedPath = resourcePath.startsWith('/') ? resourcePath.slice(1) : resourcePath; + return `${normalizedPrefix}${normalizedPath}`; +}; const SpeakerReg: React.FC = () => { const { message } = App.useApp(); @@ -48,18 +73,23 @@ const SpeakerReg: React.FC = () => { const [userOptions, setUserOptions] = useState([]); const [editingSpeaker, setEditingSpeaker] = useState(null); const [seconds, setSeconds] = useState(0); - const timerRef = useRef(null); - const autoStopTimerRef = useRef(null); + const timerRef = useRef | null>(null); + const autoStopTimerRef = useRef | null>(null); const mediaRecorderRef = useRef(null); const audioChunksRef = useRef([]); + const mountedRef = useRef(true); const { profile } = useAuth(); const isAdmin = !!(profile?.isAdmin || profile?.isPlatformAdmin); const resourcePrefix = useMemo(() => { - const configStr = sessionStorage.getItem('platformConfig'); - if (configStr) { - const config = JSON.parse(configStr); - return config.resourcePrefix || '/api/static/'; + try { + const configStr = sessionStorage.getItem('platformConfig'); + if (configStr) { + const config = JSON.parse(configStr); + return config.resourcePrefix || '/api/static/'; + } + } catch (err) { + console.warn('Parse platformConfig failed', err); } return '/api/static/'; }, []); @@ -67,7 +97,12 @@ const SpeakerReg: React.FC = () => { useEffect(() => { void fetchUsers(); return () => { + mountedRef.current = false; stopTimer(); + if (mediaRecorderRef.current?.state === 'recording') { + mediaRecorderRef.current.stop(); + } + mediaRecorderRef.current?.stream.getTracks().forEach(track => track.stop()); }; }, []); @@ -90,7 +125,7 @@ const SpeakerReg: React.FC = () => { } form.setFieldValue('userId', profile.userId); form.setFieldValue('name', profile.displayName); - }, [form, isAdmin, profile?.userId]); + }, [form, isAdmin, profile?.displayName, profile?.userId]); const fetchSpeakers = async (page = current, size = pageSize, name = queryName) => { setListLoading(true); @@ -132,10 +167,8 @@ const SpeakerReg: React.FC = () => { const resetAudioState = () => { setAudioBlob(null); - if (audioUrl) { - URL.revokeObjectURL(audioUrl); - } setAudioUrl(null); + setSeconds(0); }; const resetFormState = () => { @@ -143,6 +176,7 @@ const SpeakerReg: React.FC = () => { form.resetFields(['id', 'name', 'userId', 'remark']); if (!isAdmin && profile?.userId) { form.setFieldValue('userId', profile.userId); + form.setFieldValue('name', profile.displayName); } resetAudioState(); }; @@ -171,6 +205,14 @@ const SpeakerReg: React.FC = () => { }; const startRecording = async () => { + if (loading) { + message.warning('声纹正在提交,请稍后再录制'); + return; + } + if (!navigator.mediaDevices?.getUserMedia || typeof MediaRecorder === 'undefined') { + message.error('当前浏览器不支持录音,请使用音频文件上传'); + return; + } try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const mediaRecorder = new MediaRecorder(stream); @@ -184,6 +226,9 @@ const SpeakerReg: React.FC = () => { }; mediaRecorder.onstop = () => { + if (!mountedRef.current) { + return; + } const blob = new Blob(audioChunksRef.current, { type: 'audio/wav' }); resetAudioState(); setAudioBlob(blob); @@ -212,7 +257,15 @@ const SpeakerReg: React.FC = () => { const uploadProps: UploadProps = { beforeUpload: file => { - const isAudio = file.type.startsWith('audio/'); + if (recording) { + message.warning('请先停止录音,再上传音频文件'); + return Upload.LIST_IGNORE; + } + if (loading) { + message.warning('声纹正在提交,请稍后再上传'); + return Upload.LIST_IGNORE; + } + const isAudio = isAudioFile(file); if (!isAudio) { message.error('只能上传音频文件'); return Upload.LIST_IGNORE; @@ -222,6 +275,7 @@ const SpeakerReg: React.FC = () => { setAudioUrl(URL.createObjectURL(file)); return false; }, + disabled: recording || loading, showUploadList: false }; @@ -262,6 +316,9 @@ const SpeakerReg: React.FC = () => { try { await deleteSpeaker(speaker.id); message.success('声纹已删除'); + if (editingSpeaker?.id === speaker.id) { + resetFormState(); + } if (speakers.length === 1 && current > 1) { setCurrent(current - 1); } else { @@ -284,7 +341,7 @@ const SpeakerReg: React.FC = () => { resetAudioState(); }; - const handleUserChange = (userId: number) => { + const handleUserChange = (userId?: number) => { const selectedUser = userOptions.find(u => u.userId === userId); if (selectedUser) { form.setFieldValue('name', selectedUser.displayName || selectedUser.username); @@ -569,6 +626,9 @@ const SpeakerReg: React.FC = () => { @@ -678,14 +738,16 @@ const SpeakerReg: React.FC = () => { style={{ marginTop: 40 }} /> ) : ( - speakers.map((s) => ( + speakers.map((s) => { + const statusMeta = getSpeakerStatusMeta(s.status); + return (
{s.name}
- - {s.status === 3 ? '已就绪' : '处理中'} + + {statusMeta.label} {s.userId && ID:{s.userId}}
@@ -705,7 +767,7 @@ const SpeakerReg: React.FC = () => { )}
- )) + ); + }) )}