主题
企业管理(三)
本页对应 企业管理,共 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 统一取出来缓存成计算属性。这样页头文案、后续获取详情、提交时调新增还是更新接口,都可以复用同一个 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
}
}调试检查顺序
- 初始进入页面,
rentDialogVisible应为false,弹框不显示。 - 点击“添加合同”,检查
rentDialogVisible是否变为true,弹框是否出现。 - 点击“取消”,检查是否变为
false,弹框是否消失。 - 点击右上角关闭或触发
close,确认同样走closeDialog。 - 再接确定按钮时,按转写稿思路先校验、再提交、成功后再关闭,避免把“关闭”误当成“提交”。
这一节先不急着调接口,而是把租赁表单的“模板、数据、规则”三件套搭好。后面无论做统一校验、单独校验还是提交,都是在这三样东西上做文章。

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'),否则“请上传合同文件”的提示不会消失。
常见错误
上传营业执照后,红色校验提示不消失 现象:用户已上传成功,
请上传营业执照仍在;点确定却仍被拦。 原因:el-upload不属于el-form默认可感知的表单控件,上传完成不会通知表单校验系统,prop对应字段的状态没被重新计算。 修法:在on-success回调里先给addForm.businessLicenseId / businessLicenseUrl赋值,再this.$refs.ruleForm.validateField('businessLicenseId')单独触发一次。单独校验写了,但校验依旧不过 现象:
validateField调了,提示还是红的。 原因:调用时机在赋值之前(写在before-upload、或写在on-success但赋值在await之后 / 用了then却在校验后才赋值),此刻字段还是空值。 修法:严格保证「接口返回 → 赋值 → 再校验」的顺序;validateField的入参必须是prop上写的那个字段名,不是businessLicenseUrl。编辑页一进来就报 404 / 请求参数是 undefined 现象:新增状态下打开页面,控制台出现
/park/enterprise/undefined。 原因:详情接口无条件写在created/mounted里,没判断当前是新增还是编辑。 修法:if (this.id) { this.getEnterpriseDetail() },把「有没有 id」作为唯一的状态开关。点确定后新增、编辑接口都调了一遍 现象:编辑保存后列表多出一条新数据,或多出一次
POST。 原因:原来新增的await createExterpriseAPI(...)没删,只在下面补了if/else;或if里调完没else/return。 修法:接口调用只保留一处分支,两个分支互斥;提交成功后统一$router.back()。编辑提交后端报字段错误 现象:更新接口 500 或字段没更新。 原因:直接把
this.addForm整个丢给PUT接口,字段比接口文档多(含name、businessLicenseUrl等接口不要的),或编辑时漏传id。 修法:以接口文档字段为准,用解构只挑需要的字段,编辑态额外补id: this.id。弹框关不掉 / 控制台警告「Avoid mutating a prop」 现象:点取消弹框不关,或改了
visible报 prop 修改警告。 原因:把:visible当成v-model用,或在子组件里直接给 prop 赋值。 修法:父组件用data里的rentDialogVisible,模板:visible="rentDialogVisible"+@close="closeDialog",打开/关闭都只改这个 data 变量。
练习
- 在列表页点「编辑」,用
query携带id跳到/exterpriseAdd;同时在目标页用computed缓存这个 id,并让页面标题在「添加企业 / 编辑企业」之间自动切换。要求:直接访问不带id的地址时标题必须是「添加企业」。 - 实现「营业执照上传后自动消除校验提示」:给出
el-form-item的prop、el-upload的关键属性和on-success里的完整逻辑(含赋值与单独校验),并说明为什么不能把校验写在before-upload里。 - 写出
confirmSubmit:先统一validate,校验通过后按当前是否处于编辑态调用不同接口;编辑态要按接口文档裁剪字段并带上id;成功后给出一句区分「新增成功 / 更新成功」的提示并返回列表页。 - 给「添加合同」弹框补全开合逻辑:一个按钮打开、footer 取消关闭、右上角关闭也关闭,并说明为什么不能直接在模板上写
v-model="visible"。 - 用一句话分别说明下面四个场景该用哪种校验方式,并各写出关键代码一行:① 必填的楼宇下拉;② 手机号格式;③ 两次密码一致;④ 上传完合同文件后让
contractId的必填提示消失。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 新增企业-上传完毕对营业执照做单独校验 | 10:21 |
| 2 | 新增企业-收集表单数据调用接口 | 03:47 |
| 3 | 梳理上传流程和表单校验 | 19:03 |
| 4 | 编辑企业-携带id跳转并存储id | 03:57 |
| 5 | 编辑企业-回填数据 | 04:33 |
| 6 | 编辑企业-调用更新完成更新企业 | 07:42 |
小结
- 一个
id贯穿整条编辑链路:跳转带它、页面缓存它、文案判断用它、详情请求靠它、提交分支看它——它是这个页面唯一的状态源。 - 新增与编辑共用一个页面是常态,但「共用模板 ≠ 共用接口」,凡是请求的地方都要过一遍
id判断。 - 表单校验在这一模块里其实只有四种:基础配置校验、自定义
validator、统一validate、单字段validateField;前三种是「整体思维」,第四种是「补丁思维」。 validateField是给「不听话」的控件准备的兜底手段,判断标准很简单:这个控件会不会主动把值的变化告诉el-form,不会就得手动补一刀。- 顺序感比 API 记忆更重要:赋值 → 校验 → 提交,任何一步提前都会表现成「莫名其妙的校验失败」。
- 提交参数永远以接口文档为准,多传字段和少传字段都是错,解构裁剪是最省事的写法。
- 弹框、表单、上传这些「组件状态」都归
data管,模板只读不写,prop单向数据流不能破。 - 前端校验是体验、后端校验是底线,两边规则要一致;前端拦不住的请求,后端必须拦得住。