perf: 页面切换零延迟渲染、消除动态 chunk 加载白屏与二次 loading 闪烁、重构微滑淡入过渡
TallyNote release / linux-x64 (push) Failing after 9m14s

This commit is contained in:
Qiufeng
2026-09-04 17:29:52 +08:00
parent db37406498
commit 32f768c8ee
6 changed files with 36 additions and 20 deletions
@@ -18,6 +18,8 @@ echarts.use([LineChart, GridComponent, LegendComponent, TooltipComponent, Canvas
type Props = { timezone?: string; onNavigate?: (id: RouteId, search?: string) => void };
type ExpenseResult = { items: Expense[]; summary: { count: number; amountCents: number } };
let dashboardCache: { month: string; unreimbursed: ExpenseResult; reimbursed: ExpenseResult } | null = null;
function prefersReducedMotion(): boolean {
return typeof window !== "undefined" && typeof window.matchMedia === "function"
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
@@ -29,11 +31,12 @@ export default function DashboardPage({ timezone = "Asia/Shanghai", onNavigate }
const rawMonthParam = searchParams.get("month");
const defaultMonth = monthNow(timezone);
const month = rawMonthParam && /^\d{4}-(0[1-9]|1[0-2])$/.test(rawMonthParam) ? rawMonthParam : defaultMonth;
const [unreimbursed, setUnreimbursed] = useState<ExpenseResult>({ items: [], summary: { count: 0, amountCents: 0 } });
const [reimbursed, setReimbursed] = useState<ExpenseResult>({ items: [], summary: { count: 0, amountCents: 0 } });
const [loading, setLoading] = useState(true);
const isCached = dashboardCache?.month === month;
const [unreimbursed, setUnreimbursed] = useState<ExpenseResult>(() => (isCached ? dashboardCache!.unreimbursed : { items: [], summary: { count: 0, amountCents: 0 } }));
const [reimbursed, setReimbursed] = useState<ExpenseResult>(() => (isCached ? dashboardCache!.reimbursed : { items: [], summary: { count: 0, amountCents: 0 } }));
const [loading, setLoading] = useState(() => !isCached);
const [error, setError] = useState("");
const [loadedMonth, setLoadedMonth] = useState<string | null>(null);
const [loadedMonth, setLoadedMonth] = useState<string | null>(() => (isCached ? month : null));
const requestSequence = useRef(0);
const [reducedMotion, setReducedMotion] = useState(prefersReducedMotion);
@@ -75,6 +78,7 @@ export default function DashboardPage({ timezone = "Asia/Shanghai", onNavigate }
setUnreimbursed(pending);
setReimbursed(done);
setLoadedMonth(month);
dashboardCache = { month, unreimbursed: pending, reimbursed: done };
} catch (caught) {
if (sequence === requestSequence.current) setError((caught as Error).message);
} finally {
@@ -141,7 +145,7 @@ export default function DashboardPage({ timezone = "Asia/Shanghai", onNavigate }
<div className="tn-page-actions"><div className="tn-dashboard-actions"><div className="tn-dashboard-month-control"><Tooltip content="上个月"><Button variant="outline" shape="square" onClick={() => shiftMonth(-1)} aria-label="上个月" icon={<ChevronLeft size={16} />} /></Tooltip><DatePicker className="tn-dashboard-month" mode="month" format="YYYY-MM" value={month} onChange={(value: any) => { const next = String(value || "").slice(0, 7); if (/^\d{4}-\d{2}$/.test(next)) setDashboardMonth(next); }} inputProps={{ "aria-label": "仪表盘月份" } as any} /><Tooltip content="下个月"><Button variant="outline" shape="square" onClick={() => shiftMonth(1)} aria-label="下个月" icon={<ChevronRight size={16} />} /></Tooltip></div><div className="tn-dashboard-secondary-actions"><Button className="tn-dashboard-refresh" variant="outline" onClick={() => void load()} disabled={loading} icon={<RefreshCw size={15} />}>刷新</Button><Button className="tn-dashboard-view" theme="primary" onClick={() => onNavigate?.("expenses", expensesSearch)}>查看账目</Button></div></div></div>
</div>
{error && hasCurrentSnapshot && <Alert theme="error" icon={<AlertCircle size={16} />} message={`刷新失败:${error}。当前展示的是本月最近一次成功加载的数据。`} />}
{loading ? <div className="tn-empty" role="status" aria-live="polite"><BeamLoading text="正在汇总本月数据…" /></div> : !hasCurrentSnapshot ? <div className="tn-empty" role="alert"><Alert theme="error" icon={<AlertCircle size={16} />} message={error || "暂时无法读取仪表盘数据"} /><Button variant="outline" onClick={() => void load()} icon={<RefreshCw size={15} />}>重新加载</Button></div> : <>
{loading && !hasCurrentSnapshot ? <div className="tn-empty" role="status" aria-live="polite"><BeamLoading text="正在汇总本月数据…" /></div> : !hasCurrentSnapshot ? <div className="tn-empty" role="alert"><Alert theme="error" icon={<AlertCircle size={16} />} message={error || "暂时无法读取仪表盘数据"} /><Button variant="outline" onClick={() => void load()} icon={<RefreshCw size={15} />}>重新加载</Button></div> : <>
<div className="tn-dashboard-stats">
<Card bordered className="tn-dashboard-stat tn-dashboard-stat-total"><Statistic title="本月总额" value={totalCents / 100} prefix="¥" decimalPlaces={2} /></Card>
<Card bordered className="tn-dashboard-stat tn-dashboard-stat-count"><Statistic title="账目笔数" value={totalCount} suffix="笔" /></Card>