mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
批量语音转文字改成右下角常驻
This commit is contained in:
@@ -34,6 +34,7 @@ import UpdateDialog from './components/UpdateDialog'
|
|||||||
import UpdateProgressCapsule from './components/UpdateProgressCapsule'
|
import UpdateProgressCapsule from './components/UpdateProgressCapsule'
|
||||||
import LockScreen from './components/LockScreen'
|
import LockScreen from './components/LockScreen'
|
||||||
import { GlobalSessionMonitor } from './components/GlobalSessionMonitor'
|
import { GlobalSessionMonitor } from './components/GlobalSessionMonitor'
|
||||||
|
import { BatchTranscribeGlobal } from './components/BatchTranscribeGlobal'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -360,6 +361,9 @@ function App() {
|
|||||||
{/* 全局会话监听与通知 */}
|
{/* 全局会话监听与通知 */}
|
||||||
<GlobalSessionMonitor />
|
<GlobalSessionMonitor />
|
||||||
|
|
||||||
|
{/* 全局批量转写进度浮窗 */}
|
||||||
|
<BatchTranscribeGlobal />
|
||||||
|
|
||||||
{/* 用户协议弹窗 */}
|
{/* 用户协议弹窗 */}
|
||||||
{showAgreement && !agreementLoading && (
|
{showAgreement && !agreementLoading && (
|
||||||
<div className="agreement-overlay">
|
<div className="agreement-overlay">
|
||||||
|
|||||||
101
src/components/BatchTranscribeGlobal.tsx
Normal file
101
src/components/BatchTranscribeGlobal.tsx
Normal file
@@ -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(
|
||||||
|
<div className="batch-progress-toast">
|
||||||
|
<div className="batch-progress-toast-header">
|
||||||
|
<div className="batch-progress-toast-title">
|
||||||
|
<Loader2 size={14} className="spin" />
|
||||||
|
<span>批量转写中</span>
|
||||||
|
</div>
|
||||||
|
<button className="batch-progress-toast-close" onClick={() => setShowToast(false)} title="最小化">
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="batch-progress-toast-body">
|
||||||
|
<div className="progress-text">
|
||||||
|
<span>{progress.current} / {progress.total}</span>
|
||||||
|
<span className="progress-percent">
|
||||||
|
{progress.total > 0
|
||||||
|
? Math.round((progress.current / progress.total) * 100)
|
||||||
|
: 0}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="progress-bar">
|
||||||
|
<div
|
||||||
|
className="progress-fill"
|
||||||
|
style={{
|
||||||
|
width: `${progress.total > 0
|
||||||
|
? (progress.current / progress.total) * 100
|
||||||
|
: 0}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 批量转写结果对话框 */}
|
||||||
|
{showResult && createPortal(
|
||||||
|
<div className="batch-modal-overlay" onClick={() => setShowResult(false)}>
|
||||||
|
<div className="batch-modal-content batch-result-modal" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="batch-modal-header">
|
||||||
|
<CheckCircle size={20} />
|
||||||
|
<h3>转写完成</h3>
|
||||||
|
</div>
|
||||||
|
<div className="batch-modal-body">
|
||||||
|
<div className="result-summary">
|
||||||
|
<div className="result-item success">
|
||||||
|
<CheckCircle size={18} />
|
||||||
|
<span className="label">成功:</span>
|
||||||
|
<span className="value">{result.success} 条</span>
|
||||||
|
</div>
|
||||||
|
{result.fail > 0 && (
|
||||||
|
<div className="result-item fail">
|
||||||
|
<XCircle size={18} />
|
||||||
|
<span className="label">失败:</span>
|
||||||
|
<span className="value">{result.fail} 条</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{result.fail > 0 && (
|
||||||
|
<div className="result-tip">
|
||||||
|
<AlertCircle size={16} />
|
||||||
|
<span>部分语音转写失败,可能是语音文件损坏或网络问题</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="batch-modal-footer">
|
||||||
|
<button className="btn-primary" onClick={() => setShowResult(false)}>
|
||||||
|
确定
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2616,42 +2616,14 @@
|
|||||||
&:hover:not(:disabled) {
|
&:hover:not(:disabled) {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
&.transcribing {
|
||||||
|
color: var(--primary-color);
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量转写模态框基础样式
|
// 批量转写模态框基础样式(共享样式在 styles/batchTranscribe.scss)
|
||||||
.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); }
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量转写确认对话框
|
// 批量转写确认对话框
|
||||||
.batch-confirm-modal {
|
.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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'
|
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 { createPortal } from 'react-dom'
|
||||||
import { useChatStore } from '../stores/chatStore'
|
import { useChatStore } from '../stores/chatStore'
|
||||||
|
import { useBatchTranscribeStore } from '../stores/batchTranscribeStore'
|
||||||
import type { ChatSession, Message } from '../types/models'
|
import type { ChatSession, Message } from '../types/models'
|
||||||
import { getEmojiPath } from 'wechat-emojis'
|
import { getEmojiPath } from 'wechat-emojis'
|
||||||
import { VoiceTranscribeDialog } from '../components/VoiceTranscribeDialog'
|
import { VoiceTranscribeDialog } from '../components/VoiceTranscribeDialog'
|
||||||
@@ -175,17 +176,13 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
const [showVoiceTranscribeDialog, setShowVoiceTranscribeDialog] = useState(false)
|
||||||
const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null)
|
const [pendingVoiceTranscriptRequest, setPendingVoiceTranscriptRequest] = useState<{ sessionId: string; messageId: string } | null>(null)
|
||||||
|
|
||||||
// 批量语音转文字相关状态
|
// 批量语音转文字相关状态(进度/结果 由全局 store 管理)
|
||||||
const [isBatchTranscribing, setIsBatchTranscribing] = useState(false)
|
const { isBatchTranscribing, progress: batchTranscribeProgress, showToast: showBatchProgress, startTranscribe, updateProgress, finishTranscribe, setShowToast: setShowBatchProgress } = useBatchTranscribeStore()
|
||||||
const [batchTranscribeProgress, setBatchTranscribeProgress] = useState({ current: 0, total: 0 })
|
|
||||||
const [showBatchConfirm, setShowBatchConfirm] = useState(false)
|
const [showBatchConfirm, setShowBatchConfirm] = useState(false)
|
||||||
const [batchVoiceCount, setBatchVoiceCount] = useState(0)
|
const [batchVoiceCount, setBatchVoiceCount] = useState(0)
|
||||||
const [batchVoiceMessages, setBatchVoiceMessages] = useState<Message[] | null>(null)
|
const [batchVoiceMessages, setBatchVoiceMessages] = useState<Message[] | null>(null)
|
||||||
const [batchVoiceDates, setBatchVoiceDates] = useState<string[]>([])
|
const [batchVoiceDates, setBatchVoiceDates] = useState<string[]>([])
|
||||||
const [batchSelectedDates, setBatchSelectedDates] = useState<Set<string>>(new Set())
|
const [batchSelectedDates, setBatchSelectedDates] = useState<Set<string>>(new Set())
|
||||||
const [showBatchProgress, setShowBatchProgress] = useState(false)
|
|
||||||
const [showBatchResult, setShowBatchResult] = useState(false)
|
|
||||||
const [batchResult, setBatchResult] = useState({ success: 0, fail: 0 })
|
|
||||||
|
|
||||||
// 联系人信息加载控制
|
// 联系人信息加载控制
|
||||||
const isEnrichingRef = useRef(false)
|
const isEnrichingRef = useRef(false)
|
||||||
@@ -1280,16 +1277,13 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
const session = sessions.find(s => s.username === currentSessionId)
|
const session = sessions.find(s => s.username === currentSessionId)
|
||||||
if (!session) return
|
if (!session) return
|
||||||
|
|
||||||
setIsBatchTranscribing(true)
|
startTranscribe(voiceMessages.length)
|
||||||
setShowBatchProgress(true)
|
|
||||||
setBatchTranscribeProgress({ current: 0, total: voiceMessages.length })
|
|
||||||
|
|
||||||
// 检查模型状态
|
// 检查模型状态
|
||||||
const modelStatus = await window.electronAPI.whisper.getModelStatus()
|
const modelStatus = await window.electronAPI.whisper.getModelStatus()
|
||||||
if (!modelStatus?.exists) {
|
if (!modelStatus?.exists) {
|
||||||
alert('SenseVoice 模型未下载,请先在设置中下载模型')
|
alert('SenseVoice 模型未下载,请先在设置中下载模型')
|
||||||
setIsBatchTranscribing(false)
|
finishTranscribe(0, 0)
|
||||||
setShowBatchProgress(false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1319,15 +1313,12 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
if (result.success) successCount++
|
if (result.success) successCount++
|
||||||
else failCount++
|
else failCount++
|
||||||
completedCount++
|
completedCount++
|
||||||
setBatchTranscribeProgress({ current: completedCount, total: voiceMessages.length })
|
updateProgress(completedCount, voiceMessages.length)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsBatchTranscribing(false)
|
finishTranscribe(successCount, failCount)
|
||||||
setShowBatchProgress(false)
|
}, [sessions, currentSessionId, batchSelectedDates, batchVoiceMessages, startTranscribe, updateProgress, finishTranscribe])
|
||||||
setBatchResult({ success: successCount, fail: failCount })
|
|
||||||
setShowBatchResult(true)
|
|
||||||
}, [sessions, currentSessionId, batchSelectedDates, batchVoiceMessages])
|
|
||||||
|
|
||||||
// 批量转写:按日期的消息数量
|
// 批量转写:按日期的消息数量
|
||||||
const batchCountByDate = useMemo(() => {
|
const batchCountByDate = useMemo(() => {
|
||||||
@@ -1475,10 +1466,16 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
<button
|
<button
|
||||||
className="icon-btn batch-transcribe-btn"
|
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
||||||
onClick={handleBatchTranscribe}
|
onClick={() => {
|
||||||
disabled={isBatchTranscribing || !currentSessionId}
|
if (isBatchTranscribing) {
|
||||||
title={isBatchTranscribing ? `批量转写中 (${batchTranscribeProgress.current}/${batchTranscribeProgress.total})` : '批量语音转文字'}
|
setShowBatchProgress(true)
|
||||||
|
} else {
|
||||||
|
handleBatchTranscribe()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
disabled={!currentSessionId}
|
||||||
|
title={isBatchTranscribing ? `批量转写中 (${batchTranscribeProgress.current}/${batchTranscribeProgress.total}),点击查看进度` : '批量语音转文字'}
|
||||||
>
|
>
|
||||||
{isBatchTranscribing ? (
|
{isBatchTranscribing ? (
|
||||||
<Loader2 size={18} className="spin" />
|
<Loader2 size={18} className="spin" />
|
||||||
@@ -1813,84 +1810,6 @@ function ChatPage(_props: ChatPageProps) {
|
|||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 批量转写进度对话框 */}
|
|
||||||
{showBatchProgress && createPortal(
|
|
||||||
<div className="batch-modal-overlay">
|
|
||||||
<div className="batch-modal-content batch-progress-modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="batch-modal-header">
|
|
||||||
<Loader2 size={20} className="spin" />
|
|
||||||
<h3>正在转写...</h3>
|
|
||||||
</div>
|
|
||||||
<div className="batch-modal-body">
|
|
||||||
<div className="progress-info">
|
|
||||||
<div className="progress-text">
|
|
||||||
<span>已完成 {batchTranscribeProgress.current} / {batchTranscribeProgress.total} 条</span>
|
|
||||||
<span className="progress-percent">
|
|
||||||
{batchTranscribeProgress.total > 0
|
|
||||||
? Math.round((batchTranscribeProgress.current / batchTranscribeProgress.total) * 100)
|
|
||||||
: 0}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="progress-bar">
|
|
||||||
<div
|
|
||||||
className="progress-fill"
|
|
||||||
style={{
|
|
||||||
width: `${batchTranscribeProgress.total > 0
|
|
||||||
? (batchTranscribeProgress.current / batchTranscribeProgress.total) * 100
|
|
||||||
: 0}%`
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="batch-tip">
|
|
||||||
<span>转写过程中可以继续使用其他功能</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 批量转写结果对话框 */}
|
|
||||||
{showBatchResult && createPortal(
|
|
||||||
<div className="batch-modal-overlay" onClick={() => setShowBatchResult(false)}>
|
|
||||||
<div className="batch-modal-content batch-result-modal" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<div className="batch-modal-header">
|
|
||||||
<CheckCircle size={20} />
|
|
||||||
<h3>转写完成</h3>
|
|
||||||
</div>
|
|
||||||
<div className="batch-modal-body">
|
|
||||||
<div className="result-summary">
|
|
||||||
<div className="result-item success">
|
|
||||||
<CheckCircle size={18} />
|
|
||||||
<span className="label">成功:</span>
|
|
||||||
<span className="value">{batchResult.success} 条</span>
|
|
||||||
</div>
|
|
||||||
{batchResult.fail > 0 && (
|
|
||||||
<div className="result-item fail">
|
|
||||||
<XCircle size={18} />
|
|
||||||
<span className="label">失败:</span>
|
|
||||||
<span className="value">{batchResult.fail} 条</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{batchResult.fail > 0 && (
|
|
||||||
<div className="result-tip">
|
|
||||||
<AlertCircle size={16} />
|
|
||||||
<span>部分语音转写失败,可能是语音文件损坏或网络问题</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="batch-modal-footer">
|
|
||||||
<button className="btn-primary" onClick={() => setShowBatchResult(false)}>
|
|
||||||
确定
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>,
|
|
||||||
document.body
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/stores/batchTranscribeStore.ts
Normal file
60
src/stores/batchTranscribeStore.ts
Normal file
@@ -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<BatchTranscribeState>((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 }
|
||||||
|
})
|
||||||
|
}))
|
||||||
238
src/styles/batchTranscribe.scss
Normal file
238
src/styles/batchTranscribe.scss
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user