From adff7b9e1e5d546c80f95be4acbcfb37be65eb97 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Sun, 1 Mar 2026 16:24:12 +0800 Subject: [PATCH] feat(export): refine task center and loading interactions --- src/pages/ExportPage.scss | 121 +++++++++++++++++++++++++- src/pages/ExportPage.tsx | 173 ++++++++++++++++++++++++-------------- 2 files changed, 229 insertions(+), 65 deletions(-) diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 5f31d01..e8a1380 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -41,6 +41,13 @@ gap: 6px; } + .path-inline-row { + min-width: 0; + display: flex; + align-items: center; + gap: 8px; + } + .path-value { border: 1px dashed var(--border-color); border-radius: 10px; @@ -51,11 +58,29 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + min-width: 0; + flex: 1; + } + + .path-link { + cursor: pointer; + text-align: left; + + &:hover:not(:disabled) { + border-color: var(--primary); + color: var(--primary); + } + + &:disabled { + cursor: not-allowed; + opacity: 0.65; + } } .path-actions { display: flex; gap: 8px; + flex-shrink: 0; } .write-layout-control { @@ -75,10 +100,15 @@ font-size: 13px; text-align: left; cursor: pointer; + transition: border-color 0.12s ease; &:hover { border-color: var(--primary); } + + &.active { + border-color: var(--primary); + } } .layout-dropdown { @@ -94,8 +124,21 @@ z-index: 3000; max-height: 260px; overflow-y: auto; - opacity: 1; + opacity: 0; + transform: translateY(-4px); + pointer-events: none; + visibility: hidden; + transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s step-end; backdrop-filter: none; + will-change: opacity, transform; + + &.open { + opacity: 1; + transform: translateY(0); + pointer-events: auto; + visibility: visible; + transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s step-start; + } } .layout-option { @@ -201,6 +244,15 @@ } } +.count-loading { + color: var(--text-tertiary); + font-size: 12px; + font-weight: 500; + display: inline-flex; + align-items: baseline; + gap: 1px; +} + .task-center { border: 1px solid var(--border-color); border-radius: 12px; @@ -208,14 +260,51 @@ padding: 12px; flex-shrink: 0; + .task-center-header { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + } + .section-title { font-size: 14px; font-weight: 700; color: var(--text-primary); - margin-bottom: 8px; + margin: 0; + flex-shrink: 0; + } + + .task-summary { + margin-left: auto; + display: inline-flex; + align-items: center; + gap: 10px; + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + } + + .task-collapse-btn { + border: 1px solid var(--border-color); + background: var(--bg-secondary); + border-radius: 8px; + padding: 4px 8px; + font-size: 12px; + color: var(--text-secondary); + display: inline-flex; + align-items: center; + gap: 4px; + cursor: pointer; + + &:hover { + border-color: var(--primary); + color: var(--primary); + } } .task-empty { + margin-top: 10px; padding: 12px; background: var(--bg-secondary); border-radius: 8px; @@ -224,6 +313,7 @@ } .task-list { + margin-top: 10px; display: grid; gap: 8px; max-height: 190px; @@ -377,6 +467,7 @@ white-space: nowrap; display: inline-flex; align-items: center; + gap: 4px; &.active { border-color: var(--primary); @@ -386,6 +477,14 @@ } } +.animated-ellipsis { + display: inline-block; + width: 0; + overflow: hidden; + vertical-align: bottom; + animation: exportDots 1s steps(4, end) infinite; +} + .toolbar-actions { display: flex; align-items: center; @@ -952,6 +1051,15 @@ } } +@keyframes exportDots { + 0% { + width: 0; + } + 100% { + width: 1.8em; + } +} + @media (max-width: 1360px) { .export-top-panel { grid-template-columns: 1fr; @@ -959,6 +1067,15 @@ .global-export-controls { grid-template-columns: 1fr; + + .path-inline-row { + flex-wrap: wrap; + } + + .path-actions { + width: 100%; + justify-content: flex-end; + } } .content-card-grid { diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index c7c2daf..486d31e 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -2,6 +2,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation } from 'react-router-dom' import { Aperture, + ChevronDown, + ChevronRight, CheckSquare, Download, ExternalLink, @@ -241,6 +243,8 @@ function ExportPage() { const [isLoading, setIsLoading] = useState(true) const [isSessionEnriching, setIsSessionEnriching] = useState(false) const [isSnsStatsLoading, setIsSnsStatsLoading] = useState(true) + const [isBaseConfigLoading, setIsBaseConfigLoading] = useState(true) + const [isTaskCenterExpanded, setIsTaskCenterExpanded] = useState(false) const [sessions, setSessions] = useState([]) const [sessionMetrics, setSessionMetrics] = useState>({}) const [searchKeyword, setSearchKeyword] = useState('') @@ -296,6 +300,7 @@ function ExportPage() { const sessionLoadTokenRef = useRef(0) const loadingMetricsRef = useRef>(new Set()) const preselectAppliedRef = useRef(false) + const writeLayoutControlRef = useRef(null) useEffect(() => { tasksRef.current = tasks @@ -323,6 +328,7 @@ function ExportPage() { }, []) const loadBaseConfig = useCallback(async () => { + setIsBaseConfigLoading(true) try { const [savedPath, savedFormat, savedMedia, savedVoiceAsText, savedExcelCompactColumns, savedTxtColumns, savedConcurrency, savedWriteLayout, savedSessionMap, savedContentMap, savedSnsPostCount] = await Promise.all([ configService.getExportPath(), @@ -362,6 +368,8 @@ function ExportPage() { })) } catch (error) { console.error('加载导出配置失败:', error) + } finally { + setIsBaseConfigLoading(false) } }, []) @@ -499,6 +507,18 @@ function ExportPage() { preselectAppliedRef.current = false }, [location.key, preselectSessionIds]) + useEffect(() => { + if (!showWriteLayoutSelect) return + + const handleOutsideClick = (event: MouseEvent) => { + if (writeLayoutControlRef.current?.contains(event.target as Node)) return + setShowWriteLayoutSelect(false) + } + + document.addEventListener('mousedown', handleOutsideClick) + return () => document.removeEventListener('mousedown', handleOutsideClick) + }, [showWriteLayoutSelect]) + useEffect(() => { if (preselectAppliedRef.current) return if (sessions.length === 0 || preselectSessionIds.length === 0) return @@ -1275,6 +1295,10 @@ function ExportPage() { const formatCandidateOptions = exportDialog.scope === 'sns' ? formatOptions.filter(option => option.value === 'html' || option.value === 'json') : formatOptions + const isTabCountComputing = isLoading || isSessionEnriching + const isSessionCardStatsLoading = isLoading || isBaseConfigLoading + const taskRunningCount = tasks.filter(task => task.status === 'running').length + const taskQueuedCount = tasks.filter(task => task.status === 'queued').length const showInitialSkeleton = isLoading && sessions.length === 0 return ( @@ -1283,75 +1307,76 @@ function ExportPage() {
导出位置 -
{exportFolder || '未设置'}
-
- +
+
+ + +
-
+
写入目录方式 - - {showWriteLayoutSelect && ( -
- {writeLayoutOptions.map(option => ( - - ))} -
- )} +
+ {writeLayoutOptions.map(option => ( + + ))} +
- {showInitialSkeleton ? Array.from({ length: 6 }).map((_, index) => ( -
-
-
-
- - -
-
- - -
-
-
-
- )) : contentCards.map(card => { + {contentCards.map(card => { const Icon = card.icon + const isCardStatsLoading = card.type === 'sns' + ? (isSnsStatsLoading || isBaseConfigLoading) + : isSessionCardStatsLoading return (
@@ -1361,7 +1386,13 @@ function ExportPage() { {card.stats.map((stat) => (
{stat.label} - {isSnsStatsLoading && card.type === 'sns' ? '--' : stat.value.toLocaleString()} + + {isCardStatsLoading ? ( + + 统计中 + + ) : stat.value.toLocaleString()} +
))}
@@ -1382,9 +1413,25 @@ function ExportPage() { })}
-
-
任务中心
- {tasks.length === 0 ? ( +
+
+
任务中心
+
+ 进行中 {taskRunningCount} + 排队 {taskQueuedCount} + 总计 {tasks.length} +
+ +
+ + {isTaskCenterExpanded && (tasks.length === 0 ? (
暂无任务。点击会话导出或卡片导出后会在这里创建任务。
) : (
@@ -1422,23 +1469,23 @@ function ExportPage() {
))}
- )} + ))}