mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 07:09:12 +08:00
批量语音转文字改成右下角常驻
This commit is contained in:
@@ -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() {
|
||||
{/* 全局会话监听与通知 */}
|
||||
<GlobalSessionMonitor />
|
||||
|
||||
{/* 全局批量转写进度浮窗 */}
|
||||
<BatchTranscribeGlobal />
|
||||
|
||||
{/* 用户协议弹窗 */}
|
||||
{showAgreement && !agreementLoading && (
|
||||
<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) {
|
||||
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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Message[] | null>(null)
|
||||
const [batchVoiceDates, setBatchVoiceDates] = useState<string[]>([])
|
||||
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)
|
||||
@@ -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) {
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<button
|
||||
className="icon-btn batch-transcribe-btn"
|
||||
onClick={handleBatchTranscribe}
|
||||
disabled={isBatchTranscribing || !currentSessionId}
|
||||
title={isBatchTranscribing ? `批量转写中 (${batchTranscribeProgress.current}/${batchTranscribeProgress.total})` : '批量语音转文字'}
|
||||
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
||||
onClick={() => {
|
||||
if (isBatchTranscribing) {
|
||||
setShowBatchProgress(true)
|
||||
} else {
|
||||
handleBatchTranscribe()
|
||||
}
|
||||
}}
|
||||
disabled={!currentSessionId}
|
||||
title={isBatchTranscribing ? `批量转写中 (${batchTranscribeProgress.current}/${batchTranscribeProgress.total}),点击查看进度` : '批量语音转文字'}
|
||||
>
|
||||
{isBatchTranscribing ? (
|
||||
<Loader2 size={18} className="spin" />
|
||||
@@ -1813,84 +1810,6 @@ function ChatPage(_props: ChatPageProps) {
|
||||
</div>,
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
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