Skip to content

企业管理(二) ​

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

学习目标 ​

  1. 能够调用行业列表接口,将返回数据渲染到 el-select 的 el-option,正确绑定 value/label,并验证选中值写入 v-model。
  2. 能够使用 el-upload 的 http-request 回调获取文件对象,并从回调参数中解构出待上传的 file。
  3. 能够按接口要求创建 FormData,通过 append 添加 file 与 type 字段,调用上传接口并验证请求参数及返回的文件地址、名称。
  4. 能够配置 before-upload 校验文件类型和大小,校验失败给出提示并返回 false 阻止上传,校验通过返回 true 放行。
  5. 能够为表单绑定 model 与 rules,配置字段必填规则并调用 validate 完成统一校验;能够为手机号字段添加 validator 正则校验,失败时通过 callback(new Error(...)) 返回提示。
  6. 能够按编辑页路由要求携带企业标识参数完成跳转,并在目标页验证参数被正确接收。

知识点 ​

本节目标与功能演示 ​

这一节要完成的是“添加企业”页面的落地:先准备一个独立的新增企业组件,再把它绑定到路由上,最后从企业列表页点击“添加企业”跳转进来。为什么需要先做这一步?因为新增企业不是一个弹窗,而是一个完整页面,页面里有头部返回、企业信息表单、底部操作按钮。只有先把组件和路由打通,后面做行业下拉、营业执照上传、表单校验时,才能稳定地在这个页面里调试。

怎么想?顺序不要反过来:先有页面组件,再有路由地址,最后加入口按钮。转写稿里反复强调“先测试再绑定”的习惯,这里同样适用:路由配好后先直接访问路径,确认组件能渲染;再从列表页点击按钮,确认跳转路径一致。

怎么做?准备组件代码如下,保留原笔记代码:

vue
<template>
  <div class="add-enterprise">
    <header class="add-header">
      <div class="left">
        <span class="arrow" @click="$router.back()"><i class="el-icon-arrow-left" />返回</span>
        <span>|</span>
        <span>添加企业</span>
      </div>
      <div class="right">
        黑马程序员
      </div>
    </header>
    <main class="add-main">
      <div class="form-container">
        <div class="title">企业信息</div>
        <div class="form">
          <el-form ref="ruleForm" label-width="100px">
            <el-form-item label="企业名称" prop="name">
              <el-input v-model="addForm.name" />
            </el-form-item>
            <el-form-item label="法人" prop="name">
              <el-input v-model="addForm.legalPerson" />
            </el-form-item>
            <el-form-item label="注册地址" prop="name">
              <el-input v-model="addForm.registeredAddress" />
            </el-form-item>
            <el-form-item label="所在行业" prop="name">
              <el-select v-model="addForm.industryCode" />
            </el-form-item>
            <el-form-item label="企业联系人" prop="name">
              <el-input v-model="addForm.contact" />
            </el-form-item>
            <el-form-item label="联系电话" prop="name">
              <el-input v-model="addForm.contactNumber" />
            </el-form-item>
            <el-form-item label="营业执照" prop="name" />
          </el-form>
        </div>
      </div>
    </main>
    <footer class="add-footer">
      <div class="btn-container">
        <el-button>重置</el-button>
        <el-button type="primary">确定</el-button>
      </div>
    </footer>
  </div>
</template>

<script>
export default {
  data() {
    return {
      addForm: {
        name: '', // 企业名称
        legalPerson: '', // 法人
        registeredAddress: '', // 注册地址
        industryCode: '', // 所在行业
        contact: '', // 企业联系人
        contactNumber: '', // 联系人电话
        businessLicenseUrl: '', // 营业执照url
        businessLicenseId: '' // 营业执照id
      }
    }
  }
}
</script>

<style scoped lang="scss">
.add-enterprise {
  background-color: #f4f6f8;
  height: 100vh;

  .add-header {
    display: flex;
    justify-content: space-between;
    padding: 0 20px;
    height: 64px;
    line-height: 64px;
    font-size: 16px;
    font-weight: bold;
    background-color: #fff;

    .left {
      span {
        margin-right: 4px;
      }
      .arrow{
        cursor: pointer;
      }
    }

    .right {
      text-align: right;
    }
  }

  .add-main {
    background: #f4f6f8;
    padding: 20px 130px;

    .form-container {
      background-color: #fff;
      .title {
        height: 60px;
        line-height: 60px;
        padding-left:20px;
      }
      .form {
        margin-bottom: 20px;
        padding: 20px 65px 24px;
        .el-form{
          display: flex;
          flex-wrap: wrap;
          .el-form-item{
            width: 50%;
          }
        }
      }
    }
  }

  .add-footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    padding: 24px 50px;
    color: #000000d9;
    font-size: 14px;
    background: #fff;
    text-align: center;
  }
}
</style>

绑定路由 ​

为什么需要绑定路由?因为新增企业页面要能通过 URL 访问,也要能从列表页跳转进入;没有路由,组件只是“孤立的文件”。怎么想?在路由表里加一条记录:path 是访问地址,component 指向 Add.vue,用懒加载写法按需加载。怎么做?配置如下:

javascript
 {
    path: '/exterpriseAdd',
    component: () => import('@/views/park/enterprise/Add')
}

这里的路径拼写要保持前后一致,/exterpriseAdd 是笔记中的写法,不要一边写 exterpriseAdd、一边写 enterpriseAdd。易错点是 import 路径要和文件实际位置对应;懒加载函数要返回 import()。

