From 47756c899a7aa8e64592c694db6f473418ade11b Mon Sep 17 00:00:00 2001 From: HIsheR Date: Mon, 26 Oct 2026 13:10:40 +0800 Subject: [PATCH] =?UTF-8?q?iter(g91):=20=E8=A1=A8=E5=8D=95=E5=AD=97?= =?UTF-8?q?=E6=AE=B5=E8=A1=A5=E8=BE=85=E5=8A=A9=E8=AF=B4=E6=98=8E=20hint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ua/backlog.json | 8 ++++++++ .ua/index.json | 10 ++++++++-- apps/demo/main.ts | 2 +- llms-full.txt | 4 +++- packages/skin/ui/src/form.ts | 4 +++- packages/skin/ui/src/types.ts | 1 + packages/skin/ui/ua.manifest.json | 8 +++++++- packages/skin/ui/ui.css | 8 ++++++++ 8 files changed, 39 insertions(+), 6 deletions(-) diff --git a/.ua/backlog.json b/.ua/backlog.json index 5310871..3828910 100644 --- a/.ua/backlog.json +++ b/.ua/backlog.json @@ -1084,6 +1084,14 @@ "acceptance": "npm run changelog 重新生成(253 提交),changelog:check 不再报漂移(提交本条后按既有约定会再差一条,属正常);npm run check 全绿", "notes": "自补:发版前刷新步骤,按 g64/g77 的既定方式执行", "status": "done" + }, + { + "id": "g91", + "title": "表单字段补辅助说明 hint", + "motivation": "formItem 只有 label 与错误位,没有「格式提示」的位置:调用方只能把说明塞进 placeholder 或标签里,一报错提示就被替换得无处可看,用户不知道正确格式是什么", + "acceptance": "FormItemProps 新增可选 hint,渲染在控件下方;校验失败时 hint 让位(hidden)只留错误文案,校验通过自动恢复;样式走令牌;文档参数表(formItem 前缀)与演示同步;npm run check 全绿", + "notes": "自补:表单可用性;不改 formItem 返回句柄与 form 聚合逻辑", + "status": "open" } ] } diff --git a/.ua/index.json b/.ua/index.json index 11e5b24..b637e5a 100644 --- a/.ua/index.json +++ b/.ua/index.json @@ -1,6 +1,6 @@ { "$schema": "ui-arch-registry-index/0.1", - "generatedAt": "2026-10-26T05:08:07.129Z", + "generatedAt": "2026-10-26T05:10:23.912Z", "repoRoot": ".", "packages": [ { @@ -1697,6 +1697,11 @@ "d": "校验时机;缺省 blur(输入时自动清已报错误)", "df": "blur" }, + { + "n": "(formItem)hint", + "t": "string", + "d": "控件下方辅助说明(格式提示等);报错时让位给错误文案" + }, { "n": "onReset", "t": "() => void", @@ -1715,7 +1720,8 @@ "触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验", "单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type=\"submit\",不会与点击回调重复触发", "rules.validator 返回 Promise 即为异步规则(如「邀请码是否已被使用」远程校验):提交时逐项 await,校验期间提交按钮禁用,结论按轮次取舍避免乱序覆盖", - "formItem 返回 { el, validate, validateAsync } 句柄:validate 只跑同步规则(供 blur/input 即时反馈),validateAsync 逐条 await 供 form 聚合提交" + "formItem 返回 { el, validate, validateAsync } 句柄:validate 只跑同步规则(供 blur/input 即时反馈),validateAsync 逐条 await 供 form 聚合提交", + "需要给字段加格式说明就传 hint:平时显示在控件下方,校验失败时自动让位给错误文案,同一位置不堆两行" ] }, { diff --git a/apps/demo/main.ts b/apps/demo/main.ts index 6c11dcb..9d0ce74 100644 --- a/apps/demo/main.ts +++ b/apps/demo/main.ts @@ -421,7 +421,7 @@ const content = h( { submitText: '注册', onSubmit: () => formResult(`提交成功:${username()} / ${email()} / ${city()}`) }, [ formItem({ label: '用户名', required: true, value: username, rules: [{ min: 2, max: 12, message: '用户名需 2-12 个字符' }] }, textField({ placeholder: '2-12 个字符', value: username })), - formItem({ label: '邮箱', required: true, value: email, rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })), + formItem({ label: '邮箱', required: true, value: email, hint: '用于登录与找回密码,不会公开', rules: [{ pattern: /^[\w.-]+@[\w-]+\.[\w.]+$/, message: '邮箱格式不正确' }] }, textField({ placeholder: 'you@example.com', value: email })), formItem({ label: '城市', required: true, value: city }, select({ options: cities.map((c) => ({ value: c, label: c })), value: city, placeholder: '请选择城市…' })), formItem( { label: '邀请码', required: true, value: inviteCode, rules: [{ validator: async (v) => { await new Promise((r) => setTimeout(r, 400)); return v === 'USED' ? '该邀请码已被使用' : true; }, message: '校验未通过' }] }, diff --git a/llms-full.txt b/llms-full.txt index e6c89a7..4f9b3ea 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -1,6 +1,6 @@ # @ui-arch 组件库 Agent 文档(无图形版) -> 自动生成于 2026-10-26T05:08:07.129Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 +> 自动生成于 2026-10-26T05:10:23.912Z,数据源:各包 ua.manifest.json,勿手改。人类版文档站:apps/docs/index.html。 > 组件 = 普通函数,传 props 返回 DOM;状态用 signal 双向绑定:读 signal() 写 signal(v)。 ## 快速上手 @@ -900,6 +900,7 @@ const open = signal(false); drawer({ title: '详情', open, width: 360, onClose: - (formItem)required:boolean —— 必填:显示红星并自动追加非空规则 - (formItem)rules:FormRule[] —— 校验规则:required / min / max / pattern / validator,按序执行取首个失败 - (formItem)validateOn(默认 blur):'input' | 'blur' | 'submit' —— 校验时机;缺省 blur(输入时自动清已报错误) +- (formItem)hint:string —— 控件下方辅助说明(格式提示等);报错时让位给错误文案 - onReset:() => void —— 重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮 示例: @@ -921,6 +922,7 @@ const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: 9. 单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type="submit",不会与点击回调重复触发 10. rules.validator 返回 Promise 即为异步规则(如「邀请码是否已被使用」远程校验):提交时逐项 await,校验期间提交按钮禁用,结论按轮次取舍避免乱序覆盖 11. formItem 返回 { el, validate, validateAsync } 句柄:validate 只跑同步规则(供 blur/input 即时反馈),validateAsync 逐条 await 供 form 聚合提交 +12. 需要给字段加格式说明就传 hint:平时显示在控件下方,校验失败时自动让位给错误文案,同一位置不堆两行 现场演示:apps/docs/demo.html#sec-formlab diff --git a/packages/skin/ui/src/form.ts b/packages/skin/ui/src/form.ts index a5967ac..d3ca38f 100644 --- a/packages/skin/ui/src/form.ts +++ b/packages/skin/ui/src/form.ts @@ -35,8 +35,9 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa const mode = props.validateOn ?? 'blur'; // 校验时机;缺省失焦校验 const rules = [...(props.required ? [{ required: true }] : []), ...(props.rules ?? [])]; // 必填规则排最前 const error = h('div', { class: 'ua-form-error' }); + const hint = props.hint ? h('div', { class: 'ua-form-hint' }, props.hint) : null; // 辅助说明:报错时隐藏,避免与错误挤在一起 const label = h('label', { class: 'ua-form-label' }, props.label, props.required ? h('i', { class: 'ua-form-req' }, '*') : null); - const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, error)); + const el = h('div', { class: 'ua-form-item' }, label, h('div', { class: 'ua-form-control' }, control, hint, error)); let seq = 0; // 校验轮次:异步结论回来时若已有更新的一轮,丢弃旧结果(防乱序覆盖) /*** 错误态落点:控件根 + 内部原生控件,保证 select / checkbox 这类包裹式控件也能标红 ***/ @@ -48,6 +49,7 @@ export function formItem(props: FormItemProps, control: HTMLElement): FormItemHa /*** 落笔:错误文案 + 控件红框 + aria-invalid,视觉与读屏同源 ***/ const paint = (msg: string | null): void => { error.textContent = msg ?? ''; + if (hint) hint.hidden = !!msg; // 有错时让位:同一位置只留一条提示 control.classList.toggle('ua-invalid', !!msg); for (const node of marked()) node.setAttribute('aria-invalid', String(!!msg)); }; diff --git a/packages/skin/ui/src/types.ts b/packages/skin/ui/src/types.ts index 2e61840..43c772e 100644 --- a/packages/skin/ui/src/types.ts +++ b/packages/skin/ui/src/types.ts @@ -363,6 +363,7 @@ export interface FormItemProps { required?: boolean; // 必填:显示红星并自动追加非空规则 rules?: FormRule[]; // 附加校验规则,按序执行 validateOn?: 'input' | 'blur' | 'submit'; // 校验时机;缺省 blur + hint?: string; // 控件下方辅助说明(格式提示等);报错时让位给错误文案 } /*** 表单字段句柄:form 聚合校验用 ***/ diff --git a/packages/skin/ui/ua.manifest.json b/packages/skin/ui/ua.manifest.json index a18fc44..9ffbeac 100644 --- a/packages/skin/ui/ua.manifest.json +++ b/packages/skin/ui/ua.manifest.json @@ -1568,6 +1568,11 @@ "d": "校验时机;缺省 blur(输入时自动清已报错误)", "df": "blur" }, + { + "n": "(formItem)hint", + "t": "string", + "d": "控件下方辅助说明(格式提示等);报错时让位给错误文案" + }, { "n": "onReset", "t": "() => void", @@ -1586,7 +1591,8 @@ "触发时机用 input / change / focusout(均会冒泡),因此 checkbox / select / 单选组这类包裹式控件同样能失焦校验", "单行输入框内按 Enter 等同点击提交(textarea 内仍是换行);提交按钮为 type=\"submit\",不会与点击回调重复触发", "rules.validator 返回 Promise 即为异步规则(如「邀请码是否已被使用」远程校验):提交时逐项 await,校验期间提交按钮禁用,结论按轮次取舍避免乱序覆盖", - "formItem 返回 { el, validate, validateAsync } 句柄:validate 只跑同步规则(供 blur/input 即时反馈),validateAsync 逐条 await 供 form 聚合提交" + "formItem 返回 { el, validate, validateAsync } 句柄:validate 只跑同步规则(供 blur/input 即时反馈),validateAsync 逐条 await 供 form 聚合提交", + "需要给字段加格式说明就传 hint:平时显示在控件下方,校验失败时自动让位给错误文案,同一位置不堆两行" ] }, { diff --git a/packages/skin/ui/ui.css b/packages/skin/ui/ui.css index fd217b3..eaa711d 100644 --- a/packages/skin/ui/ui.css +++ b/packages/skin/ui/ui.css @@ -1493,6 +1493,14 @@ button.ua-step-dot:hover { flex-direction: column; gap: 4px; } +.ua-form-hint { + color: var(--ua-color-text-muted); + font-size: var(--ua-font-size-sm); + margin-top: 4px; +} +.ua-form-hint[hidden] { + display: none; +} .ua-form-error { color: var(--ua-color-danger); font-size: var(--ua-font-size-sm);