Skip to content

企业管理(三) ​

本页对应 企业管理,共 6 段视频,约 49 分钟。

学习目标 ​

  • 能够说明 el-upload 等非标准表单控件不会自动触发 el-form 校验的原因,并在上传成功回调中调用 validateField 对营业执照字段完成单独校验。
  • 能够将上传接口返回的 id 与 url 回写至表单对象对应字段,并在校验通过后收集表单数据、调用新增企业接口,成功后提示并跳转回列表页。
  • 能够完整复述图片上传流程:点击上传 → 组件打开本地文件选择框 → 上传前校验 → 获取 file 对象 → 构造 FormData 并按接口要求 append 参数 → 调用上传接口。
  • 能够区分表单的基础校验、自定义规则校验、统一校验与单字段校验四种方式,并说明各自的触发时机与返回值含义。
  • 能够在列表页点击编辑时携带 id 跳转,在编辑页通过路由 query 缓存 id,并据此适配“新增/编辑”文案显示。
  • 能够在存在 id 时请求企业详情接口完成表单回填(含营业执照图片展示),并在提交时按 id 分支调用更新接口、按接口字段要求裁剪参数。

知识点 ​

从列表进入编辑页:先明确为什么要携带 id ​

在列表页点击“编辑”时,我们不能只跳到一个空白表单页。因为编辑页和新增页通常共用同一个页面组件,页面本身并不知道用户要编辑哪一条企业数据。此时必须把当前行的唯一标识 id 带过去,后续编辑页才能根据这个 id 判断当前是“编辑状态”,并进一步获取详情、回填表单、调用更新接口。

怎么想:列表中的每一行数据都有一个 id,按钮在每一行里渲染,因此点击事件要拿到当前行的 id,而不是拿固定值。转写稿里也强调,编辑和新增共用页面时,很多地方都靠 id 是否存在来区分状态。所以跳转时要把 id 放进路由参数里。

怎么做:给编辑按钮绑定点击事件,把 scope.row.id 传进去。笔记代码如下:

vue
<el-button 
  size="mini" 
  type="text" 
  @click="editRent(scope.row.id)"
  >编辑</el-button>
  
editRent(id) {
  this.$router.push({
    path: '/exterpriseAdd',
    query: {
      id
    }
  })
}

易错点:@click="editRent(scope.row.id)" 不要写成 @click="editRent",否则方法拿不到当前行 id;方法形参要接收 id;query 中的 id 是键值简写,等价于 id: id,不要误写成别的字段名。

用 query 携带参数:为什么选 query,怎么跳 ​

为什么需要:编辑页需要通过地址栏参数拿到 id,而 query 方式会把参数拼在路由地址后面,刷新页面后仍然可以从 this.$route.query.id 读取,适合这种“跳转后还要继续使用”的场景。

怎么想:转写稿中说“通过一个 query 的方式给它跳过去”,并且路径和新增页一致。也就是说,新增和编辑进入的是同一个表单页面,只是编辑时多带一个 id。这样页面组件不用复制两份,后续通过 id 是否存在做分支即可。

怎么做:在 editRent(id) 中调用 this.$router.push,传入 path 和 query。路径按笔记保留为 /exterpriseAdd,参数写成:

js
query: {
  id
}

易错点:path 要和新增页实际路由保持一致,不能随意写;query 不要写成 params,否则刷新后参数可能丢失;id 的键名要和编辑页读取时一致,不能一边写 id、一边读 enterpriseId。

在编辑页缓存路由 id,并适配“新增/编辑”文案 ​

为什么需要:编辑页后续会多次判断“有没有 id”,如果每次都写 this.$route.query.id 会比较长,也容易写错。转写稿中明确提到,因为很多地方都要用这个 id,所以可以在计算属性里做一个缓存。

怎么想:路由上的 query.id 是当前页面判断新增/编辑的依据。把它缓存成组件内的计算属性,例如 id,后续模板和方法里统一用这个值。转写稿里还演示了文案适配:有 id 时显示“编辑企业”,没有 id 时显示“添加企业”。

怎么做:在编辑页组件中增加计算属性,读取 this.$route.query.id;在标题文案处用三元判断。转写稿的调试过程是:点列表第一个“编辑”,页面跳过来后因为有 id,文案显示为编辑;如果把 id 去掉,就变成添加状态。

易错点:不要在 data 里只取一次就完事,因为路由参数变化时计算属性能跟随;三元判断要基于缓存后的 id 是否存在;文案适配和后续接口判断要使用同一个来源,避免一个读路由、一个读旧变量。

调试与验证:跳转成功后看什么 ​

为什么需要:这类跳转不能只看代码“像对的”,要实际点击验证。转写稿中点列表第一条的“编辑”,观察页面是否跳转,并确认编辑页能拿到 id。

怎么想:先验证地址栏是否出现 query 参数,再验证页面文案是否从“添加”变成“编辑”,最后确认后续获取详情时能拿到这个 id。转写稿也提到,后面会用这个 id 去获取详情数据并回填,所以本节的跳转和缓存是后续操作的前提。

怎么做:运行项目,在列表页点击某一行“编辑”,检查路由是否进入 /exterpriseAdd?id=...;在编辑页确认 this.$route.query.id 有值;再观察标题文案是否为“编辑企业”。

易错点:只验证页面跳过去还不够,要确认 id 真的传到了;如果点击后地址栏没有 id,优先检查按钮事件是否传参、query 是否写对;如果编辑页读不到,检查读取的是 $route.query.id 而不是 $route.params.id。

本节易错点小结 ​

第一,按钮事件要传当前行 id,不能写成固定值;第二,router.push 中 path 与 query 要配套,query 的键名要和读取处一致;第三,编辑和新增共用页面时,用 id 是否存在区分状态,并把路由 id 缓存到计算属性中,方便后续复用;第四,调试时先看跳转地址,再看文案和后续详情请求参数。本节的核心就是:列表按钮传 id → router.push 用 query 携带 → 编辑页读取并缓存 id,为后续回填和更新做准备。

