fix(ui): 切换日期后数字不更新,卡片显示的是前一天的数据
useCountUp 只靠 requestAnimationFrame 推进,没有兜底。帧不来的时候 (后台标签页、被节流的 webview)setDisplay 一次都不会调用,组件继续 渲染上一个值——往前翻一天,标题的日期变了,但每一个数字还停在前一天。 不是动画没播,是把另一天的数据当成这一天理直气壮地显示出来。 实测(造的数据):08-24 → 08-23 → 08-22 三天,三张卡片全都显示 8,826, 而真实值是 8,826 / 10,232 / 10,715。 - 动画结束时间点加一个 setTimeout 兜底,保证最终值一定落地 - document.hidden 时直接跳过动画 - from 引用在收敛时同步更新,避免动画被打断后从错误的起点继续 动画是装饰,数字不是。 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,11 +36,19 @@ export function useCountUp(value: number | null, durationMs = 650): number | nul
|
||||
const reduced = usePrefersReducedMotion();
|
||||
const [display, setDisplay] = useState<number | null>(value);
|
||||
const frame = useRef<number>();
|
||||
const settleTimer = useRef<number>();
|
||||
const from = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (value == null || reduced) {
|
||||
const settle = () => {
|
||||
setDisplay(value);
|
||||
from.current = value ?? 0;
|
||||
};
|
||||
|
||||
// Nothing to animate towards, or the viewer asked for no motion. Also
|
||||
// covers a hidden tab, where requestAnimationFrame does not run at all.
|
||||
if (value == null || reduced || document.hidden) {
|
||||
settle();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,12 +60,23 @@ export function useCountUp(value: number | null, durationMs = 650): number | nul
|
||||
const t = Math.min(1, (now - start) / durationMs);
|
||||
setDisplay(origin + delta * easeOut(t));
|
||||
if (t < 1) frame.current = requestAnimationFrame(tick);
|
||||
else from.current = value;
|
||||
else settle();
|
||||
};
|
||||
|
||||
frame.current = requestAnimationFrame(tick);
|
||||
|
||||
/* The animation is decoration; the number is not.
|
||||
This used to be rAF alone, so when frames never arrived — a
|
||||
backgrounded tab, a throttled webview — `setDisplay` was never called
|
||||
and the card kept rendering the *previous* value. Stepping back a day
|
||||
changed the date in the header while every figure stayed on the day
|
||||
before: not a missing animation, but the wrong data shown confidently.
|
||||
The timer guarantees the final value lands either way. */
|
||||
settleTimer.current = window.setTimeout(settle, durationMs + 250);
|
||||
|
||||
return () => {
|
||||
if (frame.current) cancelAnimationFrame(frame.current);
|
||||
window.clearTimeout(settleTimer.current);
|
||||
};
|
||||
}, [value, durationMs, reduced]);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user