feat(ui): 统计面板指标接入 count-up 动画并完善画布 Agent 聊天 UI (吸收 BoardUI, 尊重 reduced-motion)

This commit is contained in:
maomaochong-ai
2026-09-06 20:28:53 +08:00
parent 936f2d7551
commit dafc009e2d
3 changed files with 233 additions and 39 deletions

View File

@@ -0,0 +1,48 @@
import { useEffect, useRef, useState } from "react";
const easeOutCubic = (t: number) => 1 - Math.pow(1 - t, 3);
/**
* Rolls the displayed number from `from` (defaults to the previous target)
* to `target` with an ease-out curve, so headline figures animate when data
* refreshes or a chart selection changes.
*
* Absorbed from boardui hooks/use-count-up.ts; adapted to this project:
* - `prefers-reduced-motion` is respected: the value snaps to target at once.
* - `from` is exposed so counters can start at 0 on first mount (default
* keeps boardui semantics: start at the previous value, no initial jump).
* - `round` is applied per-frame so integer counters never show fractions.
*/
export function useCountUp(target: number, duration = 320, from?: number) {
const reduced = useRef(false);
if (typeof window !== "undefined" && !reduced.current) {
reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
}
const [display, setDisplay] = useState(from ?? target);
const fromRef = useRef(from ?? target);
useEffect(() => {
if (reduced.current) {
fromRef.current = target;
setDisplay(target);
return;
}
const from = fromRef.current;
if (from === target) return;
const start = performance.now();
let raf = 0;
const tick = (now: number) => {
const t = Math.min(1, (now - start) / duration);
const value = Math.round(from + (target - from) * easeOutCubic(t));
fromRef.current = value;
setDisplay(value);
if (t < 1) raf = requestAnimationFrame(tick);
else fromRef.current = target;
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, [target, duration]);
return display;
}