先明确场景:新增和编辑为什么要共用页面 ​

根据 id 适配页头文案示意图

为什么需要:新增企业和编辑企业填写的表单字段基本一致,如果拆成两个页面,会产生大量重复代码;共用同一个页面时,又必须让用户一眼看出当前是“新增”还是“编辑”。转写稿里也明确提到:点击列表中的编辑按钮,需要携带当前行的 id 跳到同一个页面,然后根据有没有 id 来区分状态。所以核心判断条件就是路由上的 id。

怎么想:不要在每个地方都临时判断“有没有 id”,而是先把 id 统一取出来缓存成计算属性。这样页头文案、后续获取详情、提交时调新增还是更新接口,都可以复用同一个 this.id。转写稿里说“因为有很多地方都是要用的 ID,我们找个地给他缓存一下”,这就是计算属性的价值。

怎么做:在组件里声明 computed,从 this.$route.query.id 读取 id。列表页点击编辑时,把当前行 id 通过 query 带过去;跳转路径与新增页面保持一致。进入页面后,this.id 有值就是编辑,没有值就是新增。

易错点:不要在每个方法里反复写很长的 this.$route.query.id;computed 里必须 return;query 参数取到通常是字符串,这里只判断“是否存在”即可;新增时不要误传空 id,否则会被当成编辑状态。

页头文案:用三元表达式适配“编辑企业/添加企业” ​

为什么需要:页头是用户进入页面后最先看到的区域,如果新增和编辑都显示“添加企业”,编辑时就会误导用户。笔记里用 el-page-header 的 content 来控制标题,并根据 id 是否存在切换文案。

怎么想:文案只取决于一个条件:id 有值就是编辑,没有值就是新增。因此不需要写复杂的 if/else,直接在模板里用三元表达式最直接。转写稿里也是这个思路:ID 存在写“编辑企业”,否则写“添加企业”。

怎么做:保留笔记中的写法,把 content 绑定为模板字符串,并在里面做三元判断;返回按钮继续使用 @back="$router.back()"。

vue
<el-page-header 
  :content="`${id?'编辑企业':'添加企业'}`" 
  @back="$router.back()" />

computed: {
  id() {
    return this.$route.query.id
  }
}

易错点:content 前不要漏掉冒号,否则会当成普通字符串;模板字符串的反引号不要写成单引号;三元表达式里的 id 是计算属性,不要误写成 this.id(模板中通常可直接访问);返回事件不要写成普通点击方法而破坏页头默认返回逻辑。

调试过程:点编辑能跳转,去掉 id 能变回新增 ​

为什么需要:这类“根据 id 切换状态”的逻辑不复杂,但很容易因为路由参数没带上、计算属性没返回、模板绑定写错而失效。转写稿里的验证方式很直接:点击列表中的编辑按钮,观察是否跳到同一页面,并看页头是否显示“编辑企业”;再把 id 去掉,检查是否变回“添加企业”。

怎么想:调试时先看路由上有没有 query.id,再看计算属性 id 是否拿到值,最后看页头 content 是否绑定正确。不要一上来就怀疑组件库。

怎么做:点击编辑后,确认地址栏 query 中带上了 id;页面中页头显示“编辑企业”。如果手动去掉 id 或从新增入口进入,页头应显示“添加企业”。后续获取详情、提交更新时,也继续用这个 this.id 做判断,保持逻辑统一。

易错点:编辑按钮事件里忘记把当前行 id 传出去;跳转时参数没有放在 query 里;计算属性名与模板中使用名不一致;只改了页头文案,却忘了后续接口也要根据 id 区分新增和更新。转写稿还提醒:提交时要根据 id 是否存在调用不同接口,不要在前面保留新增调用后又写分支,导致逻辑一直走旧接口。

先封装企业详情接口 ​

为什么需要: 编辑企业时,页面不能只靠路由里的 ID 显示空表单,必须拿着 ID 去后端查出这家企业的名称、联系人、营业执照等信息,再把数据放回表单。详情接口就是“根据 ID 取一条企业数据”的入口。

怎么想: 这是一个查询详情的 GET 请求,ID 直接拼在 URL 路径里,不需要额外传 query 或 body。封装到 API 层后,组件里只负责调用和回填,避免把请求地址散落在页面中。

怎么做: 按笔记保留接口封装:

javascript
/**
 * 获取合同详情
 * @param {*} id
 * @returns
 */
export function getEnterpriseDetailAPI(id) {
  return request({
    url: `/park/enterprise/${id}`
  })
}

易错点: 接口地址不要多空格;ID 要确认来自当前编辑行;返回值通常在 res.data 里,回填时不要误拿整个响应对象。转写稿里反复提到的“教验”应纠正为“校验”,这里虽然不做表单校验,但字段取值同样要仔细。

进入编辑页先判断有没有 ID ​

为什么需要: 新增和编辑往往复用同一个页面。新增时没有企业 ID,不能调用详情接口;编辑时才有 ID,才需要查询并回填。否则新增页一进来就请求详情,既没有参数,也没有意义。

怎么想: 判断条件就是当前页面是否拿到了企业 ID。转写稿里说这个 ID 是从列表页点击编辑时带过来,并通过路由 query 暂存到计算属性里,后续统一用缓存后的 ID 做判断和请求。

怎么做: 在 mounted 中判断,有 ID 才调用详情方法:

vue
mounted() {
  // 省略代码 有合同id 调用详情接口
  if (this.Id) {
    this.getEnterpriseDetail()
  }
}

易错点: 笔记示例里出现了 this.rentId 和 this.Id,实际落地时要和项目里缓存的 ID 命名保持一致,不要一个地方写 ID、另一个地方写 Id。另外,详情请求要放在有 ID 的分支里,不能无条件调用。

