开屏页面细节增加;聊天页面批量解密/专业补充进度徽章并可在导出页面任务列表查询具体进度

This commit is contained in:
cc committed 2026-07-01 16:52:22 +08:00
1 parent 301cb5abce
commit 82bce9f784
7 files changed
+229 -18

No files matched your search

+46 -1
View File
@@ -205,6 +205,16 @@
background-clip: text;
}
/* 对应字母的脉冲流光动画 */
.letter.pulse-active .letter-fill-content {
background: linear-gradient(90deg, var(--gold-start) 0%, var(--gold-mid) 35%, #ffffff 50%, var(--gold-mid) 65%, var(--gold-end) 100%);
background-size: 200% 100%;
animation: gold-flow 2s infinite linear;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* 字母等待/停顿状态:优雅的微光呼吸与轻微悬浮动画 */
.brand-letters.waiting .letter {
animation: letter-waiting-breath 2.5s ease-in-out infinite;
@@ -401,6 +411,7 @@
<script>
var themeModeQuery = null;
var systemModeQuery = null;
var loadingTimeout = null;
function resolveMode(mode) {
if (mode === "dark" || mode === "light") return mode;
@@ -449,6 +460,18 @@
var metadataLeft = document.querySelector(".metadata-left");
var safePercent = Math.max(0, Math.min(100, Number(percent) || 0));
// 每次更新进度,清除之前的等待定时器
if (loadingTimeout) {
clearTimeout(loadingTimeout);
loadingTimeout = null;
}
// 移除所有字母的脉冲动画类
var letters = document.querySelectorAll("#brandLetters .letter");
letters.forEach(function(letter) {
letter.classList.remove("pulse-active");
});
if (fill) {
if (waiting) {
fill.classList.add("indeterminate");
@@ -506,7 +529,6 @@
}
// Smooth letter-by-letter filling
var letters = document.querySelectorAll("#brandLetters .letter");
if (letters.length > 0) {
var charProgress = (safePercent / 100) * letters.length;
letters.forEach(function(letter, index) {
@@ -517,6 +539,29 @@
}
});
}
// 如果当前不是 waiting 状态,且进度未达到 100%,则启动 0.5s 定时器
if (!waiting && safePercent < 100) {
loadingTimeout = setTimeout(function() {
if (fill) {
fill.classList.add("indeterminate");
fill.style.width = "100%";
if (dot) dot.style.display = "none";
}
if (letters.length > 0) {
var charProgress = (safePercent / 100) * letters.length;
var activeIndex = Math.min(letters.length - 1, Math.floor(charProgress));
if (activeIndex >= 0 && activeIndex < letters.length) {
letters[activeIndex].classList.add("pulse-active");
var wrapper = letters[activeIndex].querySelector(".letter-fill-wrapper");
if (wrapper) {
wrapper.style.width = "100%";
}
}
}
}, 500);
}
}
function setVersion(version) {
+37 -4
View File
@@ -35,7 +35,9 @@ export interface ChatHeaderProps {
isPreparingExportDialog: boolean
isBatchTranscribing: boolean
runningBatchVoiceTaskType?: BatchVoiceTaskType
batchVoiceProgress?: { current: number; total: number }
isBatchDecrypting: boolean
batchImageDecryptProgress?: { current: number; total: number }
isTriggeringSessionInsight: boolean
isRefreshingMessages: boolean
isLoadingMessages: boolean
@@ -72,7 +74,9 @@ function ChatHeader({
isPreparingExportDialog,
isBatchTranscribing,
runningBatchVoiceTaskType,
batchVoiceProgress,
isBatchDecrypting,
batchImageDecryptProgress,
isTriggeringSessionInsight,
isRefreshingMessages,
isLoadingMessages,
@@ -98,8 +102,17 @@ function ChatHeader({
? '正在准备导出模块'
: '导出当前会话'
const batchVoiceTitle = isBatchTranscribing
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中,可在导出页任务中心查看进度`
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中${batchVoiceProgress?.total ? `:${batchVoiceProgress.current}/${batchVoiceProgress.total}(${Math.round((batchVoiceProgress.current / Math.max(1, batchVoiceProgress.total)) * 100)}%)` : ''},可在导出页任务中心查看进度`
: '批量语音处理'
const batchVoiceProgressPercent = batchVoiceProgress?.total
? Math.max(0, Math.min(100, Math.round((batchVoiceProgress.current / Math.max(1, batchVoiceProgress.total)) * 100)))
: 0
const batchImageDecryptProgressPercent = batchImageDecryptProgress?.total
? Math.max(0, Math.min(100, Math.round((batchImageDecryptProgress.current / Math.max(1, batchImageDecryptProgress.total)) * 100)))
: 0
const batchImageDecryptTitle = isBatchDecrypting
? `批量解密图片中${batchImageDecryptProgress?.total ? `:${batchImageDecryptProgress.current}/${batchImageDecryptProgress.total}(${batchImageDecryptProgressPercent}%)` : ''},可在导出页任务中心查看进度`
: '批量解密图片'
return (
<div className="message-header">
@@ -174,8 +187,16 @@ function ChatHeader({
onClick={onBatchTranscribe}
disabled={!currentSessionId}
title={batchVoiceTitle}
aria-label={batchVoiceTitle}
>
{isBatchTranscribing ? <Loader2 size={18} className="spin" /> : <Mic size={18} />}
{isBatchTranscribing ? (
<>
<Loader2 size={18} className="spin" />
{batchVoiceProgress?.total ? (
<span className="batch-progress-badge">{batchVoiceProgressPercent}%</span>
) : null}
</>
) : <Mic size={18} />}
</button>
)}
{!standaloneSessionWindow && (
@@ -183,9 +204,17 @@ function ChatHeader({
className={`icon-btn batch-decrypt-btn${isBatchDecrypting ? ' transcribing' : ''}`}
onClick={onBatchDecrypt}
disabled={!currentSessionId}
title={isBatchDecrypting ? '批量解密中' : '批量解密图片'}
title={batchImageDecryptTitle}
aria-label={batchImageDecryptTitle}
>
{isBatchDecrypting ? <Loader2 size={18} className="spin" /> : <ImageIcon size={18} />}
{isBatchDecrypting ? (
<>
<Loader2 size={18} className="spin" />
{batchImageDecryptProgress?.total ? (
<span className="batch-progress-badge">{batchImageDecryptProgressPercent}%</span>
) : null}
</>
) : <ImageIcon size={18} />}
</button>
)}
<div className="jump-calendar-anchor" ref={jumpCalendarWrapRef}>
@@ -247,7 +276,11 @@ function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) {
prev.isPreparingExportDialog === next.isPreparingExportDialog &&
prev.isBatchTranscribing === next.isBatchTranscribing &&
prev.runningBatchVoiceTaskType === next.runningBatchVoiceTaskType &&
prev.batchVoiceProgress?.current === next.batchVoiceProgress?.current &&
prev.batchVoiceProgress?.total === next.batchVoiceProgress?.total &&
prev.isBatchDecrypting === next.isBatchDecrypting &&
prev.batchImageDecryptProgress?.current === next.batchImageDecryptProgress?.current &&
prev.batchImageDecryptProgress?.total === next.batchImageDecryptProgress?.total &&
prev.isTriggeringSessionInsight === next.isTriggeringSessionInsight &&
prev.isRefreshingMessages === next.isRefreshingMessages &&
prev.isLoadingMessages === next.isLoadingMessages &&
+25 -1
View File
@@ -4560,7 +4560,11 @@
}
// 批量转写按钮
.batch-transcribe-btn {
.batch-transcribe-btn,
.batch-decrypt-btn {
position: relative;
overflow: visible;
&:hover:not(:disabled) {
color: var(--primary);
}
@@ -4570,6 +4574,26 @@
cursor: pointer;
opacity: 1 !important;
}
.batch-progress-badge {
position: absolute;
right: -7px;
bottom: -5px;
min-width: 22px;
height: 15px;
padding: 0 4px;
border-radius: 999px;
border: 1px solid var(--bg-primary);
background: var(--primary);
color: #fff;
font-size: 9px;
font-weight: 700;
line-height: 14px;
text-align: center;
font-variant-numeric: tabular-nums;
pointer-events: none;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18);
}
}
// 批量转写模态框基础样式(共享样式在 styles/batchTranscribe.scss)
+6
View File
@@ -1613,6 +1613,7 @@ function ChatPage(props: ChatPageProps) {
const {
isBatchTranscribing,
runningBatchVoiceTaskType,
batchVoiceProgress,
startTranscribe,
updateProgress,
updateTranscribeTaskStatus,
@@ -1620,6 +1621,7 @@ function ChatPage(props: ChatPageProps) {
} = useBatchTranscribeStore(useShallow((state) => ({
isBatchTranscribing: state.isBatchTranscribing,
runningBatchVoiceTaskType: state.taskType,
batchVoiceProgress: state.progress,
startTranscribe: state.startTranscribe,
updateProgress: state.updateProgress,
updateTranscribeTaskStatus: state.setTaskStatus,
@@ -1627,12 +1629,14 @@ function ChatPage(props: ChatPageProps) {
})))
const {
isBatchDecrypting,
batchImageDecryptProgress,
startDecrypt,
updateDecryptProgress,
updateDecryptTaskStatus,
finishDecrypt
} = useBatchImageDecryptStore(useShallow((state) => ({
isBatchDecrypting: state.isBatchDecrypting,
batchImageDecryptProgress: state.progress,
startDecrypt: state.startDecrypt,
updateDecryptProgress: state.updateProgress,
updateDecryptTaskStatus: state.setTaskStatus,
@@ -7747,7 +7751,9 @@ function ChatPage(props: ChatPageProps) {
isPreparingExportDialog={isPreparingExportDialog}
isBatchTranscribing={isBatchTranscribing}
runningBatchVoiceTaskType={runningBatchVoiceTaskType}
batchVoiceProgress={batchVoiceProgress}
isBatchDecrypting={isBatchDecrypting}
batchImageDecryptProgress={batchImageDecryptProgress}
isTriggeringSessionInsight={isTriggeringSessionInsight}
isRefreshingMessages={isRefreshingMessages}
isLoadingMessages={isLoadingMessages}
+3 -2
View File
@@ -120,6 +120,7 @@ function ExportPage() {
resumeTask: resumeBgTask,
cancelTask: cancelBgTask
} = useBackgroundTasks()
const taskCenterBackgroundTasks = backgroundTasks.filter(task => task.sourcePage === 'export' || task.sourcePage === 'chat')
// ── 4. Dialog & Exports ──
const { dialogState, openDialog, closeDialog } = useExportDialog()
@@ -286,13 +287,13 @@ function ExportPage() {
/>
</section>
{(exportTasks.length > 0 || backgroundTasks.filter(t => t.sourcePage === 'export').length > 0) && (
{(exportTasks.length > 0 || taskCenterBackgroundTasks.length > 0) && (
<section className="export-v2-task-panel">
<TaskCenter
exportTasks={exportTasks}
onCancelExportTask={cancelExportTask}
onClearCompletedExportTasks={clearCompletedExportTasks}
backgroundTasks={backgroundTasks.filter(t => t.sourcePage === 'export')}
backgroundTasks={taskCenterBackgroundTasks}
onPauseBackgroundTask={pauseBgTask}
onResumeBackgroundTask={resumeBgTask}
onCancelBackgroundTask={cancelBgTask}
@@ -87,6 +87,20 @@
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-source-badge {
flex-shrink: 0;
font-size: 11px;
font-weight: 500;
padding: 2px 6px;
border-radius: 4px;
background: color-mix(in srgb, var(--border-color) 55%, transparent);
color: var(--text-tertiary);
}
.task-status-badge {
@@ -164,11 +178,23 @@
.progress-stats {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--text-secondary);
white-space: nowrap;
min-width: 80px;
justify-content: flex-end;
.phase {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.count {
flex-shrink: 0;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
}
}
@@ -211,6 +237,38 @@
background: #ef4444;
}
}
&.status-completed {
border-color: color-mix(in srgb, #10b981 30%, var(--border-color));
.task-status-badge {
background: color-mix(in srgb, #10b981 15%, transparent);
color: #10b981;
}
.progress-bar-fill {
background: #10b981;
}
}
&.status-failed,
&.status-canceled {
border-color: color-mix(in srgb, #ef4444 30%, var(--border-color));
.task-status-badge {
background: color-mix(in srgb, #ef4444 15%, transparent);
color: #ef4444;
}
.progress-bar-fill {
background: #ef4444;
}
}
&.status-pause_requested,
&.status-paused,
&.status-cancel_requested {
.task-status-badge {
background: color-mix(in srgb, #f59e0b 15%, transparent);
color: #d97706;
}
}
}
.bg-tasks-section {
@@ -229,9 +287,20 @@
}
.bg-task {
.task-progress-text {
font-size: 12px;
color: var(--text-secondary);
.task-info {
min-width: 0;
}
.bg-progress-area {
flex-direction: column;
align-items: stretch;
gap: 6px;
.progress-stats {
width: 100%;
justify-content: space-between;
min-width: 0;
}
}
}
}
@@ -6,7 +6,6 @@ import {
backgroundTaskSourceLabels,
backgroundTaskStatusLabels
} from '../../constants'
import { formatDurationMs } from '../../utils/format'
import './TaskCenter.scss'
interface TaskCenterProps {
@@ -20,6 +19,20 @@ interface TaskCenterProps {
onCancelBackgroundTask: (taskId: string) => void
}
const parseBackgroundTaskProgress = (progressText?: string): { current: number; total: number; percent: number } | null => {
const match = String(progressText || '').match(/(\d[\d,]*)\s*\/\s*(\d[\d,]*)/)
if (!match) return null
const current = Number(match[1].replace(/,/g, ''))
const total = Number(match[2].replace(/,/g, ''))
if (!Number.isFinite(current) || !Number.isFinite(total) || total <= 0) return null
const safeCurrent = Math.max(0, Math.min(current, total))
return {
current: safeCurrent,
total,
percent: Math.max(0, Math.min(100, Math.round((safeCurrent / total) * 100)))
}
}
const TaskCenter: React.FC<TaskCenterProps> = ({
exportTasks,
onCancelExportTask,
@@ -104,13 +117,22 @@ const TaskCenter: React.FC<TaskCenterProps> = ({
const isPausable = bgTask.status === 'running'
const isResumable = bgTask.status === 'paused'
const isCancelable = bgTask.status !== 'completed' && bgTask.status !== 'canceled' && bgTask.status !== 'failed'
const progress = parseBackgroundTaskProgress(bgTask.progressText)
const sourceLabel = backgroundTaskSourceLabels[bgTask.sourcePage] || '后台任务'
return (
<div key={bgTask.id} className={`task-card bg-task status-${bgTask.status}`}>
<div className="task-card-header">
<div className="task-info">
<span className="task-title">{backgroundTaskSourceLabels[bgTask.sourcePage] || '后台任务'}</span>
<span className="task-status-badge">{backgroundTaskStatusLabels[bgTask.status]}</span>
<span className="task-title">{bgTask.title || sourceLabel}</span>
<span className="task-source-badge">{sourceLabel}</span>
<span className="task-status-badge">
{bgTask.status === 'running' && <Loader2 className="spin" size={12} />}
{(bgTask.status === 'completed') && <CheckCircle2 size={12} />}
{(bgTask.status === 'failed' || bgTask.status === 'canceled') && <XCircle size={12} />}
{(bgTask.status === 'pause_requested' || bgTask.status === 'cancel_requested') && <Loader2 className="spin" size={12} />}
{backgroundTaskStatusLabels[bgTask.status]}
</span>
</div>
<div className="task-actions">
{isPausable && (
@@ -130,9 +152,20 @@ const TaskCenter: React.FC<TaskCenterProps> = ({
)}
</div>
</div>
{bgTask.progressText && (
<div className="task-progress-text">
{bgTask.progressText}
{(bgTask.detail || bgTask.progressText) && (
<div className="task-progress-area bg-progress-area">
{progress && (
<div className="progress-bar-bg">
<div
className="progress-bar-fill"
style={{ width: `${progress.percent}%` }}
/>
</div>
)}
<div className="progress-stats">
{bgTask.detail && <span className="phase">{bgTask.detail}</span>}
{bgTask.progressText && <span className="count">{bgTask.progressText}</span>}
</div>
</div>
)}
</div>