路由跳转 ​

为什么需要跳转入口?用户是从企业列表进入新增页的,不能靠手动输地址。怎么想?在列表页的“添加企业”按钮上绑定点击事件,用 $router.push 跳到刚才配置的路径。怎么做?代码如下:

html
 <el-button type="primary" @click="$router.push('/exterpriseAdd')">添加企业</el-button>

易错点:push 里的路径必须和路由表 path 完全一致;组件头部“返回”用的是 $router.back(),它依赖浏览器历史记录,所以一般要从列表页跳进来再测试返回。转写稿视频5在配置表单校验时提醒过,prop 要按实际字段替换;当前准备组件里多处 prop="name" 是占位结构,后续要按企业名称、法人、注册地址等字段分别调整,否则校验会串。先完成“组件—路由—跳转”这条链路,再继续做行业下拉、上传和校验,调试会更清晰。

先明确目标:把“写死行业”改成接口渲染 ​

为什么需要:之前做类似下拉框时,数据可以直接写在前端,相当于静态死数据;但“所在行业”是后端维护的可变数据,必须通过接口获取。
怎么想:先封装获取行业列表的接口,再在业务组件中拿到数据放入响应式数组,最后绑定到 el-select。
怎么做:按“接口封装 → 组件取数 → 下拉绑定 → 调试验证”的顺序推进。
易错点:不要猜接口返回字段,也不要把行业列表写死在前端。

行业列表接口示意图

封装行业列表接口 ​

为什么需要:组件里不应该直接写请求地址和请求细节,统一封装后便于复用和维护。
怎么想:这个接口是 GET 请求,不需要传参数,直接返回可选行业列表。
怎么做:在接口文件中封装 getIndustryListAPI,内部调用 request。

javascript
/**
 * 获取行业列表
 * @returns
 */
export function getIndustryListAPI() {
  return request({
    url: '/park/industry'
  })
}

易错点:接口路径、导出函数名要和组件导入时一致;不要额外编造参数。

在组件中获取数据 ​

为什么需要:下拉框的选项需要来自接口,并且要通过响应式数据驱动视图更新。
怎么想:在 data 中准备 industryList,在 methods 中写异步请求,在 mounted 中调用。转写稿里特别强调:赋值时不能猜是 date 还是 rose,要看接口实际返回,最后确认用 res.data。
怎么做:导入接口,声明数组,异步请求后赋值。

vue
<script>
import { getIndustryListAPI } from '@/api/enterprise'
export default {
  data() {
    return {
      industryList: [] // 可选行业列表
    }
  },
  mounted() {
    this.getIndustryList()
  },
  methods: {
    async getIndustryList() {
      const res = await getIndustryListAPI()
      this.industryList = res.data
    }
  }
}
</script>

易错点:mounted 不要拼错;请求是异步的,要 await;赋值要赋给 this.industryList,不要漏掉 this。调试时先别急着绑定,先看 Network 里接口是否成功,再用调试工具确认组件里的 industryList 是否已经拿到数据。

绑定 el-select 与 el-option ​

为什么需要:el-select 只负责下拉交互,具体选项要由 el-option 循环渲染。
怎么想:v-model 绑定表单里最终要提交的字段,:value 绑定选项值,:label 绑定用户看到的中文。转写稿中强调:字段名以后端实际返回为准,它不叫 id 就不要写 item.id。
怎么做:用 v-for 遍历 industryList,把 industryCode 作为值和 key,把 industryName 作为显示文本。

html
 <el-form-item label="所在行业" prop="name">
    <el-select v-model="addForm.industryCode">
      <el-option
        v-for="item in industryList"
        :key="item.industryCode"
        :value="item.industryCode"
        :label="item.industryName"
      />
    </el-select>
  </el-form-item>

易错点::key 要唯一;:value 和 :label 不要写反;v-model 要绑定表单对象里的行业字段;选中后 value 会自动写入 v-model 对应的数据,可通过调试工具观察 addForm.industryCode 是否变化。prop 要和表单校验字段对应,本节先按笔记保留。

测试效果与按需请求拓展 ​

为什么需要:绑定完必须验证“显示中文、提交 code”的交互是否符合预期。
怎么想:点击下拉框应显示 industryName,选中后应把 industryCode 收集到 v-model 上;切换选项时值应跟着变。
怎么做:刷新页面,点击下拉框测试;再打开调试工具查看表单数据。效果如下:

行业下拉框渲染效果

转写稿还补充了一个拓展:如果产品要求“一上来不请求,用户点击或聚焦时才请求”,可以把 mounted 中的请求移到 el-select 的 @focus 回调里,注意事件要绑在 el-select 上,而不是 el-option 上。这样刷新时 Network 中没有该请求,聚焦后才发起请求。
易错点:不要同时在 mounted 和 @focus 中重复请求;事件绑定位置和事件名要写对。

本节目标与整体流程 ​

营业执照上传整体流程图

这一节要完成的是“营业执照上传”的基础闭环:用户在表单里点击上传,选择本地图片;前端拿到文件对象后,按接口要求组装参数;调用上传接口;最后把接口返回的文件 id 和 url 回填到新增企业表单中。