用详情返回值回填 addForm ​

为什么需要: 表单通过 addForm 做双向绑定。详情数据查回来后,只有把接口字段赋值给 addForm 中对应字段,输入框、下拉框才会显示已有内容。

怎么想: 不要凭感觉把整个 res.data 直接塞进表单,而是按表单需要的字段做解构,再重新组装 addForm。这样字段来源清晰,也能避免把无关字段带进表单。

怎么做: 调用详情接口,解构出表单字段并赋值:

vue
async getEnterpriseDetail() {
  const res = await getEnterpriseDetailAPI(this.rentId)
  const { businessLicenseId, businessLicenseUrl, contact, contactNumber, industryCode, legalPerson, name, registeredAddress } = res.data
  this.addForm = { businessLicenseId, businessLicenseUrl, contact, contactNumber, industryCode, legalPerson, name, registeredAddress }
}

易错点: 字段名必须和接口返回、表单绑定三方一致,尤其是 businessLicenseId、businessLicenseUrl 这类长字段名。转写稿里说“结构”应理解为“解构”。如果接口返回为空,要先看 Network 中详情请求的参数和响应,再检查 res.data 是否取对。

营业执照图片要单独回显 ​

为什么需要: 普通输入框只要 addForm 有值就会显示,但营业执照是上传组件。只把 businessLicenseId、businessLicenseUrl 存进表单还不够,页面上还要把已经上传的图片显示出来,用户才能知道当前企业已有营业执照。

怎么想: 接口返回里已经有 businessLicenseUrl,它就是一个图片地址。页面中具备显示图片能力的是 img,所以用 img 的 src 绑定这个 URL。转写稿调试时也提到,先手动加一个 img,绑定 URL 后图片就能出现;如果图片太大,再补样式调整。

怎么做: 在营业执照位置增加图片标签,src 绑定 addForm.businessLicenseUrl。具体模板结构按项目已有布局放置,核心是让 URL 字段参与回显。

企业详情回填效果

易错点: 只回填 ID 不回填 URL,图片不会显示;URL 为空时不要显示破图;图片过大通常是样式问题,不是接口没返回。转写稿里“URO 地址”应纠正为“URL 地址”。

调试与验证回填结果 ​

为什么需要: 回填涉及路由 ID、详情请求、响应字段、表单字段、图片地址多个环节,必须通过实际刷新编辑页确认整条链路都通。

怎么想: 先看请求有没有发、参数是不是当前企业 ID;再看响应 data 里有没有字段;然后看 addForm 是否被赋值;最后看营业执照图片是否用 businessLicenseUrl 显示出来。

怎么做: 从列表点击编辑进入页面,刷新后观察详情接口;对照接口返回逐个检查表单字段;检查图片 src 是否等于返回的 URL;若图片尺寸异常,再补样式。转写稿中“调一个详情接口”“参数已经拼过来”就是这一验证过程。

易错点: 不要在 ID 还没准备好时提前请求;不要只看页面不查 Network;不要把新增和编辑的 ID 判断写混。回填完成后,才具备后续编辑提交的数据基础。

先明确:编辑确认修改为什么要按状态分流 ​

为什么需要:新增企业和编辑企业常常共用同一个表单页面,校验规则也可以共用,但提交动作不能共用。新增对应创建接口,编辑对应更新接口;如果编辑时仍然调用创建接口,就可能新增一条重复数据,而不是修改当前企业。

怎么想:提交前先判断当前页面处于什么状态。项目里通常用路由参数或计算属性缓存 id:有 id 说明是从列表点击“编辑”进来的,是编辑状态;没有 id 说明是“新增”状态。然后只在一个提交方法里做分支,不同分支调用不同接口。

怎么做:先封装更新接口,再在 confirmSubmit 中先做表单统一校验,校验通过后根据 id 分流。

易错点:不要在前面保留原来的新增调用,后面又写一个 if/else,否则可能一直走旧逻辑;同时注意 this.id 与 this.Id 这类命名要跟项目实际缓存字段保持一致,避免判断和传参取不到值。

封装编辑更新接口 ​

为什么需要:更新企业是独立接口,通常使用 PUT 请求,和新增接口的请求方法、语义都不同。单独封装后,组件里只负责调用,接口地址和请求方法集中在 API 文件中维护。

怎么想:照着创建接口的封装方式复制一份,把导出函数名改成更新语义,把 method 改成 PUT,请求地址使用编辑接口地址,参数继续通过 data 传递。

怎么做:本节笔记中的封装代码如下:

javascript
/**
 * 更新企业
 * @param {*} data
 * @returns
 */
export function updateExterpriseAPI(data) {
  return request({
    url: '/park/enterprise',
    method: 'PUT',
    data
  })
}

易错点:函数名、接口地址、请求方法三者要对应;更新接口不要误写成 POST。另外,笔记中的 updateExterpriseAPI 拼写要按项目现有命名为准,不要随意改动导致导入失败。

在 confirmSubmit 中区分新增与编辑 ​

为什么需要:确认按钮是同一个,但提交目标不同。统一校验只能保证表单数据合法,不能决定“创建”还是“更新”,所以必须在校验通过后按状态选择接口。

怎么想:先执行 this.$refs.ruleForm.validate,只有 valid 为真才提交。提交时判断 this.id:存在则走编辑,调用更新接口,并把当前表单数据和 id 一起提交;不存在则走新增,调用创建接口。提交成功后返回列表页。

怎么做:本节笔记中的提交代码如下:

javascript
confirmSubmit() {
  this.$refs.ruleForm.validate(async valid => {
    console.log(valid)
    if (this.id) {
      // 编辑
      await updateExterpriseAPI({
        ...this.addForm,
        id: this.Id
      })
    } else {
      // 新增
      await createExterpriseAPI(this.addForm)
    }
    this.$router.back()
  })
}

