Vue3 + Element Plus 表单开发实战:防重复提交、校验、重置、loading 统一|表单与表格规范篇

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

📑 文章目录

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。


一、前言

表单是日常开发里最常见、最容易出问题的一块:重复提交、校验混乱、重置不干净、loading 满天飞,改一次牵一发动全身。

本文不讨论 Vue 源码,只围绕「写表单时该怎么选、为什么这么选、坑会在哪」来写,目标是:

  • 防重复提交:提交中禁用按钮、拦截二次点击
  • 校验统一:表单校验逻辑集中、错误展示清
  • 重置规范:重置时数据和校验状态都能回到初始
  • loading 统一:全局/局部 loading 管理方式清晰、不散乱

全文以 Vue3 + Composition API 为例,代码可直接复用。

[⬆ 返回目录](#⬆ 返回目录)


二、核心思路:先定「规范」,再写代码

很多人一上来就写 el-formsubmit,结果:

  • 防重复:有的用 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>

说明:

  1. submitting 既控制 loading 也控制 disabled,状态唯一来源。
  2. handleSubmit 开头判断 submitting.value,双重防护。
  3. try/catch/finally 保证 submitting 一定会被重置。
  4. 校验失败不进入请求逻辑,也不会把 submitting 设为 true。

[⬆ 返回目录](#⬆ 返回目录)

3.2 常见坑

坑点 问题 建议
只在 catch 里重置 成功但没进 try 时,submitting 永远是 true finally 统一重置
loadingdisabled 仍然可以点击 同时 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 等状态。

推荐做法:

  1. 使用 resetFields() 同时重置数据和校验。
  2. 弹窗打开时执行一次重置。
  3. 提交成功后如需清空,也调用 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-itemprop 一一对应。
  • 弹窗 @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

不要再单独搞 formLoadingbtnLoading 等,容易不同步。

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,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
计算机学姐2 小时前
基于SpringBoot的校园二手书籍交易系统【个性化推荐+数据可视化统计+我买到的+我卖出的】
vue.js·spring boot·后端·mysql·信息可视化·intellij-idea·mybatis
SuperEugene2 小时前
Vue3 + Element Plus 中后台弹窗规范:开闭、传参、回调,告别弹窗地狱|Vue 组件与模板规范篇
开发语言·前端·javascript·vue.js·前端框架
桜吹雪2 小时前
在前端运行Qwen3.5原生多模态模型
前端·人工智能·机器学习
孟祥_成都2 小时前
前端下午茶:这 3 个网页特效建议收藏(送源码)
前端·javascript·css
SuperEugene2 小时前
VXE-Table 4.x 实战规范:列配置 + 合并单元格 + 虚拟滚动,避坑卡顿 / 错乱 / 合并失效|表单与表格规范篇
开发语言·前端·javascript·vue.js·前端框架·vxetable
火车叼位2 小时前
Volta 下 `corepack` 失踪之谜:问题不在 Node,而在命令入口
前端
cmd2 小时前
别再用错!5种JS类型判断方法,从原理到实战一文吃透
前端·javascript
小江的记录本2 小时前
【Redis】Redis常用命令速查表(完整版)
java·前端·数据库·redis·后端·spring·缓存
Csvn2 小时前
状态管理方案对比(Context、Zustand、Jotai 选型指南)
前端