diff --git a/src/components/BatchTranscribeGlobal.tsx b/src/components/BatchTranscribeGlobal.tsx index 51932be..3aa7c10 100644 --- a/src/components/BatchTranscribeGlobal.tsx +++ b/src/components/BatchTranscribeGlobal.tsx @@ -1,6 +1,6 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { createPortal } from 'react-dom' -import { Loader2, X, CheckCircle, XCircle, AlertCircle } from 'lucide-react' +import { Loader2, X, CheckCircle, XCircle, AlertCircle, Clock } from 'lucide-react' import { useBatchTranscribeStore } from '../stores/batchTranscribeStore' import '../styles/batchTranscribe.scss' @@ -16,10 +16,46 @@ export const BatchTranscribeGlobal: React.FC = () => { showResult, result, sessionName, + startTime, setShowToast, setShowResult } = useBatchTranscribeStore() + const [eta, setEta] = useState('') + + // 计算剩余时间 + useEffect(() => { + if (!isBatchTranscribing || !startTime || progress.current === 0) { + setEta('') + return + } + + const timer = setInterval(() => { + const now = Date.now() + const elapsed = now - startTime + const rate = progress.current / elapsed // ms per item + const remainingItems = progress.total - progress.current + + if (remainingItems <= 0) { + setEta('') + return + } + + const remainingTimeMs = remainingItems / rate + const remainingSeconds = Math.ceil(remainingTimeMs / 1000) + + if (remainingSeconds < 60) { + setEta(`${remainingSeconds}秒`) + } else { + const minutes = Math.floor(remainingSeconds / 60) + const seconds = remainingSeconds % 60 + setEta(`${minutes}分${seconds}秒`) + } + }, 1000) + + return () => clearInterval(timer) + }, [isBatchTranscribing, startTime, progress.current, progress.total]) + return ( <> {/* 批量转写进度浮窗(非阻塞) */} @@ -35,14 +71,23 @@ export const BatchTranscribeGlobal: React.FC = () => {
-
- {progress.current} / {progress.total} - - {progress.total > 0 - ? Math.round((progress.current / progress.total) * 100) - : 0}% - +
+
+ {progress.current} / {progress.total} + + {progress.total > 0 + ? Math.round((progress.current / progress.total) * 100) + : 0}% + +
+ {eta && ( +
+ + 剩余 {eta} +
+ )}
+
{ try { @@ -2511,7 +2511,7 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o // 视频懒加载 const videoAutoLoadTriggered = useRef(false) const [videoClicked, setVideoClicked] = useState(false) - + useEffect(() => { if (!isVideo || !videoContainerRef.current) return @@ -2537,11 +2537,11 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o // 视频加载中状态引用,避免依赖问题 const videoLoadingRef = useRef(false) - + // 加载视频信息(添加重试机制) const requestVideoInfo = useCallback(async () => { if (!videoMd5 || videoLoadingRef.current) return - + videoLoadingRef.current = true setVideoLoading(true) try { @@ -2563,13 +2563,13 @@ function MessageBubble({ message, session, showTime, myAvatarUrl, isGroupChat, o setVideoLoading(false) } }, [videoMd5]) - + // 视频进入视野时自动加载 useEffect(() => { if (!isVideo || !isVideoVisible) return if (videoInfo?.exists) return // 已成功加载,不需要重试 if (videoAutoLoadTriggered.current) return - + videoAutoLoadTriggered.current = true void requestVideoInfo() }, [isVideo, isVideoVisible, videoInfo, requestVideoInfo]) diff --git a/src/stores/batchTranscribeStore.ts b/src/stores/batchTranscribeStore.ts index b96f085..a6e1f1f 100644 --- a/src/stores/batchTranscribeStore.ts +++ b/src/stores/batchTranscribeStore.ts @@ -12,6 +12,7 @@ export interface BatchTranscribeState { /** 转写结果 */ result: { success: number; fail: number } /** 当前转写的会话名 */ + startTime: number sessionName: string // Actions @@ -30,6 +31,7 @@ export const useBatchTranscribeStore = create((set) => ({ showResult: false, result: { success: 0, fail: 0 }, sessionName: '', + startTime: 0, startTranscribe: (total, sessionName) => set({ isBatchTranscribing: true, @@ -37,7 +39,8 @@ export const useBatchTranscribeStore = create((set) => ({ progress: { current: 0, total }, showResult: false, result: { success: 0, fail: 0 }, - sessionName + sessionName, + startTime: Date.now() }), updateProgress: (current, total) => set({ @@ -48,7 +51,8 @@ export const useBatchTranscribeStore = create((set) => ({ isBatchTranscribing: false, showToast: false, showResult: true, - result: { success, fail } + result: { success, fail }, + startTime: 0 }), setShowToast: (show) => set({ showToast: show }), @@ -60,6 +64,7 @@ export const useBatchTranscribeStore = create((set) => ({ showToast: false, showResult: false, result: { success: 0, fail: 0 }, - sessionName: '' + sessionName: '', + startTime: 0 }) })) diff --git a/src/styles/batchTranscribe.scss b/src/styles/batchTranscribe.scss index 175cc2c..5a7256f 100644 --- a/src/styles/batchTranscribe.scss +++ b/src/styles/batchTranscribe.scss @@ -26,13 +26,25 @@ } @keyframes batchFadeIn { - from { opacity: 0; } - to { opacity: 1; } + from { + opacity: 0; + } + + to { + opacity: 1; + } } @keyframes batchSlideUp { - from { opacity: 0; transform: translateY(20px); } - to { opacity: 1; transform: translateY(0); } + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: translateY(0); + } } // 批量转写进度浮窗(非阻塞 toast) @@ -64,7 +76,9 @@ font-weight: 600; color: var(--text-primary); - svg { color: var(--primary-color); } + svg { + color: var(--primary); + } } } @@ -90,18 +104,38 @@ .batch-progress-toast-body { padding: 12px 14px; - .progress-text { + .progress-info-row { display: flex; - justify-content: space-between; - align-items: center; + flex-direction: column; + gap: 4px; margin-bottom: 8px; - font-size: 12px; - color: var(--text-secondary); - .progress-percent { - font-weight: 600; - color: var(--primary-color); - font-size: 13px; + .progress-text { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12px; + color: var(--text-secondary); + + .progress-percent { + font-weight: 600; + color: var(--primary); + font-size: 13px; + } + } + + .progress-eta { + display: flex; + align-items: center; + gap: 4px; + font-size: 11px; // 稍微小一点 + color: var(--text-tertiary, #999); // 使用更淡的颜色 + + svg { + width: 12px; + height: 12px; + opacity: 0.8; + } } } @@ -113,7 +147,7 @@ .progress-fill { height: 100%; - background: linear-gradient(90deg, var(--primary-color), var(--primary-color)); + background: linear-gradient(90deg, var(--primary), var(--primary)); border-radius: 3px; transition: width 0.3s ease; } @@ -122,8 +156,15 @@ } @keyframes batchToastSlideIn { - from { opacity: 0; transform: translateY(16px) scale(0.96); } - to { opacity: 1; transform: translateY(0) scale(1); } + from { + opacity: 0; + transform: translateY(16px) scale(0.96); + } + + to { + opacity: 1; + transform: translateY(0) scale(1); + } } // 批量转写结果对话框 @@ -138,7 +179,9 @@ padding: 1.5rem; border-bottom: 1px solid var(--border-color); - svg { color: #4caf50; } + svg { + color: #4caf50; + } h3 { margin: 0; @@ -165,7 +208,9 @@ border-radius: 8px; background: var(--bg-tertiary); - svg { flex-shrink: 0; } + svg { + flex-shrink: 0; + } .label { font-size: 14px; @@ -179,13 +224,23 @@ } &.success { - svg { color: #4caf50; } - .value { color: #4caf50; } + svg { + color: #4caf50; + } + + .value { + color: #4caf50; + } } &.fail { - svg { color: #f44336; } - .value { color: #f44336; } + svg { + color: #f44336; + } + + .value { + color: #f44336; + } } } } @@ -229,10 +284,13 @@ border: none; &.btn-primary { - background: var(--primary-color); + background: var(--primary); color: white; - &:hover { opacity: 0.9; } + + &:hover { + opacity: 0.9; + } } } } -} +} \ No newline at end of file