易错点:第一,validate 的回调是异步场景,接口调用要 await,成功后再 router.back();第二,编辑提交时不要漏传 id,否则后端不知道更新哪条数据;第三,分支里只保留一个接口调用,不要把新增接口写在分支外导致重复提交;第四,如果项目里判断用 this.id,传参却写 this.Id,要核对大小写是否一致。

更新参数要以接口要求为准 ​

为什么需要:表单里收集的字段不一定和更新接口要求的字段完全一致。转写稿中调试时也强调,提交前要对照接口文档,以接口字段要求为主;字段多了要筛选,字段少了后端无法入库。

怎么想:先看接口文档要求的参数结构,再对照 addForm 中实际收集的数据。笔记示例用 ...this.addForm 展开并补 id,适合字段基本匹配的场景;如果字段不完全匹配,就按接口要求整理参数,或通过解构只取需要字段。

怎么做:提交前打印或对比 this.addForm 与接口文档,确认 id、企业名称、营业执照 URL 等字段名称正确;编辑成功后提示“更新成功”,新增成功后提示“新增成功”,再返回列表。

易错点:不要机械地把整个表单对象直接扔给接口而不核对字段;更新时尤其要确认 id 字段名和值都存在。还有一点背景要记住:上传组件不会像普通输入框那样自动通知表单校验,编辑回填营业执照图片时要给对应 URL 字段赋值,否则图片和校验状态都可能不对。

先看目标:弹框的显示与隐藏由数据控制 ​

这一节要解决的是:点击“添加合同”按钮打开弹框,点击“取消”或触发弹框关闭时把弹框关掉。笔记给出的核心结论是:通过 visible 属性的 true 和 false 切换打开关闭弹框。也就是说,弹框是否显示,不靠直接操作 DOM,而是靠组件数据 rentDialogVisible 驱动。

为什么需要:如果直接改 DOM 的 display 或 v-if,状态会散落在各处,关闭入口一多就容易不一致。用布尔值控制,打开和关闭都只改一个数据源,逻辑清晰。

怎么想:把 rentDialogVisible 当成弹框的“开关状态”。初始为 false,点击按钮改成 true;取消或关闭时改回 false。弹框组件只负责根据这个值渲染。

怎么做:在 data 中声明 rentDialogVisible: false,在 el-dialog 上写 :visible="rentDialogVisible",按钮点击调用 addRent() 置为 true,关闭方法 closeDialog() 置为 false。

易错点:不要写成 :visible="true" 或 visible="true" 这种固定值;前者是布尔表达式,后者容易被当成字符串。也不要只打开不关闭,关闭入口要统一收口到 closeDialog。

打开弹框:按钮点击把 visible 置为 true ​

笔记中的打开入口是:

vue
<el-button size="mini" type="text" @click="addRent()">添加合同</el-button>

对应方法:

js
addRent(){
  this.rentDialogVisible = true
}

为什么需要:用户点击“添加合同”时,需要一个明确动作把弹框状态从“关”切到“开”。把打开逻辑放在方法里,而不是模板里写复杂表达式,便于后续扩展,比如打开前做权限判断、初始化表单等。

怎么想:按钮只负责触发动作,动作只负责改状态。弹框显示与否由 rentDialogVisible 决定,按钮不直接控制弹框组件。

怎么做:模板绑定 @click="addRent()",方法中把 this.rentDialogVisible = true。注意 this 指向当前组件实例。

易错点:方法名和模板名要一致;不要漏写 this;如果后续在打开时还要重置表单,应放在打开方法中,但本节笔记没有展开表单重置,不要凭空添加。

关闭弹框:取消按钮与 close 事件统一收口 ​

笔记中弹框配置为:

vue
<el-dialog
  title="添加合同"
  :visible="rentDialogVisible"
  width="580px"
  @close="closeDialog"
  >

底部取消按钮也调用同一个关闭方法:

vue
<el-button size="mini" @click="closeDialog">取 消</el-button>

关闭方法:

js
closeDialog(){
  this.rentDialogVisible = false
}

为什么需要:弹框关闭可能来自取消按钮,也可能来自右上角关闭、遮罩层点击等。把关闭逻辑统一到 closeDialog,可以保证所有入口最终都把 rentDialogVisible 改回 false。

怎么想:@close 是弹框关闭时触发的事件,取消按钮是用户主动关闭。两者都调用同一个方法,状态来源仍然只有一个。

怎么做:模板上写 @close="closeDialog",取消按钮写 @click="closeDialog",方法中只做 this.rentDialogVisible = false。

易错点:如果只在取消按钮里改 false,而右上角关闭没有处理,可能出现弹框关了但数据还是 true 的不一致状态。反过来,如果 @close 里又触发关闭动作,要注意不要造成重复调用或循环关闭。当前笔记的写法是“关闭事件里把状态置 false”,属于受控写法,保持简单即可。

确定按钮与关闭时机:本节只做开关,提交逻辑另接 ​

笔记中确定按钮目前是:

vue
<el-button size="mini" type="primary">确 定</el-button>

它没有绑定点击事件。结合转写稿里反复出现的调试过程:点击确定后要先走表单校验,校验通过才调接口;新增和编辑共用页面时,还要根据 ID 判断调新增还是更新接口,成功后再提示并跳回列表页。可以推导出:弹框的“确定”通常不是简单关闭,而是“校验—提交—成功后再关闭”的流程。

为什么需要:如果确定按钮直接调用 closeDialog,用户还没提交就关掉了弹框,数据不会保存;如果提交失败也关闭,用户看不到错误。转写稿中强调“校验通过之后才调接口”,说明确定按钮承担的是业务动作,不是纯关闭。

怎么想:把“关闭弹框”与“提交成功”分开。取消按钮负责放弃并关闭;确定按钮负责校验和提交,提交成功后再调用关闭方法。本节笔记只实现了打开关闭,所以确定按钮暂未绑定是合理的,但实际接入时要补上。

