主题
企业管理(二)
本页对应 企业管理,共 6 段视频,约 53 分钟。
学习目标
- 能够调用行业列表接口,将返回数据渲染到
el-select的el-option,正确绑定value/label,并验证选中值写入v-model。 - 能够使用
el-upload的http-request回调获取文件对象,并从回调参数中解构出待上传的file。 - 能够按接口要求创建
FormData,通过append添加file与type字段,调用上传接口并验证请求参数及返回的文件地址、名称。 - 能够配置
before-upload校验文件类型和大小,校验失败给出提示并返回false阻止上传,校验通过返回true放行。 - 能够为表单绑定
model与rules,配置字段必填规则并调用validate完成统一校验;能够为手机号字段添加validator正则校验,失败时通过callback(new Error(...))返回提示。 - 能够按编辑页路由要求携带企业标识参数完成跳转,并在目标页验证参数被正确接收。
知识点
本节目标与功能演示
这一节要完成的是“添加企业”页面的落地:先准备一个独立的新增企业组件,再把它绑定到路由上,最后从企业列表页点击“添加企业”跳转进来。为什么需要先做这一步?因为新增企业不是一个弹窗,而是一个完整页面,页面里有头部返回、企业信息表单、底部操作按钮。只有先把组件和路由打通,后面做行业下拉、营业执照上传、表单校验时,才能稳定地在这个页面里调试。
怎么想?顺序不要反过来:先有页面组件,再有路由地址,最后加入口按钮。转写稿里反复强调“先测试再绑定”的习惯,这里同样适用:路由配好后先直接访问路径,确认组件能渲染;再从列表页点击按钮,确认跳转路径一致。
怎么做?准备组件代码如下,保留原笔记代码:
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,否则文件参数传不过去。
准备自定义上传组件

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,区别只在于编辑时通过 `
常见错误
- 现象:行业下拉能展开但选项空白或选中值异常。原因:
el-option的value/label未按接口返回字段绑定,或v-model与value类型不一致。修法:对照接口响应绑定industryCode/industryName,并核对选中值类型。 - 现象:使用
http-request后控制台仍提示action必填。原因:el-upload默认上传配置仍要求action。修法:保留action="#"作为占位,实际上传逻辑放在http-request中。 - 现象:上传请求发出但后端收不到文件或提示参数缺失。原因:
FormData的 key 或type值与接口文档不一致。修法:逐字核对append('file', file)、append('type', 'businessLicense')。 - 现象:文件类型/大小不合格时已提示,但仍继续上传。原因:
before-upload没有返回布尔值或返回undefined。修法:明确return allowImgType && isLt5M,确保不合格时被拦截。 - 现象:营业执照已上传成功,但表单仍显示“请上传营业执照”。原因:上传组件未通知
el-form该字段已变化。修法:上传成功后手动调用validateField('businessLicenseId')。 - 现象:点击确定后校验未通过仍调用新增接口。原因:
validate回调中未判断valid。修法:先if (!valid) return,再执行提交逻辑。 - 现象:手机号自定义校验导致表单卡住或错误提示不出现。原因:
validator中成功/失败分支未都调用callback。修法:成功callback(),失败callback(new Error('...'))。
练习
- 将行业列表请求改为“下拉框首次聚焦时请求”,并保证多次聚焦不重复请求;用 Network 面板验收请求次数。
- 为营业执照上传补齐类型和大小校验,验收点:不合格文件只提示、不发上传请求;合格文件可正常上传。
- 给定一个表单字段错位场景:
v-model、prop、rules名称不一致,要求定位问题并修正,使对应字段校验能正常触发。 - 实现联系电话自定义校验,覆盖空值、非手机号、合法手机号三种输入;验收点:三种情况的提示和放行结果均正确。
- 设计新增/编辑复用同一页面的判断逻辑:列表页携带
query.id跳转后,页面需区分新增与编辑;列出需要检查的路由、请求和数据回填分支。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 新增企业-行业列表下拉框渲染 | 10:06 |
| 2 | 上传合同-通过el-upload得到文件对象file | 14:36 |
| 3 | 上传合同-准备formData实现核心上传逻辑 | 09:13 |
| 4 | 上传合同-上传前校验逻辑 | 10:34 |
| 5 | 新增企业-添加表单基础校验 | 05:18 |
| 6 | 新增企业-手机号自定义校验 | 03:54 |
小结
- 下拉渲染是否稳定,核心看接口字段、
value与v-model是否完全对齐。 - 自定义上传的边界要清楚:组件负责选文件,业务代码负责组参数和发请求。
- 上传前校验必须形成布尔拦截,否则“提示”和“实际行为”会脱节。
- 表单校验要保证
model字段、prop、ruleskey 三处一致。 - 上传类字段不能只依赖用户输入触发校验,成功后需主动校验该字段。
- 统一提交时,
validate的valid是唯一放行条件。 - 自定义
validator必须闭合成功和失败两个callback分支。 - 路由参数只是入口,新增/编辑差异必须在页面逻辑中显式处理。