diff --git a/.ua/index.json b/.ua/index.json index bdf584e..cea8fd7 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T18:27:12.002Z", + "generatedAt": "2026-10-26T18:28:42.413Z", "repoRoot": ".", "packages": [ { @@ -1979,7 +1979,7 @@ { "n": "submitText", "t": "string", - "d": "提交按钮文案;缺省「提交」" + "d": "提交按钮文案;缺省「提交」。异步规则校验期间按钮转圈并禁点(aria-busy),跑 400ms 的远程校验也看得见在等" }, { "n": "resetText", diff --git a/apps/demo/main.ts b/apps/demo/main.ts index fcca370..150b5b9 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -585,7 +585,7 @@ const content = h( section( 'sec-formlab', '表单校验', - 'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处——自绘控件(下拉 / 评分 / 开关)没有原生 input,焦点退到它的触发器,整块不可聚焦时落在字段容器上;onError 拿到不通字段数。错误文案带唯一 id 并被 aria-describedby 关联到控件,读屏除了「无效」还会念出具体原因。点「重置」逐项复位:原生输入框回初始值、错误态与汇总条一起清空,自绘控件(城市下拉)靠 formItem 的 reset 钩子回写信号。校验失败只把控件描红,危险色光环聚焦时才亮——常驻红环会让整片字段看着像一起报错;汇总条的条数随字段校验实时重算,改好一项就少一项、全好即消失。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。', + 'form + formItem:必填 / 长度 / 正则同步校验,validator 返回 Promise 即异步规则(提交会等待);全部通过才触发提交。校验不通过时顶部出现「有 N 项需要修改」汇总条(role=alert 会被读屏播报)并聚焦第一处——自绘控件(下拉 / 评分 / 开关)没有原生 input,焦点退到它的触发器,整块不可聚焦时落在字段容器上;onError 拿到不通字段数。错误文案带唯一 id 并被 aria-describedby 关联到控件,读屏除了「无效」还会念出具体原因。点「重置」逐项复位:原生输入框回初始值、错误态与汇总条一起清空,自绘控件(城市下拉)靠 formItem 的 reset 钩子回写信号。校验失败只把控件描红,危险色光环聚焦时才亮——常驻红环会让整片字段看着像一起报错;汇总条的条数随字段校验实时重算,改好一项就少一项、全好即消失;点提交后按钮转圈并禁点(异步规则要等 400ms,只变灰会让人以为点空了)。本例 columns: 2 走两列栅格,窄屏自动堆叠,按钮区始终整行;字段传 span 可跨列(备注整宽)。', form( { columns: 2, submitText: '注册', resetText: '重置', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) }, [ diff --git a/llms-full.txt b/llms-full.txt index 790f54c..45acef3 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T18:27:12.002Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T18:28:42.413Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -982,7 +982,7 @@ const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: - layout(默认 vertical):'vertical' | 'horizontal' —— 布局方向;缺省 vertical(horizontal 为标签左置) - columns(默认 1):1 | 2 | 3 —— 字段列数:>1 时字段走栅格铺列(沿用 grid 的窄屏单列),按钮区始终整行;缺省 1 -- submitText:string —— 提交按钮文案;缺省「提交」 +- submitText:string —— 提交按钮文案;缺省「提交」。异步规则校验期间按钮转圈并禁点(aria-busy),跑 400ms 的远程校验也看得见在等 - resetText:string —— 重置按钮文案;缺省不渲染 - onSubmit(回调):() => void —— 全部字段校验通过后的提交回调 - (formItem)label(必填):string —— 字段标签 diff --git a/packages/skin/ui/src/form.ts b/packages/skin/ui/src/form.ts index 542c6cc..0f2aff4 100644 --- a/packages/skin/ui/src/form.ts +++ b/packages/skin/ui/src/form.ts @@ -1,4 +1,4 @@ -import { effect, h } from '@ui-arch/frame'; +import { effect, h, signal } from '@ui-arch/frame'; import { button } from './button.js'; import { checkRule, checkRuleAsync } from './formRules.js'; import { grid } from './grid.js'; @@ -124,7 +124,8 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement { summary.textContent = ''; props.onReset?.(); }; - const submitBtn = button({ label: props.submitText ?? '提交', type: 'submit' }) as HTMLButtonElement; // 走原生 submit,避免 click + submit 双触发 + const busy = signal(false); // 校验进行中:按钮转圈 + aria-busy,异步规则跑 400ms 时用户看得见在等 + const submitBtn = button({ label: props.submitText ?? '提交', type: 'submit', loading: busy }) as HTMLButtonElement; // 走原生 submit,避免 click + submit 双触发 const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', type: 'button', onClick: doReset }) : null; // 重置不得提交表单 const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn); let running = false; // 提交中标记:异步校验期间屏蔽重复提交 @@ -133,7 +134,7 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement { const submit = async (): Promise => { if (running) return; // 卫语句:上一轮异步校验还没结束 running = true; - submitBtn.disabled = true; // 校验期间禁用,避免连点重复提交 + busy(true); // loading 自带禁点,不必再手写 disabled(两处都设反而容易漏复位) let firstBad: HTMLElement | null = null; let bad = 0; // 不通过的字段数 for (const item of items) { @@ -145,7 +146,7 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement { if (!target) item.el.tabIndex = -1; // 整块无可聚焦控件时把字段容器本身设为焦点落点(-1 不进 Tab 序,只接受程序聚焦) firstBad = target ?? item.el; } - submitBtn.disabled = false; + busy(false); running = false; summary.hidden = bad === 0; summary.textContent = bad ? `有 ${bad} 项需要修改,已定位到第一处` : ''; diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index 2f7f6fc..038d1b1 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1853,7 +1853,7 @@ { "n": "submitText", "t": "string", - "d": "提交按钮文案;缺省「提交」" + "d": "提交按钮文案;缺省「提交」。异步规则校验期间按钮转圈并禁点(aria-busy),跑 400ms 的远程校验也看得见在等" }, { "n": "resetText", diff --git a/packages/tools/cli/bin/ua.js b/packages/tools/cli/bin/ua.js index 9d187bd..07f53fc 100644 --- a/packages/tools/cli/bin/ua.js +++ b/packages/tools/cli/bin/ua.js @@ -1741,6 +1741,21 @@ function checkFormSummary(pkgs) { return issues; } +/*** 校验提交忙碌态:异步规则跑起来时按钮只变灰,用户会以为点空了;也防「设了 loading 忘了复位」 ***/ +function checkFormBusy(pkgs) { + const issues = []; + for (const pkg of pkgs) { + if (pkg.type !== 'skin-dom') continue; + const formPath = path.join(ROOT, pkg.dir, 'src', 'form.ts'); + if (!fs.existsSync(formPath)) continue; + const src = fs.readFileSync(formPath, 'utf8'); + if (!/loading: busy/.test(src)) issues.push(`${pkg.name}/src/form.ts 的提交按钮没接 loading:异步校验期间只变灰,用户看不出在等`); + if (!/busy\(false\)/.test(src)) issues.push(`${pkg.name}/src/form.ts 缺 busy(false) 复位:按钮会一直转圈,表单再也提交不了`); + if (/submitBtn\.disabled = true/.test(src)) issues.push(`${pkg.name}/src/form.ts 又手写 submitBtn.disabled:与 loading 双写会让禁用态漏复位,只留 loading 一条路`); + } + return issues; +} + /*** 校验同义词表:目标 id 必须真实存在(改名后残留会让回退指到空气),同一个词不得归两组(歧义会让结果随表序漂移) ***/ function checkSynonyms(ids) { const issues = []; @@ -1827,6 +1842,7 @@ function cmdVerify() { issues.push(...checkFormReset(pkgs)); // 重置按钮必须真复位 issues.push(...checkInvalidRing(pkgs)); // 错误环只该在聚焦时亮 issues.push(...checkFormSummary(pkgs)); // 汇总条必须实时重算 + issues.push(...checkFormBusy(pkgs)); // 提交忙碌态必须有可见反馈 issues.push(...checkSynonyms(new Set(pkgs.flatMap((p) => (p.docs?.components ?? []).map((c) => c.id))))); // 同义词表指向的组件必须真实存在 checkDarkTokens(issues); checkTokenParity(issues); @@ -1839,7 +1855,7 @@ function cmdVerify() { console.error(`${issues.length} violation(s)`); process.exit(1); } - console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌、暗色令牌齐全、tokens.json ↔ tokens.css 名字与值双源一致、@keyframes 无复制粘贴的重复帧、滚动容器全在细条规则内,README 组件清单与分组同步,backlog ↔ 留痕双向一致(编号严格递增、无重复 id、留痕时间线单调且带 +08:00,每个 iter 提交都有留痕),组合配方结构完整,MCP 工具清单三处一致,搜索同义词表目标存在且无歧义,combobox / slider / range 控件都有可访问名,toast 有 live 区域与 alert 抢占播报,视觉回归抽查清单十八组件 × 五维度齐全)'); + console.log('verify OK (manifest ↔ docs ↔ demo 一致,文档 props ↔ 入参接口双向一致,皮肤无硬编码颜色与未定义令牌、暗色令牌齐全、tokens.json ↔ tokens.css 名字与值双源一致、@keyframes 无复制粘贴的重复帧、滚动容器全在细条规则内,README 组件清单与分组同步,backlog ↔ 留痕双向一致(编号严格递增、无重复 id、留痕时间线单调且带 +08:00,每个 iter 提交都有留痕),组合配方结构完整,MCP 工具清单三处一致,搜索同义词表目标存在且无歧义,combobox / slider / range 控件都有可访问名,toast 有 live 区域与 alert 抢占播报,表单错误文案被 aria-describedby 关联、重置真复位、错误环只在聚焦时亮、汇总条随校验重算、提交按钮有忙碌态,视觉回归抽查清单十八组件 × 五维度齐全)'); } /*** 校验 git 提交信息是否符合 COMMITTING.md 规范(参数为 message 文件路径或直接 message 文本),违规退出码 1 ***/