怎么做:本节先保留笔记代码不动。后续接提交时,在确定按钮上绑定提交方法,方法内先做表单校验,成功后再关闭弹框。注意不要在当前笔记中臆造提交接口。

易错点:不要把 closeDialog 直接绑到确定按钮上;不要在提交失败时关闭弹框;不要因为新增和编辑共用弹框就写两份重复提交逻辑,转写稿也提醒过要按 ID 分支调不同接口,而不是复制两份流程。

完整代码保留 ​

vue
<el-button size="mini" type="text" @click="addRent()">添加合同</el-button>

<el-dialog
  title="添加合同"
  :visible="rentDialogVisible"
  width="580px"
  @close="closeDialog"
  >
  <!-- 表单区域 -->
  <div class="form-container">

  </div>
  <template #footer>
    <el-button size="mini" @click="closeDialog">取 消</el-button>
    <el-button size="mini" type="primary">确 定</el-button>
  </template>
</el-dialog>

data(){
  return {
    rentDialogVisible:false
  }
}

methods:{
  // 打开
  addRent(){
    this.rentDialogVisible = true
  },
  // 关闭
  closeDialog(){
    this.rentDialogVisible = false
  }
}

调试检查顺序 ​

  1. 初始进入页面,rentDialogVisible 应为 false,弹框不显示。
  2. 点击“添加合同”,检查 rentDialogVisible 是否变为 true,弹框是否出现。
  3. 点击“取消”,检查是否变为 false,弹框是否消失。
  4. 点击右上角关闭或触发 close,确认同样走 closeDialog。
  5. 再接确定按钮时,按转写稿思路先校验、再提交、成功后再关闭,避免把“关闭”误当成“提交”。

这一节先不急着调接口,而是把租赁表单的“模板、数据、规则”三件套搭好。后面无论做统一校验、单独校验还是提交,都是在这三样东西上做文章。

准备表单结构示意

vue
<!-- 表单模版 -->
<div class="form-container">
  <el-form ref="addForm" :model="rentForm" :rules="rentRules" label-position="top">
    <el-form-item label="租赁楼宇" prop="buildingId">
      <el-select v-model="rentForm.buildingId" placeholder="请选择">
        <el-option
          v-for="item in []"
          :key="item.id"
          :label="item.name"
          :value="item.id"
        />
      </el-select>
    </el-form-item>
    <el-form-item label="租赁起止日期" prop="rentTime">
      <el-date-picker
        v-model="rentForm.rentTime"
        type="daterange"
        range-separator="至"
        start-placeholder="开始日期"
        end-placeholder="结束日期"
        value-format="yyyy-MM-dd"
      />
    </el-form-item>
    <el-form-item label="租赁合同" prop="contractId">
      <el-upload
        action="#"
      >
        <el-button size="small" type="primary" plain>上传合同文件</el-button>
        <div slot="tip" class="el-upload__tip">支持扩展名:.doc .docx .pdf, 文件大小不超过5M</div>
      </el-upload>
    </el-form-item>
  </el-form>
</div>

data(){
  return {
    rentForm: {
        buildingId: null, // 楼宇id
        contractId: null, // 合同id
        contractUrl: '', // 合同Url
        enterpriseId: null, // 企业名称
        type: 0, // 合同类型
        rentTime: [] // 合同时间
    },
    rentRules: {
        buildingId: [
          { required: true, message: '请选择楼宇', trigger: 'change' }
        ],
        rentTime: [
          { required: true, message: '请选择租赁日期', trigger: 'change' }
        ],
        contractId: [
          { required: true, message: '请上传合同文件' }
        ]
    }
  }
}

先看整体:el-form 是容器,el-form-item 是格子 ​

为什么需要:表单不是一个输入框一个输入框散着写的,el-form 负责“收集数据、套规则、给校验入口”,el-form-item 负责“把某一项和某条规则挂上钩”。

怎么想:把 el-form 想成一个登记表::model 是登记表的数据本,:rules 是填写规范,ref 是以后喊它做校验时的名字;每个 el-form-item 上的 prop 就是“这一格在数据本里的字段名”。

怎么做:容器上写 ref="addForm" :model="rentForm" :rules="rentRules",每一项写 prop="buildingId" 这类字段名,控件里用 v-model="rentForm.buildingId" 绑定。

易错点:prop、v-model 里的字段名、rentForm 里的键名、rentRules 里的键名,四处必须完全一致。少一个,校验就会“看起来没反应”。

楼宇下拉:先把 v-for 的位置留出来 ​

为什么需要:楼宇不是写死的,是后端楼宇列表接口返回的,所以模板里先把循环结构搭好。

怎么想:下拉框的值要提交 id,显示要 name,所以 :value="item.id"、:label="item.name"、:key="item.id"。

怎么做:v-model 绑 rentForm.buildingId,v-for 先写空数组占位,等接口数据回来再替换。

易错点::value 写成 item.name,提交上去的是楼宇名字而不是 id;v-model 写成 rentForm 整个对象,选完值对不上。空数组时下拉没选项是正常的,不是写错了。

租赁起止日期:daterange 要配 value-format ​

为什么需要:日期范围控件默认给的是 Date 对象,接口一般要 yyyy-MM-dd 字符串,不转后面提交会对不上。

怎么想:type="daterange" 决定它是范围选择,v-model 绑定的必须是数组;value-format 决定“拿到手的值长什么样”。

怎么做:rentTime: [] 初始化为数组,控件上加 value-format="yyyy-MM-dd",range-separator 和两个 placeholder 只是展示文案。

易错点:rentTime 初始值写成 null,范围选择器容易出问题;忘了 value-format,校验能过但提交的是对象。

租赁合同:upload 不是普通表单控件 ​

为什么需要:合同上传这一项,el-form 的校验系统收不到“上传完成”的通知。输入框你敲完字它会通知表单,el-upload 不会。

