Skip to content

企业管理(一) ​

本页对应 企业管理,共 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。

练习 ​

  1. 参数自检:故意把 getEnterpriseListAPI 里的 params 改成 data,运行后在 Network 面板找到证据说明参数未发送,再改回来并截图对比两次请求 URL 的差异。

  2. 分页健壮性:把 pageSize 改为 2,翻到最后一页,再在搜索框输入关键词查询。观察是否出现空白页;若不修改代码会怎样,修改后请求参数应变成什么?

  3. 清空复原:不使用 @clear,改用 watch 监听 params.name,当值变为空字符串时自动重新拉取全量数据。写出实现并说明与 @clear 方案各自的取舍。

  4. 事件语义:给分页组件和输入框分别画出"子组件 → 父组件"的调用链(谁触发、调用了父组件的哪个方法、父组件方法做了什么),并用一句话概括两者的共同本质。

  5. 新增页闭环:在 Add.vue 里补齐"返回"按钮与"确定"按钮的交互骨架:返回用 $router.back(),确定时先做表单校验占位再打印 addForm,说明为什么此时还不需要接提交接口。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
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 大小写敏感,新增页面时"能跳过去"应当作为独立验收项,而不是等表单写完才发现打不开。
  • 静态模板先落地、再逐块接数据,是这个模块一路走下来的节奏——结构先对,逻辑后补,出错时排查面会小很多。