【Vue3 + Element Plus】中后台表单开发实战:从规范制定到代码落地,搞定防重复提交、校验与重置,避开表单开发90%高频坑!

📑 文章目录
- 一、前言
- 二、核心思路:先定「规范」,再写代码
- 三、防重复提交:为什么不能只靠后端?
- [3.1 完整示例:基础防重复提交](#3.1 完整示例:基础防重复提交)
- [3.2 常见坑](#3.2 常见坑)
- 四、校验规范:什么时候校验?怎么校验?
- [4.1 提交时校验](#4.1 提交时校验)
- [4.2 完整校验示例(含自定义规则)](#4.2 完整校验示例(含自定义规则))
- [五、重置规范:数据 + 校验一起清](#五、重置规范:数据 + 校验一起清)
- [5.1 完整重置示例](#5.1 完整重置示例)
- [5.2 手动赋值式「重置」](#5.2 手动赋值式「重置」)
- [六、Loading 统一:一个 submitting 管到底](#六、Loading 统一:一个 submitting 管到底)
- [6.1 需要「整表 + 提交」两个 loading 时](#6.1 需要「整表 + 提交」两个 loading 时)
- 七、表单规范小结(可直接复制)
- [八、表格场景延伸:查询表单 + 防重复](#八、表格场景延伸:查询表单 + 防重复)
- 九、总结
- [🔍 系列模块导航](#🔍 系列模块导航)
同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。
(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)
很多前端开发者都会遇到一个瓶颈:
代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。
想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验。
这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。
帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。
一、前言
表单是日常开发里最常见、最容易出问题的一块:重复提交、校验混乱、重置不干净、loading 满天飞,改一次牵一发动全身。
本文不讨论 Vue 源码,只围绕「写表单时该怎么选、为什么这么选、坑会在哪」来写,目标是:
- 防重复提交:提交中禁用按钮、拦截二次点击
- 校验统一:表单校验逻辑集中、错误展示清
- 重置规范:重置时数据和校验状态都能回到初始
- loading 统一:全局/局部 loading 管理方式清晰、不散乱
全文以 Vue3 + Composition API 为例,代码可直接复用。
[⬆ 返回目录](#⬆ 返回目录)
二、核心思路:先定「规范」,再写代码
很多人一上来就写 el-form、submit,结果:
- 防重复:有的用
v-loading,有的用disabled,有的靠拦截请求 - 校验:有的在
submit里写,有的在watch里写,有的写在接口里 - 重置:有的只清数据,有的忘记清校验,有的还把弹窗关了
建议先把「规范」定好,再按规范实现。
| 能力 | 规范说明 |
|---|---|
| 防重复提交 | 提交中统一 loading + 按钮禁用,禁止二次点击 |
| 校验 | 在提交前统一做一次表单校验,通过再请求 |
| 重置 | 重置时同时清空数据、清除校验、恢复初始状态 |
| Loading | 提交过程统一用一个 submitting 控制,不分散 |
下面按这个规范逐项落地。
[⬆ 返回目录](#⬆ 返回目录)
三、防重复提交:为什么不能只靠后端?
常见问题:
- 用户连点「提交」
- 网络慢时多次点击
- 只依赖后端幂等,前端体验差、日志乱
推荐做法:
- 提交开始:设置
submitting = true,按钮disabled+ loading - 提交结束:无论成功失败,都设置
submitting = false
这样:
- 用户体验好:明确知道「正在提交」
- 请求可控:一次提交只发一个请求
- 后端压力小:减少重复请求
3.1 完整示例:基础防重复提交
html
<template>
<el-form ref="formRef" :model="form" :rules="rules" @submit.prevent="handleSubmit">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="submitting"
:disabled="submitting"
@click="handleSubmit"
>
提交
</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const formRef = ref(null)
const submitting = ref(false)
const form = reactive({
username: '',
phone: ''
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }]
}
async function handleSubmit() {
if (submitting.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await submitFormApi(form)
ElMessage.success('提交成功')
} catch (err) {
ElMessage.error(err?.message || '提交失败')
} finally {
submitting.value = false
}
}
</script>
说明:
submitting既控制loading也控制disabled,状态唯一来源。- 在
handleSubmit开头判断submitting.value,双重防护。 - 用
try/catch/finally保证submitting一定会被重置。 - 校验失败不进入请求逻辑,也不会把
submitting设为 true。
[⬆ 返回目录](#⬆ 返回目录)
3.2 常见坑
| 坑点 | 问题 | 建议 |
|---|---|---|
只在 catch 里重置 |
成功但没进 try 时,submitting 永远是 true |
用 finally 统一重置 |
用 loading 不 disabled |
仍然可以点击 | 同时 loading + disabled |
校验失败就 return |
如果之前误设了 submitting |
校验失败时不要设置 submitting = true |
[⬆ 返回目录](#⬆ 返回目录)
四、校验规范:什么时候校验?怎么校验?
推荐规范:
- 提交时统一校验一次,通过再请求。
- 实时校验(
blur/change)只做提示,不替代提交时校验。 - 自定义规则单独抽成函数,便于复用和测试。
4.1 提交时校验
html
<script setup>
async function handleSubmit() {
if (submitting.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) {
ElMessage.warning('请完善表单信息')
return
}
submitting.value = true
try {
await submitFormApi(form)
} finally {
submitting.value = false
}
}
</script>
要点:
- 用
await formRef.value?.validate()等待校验结果。 .catch(() => false)处理校验失败,返回false便于后续判断。- 校验不通过时,不进入请求、不设置
submitting。
[⬆ 返回目录](#⬆ 返回目录)
4.2 完整校验示例(含自定义规则)
html
<template>
<el-form ref="formRef" :model="form" :rules="rules">
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" placeholder="请输入手机号" />
</el-form-item>
<el-form-item label="确认手机号" prop="confirmPhone">
<el-input v-model="form.confirmPhone" placeholder="请再次输入手机号" />
</el-form-item>
<el-form-item label="年龄" prop="age">
<el-input-number v-model="form.age" :min="1" :max="120" />
</el-form-item>
</el-form>
</template>
<script setup>
const validatePhone = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入手机号'))
} else if (!/^1[3-9]\d{9}$/.test(value)) {
callback(new Error('手机号格式不正确'))
} else {
callback()
}
}
const validateConfirmPhone = (rule, value, callback) => {
if (!value) {
callback(new Error('请再次输入手机号'))
} else if (value !== form.phone) {
callback(new Error('两次输入的手机号不一致'))
} else {
callback()
}
}
const form = reactive({
phone: '',
confirmPhone: '',
age: undefined
})
const rules = {
phone: [{ required: true, validator: validatePhone, trigger: 'blur' }],
confirmPhone: [{ required: true, validator: validateConfirmPhone, trigger: 'blur' }],
age: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ type: 'number', min: 1, max: 120, message: '年龄需在 1-120 之间', trigger: 'blur' }
]
}
</script>
说明:
validator接收(rule, value, callback),校验通过调用callback(),失败调用callback(new Error('...'))。trigger: 'blur'表示失焦时触发,change表示值变化时触发。- 多个规则会按顺序执行,遇到失败即停止。
[⬆ 返回目录](#⬆ 返回目录)
五、重置规范:数据 + 校验一起清
常见问题:
- 只重置数据,校验错误文案还在。
- 弹窗关闭再打开,上一次的校验状态还在。
- 重置后忘记清空
submitting等状态。
推荐做法:
- 使用
resetFields()同时重置数据和校验。 - 弹窗打开时执行一次重置。
- 提交成功后如需清空,也调用
resetFields()。
5.1 完整重置示例
html
<template>
<el-dialog v-model="visible" title="新增用户" @open="handleOpen">
<el-form ref="formRef" :model="form" :rules="rules">
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" :disabled="submitting" @click="handleSubmit">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup>
const visible = ref(false)
const formRef = ref(null)
const submitting = ref(false)
const form = reactive({
username: '',
phone: ''
})
const rules = { /* ... */ }
function handleOpen() {
formRef.value?.resetFields()
submitting.value = false
}
async function handleSubmit() {
if (submitting.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await submitFormApi(form)
ElMessage.success('提交成功')
visible.value = false
formRef.value?.resetFields()
submitting.value = false
} catch (err) {
ElMessage.error(err?.message || '提交失败')
} finally {
submitting.value = false
}
}
</script>
说明:
resetFields()会把表单恢复为初始值,并清除校验状态。- 若使用
reactive定义 form,初始值需要和el-form-item的prop一一对应。 - 弹窗
@open时重置,避免上次操作残留。
[⬆ 返回目录](#⬆ 返回目录)
5.2 手动赋值式「重置」
有时需要重置成「空」或固定默认值,而不是表单最初绑定的值:
js
function handleReset() {
form.username = ''
form.phone = ''
form.age = undefined
formRef.value?.clearValidate()
}
clearValidate():只清校验,不改数据。resetFields():恢复初始值并清校验。- 需要自定义默认值时,先改
form,再clearValidate()。
[⬆ 返回目录](#⬆ 返回目录)
六、Loading 统一:一个 submitting 管到底
表单相关 loading 建议只用一个 submitting:
- 按钮 loading:
:loading="submitting" - 按钮禁用:
:disabled="submitting" - 防重复:
if (submitting.value) return
不要再单独搞 formLoading、btnLoading 等,容易不同步。
6.1 需要「整表 + 提交」两个 loading 时
例如:弹窗打开要拉取详情填表,提交时再发请求。
html
<script setup>
const loading = ref(false)
const submitting = ref(false)
async function handleOpen() {
loading.value = true
try {
const res = await getDetailApi(id)
Object.assign(form, res.data)
} finally {
loading.value = false
}
}
async function handleSubmit() {
if (submitting.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await submitFormApi(form)
} finally {
submitting.value = false
}
}
</script>
[⬆ 返回目录](#⬆ 返回目录)
七、表单规范小结(可直接复制)
把前面几节串成一个「标准模板」,日常写表单可以直接基于它改:
html
<template>
<el-form ref="formRef" :model="form" :rules="rules">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" placeholder="请输入用户名" />
</el-form-item>
<el-form-item>
<el-button
type="primary"
:loading="submitting"
:disabled="submitting"
@click="handleSubmit"
>
提交
</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
const formRef = ref(null)
const submitting = ref(false)
const form = reactive({
username: ''
})
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }]
}
async function handleSubmit() {
if (submitting.value) return
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await submitFormApi(form)
ElMessage.success('提交成功')
formRef.value?.resetFields()
} catch (err) {
ElMessage.error(err?.message || '提交失败')
} finally {
submitting.value = false
}
}
function handleReset() {
formRef.value?.resetFields()
}
</script>
规范速记:
- 防重复:
submitting+disabled+ 函数开头判断。 - 校验:提交前
validate(),不通过不请求。 - 重置:
resetFields()同时清数据和校验。 - Loading:只用
submitting,在finally里重置。
[⬆ 返回目录](#⬆ 返回目录)
八、表格场景延伸:查询表单 + 防重复
表格页常见「搜索 + 重置」:
html
<template>
<el-form ref="formRef" :model="queryForm" inline>
<el-form-item label="关键词" prop="keyword">
<el-input v-model="queryForm.keyword" placeholder="请输入" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="searching" :disabled="searching" @click="handleSearch">
搜索
</el-button>
<el-button @click="handleResetSearch">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="tableLoading" :data="tableData">
</el-table>
</template>
<script setup>
const searching = ref(false)
const tableLoading = ref(false)
async function handleSearch() {
if (searching.value) return
searching.value = true
try {
await fetchTableData()
} finally {
searching.value = false
}
}
function handleResetSearch() {
formRef.value?.resetFields()
fetchTableData()
}
</script>
要点:
- 搜索用
searching,表格用tableLoading,职责分开。 - 重置后一般要重新拉列表,可封装成
fetchTableData()统一处理。
[⬆ 返回目录](#⬆ 返回目录)
九、总结
| 点 | 规范 |
|---|---|
| 防重复 | submitting 控制 loading + disabled,函数开头判断,finally 中重置 |
| 校验 | 提交前统一 validate(),自定义规则用 validator |
| 重置 | 使用 resetFields(),弹窗 @open 时调用 |
| Loading | 只用一个 submitting,必要时再区分 loading / searching |
按这套规范写,表单代码会更好维护,也更少踩坑。可以直接把文中的模板复制到项目里,按业务改字段和接口即可。
🔍 系列模块导航
📝 表单与表格规范
一、《Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇》
二、《Vue3 + Element Plus 表单校验实战:规则复用、自定义校验、提示语统一,告别混乱避坑|表单与表格规范篇》
三、《Vue3 + Element Plus 表格查询规范:条件管理、分页联动 + 避坑,标准化写法|表单与表格规范篇》
四、《Vue3 + Element Plus 表格实战:批量操作、行内编辑、跨页选中逻辑统一|表单与表格规范篇》
五、《VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇》
👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~
📚 系列总览
「前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。
更新中,敬请期待~
[⬆ 返回目录](#⬆ 返回目录)
技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护。
哪怕每次只吃透一条规范,长期下来,差距会非常明显。
后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。
觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。
我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~