怎么想:所以 contractId 这条规则虽然写了 required,但它不会因为你上传成功就自动消失。要等上传成功回调里,先把返回的 id、url 赋给 rentForm.contractId 和 rentForm.contractUrl,再手动调一次 this.$refs.addForm.validateField('contractId'),单独校验这个字段。转写稿里反复强调的一点是:必须等上传成功、拿到返回值之后再校验,数据还没回来就校验,肯定过不去。

怎么做:当前先用 action="#" 占位,按钮和 slot="tip" 的提示文案先写好;action 是必填属性,# 只是占位,等接真实上传时再替换。

易错点:slot="tip" 里的“不超过5M、支持 .doc .docx .pdf”只是提示文案,不是校验;真正的大小和类型限制要放在上传前校验里,并且前后端校验逻辑要保持一致——前端先粗略挡一层,后端再挡一层,因为接口不一定只从浏览器发出来。

rentForm:字段名就是提交时的参数名 ​

为什么需要:这些字段既是双向绑定的目标,也是后面提交接口的数据来源。

怎么想:按类型给初始值:id 类给 null,url 给 '',时间范围给 [],类型给 0。

怎么做:buildingId、contractId、contractUrl、enterpriseId、type、rentTime 一次列清。contractId 和 contractUrl 是两个字段,别混。

易错点:enterpriseId 在模板里没渲染,但提交时要用,不能漏;type: 0 是合同类型的默认值,不是“没填”。

rentRules:trigger 选 change 还是 blur ​

为什么需要:不同控件的“值变了”时机不一样,trigger 写错,规则就不触发。

怎么想:下拉、日期选择都是选完就变,用 change;contractId 这条没写 trigger,因为它本来就要靠手动 validateField 触发。

怎么做:三条规则都写 required: true 加中文 message,前两条补 trigger: 'change'。

易错点:给 el-select 写 trigger: 'blur',选了也不校验;以为 contractId 规则写完就万事大吉,忘了上传成功后手动校验这一步。

准备阶段的检查清单 ​

模板搭完,先自查三件事:prop 与 rentForm 字段是否一一对应;初始值类型是否和控件匹配;上传字段是否已经想好“成功回调里赋值再 validateField”的顺序。等这些齐了,再去接统一校验 validate,它会把所有需要校验的项跑一遍,并把布尔结果交给回调。

代码示例 ​

示例一:列表页携带 id 跳转编辑页(EnterpriseList.vue) ​

vue
<template>
  <div>
    <el-table :data="list">
      <el-table-column prop="name" label="企业名称" />
      <el-table-column label="操作" width="120">
        <template #default="scope">
          <el-button
            size="mini"
            type="text"
            @click="editRent(scope.row.id)"
          >编辑</el-button>
        </template>
      </el-table-column>
    </el-table>
  </div>
</template>

<script>
export default {
  data() {
    return {
      list: []
    }
  },
  methods: {
    // 携带当前行 id 跳到编辑页(与新增页是同一个路由)
    editRent(id) {
      this.$router.push({
        path: '/exterpriseAdd',
        query: {
          id
        }
      })
    }
  }
}
</script>

要点解释 / 易错点

  • scope.row.id 必须传,editRent 的形参 id 与 query 里的 id 同名,可以简写为 query: { id }(ES6 属性简写)。
  • 编辑页和新增页复用同一个路由 /exterpriseAdd,所以编辑时不能写成 path: '/exterpriseEdit'。
  • 用 query 而不是 params:query 参数刷新页面不会丢,params 刷新即丢失。
  • 容易踩的坑:@click="editRent(scope.row.id)" 写成了 editRent(scope.row) 却按 id 使用。

示例二:新增 / 编辑企业页(EnterpriseAdd.vue)——文案适配 + 数据回填 + 区分接口提交 ​

vue
<template>
  <div class="add-enterprise">
    <!-- 有 id 就是编辑,没有就是新增 -->
    <el-page-header
      :content="`${id ? '编辑企业' : '添加企业'}`"
      @back="$router.back()"
    />

    <el-form
      ref="ruleForm"
      :model="addForm"
      :rules="rules"
      label-position="top"
    >
      <el-form-item label="企业名称" prop="name">
        <el-input v-model="addForm.name" />
      </el-form-item>

      <el-form-item label="营业执照" prop="businessLicenseId">
        <!-- 回填时展示图片,未上传时展示上传按钮 -->
        <el-upload
          action="#"
          :show-file-list="false"
          :http-request="uploadLicense"
        >
          <img
            v-if="addForm.businessLicenseUrl"
            :src="addForm.businessLicenseUrl"
            class="license-img"
          />
          <el-button v-else size="small" type="primary" plain>上传营业执照</el-button>
        </el-upload>
      </el-form-item>

      <el-form-item label="法人" prop="legalPerson">
        <el-input v-model="addForm.legalPerson" />
      </el-form-item>

      <el-form-item label="联系人" prop="contact">
        <el-input v-model="addForm.contact" />
      </el-form-item>

      <el-form-item label="联系电话" prop="contactNumber">
        <el-input v-model="addForm.contactNumber" />
      </el-form-item>

      <el-button type="primary" @click="confirmSubmit">确 定</el-button>
    </el-form>
  </div>
</template>

<script>
import {
  getEnterpriseDetailAPI,
  updateExterpriseAPI,
  createExterpriseAPI
} from '@/api/enterprise'

