diff --git a/.ua/backlog.json b/.ua/backlog.json index 4af5989..801433c 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1524,6 +1524,14 @@ "acceptance": "slider 新增 marks(value/label),按 min-max 换算百分比绝对定位、随宽度自适应;轨道包一层不影响右侧数值;刻度 aria-hidden;文档与演示同步;npm run check 全绿", "notes": "自补:既有组件可用性细节", "status": "done" + }, + { + "id": "g146", + "title": "进度条支持环形形态", + "motivation": "看板与上传卡片普遍要环形进度,progress 只有线性条:调用方只能自带 SVG 并对齐语义色与百分比文字", + "acceptance": "progress 新增 type=line|circle,环形走 createElementNS 的 SVG 双圆 + dashoffset,中间显示百分比、aria-label 拼名称与数值,variant 换弧色;linear 行为不变;文档与演示同步;npm run check 全绿", + "notes": "自补:既有组件可用性细节(坑:h() 用 createElement 造不出可渲染 SVG,必须 createElementNS)", + "status": "open" } ] } diff --git a/.ua/index.json b/.ua/index.json index 7054936..fdc95ca 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T07:41:03.284Z", + "generatedAt": "2026-10-26T07:43:07.847Z", "repoRoot": ".", "packages": [ { @@ -1341,6 +1341,12 @@ "t": "Signal", "d": "当前进度 0-100 的信号;不传即不确定态(未知时长的加载)" }, + { + "n": "type", + "t": "'line' | 'circle'", + "df": "line", + "d": "形态:线性条或环形;环形需同时传 value(不确定态仍走线性条)" + }, { "n": "showText", "t": "boolean", @@ -1361,7 +1367,8 @@ "steps": [ "传 value 信号给定量进度,不传则退化为往复的不确定态", "label 写清这是什么进度,读屏会把名称与百分比一起念出来", - "失败或告警用 variant: 'danger' / 'warning',该颜色在满格时也不被 success 覆盖" + "失败或告警用 variant: 'danger' / 'warning',该颜色在满格时也不被 success 覆盖", + "看板 / 上传卡片要环形进度就传 type: 'circle'(同时必须传 value):SVG 双圆按 dashoffset 出弧,中间自动显示百分比,语义色同 variant" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index ca9d210..5c48d66 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -452,6 +452,7 @@ const content = h( alert({ message: '警告提示:操作可能存在风险。', variant: 'warning' }), h('div', { class: 'row' }, spinner(), spinner({ size: 26 }), spinner({ size: 22, label: '正在加载订单列表' })), progress({ value: percent, label: '上传进度' }), + h('div', { class: 'row' }, progress({ value: percent, type: 'circle', label: '上传进度(环形)' }), progress({ value: percent, type: 'circle', variant: 'success', label: '同值换语义色' })), h('div', { class: 'row' }, h('span', { class: 'out' }, '语义色:'), progress({ value: percent, variant: 'danger', label: '同步失败重试进度' })), h('div', { class: 'row' }, h('span', { class: 'out' }, '不确定态:'), progress({ label: '正在连接' })), ), diff --git a/llms-full.txt b/llms-full.txt index cd5f7ae..fa87d43 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T07:41:03.284Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T07:43:07.847Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -667,6 +667,7 @@ spinner(); spinner({ size: 26 }) 参数: - value:Signal —— 当前进度 0-100 的信号;不传即不确定态(未知时长的加载) +- type(默认 line):'line' | 'circle' —— 形态:线性条或环形;环形需同时传 value(不确定态仍走线性条) - showText:boolean —— 是否右侧显示百分比;缺省 true(不确定态忽略) - label:string —— 可访问名,如「上传进度」;缺省不确定态念「进行中」 - variant:'default' | 'success' | 'warning' | 'danger' —— 填充语义色;缺省 default(满格自动切 success) @@ -682,6 +683,7 @@ const pct = signal(0); progress({ value: pct }); setInterval(() => pct((pct() + 1. 传 value 信号给定量进度,不传则退化为往复的不确定态 2. label 写清这是什么进度,读屏会把名称与百分比一起念出来 3. 失败或告警用 variant: 'danger' / 'warning',该颜色在满格时也不被 success 覆盖 +4. 看板 / 上传卡片要环形进度就传 type: 'circle'(同时必须传 value):SVG 双圆按 dashoffset 出弧,中间自动显示百分比,语义色同 variant 现场演示:apps/docs/demo.html#sec-feedback diff --git a/packages/skin/ui/src/progress.ts b/packages/skin/ui/src/progress.ts index 18fd126..28999be 100644 --- a/packages/skin/ui/src/progress.ts +++ b/packages/skin/ui/src/progress.ts @@ -1,7 +1,8 @@ import { effect, h } from '@ui-arch/frame'; +import type { Signal } from '@ui-arch/frame'; import type { ProgressProps } from './types.js'; -/*** 进度条:value 信号驱动 0-100 定量态,缺省 value 则退化为不确定态(无限往复);满格切 success 色并停条纹 ***/ +/*** 进度条:value 信号驱动 0-100 定量态(缺省则不确定态),type='circle' 且传 value 时出环形;满格切 success 色 ***/ export function progress(props: ProgressProps = {}): HTMLElement { // —— 状态集中声明 —— const value = props.value; // 可选:不传即不确定态 @@ -17,6 +18,7 @@ export function progress(props: ProgressProps = {}): HTMLElement { 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); // 环形:SVG 版 effect(() => { const v = clamp(value()); // 依赖收集:信号变化自动写回 fill.style.width = `${v}%`; @@ -43,3 +45,33 @@ 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, label: string | undefined, variant: string, withText: boolean): 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 = `${Math.round(v)}%`; + svg.setAttribute('aria-label', `${label ?? '进度'} ${Math.round(v)}%`); // 环形没有原生 progressbar,名称自己拼 + }); + return h('div', { class: 'ua-progress ua-progress-circle' }, svg as unknown as HTMLElement) as HTMLElement; +} diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index e679c82..1e713c2 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -332,6 +332,7 @@ export interface SpinnerProps { /*** 进度条入参 ***/ export interface ProgressProps { value?: Signal; // 当前进度 0-100 的信号;不传即不确定态(未知时长的加载) + type?: 'line' | 'circle'; // 形态:线性条或环形(环形需同时传 value);缺省 line showText?: boolean; // 是否右侧显示百分比;缺省 true(不确定态忽略) label?: string; // 可访问名,如「上传进度」;缺省不确定态念「进行中」,定量态无名称 variant?: 'default' | 'success' | 'warning' | 'danger'; // 填充语义色;缺省 default(满格自动切 success) diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index e637955..1c1aed1 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1212,6 +1212,12 @@ "t": "Signal", "d": "当前进度 0-100 的信号;不传即不确定态(未知时长的加载)" }, + { + "n": "type", + "t": "'line' | 'circle'", + "df": "line", + "d": "形态:线性条或环形;环形需同时传 value(不确定态仍走线性条)" + }, { "n": "showText", "t": "boolean", @@ -1232,7 +1238,8 @@ "steps": [ "传 value 信号给定量进度,不传则退化为往复的不确定态", "label 写清这是什么进度,读屏会把名称与百分比一起念出来", - "失败或告警用 variant: 'danger' / 'warning',该颜色在满格时也不被 success 覆盖" + "失败或告警用 variant: 'danger' / 'warning',该颜色在满格时也不被 success 覆盖", + "看板 / 上传卡片要环形进度就传 type: 'circle'(同时必须传 value):SVG 双圆按 dashoffset 出弧,中间自动显示百分比,语义色同 variant" ] }, { diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index 77c103a..69b2853 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -4482,3 +4482,33 @@ h5.ua-type-title { font-size: var(--ua-font-size-md); } line-height: 14px; white-space: nowrap; } + +/* —— 环形进度:SVG 双圆,弧长由 dashoffset 控制 —— */ +.ua-progress-circle { + display: inline-flex; +} +.ua-progress-ring { + width: 64px; + height: 64px; +} +.ua-progress-ring-track { + stroke: color-mix(in srgb, var(--ua-color-border) 80%, transparent); +} +.ua-progress-ring-arc { + stroke: var(--ua-color-accent); + stroke-linecap: round; + transition: stroke-dashoffset 0.3s ease; +} +.ua-progress-ring.is-success .ua-progress-ring-arc { stroke: var(--ua-color-success); } +.ua-progress-ring.is-warning .ua-progress-ring-arc { stroke: var(--ua-color-warning); } +.ua-progress-ring.is-danger .ua-progress-ring-arc { stroke: var(--ua-color-danger); } +.ua-progress-ring-text { + fill: var(--ua-color-text); + font-size: 14px; + font-variant-numeric: tabular-nums; +} +@media (prefers-reduced-motion: reduce) { + .ua-progress-ring-arc { + transition: none; + } +}