转写稿里把流程讲得很清楚:el-upload 先负责“选文件”,我们通过自定义上传钩子拿到 file 对象;因为上传接口要求的不是普通 JSON,而是 FormData 类型参数,所以要用 new FormData() 组装;再调用封装好的 uploadAPI;上传成功后,把返回的 id、url 赋给表单字段。

为什么需要:上传不是简单把文件对象直接扔给请求库,接口字段、请求类型、回填字段都要对齐。
怎么想:先封装接口,再准备组件,最后在自定义上传回调里组装参数并回填。
怎么做:按下面三步落地。
易错点:不要一上来就写死字段名,要先看接口文档返回结构和参数名。

封装上传接口 ​

javascript
/**
 * 上传合同
 * @returns
 */
export function uploadAPI(data) {
  return request({
    url: '/upload',
    method: 'POST',
    data
  })
}

为什么需要:上传接口在业务组件里可能被多处使用,统一封装后,URL、请求方法、参数位置都集中管理。
怎么想:转写稿里先打开接口文档确认 /upload 是 POST,参数放在 data 里,所以封装时保持和项目里其他 API 一致。
怎么做:在 @/apis/enterprise 中新增 uploadAPI,组件里按需导入。
易错点:不要把上传接口写成 GET;data 位置不要随手改成 params,否则文件参数传不过去。

准备自定义上传组件 ​

el-upload 自定义上传配置示意

html
<el-form-item label="营业执照">
    <el-upload
      action="#"
      :http-request="uploadRequest"
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过500kb</div>
    </el-upload>
</el-form-item>

为什么需要:Element UI 的 el-upload 默认配置能完成简单上传,但当前项目需要自己控制请求参数、接口地址和回填逻辑,默认上传不够灵活。
怎么想:转写稿里明确说,只用它“选中本地文件、产出 file 对象”的能力,真正上传由我们自己写。http-request 就是覆盖默认上传行为的钩子。
怎么做:绑定 :http-request="uploadRequest";action 仍传 # 占位,避免组件提示 action 必传警告。
易错点:action 不是实际上传地址,别误以为写了 # 就上传到 #;真正请求在 uploadRequest 里。转写稿里“MDY”是口述误听,实际是 Element UI。

调用接口完成上传 ​

vue
import { getIndustryListAPI, uploadAPI } from '@/apis/enterprise'

async uploadRequest(data) {
  const file = data.file

  // 处理formData类型参数
  const formData = new FormData()
  formData.append('file', file)
  formData.append('type', 'businessLicense')
  const res = await uploadAPI(formData)

  // 赋值表单数据
  this.addForm.businessLicenseId = res.data.id
  this.addForm.businessLicenseUrl = res.data.url
}

为什么需要:接口要求 FormData 类型参数,里面需要 file 和 type 两个字段;type 用来区分上传的是营业执照还是合同。
怎么想:先通过 data.file 拿到 el-upload 传下来的文件对象;再 new FormData();用 append 按接口字段名填参数;最后调 uploadAPI。
怎么做:转写稿调试时先在 uploadRequest 里打印参数,展开能看到 file 对象;再在 Network 里确认请求参数包含 file、type,返回 200 后看 res.data.id、res.data.url。
易错点:不要直接把 file 传给 uploadAPI;type 的值要按接口文档写,营业执照是 businessLicense;append 的字段名不能拼错。

验证字段覆盖 ​

验证营业执照字段回填效果

为什么需要:上传成功后不能只看到“上传成功”,还要确认表单里真正提交的字段被覆盖成接口返回的 id 和 url。
怎么想:转写稿里强调“先准备参数再调接口”,上传返回后立刻把 businessLicenseId、businessLicenseUrl 写进 addForm。
怎么做:上传完成后打开调试工具或看表单数据,确认两个字段已有值;再结合截图验证页面表现。
易错点:返回结构要按实际接口看,别想当然写 res.id;字段名要和表单收集字段一致,否则页面看似上传成功,提交时却拿不到文件信息。

业务需求:为什么上传前要拦一层 ​

上传前校验文件类型和大小示意图

为什么需要: 用户选择文件时不会自动遵守业务要求,可能选错格式,也可能选了一个十几兆的大图。如果直接上传,既浪费网络,也会让后端再报错。笔记里说得很清楚:通常上传文件需要我们用程序做一些限制,防止用户选择并不符合要求的文件。

怎么想: 限制要放在“上传前”,也就是文件还没有真正发给后端时先判断。格式不对就提示并阻止;大小超限也提示并阻止;两个条件都满足才放行。

怎么做: 在 el-upload 上绑定 :before-upload="beforeUpload",同时保留自定义上传回调和占位 action:

jsx
// 绑定上传前回调
<el-upload
  action="#"
  :http-request="uploadRequest"
  :before-upload="beforeUpload"
  >
  <el-button size="small" type="primary">点击上传</el-button>
  <div slot="tip" class="el-upload__tip">支持扩展名:.png .jpg .jpeg,文件大小不得超过5M</div>
</el-upload>

易错点: action 是组件默认上传地址的必传项,但这里已经用 :http-request 覆盖默认上传行为,所以用 # 占位消除警告即可,不要以为它真的会上传到 #。

找到 before-upload 并理解拦截机制 ​

为什么需要: 只写提示文案没有用,必须让组件在上传动作发生前调用我们的校验函数。转写稿里强调:before-upload 这个方法中必须 return 的数据为 true,才会继续执行上传逻辑;校验不过就暂停,不会走上传。

