iter(g284): 新增 progressRing 环形进度组件

This commit is contained in:
HIsheR
2026-10-26 23:12:33 +08:00
parent f20f673e76
commit c4384be959
12 changed files with 239 additions and 11 deletions
+1
View File
@@ -85,3 +85,4 @@ export { loading } from './loading.js';
export { relativeTime } from './relativeTime.js';
export { sparkline } from './sparkline.js';
export { heatmap } from './heatmap.js';
export { progressRing } from './progressRing.js';
+56
View File
@@ -0,0 +1,56 @@
import { effect, h } from '@ui-arch/frame';
import type { ProgressRingProps } from './types.js';
const NS = 'http://www.w3.org/2000/svg';
// 缺省尺寸与环宽:96px 装得下百分比数字,8px 环宽在小尺寸下也不显笨
const SIZE = 96;
const THICK = 8;
/*** 建 SVG 元素并写属性:svg 必须 createElementNS,属性只能 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;
}
/*** 环形进度:SVG 双圆(底环 + 进度弧)靠 dasharray 画弧,value 变化时弧长与文字同步 ***/
export function progressRing(props: ProgressRingProps): HTMLElement {
const size = props.size ?? SIZE;
const thick = props.thickness ?? THICK;
const mid = size / 2;
const r = (size - thick) / 2; // 半径要扣掉环宽,否则弧会超出画布
const c = 2 * Math.PI * r; // 周长:dasharray 的分母
const read = (): number => (typeof props.value === 'function' ? props.value() : props.value); // 数值 / 信号同一口径
const svg = svgEl('svg', { viewBox: `0 0 ${size} ${size}`, width: size, height: size, 'aria-hidden': 'true' });
const track = svgEl('circle', { cx: mid, cy: mid, r, fill: 'none', 'stroke-width': thick, class: 'ua-ring-track' });
// 弧从 12 点方向顺时针长:整圆旋转 -90°,否则默认从 3 点位置起步
const arc = svgEl('circle', {
cx: mid, cy: mid, r, fill: 'none', 'stroke-width': thick,
class: 'ua-ring-arc', 'stroke-dasharray': `${0} ${c}`, 'stroke-linecap': 'round',
transform: `rotate(-90 ${mid} ${mid})`,
});
const text = h('span', { class: 'ua-ring-text' });
const wrap = h(
'div',
{
class: 'ua-ring',
role: 'progressbar',
'aria-valuemin': '0',
'aria-valuemax': '100',
style: `width:${size}px;height:${size}px`,
},
svg,
text,
);
svg.append(track, arc); // 底环先画,进度弧盖在上面
/*** 重绘:把百分比夹到 0-100,弧长按比例裁,文字与 aria-valuenow 同步 ***/
const paint = (): void => {
const pct = Math.min(100, Math.max(0, Math.round(read())));
arc.setAttribute('stroke-dasharray', `${(c * pct) / 100} ${c}`); // 只画这一段,剩下留空
if (props.showText !== false) text.textContent = props.label ?? `${pct}%`;
wrap.setAttribute('aria-valuenow', String(pct));
};
effect(paint);
return wrap;
}
+9
View File
@@ -1110,3 +1110,12 @@ export interface HeatmapProps {
ariaLabel?: string; // 可访问名;缺省「最近 N 周活跃度」
formatValue?: (count: number, date: string) => string; // 悬停提示文案;缺省「日期 · 数量」
}
/*** 环形进度入参 ***/
export interface ProgressRingProps {
value: number | Signal<number>; // 当前进度 0-100;超出范围自动夹取,信号变化时自动重绘
size?: number; // 外径(px);缺省 96
thickness?: number; // 环宽(px);缺省 8
showText?: boolean; // 是否显示中心文字;缺省 true(关掉时请自行保证 aria 可读)
label?: string; // 中心文字;缺省「N%」
}