diff --git a/src/pages/AnnualReportPage.scss b/src/pages/AnnualReportPage.scss index 5f58d7f..b0c986f 100644 --- a/src/pages/AnnualReportPage.scss +++ b/src/pages/AnnualReportPage.scss @@ -26,6 +26,14 @@ margin: 0 0 48px; } +.page-desc.load-summary { + margin: 0 0 28px; +} + +.page-desc.load-summary.complete { + color: var(--text-secondary); +} + .report-sections { display: flex; flex-direction: column; @@ -83,6 +91,14 @@ color: var(--text-tertiary); } +.year-grid-with-status { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 16px; + margin-bottom: 24px; +} + .year-grid { display: flex; flex-wrap: wrap; @@ -95,7 +111,39 @@ .report-section .year-grid { justify-content: flex-start; max-width: none; - margin-bottom: 24px; + margin-bottom: 0; +} + +.year-grid-with-status .year-grid { + flex: 1; +} + +.year-load-status { + display: inline-flex; + align-items: center; + font-size: 12px; + color: var(--text-tertiary); + white-space: nowrap; + margin-top: 6px; + flex-shrink: 0; +} + +.year-load-status.complete { + color: color-mix(in srgb, var(--primary) 80%, var(--text-secondary)); +} + +.dot-ellipsis { + display: inline-block; + width: 0; + overflow: hidden; + vertical-align: bottom; + animation: dot-ellipsis 1.2s steps(4, end) infinite; +} + +.year-load-status.complete .dot-ellipsis, +.page-desc.load-summary.complete .dot-ellipsis { + animation: none; + width: 0; } .year-card { @@ -185,3 +233,7 @@ from { transform: rotate(0deg); } to { transform: rotate(360deg); } } + +@keyframes dot-ellipsis { + to { width: 1.4em; } +} diff --git a/src/pages/AnnualReportPage.tsx b/src/pages/AnnualReportPage.tsx index a346c85..df7cca5 100644 --- a/src/pages/AnnualReportPage.tsx +++ b/src/pages/AnnualReportPage.tsx @@ -5,6 +5,14 @@ import './AnnualReportPage.scss' type YearOption = number | 'all' +const formatLoadElapsed = (ms: number) => { + const totalSeconds = Math.max(0, ms) / 1000 + if (totalSeconds < 60) return `${totalSeconds.toFixed(1)}s` + const minutes = Math.floor(totalSeconds / 60) + const seconds = Math.floor(totalSeconds % 60) + return `${minutes}m ${String(seconds).padStart(2, '0')}s` +} + function AnnualReportPage() { const navigate = useNavigate() const [availableYears, setAvailableYears] = useState([]) @@ -12,12 +20,33 @@ function AnnualReportPage() { const [selectedPairYear, setSelectedPairYear] = useState(null) const [isLoading, setIsLoading] = useState(true) const [isLoadingMoreYears, setIsLoadingMoreYears] = useState(false) + const [hasYearsLoadFinished, setHasYearsLoadFinished] = useState(false) + const [loadElapsedMs, setLoadElapsedMs] = useState(0) const [isGenerating, setIsGenerating] = useState(false) const [loadError, setLoadError] = useState(null) useEffect(() => { let disposed = false let taskId = '' + const loadStartedAt = Date.now() + let ticker: ReturnType | null = null + + const startTicker = () => { + setLoadElapsedMs(0) + if (ticker) clearInterval(ticker) + ticker = setInterval(() => { + if (disposed) return + setLoadElapsedMs(Date.now() - loadStartedAt) + }, 100) + } + + const stopTicker = () => { + setLoadElapsedMs(Date.now() - loadStartedAt) + if (ticker) { + clearInterval(ticker) + ticker = null + } + } const stopListen = window.electronAPI.annualReport.onAvailableYearsProgress((payload) => { if (disposed) return @@ -48,21 +77,27 @@ function AnnualReportPage() { if (payload.done) { setIsLoading(false) setIsLoadingMoreYears(false) + setHasYearsLoadFinished(true) + stopTicker() } else { setIsLoadingMoreYears(true) + setHasYearsLoadFinished(false) } }) const startLoad = async () => { setIsLoading(true) setIsLoadingMoreYears(true) + setHasYearsLoadFinished(false) setLoadError(null) + startTicker() try { const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad() if (!startResult.success || !startResult.taskId) { setLoadError(startResult.error || '加载年度数据失败') setIsLoading(false) setIsLoadingMoreYears(false) + stopTicker() return } taskId = startResult.taskId @@ -71,6 +106,7 @@ function AnnualReportPage() { setLoadError(String(e)) setIsLoading(false) setIsLoadingMoreYears(false) + stopTicker() } } @@ -78,6 +114,7 @@ function AnnualReportPage() { return () => { disposed = true + stopTicker() stopListen() if (taskId) { void window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId) @@ -109,6 +146,7 @@ function AnnualReportPage() {

正在加载年份数据(首批)...

+

已耗时 {formatLoadElapsed(loadElapsedMs)}

) } @@ -134,13 +172,36 @@ function AnnualReportPage() { return value === 'all' ? '全部时间' : `${value} 年` } + const loadedYearCount = availableYears.length + const isYearStatusComplete = hasYearsLoadFinished + const renderYearLoadStatus = () => ( +
+ {isYearStatusComplete ? ( + <>全部年份已加载完毕 + ) : ( + <> + 更多年份加载中 + + )} +
+ ) + return (

年度报告

选择年份,回顾你在微信里的点点滴滴

- {isLoadingMoreYears && ( -

已显示首批年份,正在补充更多年份...

+ {loadedYearCount > 0 && ( +

+ {isYearStatusComplete ? ( + <>已显示 {loadedYearCount} 个年份,年份数据已全部加载完毕。总耗时 {formatLoadElapsed(loadElapsedMs)} + ) : ( + <> + 已显示 {loadedYearCount} 个年份,正在补充更多年份 + (已耗时 {formatLoadElapsed(loadElapsedMs)}) + + )} +

)}
@@ -152,17 +213,20 @@ function AnnualReportPage() {
-
- {yearOptions.map(option => ( -
setSelectedYear(option)} - > - {option === 'all' ? '全部' : option} - {option === 'all' ? '时间' : '年'} -
- ))} +
+
+ {yearOptions.map(option => ( +
setSelectedYear(option)} + > + {option === 'all' ? '全部' : option} + {option === 'all' ? '时间' : '年'} +
+ ))} +
+ {renderYearLoadStatus()}
-
- {yearOptions.map(option => ( -
setSelectedPairYear(option)} - > - {option === 'all' ? '全部' : option} - {option === 'all' ? '时间' : '年'} -
- ))} +
+
+ {yearOptions.map(option => ( +
setSelectedPairYear(option)} + > + {option === 'all' ? '全部' : option} + {option === 'all' ? '时间' : '年'} +
+ ))} +
+ {renderYearLoadStatus()}