怎么想: 把 beforeUpload 当成一个“闸门”。它接收当前文件对象 file,我们在这个函数里读文件信息、给提示,最后返回一个布尔值。返回 true 放行,返回 false 拦截。

怎么做: 在组件方法里声明 beforeUpload(file),先不要急着写完整校验,可以像转写稿调试那样先 console.log(file),确认能拿到文件对象,再继续写判断。

易错点: 不要只写 if 提示却忘记最终 return;也不要让函数没有返回值。没有布尔返回值时,上传行为是否符合预期就不稳定。

编写类型和大小校验 ​

为什么需要: 业务要求“支持扩展名:.png .jpg .jpeg,文件大小不得超过5M”。所以校验要覆盖两个维度:MIME 类型和字节大小。

怎么想: 转写稿里解释了 file.type 表示文件类型,图片一般是 image/png、image/jpeg 这类形式;file.size 默认单位是字节,要除以 1024 变成 KB,再除以 1024 变成 MB。类型用数组 includes 判断,大小用换算后的数值和 5 比较。

怎么做: 按笔记代码写:

jsx
// 编写校验逻辑
beforeUpload(file) {
  const allowImgType = ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)
  const isLt5M = file.size / 1024 / 1024 < 5

  if (!allowImgType) {
    this.$message.error('上传合同图片只能是 PNG/JPG/JPEG 格式!')
  }
  if (!isLt5M) {
    this.$message.error('上传合同图片大小不能超过 5MB!')
  }
  return allowImgType && isLt5M
}

易错点: file.type 不是文件扩展名,不能拿它直接和 .png 比较;file.size 也不是 MB,必须做 /1024/1024;< 5 表示小于 5MB,刚好 5MB 是否允许要按业务确认;两个 if 只负责提示,真正拦截靠最后的 return allowImgType && isLt5M。

调试与验证 ​

为什么需要: 上传前校验涉及文件对象、MIME 类型和单位换算,光看代码容易想当然,必须实际选文件验证。

怎么想: 转写稿里的调试路径是:先打印 file 看 type、size;再准备一个不符合要求的图片和一个符合要求的图片,分别选择,观察是否提示、是否还能继续上传。

怎么做: 可以先在 beforeUpload 里打印 file,在控制台确认 type 和 size。然后选一个 B.jpg 之类的非允许格式,页面应提示“上传合同图片只能是 PNG/JPG/JPEG 格式!”并挡住;再选一个 A.png,没有提示并继续上传成功。最后检查 Network 里上传请求是否按预期发出。

易错点: 转写稿里“偏G”“勾PG”是口误,实际是 PNG、JPG;不要被同音字带偏。提示文案要和 slot="tip" 的说明保持一致,否则用户看到的规则和实际拦截规则不一致。

收尾检查 ​

为什么需要: 上传前校验不是只写一个函数就结束,按钮文案、提示文案、返回值、上传回调要形成闭环。

怎么想: 先确认 before-upload 已绑定,再确认 beforeUpload 返回布尔值,最后确认 :http-request="uploadRequest" 仍然存在。这样校验通过后才走自定义上传逻辑。

怎么做: 把提示文案统一成“支持扩展名:.png .jpg .jpeg,文件大小不得超过5M”,并保留上面的 beforeUpload 代码。若后续要调整限制,只改数组和大小阈值,同时同步页面说明。

易错点: 不要删除 :http-request 后还期待走自定义上传;不要把 before-upload 写成 beforeUpload 属性名拼错;不要只校验大小不校验类型,或只校验类型不校验大小。

5. 表单校验:先分清字段类型 ​

表单字段分类与校验思路示意图

业务背景里,表单字段可以分成两类:一类是“直接通过 Element 默认配置就能解决”的,比如必填、下拉选择必选;另一类是“需要单独校验”的,比如手机号要匹配正则、营业执照上传完成后要清掉错误提示。为什么需要先分类?因为默认规则解决通用问题,自定义校验解决业务问题,混在一起写容易漏字段。怎么想?先给所有字段配 prop 和基础规则,再针对特殊字段加 validator 或手动 validateField。怎么做?下面按“基础校验—统一校验—正则校验—上传后单独校验—提交”推进。易错点:el-form-item 的 prop 必须和 addRules 的 key、addForm 的字段名一致,否则规则不生效。

1. 表单基础校验:规则对象 + prop 绑定 ​

为什么需要?必填项不校验,用户能提交空表单。怎么想?把每个字段的规则写进 addRules,再在模板里用 :rules 绑定表单、用 prop 绑定字段。怎么做?先创建规则,再绑定表单。注意 el-select 用 trigger: 'change',输入框用 blur;营业执照字段名是 businessLicenseId,模板 prop 要一致。

jsx
// 1. 创建表单规则
data() {
  return {
    addRules: {
      name: [
        { required: true, message: '企业名称为必填', trigger: 'blur' }
      ],
      legalPerson: [
        { required: true, message: '法人为必填', trigger: 'blur' }
      ],
      registeredAddress: [
        { required: true, message: '注册地址为必填', trigger: 'blur' }
      ],
      industryCode: [
        { required: true, message: '所在行业为必填', trigger: 'change' }
      ],
      contact: [
        { required: true, message: '企业联系人为必填', trigger: 'blur' }
      ],
      contactNumber: [
        { required: true, message: '企业联系人电话为必填', trigger: 'blur' }
      ],
      businessLicenseId: [
        { required: true, message: '请上传营业执照', trigger: 'blur' }
      ]
    }
  }
}

