iter(g108): 表单补错误汇总条与 onError
This commit is contained in:
@@ -98,6 +98,8 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
|
||||
const submitBtn = button({ label: props.submitText ?? '提交', type: 'submit' }) as HTMLButtonElement; // 走原生 submit,避免 click + submit 双触发
|
||||
const resetBtn = props.resetText ? button({ label: props.resetText, variant: 'ghost', type: 'button', onClick: props.onReset }) : null; // 重置不得提交表单
|
||||
const actions = h('div', { class: 'ua-form-actions' }, submitBtn, resetBtn);
|
||||
const summary = h('div', { class: 'ua-form-summary', role: 'alert' }) as HTMLElement; // 错误汇总:读屏能一次听到共几处要改
|
||||
summary.hidden = true;
|
||||
let running = false; // 提交中标记:异步校验期间屏蔽重复提交
|
||||
|
||||
/*** 整体提交:逐项 await 校验(全部跑完以显示所有错误),有错聚焦首个并中止 ***/
|
||||
@@ -106,19 +108,26 @@ export function form(props: FormProps, items: FormItemHandle[]): HTMLElement {
|
||||
running = true;
|
||||
submitBtn.disabled = true; // 校验期间禁用,避免连点重复提交
|
||||
let firstBad: HTMLElement | null = null;
|
||||
let bad = 0; // 不通过的字段数
|
||||
for (const item of items) {
|
||||
if (!(await item.validateAsync()) || firstBad) continue; // 首错之前的项才记焦点目标
|
||||
const msg = await item.validateAsync();
|
||||
if (!msg) continue; // 卫语句:本项通过
|
||||
bad += 1;
|
||||
if (firstBad) continue; // 只把首个错误控件留作焦点目标
|
||||
firstBad = item.el.querySelector('input, textarea, select') as HTMLElement | null;
|
||||
}
|
||||
submitBtn.disabled = false;
|
||||
running = false;
|
||||
if (firstBad) { firstBad.focus(); return; } // 卫语句:有错误不提交
|
||||
summary.hidden = bad === 0;
|
||||
summary.textContent = bad ? `有 ${bad} 项需要修改,已定位到第一处` : '';
|
||||
if (firstBad) { firstBad.focus(); props.onError?.(bad); return; } // 卫语句:有错误不提交
|
||||
props.onSubmit?.();
|
||||
};
|
||||
|
||||
const el = h(
|
||||
'form',
|
||||
{ class: props.layout === 'horizontal' ? 'ua-form ua-form-h' : 'ua-form' },
|
||||
props.errorSummary === false ? null : summary, // 汇总条:关掉时连节点都不插
|
||||
...items.map((i) => i.el),
|
||||
actions,
|
||||
);
|
||||
|
||||
@@ -393,6 +393,8 @@ export interface FormProps {
|
||||
resetText?: string; // 重置按钮文案;缺省不渲染重置按钮
|
||||
onSubmit?: () => void; // 全部字段校验通过后的提交回调
|
||||
onReset?: () => void; // 重置回调
|
||||
onError?: (count: number) => void; // 校验不通过的回调,参数为不通字段数(适合埋点 / 顶部提示)
|
||||
errorSummary?: boolean; // 是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true
|
||||
}
|
||||
|
||||
/*** 时间线条目 ***/
|
||||
|
||||
@@ -1625,6 +1625,17 @@
|
||||
"n": "onReset",
|
||||
"t": "() => void",
|
||||
"d": "重置按钮点击回调;组件不自行清空字段,由调用方重置信号;不传 resetText 不渲染按钮"
|
||||
},
|
||||
{
|
||||
"n": "onError",
|
||||
"t": "(count: number) => void",
|
||||
"k": 1,
|
||||
"d": "校验不通过时的回调,参数为不通字段数(适合埋点或顶部提示)"
|
||||
},
|
||||
{
|
||||
"n": "errorSummary",
|
||||
"t": "boolean",
|
||||
"d": "是否在表单顶部显示「有 N 项需要修改」汇总条;缺省 true,自带错误 UI 时传 false"
|
||||
}
|
||||
],
|
||||
"example": "const name = signal(''); const f = form({ onSubmit: submit }, [formItem({ label: '用户名', required: true, value: name, rules: [{ min: 2, max: 12 }] }, textField({ value: name }))])",
|
||||
|
||||
@@ -3738,3 +3738,16 @@ button.ua-step-dot:hover {
|
||||
justify-content: flex-end;
|
||||
gap: var(--ua-space-2);
|
||||
}
|
||||
|
||||
/* —— 表单错误汇总条 —— */
|
||||
.ua-form-summary {
|
||||
padding: var(--ua-space-2) var(--ua-space-3);
|
||||
border: 1px solid color-mix(in srgb, var(--ua-color-danger) 45%, transparent);
|
||||
border-radius: var(--ua-radius-sm);
|
||||
background: color-mix(in srgb, var(--ua-color-danger) 10%, transparent);
|
||||
color: var(--ua-color-danger);
|
||||
font-size: var(--ua-font-size-md);
|
||||
}
|
||||
.ua-form-summary[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user