Files
UI/packages/skin/ui/src/statistic.ts
T

42 lines
2.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { effect, h } from '@ui-arch/frame';
import type { StatisticProps } from './types.js';
/*** 统计数值:标题 + 大号数字 + 前后缀 + 环比趋势,数值变化时 rAF 滚动过渡(400ms,easeOutCubic) ***/
export function statistic(props: StatisticProps): HTMLElement {
// —— 状态集中声明 ——
const precision = props.precision ?? 0; // 小数位
const reduce = typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches; // 减弱动效偏好
const num = h('span', { class: 'ua-stat-num' }, '0'); // 数字节点:逐帧改文本,不重建 DOM
let shown = 0; // 当前显示中的值(动画中间态)
let raf = 0; // 进行中的动画帧句柄,重复调用时先取消
/*** 格式化:千分位 + 固定小数位 ***/
const fmt = (v: number): string => v.toLocaleString('zh-CN', { minimumFractionDigits: precision, maximumFractionDigits: precision });
/*** 从当前显示值滚到目标值;偏好减弱动效时直接落终值 ***/
const roll = (to: number): void => {
cancelAnimationFrame(raf); // 新目标打断上一段动画,避免数值打架
if (reduce) { shown = to; num.textContent = fmt(to); return; }
const from = shown;
const start = performance.now();
const step = (t: number): void => {
const k = Math.min((t - start) / 400, 1); // 归一化进度 0→1
shown = from + (to - from) * (1 - (1 - k) ** 3); // easeOutCubic 缓出
num.textContent = fmt(shown);
if (k < 1) raf = requestAnimationFrame(step);
else shown = to; // 收尾对齐目标值,避免浮点残差
};
raf = requestAnimationFrame(step);
};
const row = h('div', { class: 'ua-stat-value' },
props.prefix ? h('span', { class: 'ua-stat-affix' }, props.prefix) : null,
num,
props.suffix ? h('span', { class: 'ua-stat-affix' }, props.suffix) : null,
props.trend ? h('span', { class: `ua-stat-trend is-${props.trend}` }, props.trend === 'up' ? '▲' : '▼') : null);
const view = h('div', { class: 'ua-stat' }, props.title ? h('div', { class: 'ua-stat-title' }, props.title) : null, row);
effect(() => roll(props.value())); // 信号变化即滚动到新值(首跑做入场动画)
return view;
}