多步骤表单必须用单个form包裹所有fieldset,每个fieldset需含legend,按钮置于内部,禁用用input.disabled,验证限当前步字段,进度用progress,状态存sessionStorage,按钮须设type="button"。

多步骤组件必须用单个 form 包裹所有 fieldset
后台管理界面的长表单一旦拆成多步,就绝不能为每步新建一个 form 标签。否则提交时只发当前步字段,后端收不到完整数据,且 FormData 无法一次性构造。常见错误是把“基本信息”“权限配置”“高级设置”各自套 form,结果点提交只拿到最后一步的值。
正确结构只有一个 form,内部按业务逻辑切分 fieldset:每个 fieldset 对应一个语义完整的子模块(如“用户角色分配”或“API 访问策略”),legend 必须是它的第一个子元素,不可省略或挪位。漏写 legend 会导致屏幕阅读器无法识别该组上下文,也破坏键盘 Tab 导航边界。
- 按钮(如“下一步”“保存并继续”)必须放在对应
fieldset内部,否则 Tab 键会跳过整步 - 禁用非当前步的交互,推荐用
input.disabled = true而非fieldset.disabled—— 后者会让字段值从form.elements中消失,且 Safari 下嵌套fieldset的禁用传递不可靠 - 不要给隐藏步的
input设required属性,浏览器校验会扫全表单,导致点“下一步”直接报错
validateStep() 必须限定作用域,不调用 form.checkValidity()
后台表单字段多、校验规则复杂(比如“角色列表至少选 1 项”“IP 白名单格式需匹配 CIDR”),全局 form.checkValidity() 会误判隐藏字段,根本不可用。验证函数必须只查当前步内显式标记的字段。
实操建议用 data-step 属性配合查询:
立即学习“前端免费学习笔记(深入)”;
function validateStep(step) {
const fields = document.querySelectorAll(`[data-step="${step}"] :is(input, select, textarea)`);
return Array.from(fields).every(field => {
if (!field.hasAttribute('required') && field.value === '') return true;
return field.checkValidity();
});
}
- 避免用
:required选择器——它匹配所有带 required 的字段,不管是否在当前步;改用:is(input, select, textarea)确保覆盖控件类型 - 对跨步依赖字段(如“启用双因素认证”勾选后,“备用邮箱”才必填),验证时要手动读取状态,不能只靠 HTML 原生属性
- 校验失败后,聚焦第一个无效字段,而不是弹 alert —— 后台用户更习惯键盘操作和快速定位
进度状态必须同步更新 aria-current 和焦点
后台管理员常依赖键盘导航,仅靠 CSS 控制 display 或 visibility 不够。切换步骤时,必须做三件事:设当前 fieldset 的 aria-current="step"、移除上一步的该属性、把焦点移到当前步第一个可聚焦元素。
- 别用
element.scrollIntoView()替代element.focus()—— 它不保证获得焦点,尤其在 modal 或 iframe 场景下失效 - 如果当前步首个字段是
type="hidden"或已被disabled,要主动找下一个tabindex="0"或可聚焦的input,否则键盘用户卡住 - 进度条用原生
progress元素,设max和value即可;别用meter,它语义是度量值,不是流程进度 - Safari 对
progress样式支持弱,若需带文字标签的横条(如“2/5”),退回到div+aria-valuenow+aria-valuemax
刷新或后退后状态恢复必须存 sessionStorage,而非 localStorage
后台页面常开久、切 tab 多,用户刷新或误点后退键时,表单不能丢数据。但敏感字段(如密钥、Token、LDAP 密码)绝不能进 localStorage —— 关闭标签页后仍残留,风险极高。
正确做法是在每次切步前存两样东西:
sessionStorage.setItem('adminFormStep', String(nextIndex))sessionStorage.setItem('adminFormData', JSON.stringify(currentData))
页面加载时,在 JS 初始化最开头就读取并还原。注意:currentData 应只存用户改过的字段(对比 input.defaultValue),避免把空字符串或 null 当有效值写入;字段名冲突(如两步都有 name="desc")必须加命名空间前缀,例如 "basic_desc" 和 "advanced_desc"。
最容易被忽略的是:所有“下一步”“上一步”按钮必须显式声明 type="button"。没写的 button 默认是 submit,一点就触发表单提交,后台接口收到半截数据还可能触发幂等校验失败。



