// 2. 绑定表单规则
<el-form :model="addForm" :rules="addRules" label-width="100px">
  <el-form-item label="企业名称" prop="name">
    <el-input v-model="addForm.name" />
  </el-form-item>
  <el-form-item label="法人" prop="legalPerson">
    <el-input v-model="addForm.legalPerson" />
  </el-form-item>
  <el-form-item label="注册地址" prop="registeredAddress">
    <el-input v-model="addForm.registeredAddress" />
  </el-form-item>
  <el-form-item label="所在行业" prop="industryCode">
    <el-select v-model="addForm.industryCode">
      <el-option
        v-for="item in industryList"
        :key="item.industryCode"
        :value="item.industryCode"
        :label="item.industryName"
      />
    </el-select>
  </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-form-item label="营业执照" prop="businessLicenseId">
    <el-upload
      action="#"
      :http-request="uploadRequest"
      :before-upload="beforeUpload"
    >
      <el-button size="small" type="primary">点击上传</el-button>
      <div slot="tip" class="el-upload__tip">支持扩展名:.png .jpg .jpeg,文件大小不得超过5M</div>
    </el-upload>
  </el-form-item>
</el-form>

易错点:转写稿里调试时是逐个点字段看提示是否出现;如果某个字段不提示,先查 prop 是否写错、规则 key 是否对得上。

2. 统一校验:拿到表单实例调用 validate ​

为什么需要?用户点“确定”时要一次性校验所有项,而不是每个字段单独触发。怎么想?给 el-form 加 ref,通过 this.$refs.ruleForm.validate 拿到结果。怎么做?回调里判断 valid,为 true 才继续提交。转写稿里也提到 validate 返回 Promise,但这里用回调更直观。

jsx
<el-form ref="ruleForm"></el-form>

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

confirmSubmit() {
  this.$refs.ruleForm.validate(valid => {
    console.log(valid)
  })
}

易错点:ref 名字要和 this.$refs 一致;点确定后所有必填项都应触发,包括营业执照。

3. 正则校验手机号:默认规则不够时加 validator ​

为什么需要?required 只能保证非空,不能保证是手机号。怎么想?在 contactNumber 规则数组里追加一个对象,用 validator 指向自定义函数。怎么做?函数接收 rule、value、callback,用 /^1[3-9]\d{9}$/ 测试,通过 callback(),失败 callback(new Error(...))。转写稿调试时先测必填,再输入 132 提示“请输入正确的手机号”,输入 12345678 才满足。

jsx
const validatePhone = (rule, value, callback) => {
  if (/^1[3-9]\d{9}$/.test(value)) {
    callback()
  } else {
    callback(new Error('请输入正常的手机号'))
  }
}

contactNumber: [
    { required: true, message: '企业联系人电话为必填', trigger: 'blur' },
    { validator: validatePhone, trigger: 'blur' }
]

易错点:validator 拼写别错;callback 必须调用,否则校验会卡住;正则要加首尾 ^ $。

4. 上传完毕单独校验营业执照字段 ​

为什么需要?上传成功后 el-form 不一定收到“值已变化”的通知,错误提示可能还留着。怎么想?上传逻辑完成后,手动调用表单实例的 validateField 只校验营业执照字段。怎么做?在 uploadRequest 上传完成后执行 this.$refs.ruleForm.validateField('businessLicenseId')。

jsx
async uploadRequest(data) {
    // 上传逻辑...
     
    // 单独校验表单,清除错误信息
    this.$refs.ruleForm.validateField('businessLicenseId')
}

易错点:字段名必须是 businessLicenseId;上传前校验是另一层逻辑,这里解决的是上传完成后的表单校验通知。

5. 提交表单:校验通过再调接口 ​

为什么需要?不校验直接提交会把脏数据发给后端。怎么想?统一校验通过后,收集 addForm,调新增接口,成功提示并返回列表页。怎么做?先封装 createExterpriseAPI,再在 confirmSubmit 里 await 接口,最后 this.$router.back()。转写稿里强调 if (!valid) return,避免失败还提交。

javascript
/**
 * 创建公司
 * @param {*} data
 * @returns
 */
export function createExterpriseAPI(data) {
  return request({
    url: '/park/enterprise',
    method: 'POST',
    data
  })
}
javascript
confirmSubmit() {
  this.$refs.ruleForm.validate(async valid => {
    if (!valid) return
    // 1. 调用接口
    await createExterpriseAPI(this.addForm)
    // 2. 返回列表页
    this.$router.back()
  })
}

易错点:validate 回调里用了 async,要保证接口失败时有错误处理;提交前确认 addForm 已包含上传接口返回的营业执照信息。

先明确本节要解决的问题 ​

编辑按钮出现在列表的每一行里。用户点哪一行,系统就必须知道“要编辑的是哪一条数据”。如果只跳转到编辑页却不携带标识,编辑页就无法判断该加载哪条记录,只能让用户重新选择,交互就断了。

因此这里的核心不是“按钮长什么样”,而是:点击时把当前行的唯一标识 id 交给路由,再由目标页按约定读取。笔记中的按钮只是触发器,真正完成参数传递的是 editRent 里的 this.$router.push。

按钮层:用作用域插槽拿到当前行 id ​

为什么需要:表格每一行都有自己的数据,按钮必须绑定“当前行”,不能绑定表头或固定值。

