主题
企业管理(一)
本页对应 企业管理,共 7 段视频,约 33 分钟。
学习目标
- 能依据接口文档在
api目录中封装企业列表查询接口,正确传递name、page、pageSize三个参数,并在组件中调用该接口,完成响应数据向表格的赋值与渲染。 - 能使用分页组件的
total、page-size属性正确计算并展示总页数,绑定current-change事件,在事件回调中先更新请求参数再调用接口,实现点击页码切换数据。 - 能通过浏览器 Network 面板核验请求参数是否真实携带,并据此定位“参数未生效”的两类原因(调用时漏传实参、封装接口时键名写错或未包在
params中)。 - 能通过
v-model双向绑定搜索关键词并实现查询逻辑,在查询前将页码重置为 1,说明该重置的必要性。 - 能利用输入框的
clearable属性与clear事件实现清除搜索,并说明“子组件调用父组件方法以通知父组件做事”这一子传父的本质。 - 能在路由配置中注册新增企业页面并保证路径大小写正确,通过行内跳转实现“添加企业”按钮的页面跳转。
知识点
先明确:为什么要单独封装企业列表接口
企业管理页一上来就要展示企业列表,后面还会接分页、搜索、清空搜索等交互。如果每个组件里都直接写一次请求地址、请求方法、参数名,短期能跑,长期会出现三个问题:地址散落、参数写法不统一、改接口时到处找。更稳妥的做法是把“获取企业列表”这件事收敛到 API 层:组件只关心“我要第几页、每页几条、企业名称是什么”,不关心底层 request 怎么配置。
转写稿里先看接口文档:企业列表是一个 GET 请求,地址为 /park/enterprise,参数有 name、page、pageSize。这三个参数都属于请求参数,可以放在同一个对象里传给封装函数。于是封装思路就很清楚:导入项目统一的 request,导出一个函数,函数接收 params,内部调用 request({ url, params })。GET 请求的 method 可以省略,但参数必须放在 params 里。
代码落地:保留本节接口封装代码
javascript
import request from '@/utils/request'
/**
* 获取企业列表
* @param { page, pageSize} params
* @returns
*/
export function getEnterpriseListAPI(params) {
return request({
url: '/park/enterprise',
params
})
}这段代码的关键点有三个。第一,request 是项目已经封装好的请求模块,业务接口复用它,避免重复写 baseURL、拦截器等逻辑。第二,函数参数用 params 整体接收,调用方传什么对象,这里就原样交给 request 的 params 字段。第三,url 只写接口路径,不写完整域名。转写稿中口述“get 可以省略”,对应到这里就是没有显式写 method: 'get',因为 request 默认按 GET 处理。
注释里当前写的是 @param { page, pageSize} params,而转写稿明确接口还有 name 参数。实际开发中建议注释与真实参数保持一致,避免后续同学误以为只能传分页参数。这里保留原代码,但理解时要按 name、page、pageSize 三个字段来用。
业务组件怎么调用:先准备参数对象,再调 API
封装完成后,业务组件不应再关心 /park/enterprise 这个地址,而是导入 getEnterpriseListAPI,准备一个参数对象,例如包含 name、page、pageSize。转写稿里的做法是先声明响应数据 list,再写一个 getList 方法专门做接口调用和数据赋值,并在生命周期里执行。调用时把参数对象传给 getEnterpriseListAPI(params),拿到响应后再把列表数据赋给 list。
这里要建立“先修改参数,后调用接口”的顺序意识。分页、搜索、清空搜索时,都是先把 params 里的 page 或 name 改成最新值,再调用 getList。如果顺序反过来,接口拿到的仍是旧参数。转写稿中搜索时先把 page 重置为 1,也是同一个道理:用户可能已经翻到后面几页,搜索后数据量变少,如果不回到第一页,容易出现空白页。
调试重点:不要只盯代码,要看 Network
转写稿里专门演示了一个易错场景:GET 请求本来应该写 params,却误写成了 data。代码看上去“参数也传了”,但 Network 面板里请求 URL 后面没有拼上 page、pageSize,后端自然按默认条件返回数据。强调:是否真的把参数传过去了,不要以代码为主,要以 Network 的实际表现为主。
排查时可以按这个顺序走:先看 Network 里请求 URL 是否出现 ?page=1&pageSize=...&name=... 这类参数拼接;如果没有,先检查调用函数时有没有把实参传进去;再检查封装接口时是不是把 GET 参数写成了 data,或者 params 拼写错误。只要 Network 里没有参数,就说明参数没有真正发出去,不能凭代码主观判断。
易错点清单
第一,GET 请求参数要用 params,不要用 data。第二,调用 getEnterpriseListAPI 时不要忘记传参数对象。第三,导出用 export function,导入时要对应使用命名导入。第四,参数对象不要写成字符串,应传 { name, page, pageSize } 这样的对象。第五,注释参数与实际接口参数尽量一致。第六,调试时以 Network 面板为准,不要只相信代码表面。把接口封装这一层做稳,后面的表格渲染、分页和搜索才有可靠的数据入口。
从静态模板到数据驱动
企业管理页面先搭静态结构:搜索区、添加按钮区、表格区、分页器。静态模板只能看结构,真正列表要来自后端。为什么需要:表格数据、总数、分页都依赖接口;怎么想:先声明响应式数据,再封装接口,在组件挂载后请求并赋值;怎么做:本节笔记代码如下,保留原代码:
vue
<script>
import { getEnterpriseListAPI } from '@/apis/enterprise'
export default {
name: 'Building',
data() {
return {
exterpriseList: [],
params: {
page: 1,
pageSize: 10
}
}
},
mounted() {
this.getExterpriseList()
},
methods: {
async getExterpriseList() {
const res = await getEnterpriseListAPI(this.params)
this.exterpriseList = res.data.rows
}
}
}
</script>易错点:exterpriseList 是笔记中的拼写,模板绑定要一致;params 不要写成 parents;mounted 不要写成 monkey。
先声明列表和请求参数
为什么需要:列表初始为空,接口返回后要触发视图更新;请求参数要集中管理,分页、搜索都复用它。怎么想:data 中放 exterpriseList: [] 和 params: { page: 1, pageSize: 10 };page 是当前页,pageSize 是每页条数。怎么做:模板表格绑定 exterpriseList,分页器绑定 total 与 pageSize。易错点:total 要用数字占位,不要写空字符串;pageSize 演示时可设小值,转写稿中设为 2 以分三页。
在 mounted 中发起首次请求
为什么需要:组件挂载后页面结构已就绪,适合拉取首屏数据。怎么想:单独封装 getExterpriseList,里面只做接口调用和赋值;mounted 只负责启动。怎么做:const res = await getEnterpriseListAPI(this.params),再把 res.data.rows 赋给列表。转写稿中接口返回列表在 rows,总数在 total,所以分页总数要补 this.total = res.data.total。易错点:不要把请求逻辑散在模板里;赋值字段要按接口文档,不要凭感觉写。
接口封装与 GET 参数传递
为什么需要:统一接口地址和参数入口,业务组件只传参。怎么想:企业列表是 GET 请求,参数有 name、page、pageSize,可放在一个对象里传。怎么做:接口函数接收 params,GET 请求把参数放在 params 配置中。易错点:转写稿特别演示:GET 请求若误写成 data,Network 里参数不会出现在 URL 问号后,后端仍返回全部 5 条;此时不要只盯代码,要打开 Network 刷新,看请求 URL 是否带参数。是否真的传参,以 Network 表现为主。
分页获取数据
为什么需要:数据多时不能一次全取。怎么想:分页器需要 total 和 pageSize 算出页数;点击页码时拿到当前页,改 params.page 后重新请求。怎么做:绑定 current-change,回调接收当前页,先 this.params.page = val,再调用 getExterpriseList()。转写稿强调“先修改参数后调用接口”,这样请求参数已是最新页。易错点:顺序反了会请求旧页;current-change 是组件事件名,不要写成普通方法名。
搜索与清空搜索
为什么需要:按企业名称过滤列表。怎么想:输入框双向绑定 params.name,点击查询时把 page 重置为 1,再请求。为什么重置:用户可能已点到第 5 页,搜索后结果很少,第 5 页会空白。怎么做:this.params.page = 1; this.getExterpriseList()。清空搜索时,输入框的 clearable 属性会生成叉号,绑定 clear 事件,在回调里直接调 getExterpriseList();此时 params.name 已被清空,所以能拉回全部数据。易错点:清空后不重置页码;把 clearable 拼错;把 clear 事件当成数据回传。转写稿还强调:子传父的本质是子组件调用父组件方法通知做事,不一定只传数据。
调试与易错点清单
为什么需要:接口问题只看代码很慢。怎么想:先看 Network 的请求 URL、参数、响应;再看代码。怎么做:刷新面板,检查 GET 参数是否拼接、rows 和 total 是否存在。易错点:params 拼写、mounted 拼写、列表字段拼写;GET 参数误写 data;分页总数未赋值;搜索不重置 page;清空搜索不重新请求。按“声明状态→封装接口→挂载请求→赋值渲染→分页搜索复用参数”的流程走,组件中获取数据就稳定了。
先看目标:模板负责“怎么显示”,数据负责“显示什么”
为什么需要:企业管理列表不能把企业名称、联系人、电话写死在模板里,否则接口一变就要改页面。el-table 的职责是把“数据列表”按“列配置”渲染出来,所以模板里必须解决两件事:整张表的数据从哪来、每一列取哪个字段。
怎么想:把 el-table 看成一个容器::data 决定“有哪些行”,el-table-column 的 prop 决定“这一列从每行对象里取哪个属性”。笔记里的核心思路就是:通过 data 属性给 el-table 绑定数据列表;通过 prop 属性指定当前列要渲染的字段名称,字段名参考接口文档。
怎么做:保留本节模板代码,重点看 :data="exterpriseList" 和各个 prop:
vue
<template>
<div class="building-container">
<!-- 表格区域 -->
<div class="table">
<el-table style="width: 100%" :data="exterpriseList">
<el-table-column type="index" label="序号" />
<el-table-column label="企业名称" width="320" prop="name" />
<el-table-column label="联系人" prop="contact" />
<el-table-column
label="联系电话"
prop="contactNumber"
/>
<el-table-column label="操作">
<template #default="scope">
<el-button size="mini" type="text">添加合同</el-button>
<el-button size="mini" type="text">查看</el-button>
<el-button size="mini" type="text">编辑</el-button>
<el-button size="mini" type="text">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</template>易错点::data 绑定的是数组,不是字符串;prop 写的是接口返回对象里的字段名,不是中文标题;label 才是列头显示文字。不要把 prop="name" 写成 prop="企业名称"。
data 绑定:先有响应式列表,再交给表格
为什么需要:el-table 不会自己发请求,它只认传进去的数组。所以要先在脚本区声明企业列表数据,再调接口拿到数据并赋值,最后模板才能渲染。
怎么想:按之前表格渲染流程走:声明响应式数据 → 封装获取列表的方法 → 在 mounted 中调用 → 接口返回后赋值。转写稿里明确提到:先准备一个列表,标注为企业列表;封装 getList 方法专门做接口调用和数据赋值;在 mounted 里执行;模板里先用空数组占位,再替换成真实列表。
怎么做:脚本里准备 exterpriseList,请求接口时传 name、page、pageSize 这类参数,拿到返回数据后把列表数据赋给 exterpriseList。转写稿中返回数据里列表字段是 rows,因此赋值时要取到 rows,而不是把整个响应对象直接交给表格。模板里保持 :data="exterpriseList"。
易错点:初始值不要写成空字符串,空数组更合适;赋值路径要看接口返回结构,不能想当然;如果表格空白,先看 network 里返回数据是否有 rows,再看赋值有没有写对。
prop 绑定:每一列按字段名取数据
为什么需要:接口返回的每个企业对象有很多字段,列要显示哪一项,必须通过 prop 告诉 el-table。否则列头有了,内容出不来。
怎么想:打开接口返回数据,点开每一个对象,看对象里的字段名。prop 绑哪个字段,当前列就会把该字段逐行取出来显示。转写稿里强调:正常开发时模板不会有人替你写好,需要自己根据后端返回对象字段来写 prop。
怎么做:企业名称列写 prop="name",联系人列写 prop="contact",联系电话列写 prop="contactNumber"。这些字段名要与接口文档、network 返回对象保持一致。序号列用 type="index",不需要 prop。
易错点:prop 大小写要一致;不要把 prop 和 label 混用;如果字段在嵌套对象里,不能直接写错层级。操作列没有直接写 prop,而是用 #default="scope" 放按钮,这是模板里的另一种列内容写法。
调试与验证:以 network 的实际表现为主
为什么需要:只看代码容易误判,尤其是请求参数有没有真正传过去、返回结构到底是什么。转写稿里专门补充:是否真的把参数传过去,不要以代码为主,要以 network 面板表现为主。
怎么想:刷新页面后看请求:GET 请求参数应出现在 URL 问号后或请求参数区;返回数据里找列表字段。若表格没数据,先确认请求是否成功、参数是否带上、返回的 rows 是否存在。
怎么做:打开调试工具,刷新列表,检查请求参数和返回数据;确认 exterpriseList 拿到的是数组;再检查模板 :data 和 prop 是否与字段对应。
易错点:GET 请求参数应放在 params 里,不要写成 data;不要只盯着代码里的对象,network 里没带上就是没带上;字段名不一致时,表格不会报大错,但内容会空,容易误判为接口没返回。
分页要解决什么问题
为什么需要: 企业列表数据可能很多,如果一次性全部请求并渲染,传输和渲染成本都会变高,用户也不容易浏览。分页的目标是“只取当前页需要的数据”,同时让用户知道总共有多少条、可以翻到第几页。
怎么想: 分页器不需要我们手写页码按钮,Element UI 的 el-pagination 会根据“总数”和“每页条数”自动计算页数。笔记里的核心公式是:
页数 = 总数 / 每页条数
所以前端要准备两个关键数据:total(后端返回的总条数)和 pageSize(每页几条)。total 不是当前页条数,而是满足当前查询条件的全部数据条数。
怎么做: 先在 data 中声明 total,初始值写 0,不要写空串或 null,否则分页器计算容易不匹配。
jsx
data() {
return {
total: 0
}
}易错点: 不要把 total 理解成 rows.length。rows.length 只是当前页返回的条数,不能用来算总页数。
请求列表后把总数赋给 total
为什么需要: 分页器要知道总条数,才能算出总页数;而这个总条数只能由后端接口返回。因此列表请求成功后,除了给表格赋值 rows,还要给 total 赋值。
怎么想: 列表接口一般会返回类似 res.data.rows 和 res.data.total 的结构:rows 是当前页数据,total 是总条数。我们渲染表格用 rows,渲染分页用 total。
怎么做: 在获取企业列表的方法中完成两次赋值:
jsx
async getExterpriseList() {
const res = await getEnterpriseListAPI(this.params)
this.exterpriseList = res.data.rows
this.total = res.data.total
}易错点: 只赋值 exterpriseList 而忘记 this.total = res.data.total,页面会出现“表格有数据,但分页器总数不对/页数不对”的情况。
用 total 和 page-size 渲染分页器
为什么需要: 分页器需要知道总条数和每页条数,才能显示总页数、上一页、页码和下一页。layout 用来控制显示哪些部分。
怎么想: 笔记中的 layout="total, prev, pager, next" 表示显示总条数、上一页、页码、下一页。:total 绑定总条数,:page-size 绑定每页条数。page-size 应该来自请求参数 params.pageSize,这样分页器和接口请求使用同一份配置。
怎么做: 模板中保留笔记代码:
jsx
<el-pagination
layout="total, prev, pager, next"
:total="total"
:page-size="params.pageSize"
/>转写稿中为了测试,把 pageSize 暂设为 2,5 条数据就会分出 3 页。这是调试手段,不是固定业务值。
易错点: :total 绑定的是总数,不是当前页条数;:page-size 要和请求参数保持一致,否则分页器显示的页数和接口实际取的条数会对不上。
点击页码时重新获取当前页数据
为什么需要: 分页器只负责“显示页码”和“触发页码变化”,它不会自动帮你请求接口。用户点击第 2 页时,必须把当前页码同步到请求参数里,再重新调用列表接口。
怎么想: 转写稿强调两个动作:第一,分页;第二,点击页码获取数据。页码变化使用 current-change 事件,回调里能拿到当前页码。关键顺序是:先修改请求参数,再调用接口。这样接口拿到的 params.page 才是最新页码。
怎么做: 给分页器绑定事件,并在方法中先改 params.page,再请求:
jsx
<el-pagination
layout="total, prev, pager, next"
:total="total"
:page-size="params.pageSize"
@current-change="handleCurrentChange"
/>jsx
handleCurrentChange(page) {
this.params.page = page
this.getExterpriseList()
}易错点: 如果先调接口再改 params.page,请求用的还是旧页码;如果忘记在回调里接收页码,就无法知道用户点了第几页。
用 Network 验证参数是否真的传过去
为什么需要: 分页不生效时,很多人只看代码,觉得“我明明传了参数”。但代码写对不等于请求真的带上了参数,必须以浏览器 Network 面板的实际请求为准。
怎么想: GET 请求的参数会拼在 URL 后面,Network 里能看到 query string。转写稿给了一个典型错误:GET 请求封装时把 params 写成了 data,结果参数没有真正拼到请求上,后端仍按默认条件返回了 5 条数据。此时看代码可能“像传了”,但 Network 里没有对应参数。
怎么做: 打开 Network,刷新列表,点第 2 页,检查请求 URL 中是否出现 page=2、pageSize=... 等参数;同时看响应里的 rows 和 total 是否符合预期。
易错点: 不要只相信代码。GET 请求要确认参数键名正确,常见是 params 而不是 data;发现参数没传,先检查函数实参有没有传,再检查接口封装里的参数对象。
小结
渲染分页的完整链路是:声明 total → 请求列表拿到 rows 和 total → 表格绑定 rows → 分页器绑定 total 与 pageSize → 监听 current-change → 先改 params.page 再重新请求。调试时以 Network 为准,重点核对 page、pageSize 和 total 是否一致。
分页切换到底在切什么
为什么需要:企业管理列表的数据不是一次性全部铺在页面上,而是分页展示。用户点击第 2 页、第 3 页时,页面不能只把本地数组切一段,而要把“当前页码”作为请求参数重新向后端要数据。否则搜索条件、总数、每页条数一变,前端本地切分就容易和后端数据不一致。
怎么想:分页器组件内部知道用户点了第几页,但列表请求逻辑在父组件里。所以思路是“子组件把当前页码通过事件交给父组件,父组件改请求参数,再复用原来的列表请求方法”。笔记里的三步就是:通过事件拿到当前页数;以当前页数作为参数发送数据请求获取当前页数据;重新把列表交给 buildingList。在本节代码里,最后一步落到重新调用 getExterpriseList(),由它完成请求和列表赋值。
怎么做:给分页组件绑定 @current-change,回调接收当前页码,先改 this.params.page,再调用列表请求方法。核心代码保留如下:
jsx
<el-pagination
@current-change="pageChange"
/>
pageChange(page) {
// 更改参数
this.params.page = page
// 重新获取数据渲染
this.getExterpriseList()
}易错点:事件名要写对,是 current-change;回调参数 page 就是当前页码;顺序不能反。转写稿里特别强调“先修改参数后调用接口”,因为 getExterpriseList() 内部会直接使用 this.params 发请求。如果先调接口再改页码,这次请求用的还是旧页码。
先让分页器具备分页条件
为什么需要:分页器要显示总页数,必须知道总条数和每页条数。转写稿里说页数按“总数除以每页条数”分出来,分页组件通过 total 和 page-size 这类属性绑定。
怎么想:total 来自接口返回的总数,不能凭空写死;page-size 要和请求参数里的每页条数保持一致。视频里为了测试方便,把每页条数临时设为 2,5 条数据就分出了 3 页。
怎么做:在响应式数据里准备总数,初始值用 0 占位;把 total 绑定到分页器;把 page-size 与请求参数中的 pageSize 对应;接口返回后,用真实 total 覆盖初始值。
易错点:总数不要写成空字符串或空值,否则分页计算不匹配;page-size 和请求参数不要一个 2 一个 10,否则页面显示和实际请求对不上。转写稿里说“别写空的”,就是提醒初始值要符合数字类型。
点击页码后的完整链路
为什么需要:只改页码不请求,表格还是旧数据;只请求不传页码,后端仍按第一页返回。必须把“页码变化”传导到请求参数里。
怎么想:用户点第 2 页,current-change 触发,pageChange(page) 收到 2;把 this.params.page 改成 2;调用 getExterpriseList();方法内部用最新的 params 发请求,拿到当前页数据后赋给列表。这样列表自然重新渲染。
怎么做:按笔记代码绑定和调用即可。调试时打开 Network,点第 2 页,看请求参数里的 page 是否变成 2;点第 3 页,是否变成 3。转写稿的验证过程就是这样:点第二页,请求参数 page 为 2;点第三页,page 为 3。
易错点:不要在前端自己截取数组冒充分页;不要忘记接口返回后给列表赋值;不要在 pageChange 里只写 this.params.page = page 而不调用请求方法。
用 Network 验证参数,而不是只盯代码
为什么需要:转写稿视频 4 专门补充了一个调试问题:参数看起来在代码里写了,但接口返回仍是 5 条,说明参数根本没传到后端。此时只看代码容易“觉得没问题”。
怎么想:GET 请求的参数会拼在 URL 后面,Network 里能看到问号和参数。如果 Network 里没有 page、pageSize 这些参数,就说明请求配置没生效。转写稿里故意把 params 写错成 date 一类字段,结果接口仍返回全部数据;检查后发现 GET 请求的配置项应该写 params,而不是随便写别的键。
怎么做:点开 Network,刷新或点击页码,检查请求 URL 和请求参数;再检查函数调用时有没有把参数对象传进去;最后检查接口封装里 GET 请求第二个参数是否写成 { params: ... }。转写稿提醒:用 request.get 这类写法时,不要忘了加一个大括号再写 params。
易错点:不要相信“代码里写了”就等于“请求发出去了”;不要把小写的 params 写成 parents、data、date 等;GET 请求参数位置和 POST 请求体位置不要混。
和搜索联动时别忘了重置页码
为什么需要:用户可能已经点到第 5 页,再输入关键词搜索。如果搜索时仍带着 page=5,而搜索结果只有一两条,就可能出现当前页空白。转写稿在搜索功能里明确说要把页码重置为 1,再调用 getExterpriseList()。
怎么想:搜索条件变化后,结果集重新计算,原来的页码没有意义,最稳妥是从第一页开始看。清空搜索时也是同理:输入框清空后 params.name 变为空,重新请求全部数据,页码状态要符合当前列表场景。
怎么做:在查询回调里先 this.params.page = 1,再调用列表请求;分页切换则只改 page,不要重置搜索条件。
易错点:搜索时只改 name 不改 page,可能请求到不存在的页;分页切换时误把搜索关键词清空,导致条件丢失。核心原则是:分页只改页码,搜索改条件并回到第一页。
本节目标:先看功能,再把“添加企业”页面和路由搭起来
为什么需要: 企业管理模块里,“添加企业”不是弹窗,而是要进入一个全新的页面。列表页负责展示和搜索,新增页负责填写企业信息,所以必须先把新增页组件和路由准备好,后面才能做表单校验、上传营业执照等逻辑。
怎么想: 按“先路由、后交互”的顺序做。先准备一个静态的添加企业组件,再在路由表里注册地址,最后从列表页的“添加企业”按钮跳过去。转写稿里也强调:先复制路径手动访问,确认页面能显示,再回来绑定点击跳转。
怎么做: 先准备组件。下面代码是本节笔记中的静态结构,保留了返回、标题、表单区域和底部按钮:
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>易错点: addForm 里的字段要和 v-model 一一对应;笔记里多个 el-form-item 的 prop 暂时都写成 name,后面做表单校验时要按真实字段改,否则校验会串。营业执照这一项目前只是占位,上传功能本节不实现。返回按钮用 $router.back(),不要写成固定跳转。
绑定路由:让地址能直接打开新增页
为什么需要: 组件写好只是文件存在,浏览器地址访问不到。只有注册路由,/exterpriseAdd 才能映射到 Add 组件。
怎么想: 这个页面和列表页同级,所以直接复制一条相近路由,改路径和组件路径。转写稿里提醒路径大小写敏感,外层是 park,内层是 enterprise,最后是 Add,要逐段核对。
怎么做: 路由配置如下:
javascript
{
path: '/exterpriseAdd',
component: () => import('@/views/park/enterprise/Add')
}调试过程: 先复制 /exterpriseAdd 到地址栏手动访问,确认页面能显示。如果显示不出来,先查路由是否注册、组件路径大小写是否一致,而不是急着绑按钮。
易错点: 跳转地址和路由 path 必须完全一致;import 路径中的 park/enterprise/Add 大小写要匹配真实文件。路径拼错时,点击按钮会跳空白或报错。
路由跳转:从列表页进入添加页
为什么需要: 用户是在企业管理列表页点击“添加企业”进入新增页,所以按钮必须触发路由跳转。
怎么想: 这里没有参数,只是简单跳转,可以直接在按钮上写行内 $router.push,不必额外封装方法。
怎么做:
html
<el-button type="primary" @click="$router.push('/exterpriseAdd')">添加企业</el-button>测试: 回到列表页点击“添加企业”,能进入添加页;再点页面左上角返回,能回到列表页。
易错点: 点击没反应时,先看路由是否注册、push 路径是否与 path 完全一致;不要先怀疑按钮样式。转写稿里“录径”就是“路径”,“天加企业”就是“添加企业”,写代码时以真实英文路径为准。
本节边界与检查清单
本节只完成“功能演示、静态组件、路由绑定、点击跳转”。上传营业执照、表单校验、合同列表等属于后续内容,不在本节展开。检查四件事:组件能否手动访问、路由路径是否一致、按钮能否跳转、返回能否回列表。做完这些,添加企业页的骨架就搭好了。
代码示例
1. 封装获取企业列表接口 src/apis/enterprise.js
javascript
import request from '@/utils/request'
/**
* 获取企业列表
* @param { page, pageSize, name } params
* @returns
*/
export function getEnterpriseListAPI(params) {
return request({
url: '/park/enterprise',
params
})
}关键行与易错点
url: '/park/enterprise'是接口地址,baseURL已在@/utils/request中统一配置,这里只写路径。- 必须写
params这个 key(GET 请求的参数键就是params)。若误写成data,参数根本不会拼到 URL 上,接口返回的仍是全量数据——排查时不要只看代码,要打开 Network 面板确认请求 URL 后面有没有?page=1&pageSize=10。 - 三个参数(
name、page、pageSize)都属于同一类请求参数,所以统一用对象params透传,函数内不再逐个解构。
2. 列表页组件:表格 + 分页 + 搜索 + 清空 src/views/park/enterprise/index.vue
vue
<template>
<div class="building-container">
<!-- 搜索区域 -->
<div class="search">
<el-input
v-model="params.name"
placeholder="请输入企业名称"
clearable
style="width: 240px"
@clear="clearSearch"
/>
<el-button type="primary" @click="search">查询</el-button>
<el-button type="primary" @click="$router.push('/exterpriseAdd')">添加企业</el-button>
</div>
<!-- 表格区域 -->
<div class="table">
<el-table style="width: 100%" :data="exterpriseList">
<el-table-column type="index" label="序号" />
<el-table-column label="企业名称" width="320" prop="name" />
<el-table-column label="联系人" prop="contact" />
<el-table-column label="联系电话" prop="contactNumber" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="mini" type="text">添加合同</el-button>
<el-button size="mini" type="text">查看</el-button>
<el-button size="mini" type="text">编辑</el-button>
<el-button size="mini" type="text">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页区域 -->
<el-pagination
layout="total, prev, pager, next"
:total="total"
:page-size="params.pageSize"
@current-change="pageChange"
/>
</div>
</template>
<script>
import { getEnterpriseListAPI } from '@/apis/enterprise'
export default {
name: 'Enterprise',
data() {
return {
exterpriseList: [], // 企业列表
total: 0, // 总条数,用 0 占位,不要写 null/空串
params: {
page: 1,
pageSize: 10,
name: '' // 搜索关键词,与输入框双向绑定
}
}
},
mounted() {
this.getExterpriseList()
},
methods: {
async getExterpriseList() {
const res = await getEnterpriseListAPI(this.params)
this.exterpriseList = res.data.rows // 列表数据
this.total = res.data.total // 总条数 -> 分页组件
},
// 点击页码切换
pageChange(page) {
this.params.page = page // 先改参数
this.getExterpriseList() // 再发请求(此时参数已是最新)
},
// 点击查询
search() {
this.params.page = 1 // 关键词变化必须回到第 1 页
this.getExterpriseList()
},
// 点击输入框小叉号
clearSearch() {
this.getExterpriseList() // name 已被 clearable 清空,重新拉全量
}
}
}
</script>关键行与易错点
:data="exterpriseList"给表格绑定数据源,prop="name"等指定当前列渲染的字段名,必须与接口返回对象里的字段一致(name/contact/contactNumber)。- 分页页数 = 总数 / 每页条数,所以
:total绑total、:page-size绑params.pageSize;total初始值给0而不是空值,否则分页组件计算异常。 pageChange里先修改this.params.page,再调用接口,顺序反了会请求到旧页码的数据。search()中把page重置为1:用户可能已经翻到第 5 页,搜索后结果很少,第 5 页会是空白。clearable只是让输入框出现小叉号,清空动作必须靠@clear事件通知父组件;其本质是子组件调用父组件的方法(子传父),不一定非要传数据出来。clearSearch中也可以顺手this.params.page = 1,避免清空后页码越界。
3. 新增企业页与路由绑定 src/views/park/enterprise/Add.vue + router/index.js
vue
<!-- src/views/park/enterprise/Add.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="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-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="businessLicenseUrl" />
</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>javascript
// router/index.js 中新增一条路由
{
path: '/exterpriseAdd',
component: () => import('@/views/park/enterprise/Add')
}html
<!-- 列表页中的跳转按钮(已包含在上面的列表页模板中) -->
<el-button type="primary" @click="$router.push('/exterpriseAdd')">添加企业</el-button>关键行与易错点
- 路由路径大小写敏感:
/exterpriseAdd中A必须大写,且要和$router.push('/exterpriseAdd')完全一致,写错就跳到空白页。 component使用() => import(...)懒加载,路径要与views/park/enterprise/Add.vue实际文件位置对应。- 这是无参数跳转,直接用行内
$router.push('/exterpriseAdd')即可;需要带参数时才改用query/params写法。 - 每个
el-form-item的prop要和v-model绑定的字段名一致,否则后续表单校验规则匹配不上(笔记模板里都写了prop="name",实际开发要按字段逐个改正)。 - 返回按钮用
$router.back()走浏览器历史,比硬编码路径更稳。
常见错误
现象:分页/搜索参数明明在
params里写了,接口返回的却仍是全量数据,Network 面板里请求 URL 干干净净没有?page=1&pageSize=2。 原因:request封装里把 GET 请求的参数键写成了data,而request内部只认params;或调用 API 函数时漏传实参。 修法:GET 一律用params(POST 才用data),并以 Network 面板实际请求为准判断参数是否真的发出,不要只盯着自己的代码看。现象:点击第 2 页,表格数据不刷新;或刷新了但请求里
page还是 1。 原因:只调了getExterpriseList()却没先改this.params.page;或两句顺序写反(先请求后改参数)。 修法:固定顺序——先更新this.params,再调用请求方法,让请求方法永远读到最新参数。现象:搜索后列表空白,但把
page手动改回 1 又有数据。 原因:用户此前已翻到第 N 页,搜索后仍带着page=N去查,而筛选结果不足 N 页。 修法:查询回调里第一句就是this.params.page = 1,再发请求。现象:点了输入框的小叉号,内容清空了,但列表还是上次搜索的结果。 原因:只加了
clearable属性拿到叉号 UI,没有绑定@clear事件,也没重新拉数据。 修法:@clear绑定回调,在回调里直接复用getExterpriseList()(此时params.name已被清空,天然就是查全部)。现象:
el-table某列整列空白,其他列正常。 原因:prop写的字段名与接口返回对象里的 key 不一致(拼写、驼峰/下划线差异)。 修法:打开 Network 展开data.rows[0],逐字段比对prop,以真实返回结构为准。现象:分页组件不显示或显示
共 undefined 条。 原因:total初始值写成''/ 未声明,或接口返回后忘记赋值。 修法:data里初始化为数字0,请求成功后this.total = res.data.total。现象:点"添加企业"白屏 / 404。 原因:
router里path与目录名大小写不一致(enterprise/Enterprise、Add/add)。 修法:路径大小写敏感,逐一核对views/park/enterprise/Add.vue的真实路径后再写path。
练习
参数自检:故意把
getEnterpriseListAPI里的params改成data,运行后在 Network 面板找到证据说明参数未发送,再改回来并截图对比两次请求 URL 的差异。分页健壮性:把
pageSize改为 2,翻到最后一页,再在搜索框输入关键词查询。观察是否出现空白页;若不修改代码会怎样,修改后请求参数应变成什么?清空复原:不使用
@clear,改用watch监听params.name,当值变为空字符串时自动重新拉取全量数据。写出实现并说明与@clear方案各自的取舍。事件语义:给分页组件和输入框分别画出"子组件 → 父组件"的调用链(谁触发、调用了父组件的哪个方法、父组件方法做了什么),并用一句话概括两者的共同本质。
新增页闭环:在
Add.vue里补齐"返回"按钮与"确定"按钮的交互骨架:返回用$router.back(),确定时先做表单校验占位再打印addForm,说明为什么此时还不需要接提交接口。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 企业管理-功能演示和模版创建 | 03:51 |
| 2 | 企业管理-渲染基础table列表 | 06:52 |
| 3 | 企业管理-分页逻辑实现 | 04:11 |
| 4 | 补充-参数的监测以实际network为主 | 03:11 |
| 5 | 企业管理-搜索功能实现 | 04:32 |
| 6 | 企业管理-清除搜索功能 | 07:45 |
| 7 | 新增企业-路由绑定跳转 | 03:32 |
小结
- 列表页的三件套(数据、分页、搜索)本质是同一个动作的三种触发方式:改参数 → 发请求 → 覆盖列表,任何新需求都往这个模型里套。
- 参数是否生效的唯一裁判是 Network 面板,代码看起来"明明传了"不构成证据;排查顺序永远是先看请求,再看代码。
- 先改参数、后发请求 是这类页面的铁律,顺序写反就会出现"点了没反应""数据慢一拍"的诡异现象。
- 搜索、清空、切页这类操作都会改变查询条件,凡是条件变化就要考虑
page是否该重置为 1。 prop与total这类"字符串/数字绑定"错误不会报错,只会静默显示空白,所以模板写完必须肉眼对照接口返回结构。- 子传父的实质不是"传数据",而是子组件触发父组件的方法;只要父组件需要因子的内部动作而做点什么,就应该绑定自定义事件。
- 路由
path大小写敏感,新增页面时"能跳过去"应当作为独立验收项,而不是等表单写完才发现打不开。 - 静态模板先落地、再逐块接数据,是这个模块一路走下来的节奏——结构先对,逻辑后补,出错时排查面会小很多。