From fe0e2e6592d0d5c049260671de1dd3e67cc544cf Mon Sep 17 00:00:00 2001 From: xuncha <1658671838@qq.com> Date: Fri, 6 Feb 2026 23:09:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=B9=E9=87=8F=E8=AF=AD=E9=9F=B3=E8=BD=AC?= =?UTF-8?q?=E6=96=87=E5=AD=97=E6=94=B9=E6=88=90=E5=8F=B3=E4=B8=8B=E8=A7=92?= =?UTF-8?q?=E5=B8=B8=E9=A9=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 4 + src/components/BatchTranscribeGlobal.tsx | 101 ++++++++++ src/pages/ChatPage.scss | 224 +-------------------- src/pages/ChatPage.tsx | 119 ++---------- src/stores/batchTranscribeStore.ts | 60 ++++++ src/styles/batchTranscribe.scss | 238 +++++++++++++++++++++++ 6 files changed, 428 insertions(+), 318 deletions(-) create mode 100644 src/components/BatchTranscribeGlobal.tsx create mode 100644 src/stores/batchTranscribeStore.ts create mode 100644 src/styles/batchTranscribe.scss diff --git a/src/App.tsx b/src/App.tsx index bcfdce9..2491727 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -34,6 +34,7 @@ import UpdateDialog from './components/UpdateDialog' import UpdateProgressCapsule from './components/UpdateProgressCapsule' import LockScreen from './components/LockScreen' import { GlobalSessionMonitor } from './components/GlobalSessionMonitor' +import { BatchTranscribeGlobal } from './components/BatchTranscribeGlobal' function App() { const navigate = useNavigate() @@ -360,6 +361,9 @@ function App() { {/* 全局会话监听与通知 */} + {/* 全局批量转写进度浮窗 */} + + {/* 用户协议弹窗 */} {showAgreement && !agreementLoading && (
diff --git a/src/components/BatchTranscribeGlobal.tsx b/src/components/BatchTranscribeGlobal.tsx new file mode 100644 index 0000000..b6f15b6 --- /dev/null +++ b/src/components/BatchTranscribeGlobal.tsx @@ -0,0 +1,101 @@ +import React from 'react' +import { createPortal } from 'react-dom' +import { Loader2, X, CheckCircle, XCircle, AlertCircle } from 'lucide-react' +import { useBatchTranscribeStore } from '../stores/batchTranscribeStore' +import '../styles/batchTranscribe.scss' + +/** + * 全局批量转写进度浮窗 + 结果弹窗 + * 挂载在 App 层,切换页面时不会消失 + */ +export const BatchTranscribeGlobal: React.FC = () => { + const { + isBatchTranscribing, + progress, + showToast, + showResult, + result, + setShowToast, + setShowResult + } = useBatchTranscribeStore() + + return ( + <> + {/* 批量转写进度浮窗(非阻塞) */} + {showToast && isBatchTranscribing && createPortal( +
+
+
+ + 批量转写中 +
+ +
+
+
+ {progress.current} / {progress.total} + + {progress.total > 0 + ? Math.round((progress.current / progress.total) * 100) + : 0}% + +
+
+
0 + ? (progress.current / progress.total) * 100 + : 0}%` + }} + /> +
+
+
, + document.body + )} + + {/* 批量转写结果对话框 */} + {showResult && createPortal( +
setShowResult(false)}> +
e.stopPropagation()}> +
+ +

转写完成

+
+
+
+
+ + 成功: + {result.success} 条 +
+ {result.fail > 0 && ( +
+ + 失败: + {result.fail} 条 +
+ )} +
+ {result.fail > 0 && ( +
+ + 部分语音转写失败,可能是语音文件损坏或网络问题 +
+ )} +
+
+ +
+
+
, + document.body + )} + + ) +} diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 3887409..108352e 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2616,42 +2616,14 @@ &:hover:not(:disabled) { color: var(--primary-color); } + &.transcribing { + color: var(--primary-color); + cursor: pointer; + opacity: 1 !important; + } } -// 批量转写模态框基础样式 -.batch-modal-overlay { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - backdrop-filter: blur(4px); - display: flex; - align-items: center; - justify-content: center; - z-index: 10000; - animation: batchFadeIn 0.2s ease-out; -} - -.batch-modal-content { - background: var(--bg-primary); - border-radius: 12px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); - max-height: 90vh; - overflow-y: auto; - animation: batchSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); -} - -@keyframes batchFadeIn { - from { opacity: 0; } - to { opacity: 1; } -} - -@keyframes batchSlideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } -} +// 批量转写模态框基础样式(共享样式在 styles/batchTranscribe.scss) // 批量转写确认对话框 .batch-confirm-modal { @@ -2845,187 +2817,3 @@ } } } - -// 批量转写进度对话框 -.batch-progress-modal { - width: 420px; - max-width: 90vw; - - .batch-modal-header { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1.5rem; - border-bottom: 1px solid var(--border-color); - - svg { color: var(--primary-color); } - - h3 { - margin: 0; - font-size: 18px; - font-weight: 600; - color: var(--text-primary); - } - } - - .batch-modal-body { - padding: 1.5rem; - - .progress-info { - .progress-text { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 0.75rem; - font-size: 14px; - color: var(--text-secondary); - - .progress-percent { - font-weight: 600; - color: var(--primary-color); - font-size: 16px; - } - } - - .progress-bar { - height: 8px; - background: var(--bg-tertiary); - border-radius: 4px; - overflow: hidden; - margin-bottom: 1rem; - - .progress-fill { - height: 100%; - background: linear-gradient(90deg, var(--primary-color), var(--primary-color)); - border-radius: 4px; - transition: width 0.3s ease; - } - } - } - - .batch-tip { - display: flex; - align-items: center; - justify-content: center; - padding: 0.75rem; - background: var(--bg-tertiary); - border-radius: 8px; - - span { - font-size: 13px; - color: var(--text-secondary); - } - } - } -} - -// 批量转写结果对话框 -.batch-result-modal { - width: 420px; - max-width: 90vw; - - .batch-modal-header { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1.5rem; - border-bottom: 1px solid var(--border-color); - - svg { color: #4caf50; } - - h3 { - margin: 0; - font-size: 18px; - font-weight: 600; - color: var(--text-primary); - } - } - - .batch-modal-body { - padding: 1.5rem; - - .result-summary { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1rem; - - .result-item { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 1rem; - border-radius: 8px; - background: var(--bg-tertiary); - - svg { flex-shrink: 0; } - - .label { - font-size: 14px; - color: var(--text-secondary); - } - - .value { - margin-left: auto; - font-size: 18px; - font-weight: 600; - } - - &.success { - svg { color: #4caf50; } - .value { color: #4caf50; } - } - - &.fail { - svg { color: #f44336; } - .value { color: #f44336; } - } - } - } - - .result-tip { - display: flex; - align-items: flex-start; - gap: 0.5rem; - padding: 0.75rem; - background: rgba(255, 152, 0, 0.1); - border-radius: 8px; - border: 1px solid rgba(255, 152, 0, 0.3); - - svg { - flex-shrink: 0; - margin-top: 2px; - color: #ff9800; - } - - span { - font-size: 13px; - color: var(--text-secondary); - line-height: 1.5; - } - } - } - - .batch-modal-footer { - display: flex; - justify-content: flex-end; - padding: 1rem 1.5rem; - border-top: 1px solid var(--border-color); - - button { - padding: 0.5rem 1.5rem; - border-radius: 8px; - font-size: 14px; - font-weight: 500; - cursor: pointer; - transition: all 0.2s; - border: none; - - &.btn-primary { - background: var(--primary-color); - color: white; - &:hover { opacity: 0.9; } - } - } - } -} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index bbbab33..8d118c0 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1,7 +1,8 @@ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react' -import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, XCircle, Copy, Check } from 'lucide-react' +import { Search, MessageSquare, AlertCircle, Loader2, RefreshCw, X, ChevronDown, Info, Calendar, Database, Hash, Play, Pause, Image as ImageIcon, Link, Mic, CheckCircle, Copy, Check } from 'lucide-react' import { createPortal } from 'react-dom' import { useChatStore } from '../stores/chatStore' +import { useBatchTranscribeStore } from '../stores/batchTranscribeStore' import type { ChatSession, Message } from '../types/models' import { getEmojiPath } from 'wechat-emojis' import { VoiceTranscribeDialog } from '../components/VoiceTranscribeDialog' @@ -175,17 +176,13 @@ function ChatPage(_props: ChatPageProps) { const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false) const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null) - // 批量语音转文字相关状态 - const [isBatchTranscribing, setIsBatchTranscribing] = useState(false) - const [batchTranscribeProgress, setBatchTranscribeProgress] = useState({ current: 0, total: 0 }) + // 批量语音转文字相关状态(进度/结果 由全局 store 管理) + const { isBatchTranscribing, progress: batchTranscribeProgress, showToast: showBatchProgress, startTranscribe, updateProgress, finishTranscribe, setShowToast: setShowBatchProgress } = useBatchTranscribeStore() const [showBatchConfirm, setShowBatchConfirm] = useState(false) const [batchVoiceCount, setBatchVoiceCount] = useState(0) const [batchVoiceMessages, setBatchVoiceMessages] = useState(null) const [batchVoiceDates, setBatchVoiceDates] = useState([]) const [batchSelectedDates, setBatchSelectedDates] = useState>(new Set()) - const [showBatchProgress, setShowBatchProgress] = useState(false) - const [showBatchResult, setShowBatchResult] = useState(false) - const [batchResult, setBatchResult] = useState({ success: 0, fail: 0 }) // 联系人信息加载控制 const isEnrichingRef = useRef(false) @@ -1280,16 +1277,13 @@ function ChatPage(_props: ChatPageProps) { const session = sessions.find(s => s.username === currentSessionId) if (!session) return - setIsBatchTranscribing(true) - setShowBatchProgress(true) - setBatchTranscribeProgress({ current: 0, total: voiceMessages.length }) + startTranscribe(voiceMessages.length) // 检查模型状态 const modelStatus = await window.electronAPI.whisper.getModelStatus() if (!modelStatus?.exists) { alert('SenseVoice 模型未下载,请先在设置中下载模型') - setIsBatchTranscribing(false) - setShowBatchProgress(false) + finishTranscribe(0, 0) return } @@ -1319,15 +1313,12 @@ function ChatPage(_props: ChatPageProps) { if (result.success) successCount++ else failCount++ completedCount++ - setBatchTranscribeProgress({ current: completedCount, total: voiceMessages.length }) + updateProgress(completedCount, voiceMessages.length) }) } - setIsBatchTranscribing(false) - setShowBatchProgress(false) - setBatchResult({ success: successCount, fail: failCount }) - setShowBatchResult(true) - }, [sessions, currentSessionId, batchSelectedDates, batchVoiceMessages]) + finishTranscribe(successCount, failCount) + }, [sessions, currentSessionId, batchSelectedDates, batchVoiceMessages, startTranscribe, updateProgress, finishTranscribe]) // 批量转写:按日期的消息数量 const batchCountByDate = useMemo(() => { @@ -1475,10 +1466,16 @@ function ChatPage(_props: ChatPageProps) {
, document.body )} - - {/* 批量转写进度对话框 */} - {showBatchProgress && createPortal( -
-
e.stopPropagation()}> -
- -

正在转写...

-
-
-
-
- 已完成 {batchTranscribeProgress.current} / {batchTranscribeProgress.total} 条 - - {batchTranscribeProgress.total > 0 - ? Math.round((batchTranscribeProgress.current / batchTranscribeProgress.total) * 100) - : 0}% - -
-
-
0 - ? (batchTranscribeProgress.current / batchTranscribeProgress.total) * 100 - : 0}%` - }} - /> -
-
-
- 转写过程中可以继续使用其他功能 -
-
-
-
, - document.body - )} - - {/* 批量转写结果对话框 */} - {showBatchResult && createPortal( -
setShowBatchResult(false)}> -
e.stopPropagation()}> -
- -

转写完成

-
-
-
-
- - 成功: - {batchResult.success} 条 -
- {batchResult.fail > 0 && ( -
- - 失败: - {batchResult.fail} 条 -
- )} -
- {batchResult.fail > 0 && ( -
- - 部分语音转写失败,可能是语音文件损坏或网络问题 -
- )} -
-
- -
-
-
, - document.body - )}
) } diff --git a/src/stores/batchTranscribeStore.ts b/src/stores/batchTranscribeStore.ts new file mode 100644 index 0000000..b8ae357 --- /dev/null +++ b/src/stores/batchTranscribeStore.ts @@ -0,0 +1,60 @@ +import { create } from 'zustand' + +export interface BatchTranscribeState { + /** 是否正在批量转写 */ + isBatchTranscribing: boolean + /** 转写进度 */ + progress: { current: number; total: number } + /** 是否显示进度浮窗 */ + showToast: boolean + /** 是否显示结果弹窗 */ + showResult: boolean + /** 转写结果 */ + result: { success: number; fail: number } + + // Actions + startTranscribe: (total: number) => void + updateProgress: (current: number, total: number) => void + finishTranscribe: (success: number, fail: number) => void + setShowToast: (show: boolean) => void + setShowResult: (show: boolean) => void + reset: () => void +} + +export const useBatchTranscribeStore = create((set) => ({ + isBatchTranscribing: false, + progress: { current: 0, total: 0 }, + showToast: false, + showResult: false, + result: { success: 0, fail: 0 }, + + startTranscribe: (total) => set({ + isBatchTranscribing: true, + showToast: true, + progress: { current: 0, total }, + showResult: false, + result: { success: 0, fail: 0 } + }), + + updateProgress: (current, total) => set({ + progress: { current, total } + }), + + finishTranscribe: (success, fail) => set({ + isBatchTranscribing: false, + showToast: false, + showResult: true, + result: { success, fail } + }), + + setShowToast: (show) => set({ showToast: show }), + setShowResult: (show) => set({ showResult: show }), + + reset: () => set({ + isBatchTranscribing: false, + progress: { current: 0, total: 0 }, + showToast: false, + showResult: false, + result: { success: 0, fail: 0 } + }) +})) diff --git a/src/styles/batchTranscribe.scss b/src/styles/batchTranscribe.scss new file mode 100644 index 0000000..175cc2c --- /dev/null +++ b/src/styles/batchTranscribe.scss @@ -0,0 +1,238 @@ +// 批量转写 - 共享基础样式(overlay / modal-content / animations) +// 被 ChatPage.scss 和 BatchTranscribeGlobal.tsx 同时使用 + +.batch-modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: batchFadeIn 0.2s ease-out; +} + +.batch-modal-content { + background: var(--bg-primary); + border-radius: 12px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + max-height: 90vh; + overflow-y: auto; + animation: batchSlideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1); +} + +@keyframes batchFadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +@keyframes batchSlideUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +// 批量转写进度浮窗(非阻塞 toast) +.batch-progress-toast { + position: fixed; + bottom: 24px; + right: 24px; + width: 320px; + background: var(--bg-primary); + border-radius: 12px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18); + border: 1px solid var(--border-color); + z-index: 10000; + animation: batchToastSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1); + overflow: hidden; + + .batch-progress-toast-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 14px; + border-bottom: 1px solid var(--border-color); + + .batch-progress-toast-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: var(--text-primary); + + svg { color: var(--primary-color); } + } + } + + .batch-progress-toast-close { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border: none; + background: transparent; + border-radius: 6px; + color: var(--text-secondary); + cursor: pointer; + transition: background 0.15s, color 0.15s; + + &:hover { + background: var(--bg-tertiary); + color: var(--text-primary); + } + } + + .batch-progress-toast-body { + padding: 12px 14px; + + .progress-text { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + font-size: 12px; + color: var(--text-secondary); + + .progress-percent { + font-weight: 600; + color: var(--primary-color); + font-size: 13px; + } + } + + .progress-bar { + height: 6px; + background: var(--bg-tertiary); + border-radius: 3px; + overflow: hidden; + + .progress-fill { + height: 100%; + background: linear-gradient(90deg, var(--primary-color), var(--primary-color)); + border-radius: 3px; + transition: width 0.3s ease; + } + } + } +} + +@keyframes batchToastSlideIn { + from { opacity: 0; transform: translateY(16px) scale(0.96); } + to { opacity: 1; transform: translateY(0) scale(1); } +} + +// 批量转写结果对话框 +.batch-result-modal { + width: 420px; + max-width: 90vw; + + .batch-modal-header { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1.5rem; + border-bottom: 1px solid var(--border-color); + + svg { color: #4caf50; } + + h3 { + margin: 0; + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + } + } + + .batch-modal-body { + padding: 1.5rem; + + .result-summary { + display: flex; + flex-direction: column; + gap: 0.75rem; + margin-bottom: 1rem; + + .result-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 1rem; + border-radius: 8px; + background: var(--bg-tertiary); + + svg { flex-shrink: 0; } + + .label { + font-size: 14px; + color: var(--text-secondary); + } + + .value { + margin-left: auto; + font-size: 18px; + font-weight: 600; + } + + &.success { + svg { color: #4caf50; } + .value { color: #4caf50; } + } + + &.fail { + svg { color: #f44336; } + .value { color: #f44336; } + } + } + } + + .result-tip { + display: flex; + align-items: flex-start; + gap: 0.5rem; + padding: 0.75rem; + background: rgba(255, 152, 0, 0.1); + border-radius: 8px; + border: 1px solid rgba(255, 152, 0, 0.3); + + svg { + flex-shrink: 0; + margin-top: 2px; + color: #ff9800; + } + + span { + font-size: 13px; + color: var(--text-secondary); + line-height: 1.5; + } + } + } + + .batch-modal-footer { + display: flex; + justify-content: flex-end; + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + + button { + padding: 0.5rem 1.5rem; + border-radius: 8px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; + border: none; + + &.btn-primary { + background: var(--primary-color); + color: white; + &:hover { opacity: 0.9; } + } + } + } +}