怎么想:按钮放在表格的作用域插槽里,通常能通过 scope.row 访问当前行对象;编辑需要的是稳定标识,所以传 scope.row.id,而不是把整行对象塞进地址栏。

怎么做:笔记代码如下,@click="editRent(scope.row.id)" 表示点击时调用方法并传入当前行 id。size="mini"、type="text" 只影响按钮展示,不参与传参逻辑。

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

易错点:不要把 scope.row.id 写成 scope.row.id();不要漏掉方法参数;如果当前行没有 id,要先确认列表接口返回字段名,不能想当然。转写稿里反复强调“以实际返回的数据字段名为准”,这个原则同样适用于这里的 id:列表行里叫 id 才写 id,若项目里叫别的名字,就应使用真实字段。

方法层:先准备 query,再执行 push ​

为什么需要:把跳转逻辑封装进 editRent(id),按钮只负责“交 id”,方法负责“怎么跳”。这样以后要加权限判断、埋点或额外参数,都有固定入口。

怎么想:转写稿在讲接口调用时提到“先准备参数,再调接口”。跳转同理:先确认目标路由 path,再把 id 放进 query,最后 push。query: { id } 是对象简写,等价于 query: { id: id }。

怎么做:path: '/exterpriseAdd' 指定目标地址;query 中的 id 会以查询字符串形式出现在地址栏,例如目标页地址会带 ?id=...。这种写法适合编辑页需要“可刷新、可复制地址仍保留 id”的场景。

易错点:第一,path 一般要写完整前导斜杠,拼写要和路由表一致;第二,不要把 query 和 params 混用。用 path 跳转时,params 在部分路由版本/刷新场景下不可靠,而 query 会进入 URL,更直观;第三,query 的值最终会变成字符串,目标页比较时要留意类型;第四,id 为空时仍会跳转,目标页会拿到空值,应在传参前保证 id 有效。

目标页读取与验证顺序 ​

为什么需要:push 只是把参数带到目标地址,编辑页还要按约定读取 query.id,否则参数等于没传。

怎么想:先验证“按钮有没有把正确 id 传进方法”,再验证“push 后的地址是否带 id”,最后验证“目标页能否读到 id”。这符合转写稿中“先过去测试接口是否成功,再往下绑定渲染”的排查顺序:先看数据有没有到,再看界面有没有用上。

怎么做:按项目使用的 Vue Router 版本,在目标页从 $route.query.id 一类的位置读取参数;读到后再决定请求详情或进入新增模式。本节笔记只给了跳转端代码,没有给目标页读取代码,因此不虚构具体实现。

易错点:不要以为 push 后列表页会自动刷新;编辑保存后是否需要回到列表、是否重新请求列表,是后续逻辑;不要把敏感信息放进 query;地址栏参数可被用户修改,目标页仍要做合法性校验。

小结 ​

这段代码的关键链路是:行内按钮拿到 scope.row.id → editRent 接收 id → router.push 用 path 指定编辑页、用 query 携带 id → 目标页按约定读取。调试时按“入参 id、跳转地址、目标页读取”三步查,比直接盯着页面是否跳转更稳。

代码示例 ​

示例一:新增企业页面(Add.vue)——行业下拉渲染 + 营业执照上传 + 表单校验 + 提交 ​

vue
<template>
  <div class="add-enterprise">
    <header class="add-header">
      <div class="left">
        <span class="arrow" @click="$router.back()"><i class="el-icon-arrow-left" />返回</span>
        <span>|</span>
        <span>添加企业</span>
      </div>
      <div class="right">
        黑马程序员
      </div>
    </header>

    <main class="add-main">
      <div class="form-container">
        <div class="title">企业信息</div>
        <div class="form">
          <el-form ref="ruleForm" :model="addForm" :rules="addRules" label-width="100px">
            <el-form-item label="企业名称" prop="name">
              <el-input v-model="addForm.name" />
            </el-form-item>
            <el-form-item label="法人" prop="legalPerson">
              <el-input v-model="addForm.legalPerson" />
            </el-form-item>
            <el-form-item label="注册地址" prop="registeredAddress">
              <el-input v-model="addForm.registeredAddress" />
            </el-form-item>
            <el-form-item label="所在行业" prop="industryCode">
              <el-select v-model="addForm.industryCode">
                <el-option
                  v-for="item in industryList"
                  :key="item.industryCode"
                  :value="item.industryCode"
                  :label="item.industryName"
                />
              </el-select>
            </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-form-item label="营业执照" prop="businessLicenseId">
              <el-upload
                action="#"
                :http-request="uploadRequest"
                :before-upload="beforeUpload"
              >
                <el-button size="small" type="primary">点击上传</el-button>
                <div slot="tip" class="el-upload__tip">支持扩展名:.png .jpg .jpeg,文件大小不得超过5M</div>
              </el-upload>
            </el-form-item>
          </el-form>
        </div>
      </div>
    </main>

    <footer class="add-footer">
      <div class="btn-container">
        <el-button>重置</el-button>
        <el-button type="primary" @click="confirmSubmit">确定</el-button>
      </div>
    </footer>
  </div>
</template>

<script>
import { getIndustryListAPI, uploadAPI, createExterpriseAPI } from '@/api/enterprise'