export default {
  data() {
    return {
      addForm: {
        businessLicenseId: '',
        businessLicenseUrl: '',
        contact: '',
        contactNumber: '',
        industryCode: '',
        legalPerson: '',
        name: '',
        registeredAddress: ''
      },
      rules: {
        name: [{ required: true, message: '请输入企业名称', trigger: 'blur' }],
        legalPerson: [{ required: true, message: '请输入法人', trigger: 'blur' }],
        contact: [{ required: true, message: '请输入联系人', trigger: 'blur' }],
        contactNumber: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
        businessLicenseId: [{ required: true, message: '请上传营业执照' }]
      }
    }
  },

  computed: {
    // 把路由上的 id 缓存成计算属性,页面里到处都能用
    id() {
      return this.$route.query.id
    }
  },

  mounted() {
    // 只有编辑状态(有 id)才需要请求详情
    if (this.id) {
      this.getEnterpriseDetail()
    }
  },

  methods: {
    async getEnterpriseDetail() {
      const res = await getEnterpriseDetailAPI(this.id)
      const {
        businessLicenseId,
        businessLicenseUrl,
        contact,
        contactNumber,
        industryCode,
        legalPerson,
        name,
        registeredAddress
      } = res.data
      this.addForm = {
        businessLicenseId,
        businessLicenseUrl,
        contact,
        contactNumber,
        industryCode,
        legalPerson,
        name,
        registeredAddress
      }
    },

    // 上传营业执照:拿到返回值后回填 + 单独校验营业执照字段
    async uploadLicense({ file }) {
      // 此处调用上传接口(见上传小节),返回 data 里有 id 和 url
      const res = await this.uploadFileAPI(file)
      this.addForm.businessLicenseId = res.data.id
      this.addForm.businessLicenseUrl = res.data.url
      // 上传组件不会自动通知 el-form,必须手动单独校验该字段
      this.$refs.ruleForm.validateField('businessLicenseId')
    },

    confirmSubmit() {
      this.$refs.ruleForm.validate(async (valid) => {
        if (!valid) return
        if (this.id) {
          // 编辑:把 id 一起提交
          await updateExterpriseAPI({
            ...this.addForm,
            id: this.id
          })
          this.$message.success('更新成功')
        } else {
          // 新增
          await createExterpriseAPI(this.addForm)
          this.$message.success('新增成功')
        }
        this.$router.back()
      })
    }
  }
}
</script>

<style scoped>
.license-img {
  width: 120px;
  height: 120px;
  object-fit: contain;
}
</style>

要点解释 / 易错点

  • computed 里的 id 直接从 $route.query.id 取,页面多处判断(文案、是否请求详情、提交哪个接口)都复用它,避免到处写长串路径。
  • mounted 里必须先 if (this.id) 再请求详情,否则新增页也会发一次详情请求。
  • getEnterpriseDetail 用解构 + 整体替换赋值:字段必须和 data 里的 addForm 一一对应,少写一个字段就会丢,v-model 绑定的值也会变空。
  • uploadLicense 里必须先 await 拿到返回数据、赋值,再调 validateField。如果不等接口返回就校验(或漏写 await 走 .then 回调),此时 businessLicenseId 还是空,校验必然不通过。
  • 上传组件 el-upload 不属于 el-form 默认监听的表单控件,上传完成后不会通知表单,所以“请上传营业执照”的提示不会自动消失,必须手动 validateField('businessLicenseId')(字段名要和 prop 一致)。
  • confirmSubmit 里用 if (!valid) return 提前返回,不要写成“先调一次新增接口,再写 if/else 调更新接口”,那样新增接口会重复执行。
  • 编辑提交的参数字段要以接口文档为准,多出来的字段要手动筛掉或解构掉。

示例三:弹框打开/关闭 + 表单准备(AddRentDialog.vue) ​

vue
<template>
  <div>
    <el-button size="mini" type="text" @click="addRent()">添加合同</el-button>

    <el-dialog
      title="添加合同"
      :visible="rentDialogVisible"
      width="580px"
      @close="closeDialog"
    >
      <div class="form-container">
        <el-form
          ref="addForm"
          :model="rentForm"
          :rules="rentRules"
          label-position="top"
        >
          <el-form-item label="租赁楼宇" prop="buildingId">
            <el-select v-model="rentForm.buildingId" placeholder="请选择">
              <el-option
                v-for="item in []"
                :key="item.id"
                :label="item.name"
                :value="item.id"
              />
            </el-select>
          </el-form-item>

          <el-form-item label="租赁起止日期" prop="rentTime">
            <el-date-picker
              v-model="rentForm.rentTime"
              type="daterange"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
              value-format="yyyy-MM-dd"
            />
          </el-form-item>

          <el-form-item label="租赁合同" prop="contractId">
            <el-upload action="#">
              <el-button size="small" type="primary" plain>上传合同文件</el-button>
              <div slot="tip" class="el-upload__tip">
                支持扩展名:.doc .docx .pdf, 文件大小不超过5M
              </div>
            </el-upload>
          </el-form-item>
        </el-form>
      </div>

      <template #footer>
        <el-button size="mini" @click="closeDialog">取 消</el-button>
        <el-button size="mini" type="primary">确 定</el-button>
      </template>
    </el-dialog>
  </div>
</template>

<script>
export default {
  data() {
    return {
      rentDialogVisible: false,
      rentForm: {
        buildingId: null,    // 楼宇id
        contractId: null,    // 合同id
        contractUrl: '',     // 合同Url
        enterpriseId: null,  // 企业名称
        type: 0,             // 合同类型
        rentTime: []         // 合同时间
      },
      rentRules: {
        buildingId: [{ required: true, message: '请选择楼宇', trigger: 'change' }],
        rentTime: [{ required: true, message: '请选择租赁日期', trigger: 'change' }],
        contractId: [{ required: true, message: '请上传合同文件' }]
      }
    }
  },
  methods: {
    // 打开弹框
    addRent() {
      this.rentDialogVisible = true
    },
    // 关闭弹框
    closeDialog() {
      this.rentDialogVisible = false
    }
  }
}
</script>

