79 lines
4.0 KiB
TypeScript
79 lines
4.0 KiB
TypeScript
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;
|
||
}
|