// 手机号自定义校验:默认规则满足不了复杂场景时使用
const validatePhone = (rule, value, callback) => {
  if (/^1[3-9]\d{9}$/.test(value)) {
    callback()
  } else {
    callback(new Error('请输入正常的手机号'))
  }
}

export default {
  data() {
    return {
      industryList: [], // 可选行业列表
      addForm: {
        name: '', // 企业名称
        legalPerson: '', // 法人
        registeredAddress: '', // 注册地址
        industryCode: '', // 所在行业
        contact: '', // 企业联系人
        contactNumber: '', // 联系人电话
        businessLicenseUrl: '', // 营业执照url
        businessLicenseId: '' // 营业执照id
      },
      addRules: {
        name: [{ required: true, message: '企业名称为必填', trigger: 'blur' }],
        legalPerson: [{ required: true, message: '法人为必填', trigger: 'blur' }],
        registeredAddress: [{ required: true, message: '注册地址为必填', trigger: 'blur' }],
        industryCode: [{ required: true, message: '所在行业为必填', trigger: 'change' }],
        contact: [{ required: true, message: '企业联系人为必填', trigger: 'blur' }],
        contactNumber: [
          { required: true, message: '企业联系人电话为必填', trigger: 'blur' },
          { validator: validatePhone, trigger: 'blur' }
        ],
        businessLicenseId: [{ required: true, message: '请上传营业执照', trigger: 'blur' }]
      }
    }
  },
  mounted() {
    this.getIndustryList()
  },
  methods: {
    async getIndustryList() {
      const res = await getIndustryListAPI()
      this.industryList = res.data
    },
    // 上传前校验:返回 false 会中断上传
    beforeUpload(file) {
      const allowImgType = ['image/jpeg', 'image/png', 'image/jpg'].includes(file.type)
      const isLt5M = file.size / 1024 / 1024 < 5

      if (!allowImgType) {
        this.$message.error('上传合同图片只能是 PNG/JPG/JPEG 格式!')
      }
      if (!isLt5M) {
        this.$message.error('上传合同图片大小不能超过 5MB!')
      }
      return allowImgType && isLt5M
    },
    // 自定义上传:只借用 el-upload 选文件的能力
    async uploadRequest(data) {
      const file = data.file

      // 处理 formData 类型参数
      const formData = new FormData()
      formData.append('file', file)
      formData.append('type', 'businessLicense')
      const res = await uploadAPI(formData)

      // 赋值表单数据
      this.addForm.businessLicenseId = res.data.id
      this.addForm.businessLicenseUrl = res.data.url

      // 上传完毕单独校验营业执照字段,清除错误痕迹
      this.$refs.ruleForm.validateField('businessLicenseId')
    },
    confirmSubmit() {
      this.$refs.ruleForm.validate(async valid => {
        if (!valid) return
        // 1. 调用接口
        await createExterpriseAPI(this.addForm)
        // 2. 返回列表页
        this.$router.back()
      })
    }
  }
}
</script>

<style scoped lang="scss">
.add-enterprise {
  background-color: #f4f6f8;
  height: 100vh;

  .add-header {
    display: flex;
    justify-content: space-between;
    padding: 0 20px;
    height: 64px;
    line-height: 64px;
    font-size: 16px;
    font-weight: bold;
    background-color: #fff;

    .left {
      span {
        margin-right: 4px;
      }
      .arrow {
        cursor: pointer;
      }
    }

    .right {
      text-align: right;
    }
  }

  .add-main {
    background: #f4f6f8;
    padding: 20px 130px;

    .form-container {
      background-color: #fff;
      .title {
        height: 60px;
        line-height: 60px;
        padding-left: 20px;
      }
      .form {
        margin-bottom: 20px;
        padding: 20px 65px 24px;
        .el-form {
          display: flex;
          flex-wrap: wrap;
          .el-form-item {
            width: 50%;
          }
        }
      }
    }
  }

  .add-footer {
    position: fixed;
    bottom: 0;
    width: 100%;
    padding: 24px 50px;
    color: #000000d9;
    font-size: 14px;
    background: #fff;
    text-align: center;
  }
}
</style>

关键行与易错点

  • el-form 必须同时绑定 :model="addForm" 和 :rules="addRules",否则校验不生效;每个 el-form-item 的 prop 必须与字段名严格一致(营业执照绑的是 businessLicenseId,不是 name)。
  • el-select 的 v-for 写在 el-option 上,:value 绑 industryCode、:label 绑 industryName——字段名以后端返回的数据为准,不能想当然写 id / name。
  • el-upload 的 action="#" 只是占位,作用是消除"action 为必传项"的警告;真正上传走的是 :http-request 覆盖默认行为。
  • uploadRequest(data) 里 data.file 才是文件对象;请求参数必须是 FormData,用 append('file', file) 和 append('type', 'businessLicense') 逐个塞入,不能直接传对象。
  • beforeUpload 必须 return allowImgType && isLt5M,返回 false 上传会被拦截;文件大小单位是字节,要 /1024/1024 才换算成 MB。
  • 上传成功后表单校验痕迹不会自动消失,需手动 this.$refs.ruleForm.validateField('businessLicenseId') 单独校验该字段。
  • validate 回调里 if (!valid) return 是统一校验的入口守卫;自定义校验函数的 validator 单词别拼错,写错不会报错但规则不生效。

示例二:接口封装与路由绑定 ​

js
// src/api/enterprise.js
import request from '@/utils/request'

/**
 * 获取行业列表
 */
