feat(report): improve years loading status messaging

This commit is contained in:
tisonhuang
2026-03-04 16:43:02 +08:00
parent 28d68d8a8e
commit 16e237b698
2 changed files with 144 additions and 25 deletions

View File

@@ -26,6 +26,14 @@
margin: 0 0 48px; margin: 0 0 48px;
} }
.page-desc.load-summary {
margin: 0 0 28px;
}
.page-desc.load-summary.complete {
color: var(--text-secondary);
}
.report-sections { .report-sections {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -83,6 +91,14 @@
color: var(--text-tertiary); 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 { .year-grid {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -95,7 +111,39 @@
.report-section .year-grid { .report-section .year-grid {
justify-content: flex-start; justify-content: flex-start;
max-width: none; 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 { .year-card {
@@ -185,3 +233,7 @@
from { transform: rotate(0deg); } from { transform: rotate(0deg); }
to { transform: rotate(360deg); } to { transform: rotate(360deg); }
} }
@keyframes dot-ellipsis {
to { width: 1.4em; }
}

View File

@@ -5,6 +5,14 @@ import './AnnualReportPage.scss'
type YearOption = number | 'all' 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() { function AnnualReportPage() {
const navigate = useNavigate() const navigate = useNavigate()
const [availableYears, setAvailableYears] = useState<number[]>([]) const [availableYears, setAvailableYears] = useState<number[]>([])
@@ -12,12 +20,33 @@ function AnnualReportPage() {
const [selectedPairYear, setSelectedPairYear] = useState<YearOption | null>(null) const [selectedPairYear, setSelectedPairYear] = useState<YearOption | null>(null)
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const [isLoadingMoreYears, setIsLoadingMoreYears] = useState(false) const [isLoadingMoreYears, setIsLoadingMoreYears] = useState(false)
const [hasYearsLoadFinished, setHasYearsLoadFinished] = useState(false)
const [loadElapsedMs, setLoadElapsedMs] = useState(0)
const [isGenerating, setIsGenerating] = useState(false) const [isGenerating, setIsGenerating] = useState(false)
const [loadError, setLoadError] = useState<string | null>(null) const [loadError, setLoadError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
let disposed = false let disposed = false
let taskId = '' let taskId = ''
const loadStartedAt = Date.now()
let ticker: ReturnType<typeof setInterval> | 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) => { const stopListen = window.electronAPI.annualReport.onAvailableYearsProgress((payload) => {
if (disposed) return if (disposed) return
@@ -48,21 +77,27 @@ function AnnualReportPage() {
if (payload.done) { if (payload.done) {
setIsLoading(false) setIsLoading(false)
setIsLoadingMoreYears(false) setIsLoadingMoreYears(false)
setHasYearsLoadFinished(true)
stopTicker()
} else { } else {
setIsLoadingMoreYears(true) setIsLoadingMoreYears(true)
setHasYearsLoadFinished(false)
} }
}) })
const startLoad = async () => { const startLoad = async () => {
setIsLoading(true) setIsLoading(true)
setIsLoadingMoreYears(true) setIsLoadingMoreYears(true)
setHasYearsLoadFinished(false)
setLoadError(null) setLoadError(null)
startTicker()
try { try {
const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad() const startResult = await window.electronAPI.annualReport.startAvailableYearsLoad()
if (!startResult.success || !startResult.taskId) { if (!startResult.success || !startResult.taskId) {
setLoadError(startResult.error || '加载年度数据失败') setLoadError(startResult.error || '加载年度数据失败')
setIsLoading(false) setIsLoading(false)
setIsLoadingMoreYears(false) setIsLoadingMoreYears(false)
stopTicker()
return return
} }
taskId = startResult.taskId taskId = startResult.taskId
@@ -71,6 +106,7 @@ function AnnualReportPage() {
setLoadError(String(e)) setLoadError(String(e))
setIsLoading(false) setIsLoading(false)
setIsLoadingMoreYears(false) setIsLoadingMoreYears(false)
stopTicker()
} }
} }
@@ -78,6 +114,7 @@ function AnnualReportPage() {
return () => { return () => {
disposed = true disposed = true
stopTicker()
stopListen() stopListen()
if (taskId) { if (taskId) {
void window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId) void window.electronAPI.annualReport.cancelAvailableYearsLoad(taskId)
@@ -109,6 +146,7 @@ function AnnualReportPage() {
<div className="annual-report-page"> <div className="annual-report-page">
<Loader2 size={32} className="spin" style={{ color: 'var(--text-tertiary)' }} /> <Loader2 size={32} className="spin" style={{ color: 'var(--text-tertiary)' }} />
<p style={{ color: 'var(--text-tertiary)', marginTop: 16 }}>...</p> <p style={{ color: 'var(--text-tertiary)', marginTop: 16 }}>...</p>
<p style={{ color: 'var(--text-tertiary)', marginTop: 8 }}> {formatLoadElapsed(loadElapsedMs)}</p>
</div> </div>
) )
} }
@@ -134,13 +172,36 @@ function AnnualReportPage() {
return value === 'all' ? '全部时间' : `${value}` return value === 'all' ? '全部时间' : `${value}`
} }
const loadedYearCount = availableYears.length
const isYearStatusComplete = hasYearsLoadFinished
const renderYearLoadStatus = () => (
<div className={`year-load-status ${isYearStatusComplete ? 'complete' : 'loading'}`}>
{isYearStatusComplete ? (
<></>
) : (
<>
<span className="dot-ellipsis" aria-hidden="true">...</span>
</>
)}
</div>
)
return ( return (
<div className="annual-report-page"> <div className="annual-report-page">
<Sparkles size={32} className="header-icon" /> <Sparkles size={32} className="header-icon" />
<h1 className="page-title"></h1> <h1 className="page-title"></h1>
<p className="page-desc"></p> <p className="page-desc"></p>
{isLoadingMoreYears && ( {loadedYearCount > 0 && (
<p className="page-desc">...</p> <p className={`page-desc load-summary ${isYearStatusComplete ? 'complete' : 'loading'}`}>
{isYearStatusComplete ? (
<> {loadedYearCount} {formatLoadElapsed(loadElapsedMs)}</>
) : (
<>
{loadedYearCount} <span className="dot-ellipsis" aria-hidden="true">...</span>
{formatLoadElapsed(loadElapsedMs)}
</>
)}
</p>
)} )}
<div className="report-sections"> <div className="report-sections">
@@ -152,17 +213,20 @@ function AnnualReportPage() {
</div> </div>
</div> </div>
<div className="year-grid"> <div className="year-grid-with-status">
{yearOptions.map(option => ( <div className="year-grid">
<div {yearOptions.map(option => (
key={option} <div
className={`year-card ${option === 'all' ? 'all-time' : ''} ${selectedYear === option ? 'selected' : ''}`} key={option}
onClick={() => setSelectedYear(option)} className={`year-card ${option === 'all' ? 'all-time' : ''} ${selectedYear === option ? 'selected' : ''}`}
> onClick={() => setSelectedYear(option)}
<span className="year-number">{option === 'all' ? '全部' : option}</span> >
<span className="year-label">{option === 'all' ? '时间' : '年'}</span> <span className="year-number">{option === 'all' ? '全部' : option}</span>
</div> <span className="year-label">{option === 'all' ? '时间' : '年'}</span>
))} </div>
))}
</div>
{renderYearLoadStatus()}
</div> </div>
<button <button
@@ -196,17 +260,20 @@ function AnnualReportPage() {
</div> </div>
</div> </div>
<div className="year-grid"> <div className="year-grid-with-status">
{yearOptions.map(option => ( <div className="year-grid">
<div {yearOptions.map(option => (
key={`pair-${option}`} <div
className={`year-card ${option === 'all' ? 'all-time' : ''} ${selectedPairYear === option ? 'selected' : ''}`} key={`pair-${option}`}
onClick={() => setSelectedPairYear(option)} className={`year-card ${option === 'all' ? 'all-time' : ''} ${selectedPairYear === option ? 'selected' : ''}`}
> onClick={() => setSelectedPairYear(option)}
<span className="year-number">{option === 'all' ? '全部' : option}</span> >
<span className="year-label">{option === 'all' ? '时间' : '年'}</span> <span className="year-number">{option === 'all' ? '全部' : option}</span>
</div> <span className="year-label">{option === 'all' ? '时间' : '年'}</span>
))} </div>
))}
</div>
{renderYearLoadStatus()}
</div> </div>
<button <button