From 0a5665e2960cdd953eb45800178a71269f2f8a25 Mon Sep 17 00:00:00 2001 From: HIsheR Date: Tue, 27 Oct 2026 01:56:10 +0800 Subject: [PATCH] =?UTF-8?q?iter(g327):=20stepper=20=E8=A1=A5=E5=A4=B1?= =?UTF-8?q?=E8=B4=A5=E6=80=81=E4=B8=8E=E6=95=B4=E6=9D=A1=E5=9B=9E=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 9 +++++++++ .ua/index.json | 12 +++++++++--- apps/demo/main.ts | 11 ++++++++++- llms-full.txt | 10 ++++++---- packages/skin/ui/src/stepper.ts | 32 +++++++++++++++++++++++-------- packages/skin/ui/src/types.ts | 1 + packages/skin/ui/ua.manifest.json | 10 ++++++++-- packages/skin/ui/ui.css | 12 ++++++++++++ 8 files changed, 79 insertions(+), 18 deletions(-) diff --git a/.ua/backlog.json b/.ua/backlog.json index e186110..ba58469 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -3329,6 +3329,15 @@ "motivation": "库内 id 全是英文(modal / select / spinner),而人和 Agent 张口就是「走马灯」「悬浮按钮」「分格输入」。这类词在 id / 标题 / 参数名 / 参数说明 / 步骤五层与配方三层里都没有原文,search 只剩 editDistance ≤ 3 的英文近似——对中文完全无效,于是「没有这个组件」的错误结论被反复得出,实际 carousel / floatButton / pinInput 都在库里。", "acceptance": "① 新增 lib/synonyms.mjs:中文口语词 → 组件 id 的一组映射,一词只许归一组;② ua search 在五层 + 配方三层全空时按表回退(多词取交集,交集空退并集),命中行标「← 同义词「走马灯」」、页脚注明「按同义词表回退」,正常命中路径零变化不掺水;③ MCP search_components 共用同一份 fallbackBySynonym,返回项 matched.field 为「同义词」,CLI 与 MCP 口径不漂移;④ ua verify 校验表内每个 id 真实存在、同一个词不归两组,组件改名后残留即门禁红;⑤ npm run check 全绿。", "notes": "" + }, + { + "id": "g327", + "domain": "tune", + "status": "open", + "title": "stepper 补失败态:提交没过的那一步不再无处表达", + "motivation": "步骤条只有 done/current/upcoming 三态,某步校验失败时只能把 active 退回去,于是「失败」和「还没走到」长得一模一样——用户看不出是卡住了还是失败了;另外 clickable 时只有圆点能点,标题文字点不动,手指落在文字上是空操作。", + "acceptance": "① StepperItem 增可选 status: 'error':该步红点 + 感叹号 + 标题标红,优先级高于进度态、不进 done 也不占 current、不参与回跳;② 失败符号带 role=img + aria-label「第 N 步失败:xx」,读屏不读孤立感叹号;③ clickable 时整个步骤条可点(圆点按钮 + 标题文字),点按钮与点文字各只触发一次 onChange;④ 样式只消费 --ua-*(danger 色 / alpha-soft 淡底 / weight-semibold),audit:visual 计数不涨;⑤ manifest props/步骤与演示同步,npm run check 全绿。", + "notes": "" } ] } diff --git a/.ua/index.json b/.ua/index.json index ab50233..19a35c1 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T17:17:55.311Z", + "generatedAt": "2026-10-26T17:55:42.939Z", "repoRoot": ".", "packages": [ { @@ -1527,7 +1527,12 @@ "n": "steps", "t": "StepperItem[]", "r": 1, - "d": "步骤列表,每项 { label, description? }" + "d": "步骤列表,每项 { label, description?, status? }" + }, + { + "n": "status", + "t": "'error'", + "d": "写在 steps 的每一项里:该项标为失败(红点 + 感叹号 + 读屏「第 N 步:xx(失败)」),优先级高于进度态,也不参与回跳" }, { "n": "active", @@ -1557,7 +1562,8 @@ "steps": [ "用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态", "流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随", - "clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态", + "clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态;点圆点(键盘可 Tab)或标题文字都算,一次点击只回调一次", + "某步失败时给该项加 status: 'error':圆点变红加感叹号、标题标红、读屏读「第 N 步:xx(失败)」;它既不进 done 也不占 current,因此不参与回跳——「失败」和「还没走到」只能是两种视觉", "侧栏或移动端传 direction: 'vertical':圆点成一列、说明文字紧贴右侧,步与步之间由圆点下方的竖线相连(末步不画线,已完成的段用主色标进度)" ] }, diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 37a182d..86f345c 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -659,7 +659,7 @@ const content = h( section( 'sec-stepper', '步骤条', - 'stepper:done/current/upcoming 三态随 active 信号流转,clickable 时点已完成步骤回跳;direction=vertical 走竖排。', + 'stepper:done/current/upcoming 三态随 active 信号流转,clickable 时点已完成步骤(圆点或标题)回跳;某步 status=error 标失败;direction=vertical 走竖排。', stepper({ steps: [{ label: '确认订单', description: '收货信息与商品清单' }, { label: '支付', description: '选择支付方式' }, { label: '完成', description: '等待发货' }], active: stepIdx, @@ -672,6 +672,15 @@ const content = h( active: signal(2), // 独立信号:演示停在第三步 direction: 'vertical', }), + divider({ dashed: true }), + stepper({ // 失败态:提交没过的那一步单独标红,既不算已完成也不顶替当前步 + steps: [ + { label: '填写资料', description: '4 项必填已齐' }, + { label: '人脸核身', description: '活体检测未通过,请重试', status: 'error' }, + { label: '提交审核', description: '核身通过后自动提交' }, + ], + active: signal(2), // 当前步仍在第三步:失败步不是「前进到的位置」 + }), h( 'div', { class: 'row' }, diff --git a/llms-full.txt b/llms-full.txt index 764d74a..794ee0a 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T17:17:55.311Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T17:55:42.939Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -751,7 +751,8 @@ const tab = signal('a'); tabs({ tabs: [{ value: 'a', label: '概览', closable: 参数: -- steps(必填):StepperItem[] —— 步骤列表,每项 { label, description? } +- steps(必填):StepperItem[] —— 步骤列表,每项 { label, description?, status? } +- status:'error' —— 写在 steps 的每一项里:该项标为失败(红点 + 感叹号 + 读屏「第 N 步:xx(失败)」),优先级高于进度态,也不参与回跳 - active(必填):Signal —— 当前步索引(0 基)信号,双向绑定:回跳写回、外部赋值同步 - clickable:boolean —— 已完成步骤是否可点击回跳;缺省 false - direction(默认 'horizontal'):'horizontal' | 'vertical' —— 排列方向:horizontal 横排连接线在节点之间;vertical 竖排、连接线落在圆点下方,已走完的一段用主色 @@ -767,8 +768,9 @@ const step = signal(1); stepper({ steps: [{ label: '下单' }, { label: '支付' 1. 用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态 2. 流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随 -3. clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态 -4. 侧栏或移动端传 direction: 'vertical':圆点成一列、说明文字紧贴右侧,步与步之间由圆点下方的竖线相连(末步不画线,已完成的段用主色标进度) +3. clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态;点圆点(键盘可 Tab)或标题文字都算,一次点击只回调一次 +4. 某步失败时给该项加 status: 'error':圆点变红加感叹号、标题标红、读屏读「第 N 步:xx(失败)」;它既不进 done 也不占 current,因此不参与回跳——「失败」和「还没走到」只能是两种视觉 +5. 侧栏或移动端传 direction: 'vertical':圆点成一列、说明文字紧贴右侧,步与步之间由圆点下方的竖线相连(末步不画线,已完成的段用主色标进度) 现场演示:apps/docs/demo.html#sec-stepper diff --git a/packages/skin/ui/src/stepper.ts b/packages/skin/ui/src/stepper.ts index 949a3bd..6e8e64c 100644 --- a/packages/skin/ui/src/stepper.ts +++ b/packages/skin/ui/src/stepper.ts @@ -1,23 +1,39 @@ import { effect, h } from '@ui-arch/frame'; import type { StepperProps } from './types.js'; -/*** 步骤条:done/current/upcoming 三态流程指示(横向 / 竖向),clickable 时已完成步骤可点回跳并写回 active 信号 ***/ +/*** 步骤条:done/current/upcoming 三态流程指示(横向 / 竖向),clickable 时已完成步骤整条可点回跳;status='error' 的步骤独立标失败(提交没过既不是 done 也不是 current,回退 active 会让「失败」与「还没走到」混为一谈) ***/ export function stepper(props: StepperProps): HTMLElement { const active = props.active; + + /*** 回跳:写回 active 并回调;不可点或未完成一律不动(失败的步骤也不给回跳,它才是要修的那一步) ***/ const clickStep = (i: number): void => { if (!props.clickable || i >= active()) return; // 卫语句:不可点或未完成不回跳 - active(i); if (props.onChange) props.onChange(i); }; // 写回信号并回调 + active(i); if (props.onChange) props.onChange(i); }; + + /*** 整条可点:圆点本身是可聚焦按钮,点标题文字走 li 这一层;点在按钮上交给按钮自己,避免一次点击回调两次 ***/ + const bindRowClick = (li: HTMLElement, i: number): void => { + li.addEventListener('click', (e: MouseEvent) => { + if ((e.target as HTMLElement).closest('button')) return; // 卫语句:圆点按钮的点击已由按钮处理 + clickStep(i); + }); + }; + const render = (): HTMLElement => { const cur = active(); const items = props.steps.map((s, i) => { - const state = i < cur ? 'done' : i === cur ? 'current' : 'upcoming'; // 三态:已完成 / 当前 / 未到 - const mark = state === 'done' ? '✓' : String(i + 1); // 完成打勾,其余显示序号 - const clickable = !!props.clickable && i < cur; // 仅已完成步骤可回跳 - const head = clickable + const failed = s.status === 'error'; // 失败态优先级最高:不参与进度比较 + const state = failed ? 'error' : i < cur ? 'done' : i === cur ? 'current' : 'upcoming'; // 三态 + 失败 + const mark = failed ? '!' : state === 'done' ? '✓' : String(i + 1); // 完成打勾、失败打感叹号,其余显示序号 + const clickable = !!props.clickable && i < cur && !failed; // 仅已完成且未失败的可回跳 + const dot = clickable ? h('button', { class: 'ua-step-dot', type: 'button', onClick: () => clickStep(i) }, mark) - : h('span', { class: 'ua-step-dot' }, mark); + : h('span', { class: 'ua-step-dot', ...(failed ? { role: 'img', 'aria-label': `第 ${i + 1} 步失败:${s.label}` } : {}) }, mark); // 失败的感叹号需要读屏语义,其余态序号由标题承载 const body = h('div', { class: 'ua-step-text' }, h('span', { class: 'ua-step-label' }, s.label), s.description ? h('span', { class: 'ua-step-desc' }, s.description) : null); - const li = h('li', { class: `ua-step ua-step-${state}`, 'aria-current': state === 'current' ? 'step' : null }, head, body); + const li = h('li', { + class: `ua-step ua-step-${state}${clickable ? ' is-clickable' : ''}`, + 'aria-current': state === 'current' ? 'step' : null, + }, dot, body) as HTMLElement; + if (clickable) bindRowClick(li, i); return li; }); const cls = props.direction === 'vertical' ? 'ua-stepper is-vertical' : 'ua-stepper'; // 方向只影响布局类,渲染逻辑共用 diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 3015e9f..7fb7492 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -232,6 +232,7 @@ export interface InputNumberProps { export interface StepperItem { label: string; // 步骤标题 description?: string; // 步骤补充说明;缺省不渲染 + status?: 'error'; // 该步失败(红点 + 感叹号 + 读屏「失败」);缺省按进度推 done/current/upcoming } /*** 步骤条入参 ***/ diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 6b50430..c7aa0c7 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1397,7 +1397,12 @@ "n": "steps", "t": "StepperItem[]", "r": 1, - "d": "步骤列表,每项 { label, description? }" + "d": "步骤列表,每项 { label, description?, status? }" + }, + { + "n": "status", + "t": "'error'", + "d": "写在 steps 的每一项里:该项标为失败(红点 + 感叹号 + 读屏「第 N 步:xx(失败)」),优先级高于进度态,也不参与回跳" }, { "n": "active", @@ -1427,7 +1432,8 @@ "steps": [ "用 signal(0) 创建当前步索引,传入 active;索引之前的步骤显示 ✓ 已完成态", "流程推进时外部赋值即可:step(step() + 1),组件三态自动跟随", - "clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态", + "clickable: true 时已完成步骤可点击回跳并写回信号,配合 onChange 同步业务状态;点圆点(键盘可 Tab)或标题文字都算,一次点击只回调一次", + "某步失败时给该项加 status: 'error':圆点变红加感叹号、标题标红、读屏读「第 N 步:xx(失败)」;它既不进 done 也不占 current,因此不参与回跳——「失败」和「还没走到」只能是两种视觉", "侧栏或移动端传 direction: 'vertical':圆点成一列、说明文字紧贴右侧,步与步之间由圆点下方的竖线相连(末步不画线,已完成的段用主色标进度)" ] }, diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index b0841c5..3655241 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1172,6 +1172,15 @@ button.ua-step-dot:hover { color: var(--ua-color-accent); box-shadow: var(--ua-focus-ring); } +.ua-step-error .ua-step-dot { + border-color: var(--ua-color-danger); + background: color-mix(in srgb, var(--ua-color-danger) var(--ua-alpha-soft), transparent); /* 失败步骤只给淡底,不抢当前步的光环 */ + color: var(--ua-color-danger); + font-weight: var(--ua-weight-semibold); +} +.ua-step.is-clickable .ua-step-text { + cursor: pointer; /* 整条可点:文字区给出与圆点一致的手型 */ +} .ua-step-text { display: flex; flex-direction: column; @@ -1187,6 +1196,9 @@ button.ua-step-dot:hover { color: var(--ua-color-text); font-weight: var(--ua-weight-semibold); } +.ua-step-error .ua-step-label { + color: var(--ua-color-danger); /* 标题跟着标红:只标圆点时正文仍像正常步骤 */ +} .ua-step-desc { font-size: var(--ua-font-size-sm); color: var(--ua-color-text-muted);