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

79 lines
4.0 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 { Signal } from '@ui-arch/frame';
import type { ProgressProps } from './types.js';
/*** 进度条:value 信号驱动 0-100 定量态(缺省则不确定态),type='circle' 且传 value 时出环形;满格切 success 色,format 可自定义文本 ***/
export function progress(props: ProgressProps = {}): HTMLElement {
// —— 状态集中声明 ——
const value = props.value; // 可选:不传即不确定态
const fill = h('div', { class: 'ua-progress-fill' });
const track = h('div', { class: 'ua-progress-track' }, fill);
const text = h('span', { class: 'ua-progress-text' });
const withText = props.showText !== false && !!value; // 不确定态没有百分比可显示
const label = props.label; // 可访问名:读屏念「上传进度 50%」而不是干巴巴的百分比
const variant = props.variant ?? 'default'; // 语义色:default 之外都带类名,满格时仍以 variant 为准
const cls = `ua-progress${variant === 'default' ? '' : ` ua-progress-${variant}`}`;
const textOf = (v: number): string => (props.format ? props.format(v) : `${Math.round(v)}%`); // 进度文本统一出口:format 优先
if (!value) {
fill.classList.add('ua-indeterminate'); // 宽度与往复动画交给 CSS
return h('div', { class: cls, role: 'progressbar', 'aria-label': label ?? '进行中' }, track);
}
if (props.type === 'circle') return ring(value, label, variant, withText, textOf); // 环形:SVG 版
effect(() => {
const v = clamp(value()); // 依赖收集:信号变化自动写回
fill.style.width = `${v}%`;
fill.classList.toggle('ua-done', v >= 100); // 满格切语义色
text.textContent = textOf(v);
});
return h(
'div',
{
class: cls,
role: 'progressbar',
'aria-label': label,
'aria-valuemin': '0',
'aria-valuemax': '100',
'aria-valuenow': () => String(Math.round(clamp(value()))),
},
track,
withText ? text : null,
);
}
/*** 进度钳制到 0-100 ***/
function clamp(v: number): number {
if (Number.isNaN(v)) return 0; // 卫语句:非法值归零
return Math.min(100, Math.max(0, v));
}
/*** 环形进度:SVG 双圆用 dashoffset 表达弧长(必须 createElementNS,h() 造不出可渲染的 SVG) ***/
function ring(value: Signal<number>, label: string | undefined, variant: string, withText: boolean, textOf: (v: number) => string): HTMLElement {
const NS = 'http://www.w3.org/2000/svg'; // SVG 命名空间
const R = 26; // 半径:viewBox 64 两侧留出线宽
const C = 2 * Math.PI * R; // 周长:dasharray 的基准长度
const svg = document.createElementNS(NS, 'svg');
svg.setAttribute('viewBox', '0 0 64 64');
svg.setAttribute('class', `ua-progress-ring${variant === 'default' ? '' : ` is-${variant}`}`);
svg.setAttribute('role', 'img');
const base = { cx: '32', cy: '32', r: String(R), fill: 'none', 'stroke-width': '6' }; // 两圆共用几何
const track = document.createElementNS(NS, 'circle');
const arc = document.createElementNS(NS, 'circle');
for (const [k, v] of Object.entries({ ...base, class: 'ua-progress-ring-track' })) track.setAttribute(k, v);
for (const [k, v] of Object.entries({ ...base, class: 'ua-progress-ring-arc', 'stroke-dasharray': String(C), transform: 'rotate(-90 32 32)' })) arc.setAttribute(k, v); // 从 12 点起画
svg.append(track, arc);
const txt = document.createElementNS(NS, 'text');
txt.setAttribute('x', '32');
txt.setAttribute('y', '36');
txt.setAttribute('text-anchor', 'middle');
txt.setAttribute('class', 'ua-progress-ring-text');
if (withText) svg.append(txt);
effect(() => {
const v = clamp(value());
arc.setAttribute('stroke-dashoffset', String(C * (1 - v / 100))); // 弧长按比例收缩
txt.textContent = textOf(v);
svg.setAttribute('aria-label', `${label ?? '进度'} ${textOf(v)}`); // 环形没有原生 progressbar,名称自己拼
});
return h('div', { class: 'ua-progress ua-progress-circle' }, svg as unknown as HTMLElement) as HTMLElement;
}