开屏页面细节增加;聊天页面批量解密/专业补充进度徽章并可在导出页面任务列表查询具体进度
This commit is contained in:
1 parent
301cb5abce
commit
82bce9f784
7 files changed
+229
-18
No files matched your search
+46
-1
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user