export function getIndustryListAPI() {
  return request({
    url: '/park/industry'
  })
}

/**
 * 上传合同 / 营业执照
 */
export function uploadAPI(data) {
  return request({
    url: '/upload',
    method: 'POST',
    data
  })
}

/**
 * 创建公司
 */
export function createExterpriseAPI(data) {
  return request({
    url: '/park/enterprise',
    method: 'POST',
    data
  })
}
js
// src/router/index.js(节选)
{
  path: '/exterpriseAdd',
  component: () => import('@/views/park/enterprise/Add')
}

关键行与易错点

  • uploadAPI 的 data 是 FormData,请求库检测到非字符串参数会自动把 Content-Type 切成 multipart/form-data,不用手写请求头。
  • getIndustryListAPI 是 GET 且无参,直接 request({ url }) 即可,别画蛇添足加 method: 'GET' 之外的参数。
  • 路由用 () => import(...) 懒加载,路径大小写要与目录文件名一致(Add 对应 Add.vue)。
  • 组件里 import { getIndustryListAPI, uploadAPI, createExterpriseAPI } from '@/api/enterprise',三个接口一次性导入,路径别写成 @/apis。

示例三:列表页跳转(新增 / 编辑携带参数) ​

vue
<!-- src/views/park/enterprise/index.vue(列表页中与本节相关的部分) -->
<template>
  <div class="enterprise-list">
    <!-- 新增:直接跳路由 -->
    <el-button type="primary" @click="$router.push('/exterpriseAdd')">添加企业</el-button>

    <!-- 编辑:实际写在 el-table 的操作列里,id 取 scope.row.id -->
    <el-button
      size="mini"
      type="text"
      @click="editRent(1)"
    >编辑</el-button>
  </div>
</template>

<script>
export default {
  methods: {
    editRent(id) {
      this.$router.push({
        path: '/exterpriseAdd',
        query: {
          id
        }
      })
    }
  }
}
</script>

关键行与易错点

  • 新增和编辑复用同一个路由 /exterpriseAdd,区别只在于编辑时通过 `

常见错误 ​

  1. 现象:行业下拉能展开但选项空白或选中值异常。原因:el-option 的 value/label 未按接口返回字段绑定,或 v-model 与 value 类型不一致。修法:对照接口响应绑定 industryCode/industryName,并核对选中值类型。
  2. 现象:使用 http-request 后控制台仍提示 action 必填。原因:el-upload 默认上传配置仍要求 action。修法:保留 action="#" 作为占位,实际上传逻辑放在 http-request 中。
  3. 现象:上传请求发出但后端收不到文件或提示参数缺失。原因:FormData 的 key 或 type 值与接口文档不一致。修法:逐字核对 append('file', file)、append('type', 'businessLicense')。
  4. 现象:文件类型/大小不合格时已提示,但仍继续上传。原因:before-upload 没有返回布尔值或返回 undefined。修法:明确 return allowImgType && isLt5M,确保不合格时被拦截。
  5. 现象:营业执照已上传成功,但表单仍显示“请上传营业执照”。原因:上传组件未通知 el-form 该字段已变化。修法:上传成功后手动调用 validateField('businessLicenseId')。
  6. 现象:点击确定后校验未通过仍调用新增接口。原因:validate 回调中未判断 valid。修法:先 if (!valid) return,再执行提交逻辑。
  7. 现象:手机号自定义校验导致表单卡住或错误提示不出现。原因:validator 中成功/失败分支未都调用 callback。修法:成功 callback(),失败 callback(new Error('...'))。

练习 ​

  1. 将行业列表请求改为“下拉框首次聚焦时请求”,并保证多次聚焦不重复请求;用 Network 面板验收请求次数。
  2. 为营业执照上传补齐类型和大小校验,验收点:不合格文件只提示、不发上传请求;合格文件可正常上传。
  3. 给定一个表单字段错位场景:v-model、prop、rules 名称不一致,要求定位问题并修正,使对应字段校验能正常触发。
  4. 实现联系电话自定义校验,覆盖空值、非手机号、合法手机号三种输入;验收点:三种情况的提示和放行结果均正确。
  5. 设计新增/编辑复用同一页面的判断逻辑:列表页携带 query.id 跳转后,页面需区分新增与编辑;列出需要检查的路由、请求和数据回填分支。

原视频对照 ​

#视频时长
1新增企业-行业列表下拉框渲染10:06
2上传合同-通过el-upload得到文件对象file14:36
3上传合同-准备formData实现核心上传逻辑09:13
4上传合同-上传前校验逻辑10:34
5新增企业-添加表单基础校验05:18
6新增企业-手机号自定义校验03:54

小结 ​

  1. 下拉渲染是否稳定,核心看接口字段、value 与 v-model 是否完全对齐。
  2. 自定义上传的边界要清楚:组件负责选文件,业务代码负责组参数和发请求。
  3. 上传前校验必须形成布尔拦截,否则“提示”和“实际行为”会脱节。
  4. 表单校验要保证 model 字段、prop、rules key 三处一致。
  5. 上传类字段不能只依赖用户输入触发校验,成功后需主动校验该字段。
  6. 统一提交时,validate 的 valid 是唯一放行条件。
  7. 自定义 validator 必须闭合成功和失败两个 callback 分支。
  8. 路由参数只是入口,新增/编辑差异必须在页面逻辑中显式处理。