iter(g276): 新增迷你趋势线组件

This commit is contained in:
HIsheR
2026-10-26 23:02:15 +08:00
parent 573d3aa8a9
commit eac7d4b337
12 changed files with 215 additions and 10 deletions
+1
View File
@@ -83,3 +83,4 @@ export { codeBlock } from './codeBlock.js';
export { signaturePad } from './signaturePad.js';
export { loading } from './loading.js';
export { relativeTime } from './relativeTime.js';
export { sparkline } from './sparkline.js';
+41
View File
@@ -0,0 +1,41 @@
import { h } from '@ui-arch/frame';
import type { SparklineProps } from './types.js';
// SVG 命名空间:createElement 不认 svg 标签,必须走 createElementNS
const NS = 'http://www.w3.org/2000/svg';
// 缺省尺寸:120×32 塞得进统计卡片的右上角
const W = 120;
const H = 32;
/*** 建 SVG 元素并批量写属性:svg 的属性都得 setAttribute,写个小工具省掉每处重复 ***/
function svgEl(tag: string, attrs: Record<string, string | number>): SVGElement {
const el = document.createElementNS(NS, tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, String(v));
return el;
}
/*** 迷你趋势线:把一串数值压成一条折线(可选面积填充 + 末点标记),不画坐标轴、不带交互,只表达走向 ***/
export function sparkline(props: SparklineProps): SVGElement {
const values = props.values.filter((v) => Number.isFinite(v)); // 脏数据先剔掉,否则整条线会被一个 undefined 拉平
const w = props.width ?? W;
const hgt = props.height ?? H;
const pad = 2; // 内缩 2px:线宽与末点圆点不会被 viewBox 边缘裁掉
const svg = svgEl('svg', { viewBox: `0 0 ${w} ${hgt}`, width: w, height: hgt, class: 'ua-spark', role: 'img', 'aria-label': props.ariaLabel ?? '趋势图' });
if (values.length < 2) return svg; // 少于两点没有「趋势」可言:给空图,别画一根假横线
const min = Math.min(...values);
const max = Math.max(...values);
const span = max - min || 1; // 全等值时除零保护:所有点落在中线
const x = (i: number): number => pad + (i * (w - pad * 2)) / (values.length - 1); // 首末点贴边,中间均分
const y = (v: number): number => pad + (1 - (v - min) / span) * (hgt - pad * 2); // 值越大越靠上(SVG y 轴向下)
const points = values.map((v, i) => `${x(i).toFixed(2)},${y(v).toFixed(2)}`).join(' ');
if (props.area !== false) {
// 面积:折线两端下探到底边闭合,填充走 currentColor 的低透明度(颜色由 CSS 的 color 决定)
svg.append(svgEl('polygon', { points: `${points} ${x(values.length - 1).toFixed(2)},${hgt} ${x(0).toFixed(2)},${hgt}`, class: 'ua-spark-area' }));
}
svg.append(svgEl('polyline', { points, class: 'ua-spark-line', 'stroke-linejoin': 'round', 'stroke-linecap': 'round' }));
if (props.marker !== false) {
// 末点标记:一眼看出当前值落在哪,不用去数最后一段的斜率
svg.append(svgEl('circle', { cx: x(values.length - 1).toFixed(2), cy: y(values[values.length - 1]).toFixed(2), r: 2, class: 'ua-spark-dot' }));
}
return svg;
}
+10
View File
@@ -1087,3 +1087,13 @@ export interface RelativeTimeProps {
fallback?: string; // 时间非法时的占位文案;缺省「—」
ariaLabel?: string; // 可访问名(如「下单时间」),让读屏知道这个时间是什么
}
/*** 迷你趋势线入参 ***/
export interface SparklineProps {
values: number[]; // 数值序列(按时间顺序);非有限数会被剔除
width?: number; // 宽度(px);缺省 120
height?: number; // 高度(px);缺省 32
area?: boolean; // 是否填充折线下方面积;缺省 true
marker?: boolean; // 是否标记末点;缺省 true(一眼看出当前值位置)
ariaLabel?: string; // 可访问名;缺省「趋势图」(趋势无法只靠图形传达,请把结论写进旁边文案)
}
+47 -1
View File
@@ -97,7 +97,8 @@
"codeBlock(props):代码块,等宽多行 + 行号列 + 标题行 + 一键复制原文,只展示不高亮",
"signaturePad(props):手写签名板,指针绘制 + DPR 位图,返回 handle(el / clear / toDataURL / isEmpty)",
"loading(props, ...children):区域加载遮罩,内容原地降透明度 + 转圈文案,带延迟出场避免快请求闪烁",
"relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值"
"relativeTime(props):把时间戳渲染成「3 分钟前」并自动刷新,超阈值改显示绝对日期,title 恒给精确值",
"sparkline(props):迷你趋势线,把数值序列压成折线(可填充 + 末点标记),无坐标轴无交互"
],
"keyTypes": [
"ButtonProps",
@@ -4300,6 +4301,51 @@
"精确时间放在 title:鼠标悬停可看,不必为「想知道具体哪天」再点开详情",
"时间非法(空串 / 格式错)不猜也不抛,显示 fallback(缺省「—」),列表不会因一条脏数据空白"
]
},
{
"id": "sparkline",
"title": "迷你趋势线 Sparkline",
"demo": "sec-spark",
"props": [
{
"n": "values",
"t": "number[]",
"d": "数值序列(按时间顺序);非有限数会被剔除"
},
{
"n": "width",
"t": "number",
"d": "宽度(px);缺省 120"
},
{
"n": "height",
"t": "number",
"d": "高度(px);缺省 32"
},
{
"n": "area",
"t": "boolean",
"d": "是否填充折线下方面积;缺省 true"
},
{
"n": "marker",
"t": "boolean",
"d": "是否标记末点;缺省 true(一眼看出当前值位置)"
},
{
"n": "ariaLabel",
"t": "string",
"d": "可访问名;缺省「趋势图」"
}
],
"example": "sparkline({ values: last7Days, width: 140, height: 36 })",
"steps": [
"把按时间顺序的数值数组交给 values:不用自己算最大最小与像素坐标,组件按 viewBox 等比缩放",
"和 statistic 并排用:主数值走 statistic、走向走 sparkline,趋势线不画坐标轴也不带交互",
"不要面积就传 area: false,不要末点圆点传 marker: false(极窄的表格单元格里圆点会糊成一团)",
"改颜色在父级设 color:线、面积、圆点都读 currentColor,放进 card 或表格行自动跟随文字色",
"少于两个点时画不出趋势,组件返回空图——请在外面判空并给「暂无数据」,别让它空着占位"
]
}
]
}
+22
View File
@@ -5786,3 +5786,25 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); }
font-variant-numeric: tabular-nums; /* 等宽数字:列表里时间列不左右抖动 */
white-space: nowrap; /* 「3 分钟前」不该被折成两行 */
}
/* ---- 迷你趋势线:颜色一律走 currentColor,因此放进任何容器都自动跟随该处文字色 ---- */
.ua-spark {
display: block;
overflow: visible; /* 末点圆点可能压在边缘,别被 svg 自身裁掉 */
color: var(--ua-color-accent); /* 缺省跟主题强调色;想换色在父级改 color 即可 */
}
.ua-spark-line {
fill: none;
stroke: currentColor;
stroke-width: 1.5; /* 细线:趋势线是辅助信息,不该抢主数值 */
}
.ua-spark-area {
fill: color-mix(in srgb, currentColor var(--ua-alpha-tint), transparent); /* 面积比线更淡一层,避免压过折线 */
stroke: none;
}
.ua-spark-dot {
fill: currentColor;
}