要点解释 / 易错点

  • el-dialog 的显隐完全由 :visible 控制,rentDialogVisible 为 true 打开、false 关闭;@close="closeDialog" 保证点右上角 × 或点取消都能把状态改回 false。
  • 弹框关闭后再打开,rentForm 里可能还残留上一次的数据,需要时在 addRent() 里重置表单(this.$refs.addForm.resetFields())。
  • rentRules 的 key 必须和 el-form-item 的 prop、rentForm 里的字段名三者一致,否则校验不生效。
  • el-date-picker 的 type="daterange" 绑定的必须是数组,且要加 value-format="yyyy-MM-dd" 才能在提交时拿到字符串而不是 Date 对象。
  • el-upload 同样不受 el-form 自动监听,上传完合同后要像营业执照那样手动 validateField('contractId'),否则“请上传合同文件”的提示不会消失。

常见错误 ​

  1. 上传营业执照后,红色校验提示不消失 现象:用户已上传成功,请上传营业执照 仍在;点确定却仍被拦。 原因:el-upload 不属于 el-form 默认可感知的表单控件,上传完成不会通知表单校验系统,prop 对应字段的状态没被重新计算。 修法:在 on-success 回调里先给 addForm.businessLicenseId / businessLicenseUrl 赋值,再 this.$refs.ruleForm.validateField('businessLicenseId') 单独触发一次。

  2. 单独校验写了,但校验依旧不过 现象:validateField 调了,提示还是红的。 原因:调用时机在赋值之前(写在 before-upload、或写在 on-success 但赋值在 await 之后 / 用了 then 却在校验后才赋值),此刻字段还是空值。 修法:严格保证「接口返回 → 赋值 → 再校验」的顺序;validateField 的入参必须是 prop 上写的那个字段名,不是 businessLicenseUrl。

  3. 编辑页一进来就报 404 / 请求参数是 undefined 现象:新增状态下打开页面,控制台出现 /park/enterprise/undefined。 原因:详情接口无条件写在 created/mounted 里,没判断当前是新增还是编辑。 修法:if (this.id) { this.getEnterpriseDetail() },把「有没有 id」作为唯一的状态开关。

  4. 点确定后新增、编辑接口都调了一遍 现象:编辑保存后列表多出一条新数据,或多出一次 POST。 原因:原来新增的 await createExterpriseAPI(...) 没删,只在下面补了 if/else;或 if 里调完没 else/return。 修法:接口调用只保留一处分支,两个分支互斥;提交成功后统一 $router.back()。

  5. 编辑提交后端报字段错误 现象:更新接口 500 或字段没更新。 原因:直接把 this.addForm 整个丢给 PUT 接口,字段比接口文档多(含 name、businessLicenseUrl 等接口不要的),或编辑时漏传 id。 修法:以接口文档字段为准,用解构只挑需要的字段,编辑态额外补 id: this.id。

  6. 弹框关不掉 / 控制台警告「Avoid mutating a prop」 现象:点取消弹框不关,或改了 visible 报 prop 修改警告。 原因:把 :visible 当成 v-model 用,或在子组件里直接给 prop 赋值。 修法:父组件用 data 里的 rentDialogVisible,模板 :visible="rentDialogVisible" + @close="closeDialog",打开/关闭都只改这个 data 变量。

练习 ​

  1. 在列表页点「编辑」,用 query 携带 id 跳到 /exterpriseAdd;同时在目标页用 computed 缓存这个 id,并让页面标题在「添加企业 / 编辑企业」之间自动切换。要求:直接访问不带 id 的地址时标题必须是「添加企业」。
  2. 实现「营业执照上传后自动消除校验提示」:给出 el-form-item 的 prop、el-upload 的关键属性和 on-success 里的完整逻辑(含赋值与单独校验),并说明为什么不能把校验写在 before-upload 里。
  3. 写出 confirmSubmit:先统一 validate,校验通过后按当前是否处于编辑态调用不同接口;编辑态要按接口文档裁剪字段并带上 id;成功后给出一句区分「新增成功 / 更新成功」的提示并返回列表页。
  4. 给「添加合同」弹框补全开合逻辑:一个按钮打开、footer 取消关闭、右上角关闭也关闭,并说明为什么不能直接在模板上写 v-model="visible"。
  5. 用一句话分别说明下面四个场景该用哪种校验方式,并各写出关键代码一行:① 必填的楼宇下拉;② 手机号格式;③ 两次密码一致;④ 上传完合同文件后让 contractId 的必填提示消失。

原视频对照 ​

#视频时长
1新增企业-上传完毕对营业执照做单独校验10:21
2新增企业-收集表单数据调用接口03:47
3梳理上传流程和表单校验19:03
4编辑企业-携带id跳转并存储id03:57
5编辑企业-回填数据04:33
6编辑企业-调用更新完成更新企业07:42

小结 ​

  1. 一个 id 贯穿整条编辑链路:跳转带它、页面缓存它、文案判断用它、详情请求靠它、提交分支看它——它是这个页面唯一的状态源。
  2. 新增与编辑共用一个页面是常态,但「共用模板 ≠ 共用接口」,凡是请求的地方都要过一遍 id 判断。
  3. 表单校验在这一模块里其实只有四种:基础配置校验、自定义 validator、统一 validate、单字段 validateField;前三种是「整体思维」,第四种是「补丁思维」。
  4. validateField 是给「不听话」的控件准备的兜底手段,判断标准很简单:这个控件会不会主动把值的变化告诉 el-form,不会就得手动补一刀。
  5. 顺序感比 API 记忆更重要:赋值 → 校验 → 提交,任何一步提前都会表现成「莫名其妙的校验失败」。
  6. 提交参数永远以接口文档为准,多传字段和少传字段都是错,解构裁剪是最省事的写法。
  7. 弹框、表单、上传这些「组件状态」都归 data 管,模板只读不写,prop 单向数据流不能破。
  8. 前端校验是体验、后端校验是底线,两边规则要一致;前端拦不住的请求,后端必须拦得住。