iter(g10): 新增 statistic 统计数值组件

This commit is contained in:
HIsheR
2026-10-26 10:25:07 +08:00
parent cf65939af4
commit 2f8ccb68d8
12 changed files with 260 additions and 12 deletions
+1
View File
@@ -31,6 +31,7 @@ export { rate } from './rate.js';
export { empty } from './empty.js';
export { divider } from './divider.js';
export { descriptions } from './descriptions.js';
export { statistic } from './statistic.js';
export { timeline } from './timeline.js';
export { modal } from './modal.js';
export { drawer } from './drawer.js';
+41
View File
@@ -0,0 +1,41 @@
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;
}
+10
View File
@@ -388,4 +388,14 @@ export interface DescriptionsProps {
title?: string; // 分组标题;缺省不渲染
}
/*** 统计数值入参 ***/
export interface StatisticProps {
title?: string; // 指标名;缺省不渲染标题行
value: Signal<number>; // 数值信号:变化时滚动过渡到新值(首跑即入场动画)
precision?: number; // 小数位;缺省 0
prefix?: string; // 数字前缀(如 ¥);缺省不渲染
suffix?: string; // 数字后缀(如 元 / %);缺省不渲染
trend?: 'up' | 'down'; // 环比方向:up 红 / down 绿(A 股口径);缺省不渲染
}
export type { ColumnDef, RowKey }; // 再导出常用内核类型,方便调用方
+52 -2
View File
@@ -50,7 +50,8 @@
"image(props):图片,缩略图点击 / 回车开遮罩大图预览(Esc 或点击遮罩关闭),width 控制缩略宽",
"timeline(props):时间线,items 按序渲染纵向事件流(标题 / 时间 / 正文 + 语义色节点),mode 控制轴线左右,pending 追加进行中节点",
"segmented(props):分段控制器,胶囊分段 + 滑动滑块,value 信号双向绑定,←→ 环形导航,整组或单项可禁用",
"descriptions(props):描述列表,items(label/value/span) 铺进 column 列网格,bordered 切表格形态,窄屏自动单列"
"descriptions(props):描述列表,items(label/value/span) 铺进 column 列网格,bordered 切表格形态,窄屏自动单列",
"statistic(props):统计数值,标题 + 大号数字 + 前后缀 + 环比趋势,value 变化时 rAF 滚动过渡(400ms easeOutCubic),减弱动效偏好下直落终值"
],
"keyTypes": [
"ButtonProps",
@@ -92,7 +93,8 @@
"TimelineProps",
"SegmentedProps",
"DescriptionItem",
"DescriptionsProps"
"DescriptionsProps",
"StatisticProps"
]
},
"usage": "const checked = signal(false); checkbox({ label: '同意', checked }); const open = signal(false); modal({ title: '确认', open }, '内容')",
@@ -1582,6 +1584,54 @@
"窄屏自动降为单列,无需在调用方写媒体查询",
"纯展示组件,无状态无回调:值变了由调用方重新渲染或传函数式 value"
]
},
{
"id": "statistic",
"title": "统计数值 Statistic",
"demo": "sec-statistic",
"props": [
{
"n": "title",
"t": "string",
"d": "指标名(小字灰标题);缺省不渲染"
},
{
"n": "value",
"t": "Signal<number>",
"r": 1,
"d": "数值信号:变化时从当前显示值滚动到新值,首跑即入场动画"
},
{
"n": "precision",
"t": "number",
"d": "小数位,千分位分隔自动带;缺省 0",
"df": "0"
},
{
"n": "prefix",
"t": "string",
"d": "数字前缀(如 ¥);缺省不渲染"
},
{
"n": "suffix",
"t": "string",
"d": "数字后缀(如 元 / %);缺省不渲染"
},
{
"n": "trend",
"t": "'up' | 'down'",
"d": "环比方向:up 红 / down 绿(A 股涨红跌绿口径);缺省不渲染"
}
],
"example": "const gmv = signal(128000); statistic({ title: '本月 GMV', value: gmv, prefix: '¥', precision: 2, trend: 'up' })",
"steps": [
"value 必须传信号:组件用 effect 监听,值变了自动滚动过渡,不要在外部手动改文本",
"首跑也会从 0 滚到目标值(入场动画);不想要就初始化信号为终值后在挂载后再改",
"precision 控制小数位,千分位由 toLocaleString 自动加,不要自己拼格式化",
"trend 只表达方向与颜色(涨红跌绿),数值本身与环比幅度仍由调用方组织",
"多条指标并排时用 descriptions 或 flex 容器排列,本组件不自带网格",
"用户开启「减弱动效」时组件直接落终值,无需调用方判断"
]
}
]
}
+42
View File
@@ -1880,3 +1880,45 @@ button.ua-step-dot:hover {
grid-template-columns: minmax(0, 1fr);
}
}
/* —— 统计数值:大号等宽数字 + 前后缀 + 环比趋势,涨红跌绿 —— */
.ua-stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.ua-stat-title {
color: var(--ua-color-text-muted);
font-size: var(--ua-font-size-sm);
}
.ua-stat-value {
display: inline-flex;
align-items: baseline;
gap: 3px;
}
.ua-stat-num {
color: var(--ua-color-text);
font-size: 26px;
font-weight: 600;
line-height: 1.2;
font-variant-numeric: tabular-nums;
}
.ua-stat-affix {
color: var(--ua-color-text-muted);
font-size: var(--ua-font-size-sm);
}
.ua-stat-trend {
font-size: var(--ua-font-size-sm);
margin-left: 2px;
}
.ua-stat-trend.is-up {
color: var(--ua-color-danger);
}
.ua-stat-trend.is-down {
color: var(--ua-color-success);
}
@media (prefers-reduced-motion: reduce) {
.ua-stat-num {
transition: none !important;
}
}