Skip to content

编辑月卡与删除(一) ​

本页对应 编辑月卡与删除,共 7 段视频,约 1 小时 19 分。

学习目标 ​

  • 能够依据接口文档封装列表查询接口,组织分页参数,并在页码切换时重新请求数据、渲染表格。
  • 能够查阅组件库文档,完成日期范围选择器、下拉框和表单规则绑定,实现缴费信息表单的基础校验。
  • 能够实现两个表单的串行校验,并在两个表单均校验通过后触发提交逻辑。
  • 能够收集表单数据,按接口要求完成字段合并、删除和日期格式化等二次处理,并调用新增接口提交。
  • 能够处理提交成功后的提示与返回列表逻辑,并使用 query 传参携带 id 跳转编辑页且完成参数读取验证。

知识点 ​

实现思路总览 ​

实现思路流程图

这一节的核心不是背某个组件的写法,而是建立后台管理系统中“列表—分页—新增/编辑—校验—提交”的通用实现思路。转写稿里反复强调:列表渲染、分页、增删改查都有标准步骤,先按流程跑通,再根据接口文档和组件文档补细节。下面按知识点拆开。

列表渲染与分页 ​

为什么需要:后台系统大量页面都是“请求列表→渲染表格→分页切换”。如果每次凭感觉写,容易漏掉接口封装、响应式数据或分页联动。

怎么想:先按标准步骤想:封装列表接口→准备响应式列表数据→封装请求方法→在生命周期中调用→把数据绑定到表格组件。分页则围绕公式“页数 = 总条数 / 每页条数”,组件负责展示页码,业务负责拿当前页重新请求。

怎么做:接口封装重点看 URL、method、参数位置;简单请求放 params,复杂请求放 data。列表数据初始值按后端返回决定,通常是数组。请求方法里做“调接口 + 赋值”,必要时在调用前处理参数、赋值前处理数据。生命周期里 created 和 mounted 在当前网络请求场景下差别很小,不必过度纠结。表格绑定要看组件文档要求,例如表格常见绑定 data,但属性名不是死记的。分页组件通常传 total、pageSize,监听页码变化事件,拿到当前页后修改请求参数里的页码,再重新调用获取列表的方法。

易错点:URL 错会直接 404;method 错后面不用查;参数拼写、类型、位置最容易错。分页事件本质是子传父,组件内部已经 $emit,我们只需绑定回调接收页码。表格数据要求数组时传对象会报类型错误。

状态适配 ​

为什么需要:后端常返回状态码,如 0/1,而页面要显示“男/女”“启用/禁用”等中文,不能直接把码值渲染给用户。

怎么想:先判断可转化状态数量。只有两三个,用三元表达式最直接;状态多,用映射表做一一对应。

怎么做:少量状态可写 status === 0 ? '女' : '男'。状态多时先写转换函数,内部用对象保存码值与中文的对应关系,再通过 obj[status] 取值返回。转写稿中明确说对象本身具备一一对应结构,适合做映射。

易错点:不要把映射关系散落在模板里;状态码类型要一致,字符串 '0' 和数字 0 可能取不到;映射函数要保证兜底,避免未知码值显示 undefined。

新增、表单校验与组件库文档 ​

为什么需要:新增和编辑是成对出现的通用功能。表单项少可弹框,表单项多通常跳新路由;只要有表单,就通常需要校验。

怎么想:先看表单项复杂度决定交互形式;再看校验分“单项校验”和“统一校验”。单项校验覆盖必填、格式等常规规则,统一校验在提交前整体调用实例方法。遇到没见过的组件,不要先抠所有参数,而是先跑最基础 demo,再找最贴近业务的示例,最后查属性、事件、插槽和实例方法。

怎么做:表单绑定 model 和 rules,各字段用 v-model 收集数据。提交前打开 Vue Devtools 检查双向绑定是否收集成功。统一校验调用表单实例的 validate 方法。自定义校验函数参数位置不能错,通常关注 value 和 callback:通过与否都要调用 callback。下拉框要准备对象数组,绑定 label、value 等字段;日期范围选择器要用对应组件,并通过 valueFormat 把绑定值格式化成接口需要的字符串。

易错点:不要用中文做新增/编辑判断;自定义校验参数位置不能乱;日期范围初始常是数组或 Date 对象,不处理会与接口要求的字符串类型不一致;组件属性名要看文档,不要死记。

表单数据二次处理与提交 ​

为什么需要:组件收集到的数据经常和接口要求不一致,可能字段名多了、少了,或类型不对。转写稿中调试时发现表单里有 payTime,但接口要的是 startDate 和 endDate,且时间还是对象类型,所以必须二次处理。

怎么想:先打开接口文档确认字段名、类型、是否必填;再打开 Devtools 看当前收集数据;逐项对照,能直接提交才直接提交,否则先合并、改名、删字段、转类型。

怎么做:可先展开两个表单对象合并成新对象,再补接口需要的字段,例如从日期范围数组中取开始、结束时间;不需要的字段用 delete 删除;时间通过 valueFormat="YYYY-MM-DD" 转成字符串。提交前打开 Network 面板看请求参数,确认不多不少、名称和类型匹配。

易错点:字段多一个或少一个都可能出错;时间对象不是字符串;车牌号等正则前后端要统一,转写稿中就出现前端正则与后端要求位数不一致导致校验失败。提交成功后通常提示用户、重新拉列表、重置表单或路由返回。

编辑回填与路由传参 ​

为什么需要:编辑和新增常共用同一页面,但编辑要先携带 id 跳转,再根据 id 取详情回填;提交时也要根据是否有 id 判断调更新接口还是新增接口。

怎么想:列表行内按钮先拿当前行 id,跳转时用路由传参;目标页取参后请求详情并回填。传参方式和取参方式必须对应。

怎么做:表格作用域插槽中通过 scope.row 拿当前行对象,再取 scope.row.id。跳转可用:

js
this.$router.push({
  path: '/card/add',
  query: { id }
})

目标页用 this.$route.query.id 取参。若用 params,路由配置和取参方式也要对应。

易错点:params 与 query 混用;跳转路径写错;用中文判断编辑/新增;忘记用 id 请求详情导致回填失败。编辑流程本质是:点编辑→带 id 跳转→取详情回填→用户修改→按 id 判断提交更新接口。

接口封装:先把请求三要素对齐 ​

为什么需要:列表页的数据不是写死在页面里的,而是从后端接口取回来的。若每个组件里都直接写请求地址和参数,后续接口地址、参数名一改,就要到处改。把请求封装到 apis/card 中,组件只关心“调哪个方法、传什么参数”,职责更清晰。

怎么想:转写稿反复强调,封装接口先看三要素:URL、method、参数。当前笔记里的列表接口是 /parking/card/list,参数是 page、pageSize,通过 params 传递。先按接口文档把 URL、请求方法、参数位置对齐,再写组件逻辑。若 URL 错了会 404,后面不用查;若 method 错了,接口也通不了;参数最容易错,要保证拼写、类型、传递位置与文档一致。

怎么做:在 @/apis/card.js 中按需导出 getCardListAPI,内部调用统一的 request:

javascript
import request from '@/utils/request'

/**
 * 获取楼宇列表
 * @param { page, pageSize} params
 * @returns
 */
export function getCardListAPI(params) {
  return request({
    url: '/parking/card/list',
    params
  })
}

易错点:URL 少写或多写斜杠;把 params 写成 data;参数名写成 pageSize 之外的形式;方法不是 GET 却仍用 params。调试时先打开 Network 面板看请求 URL、请求方法和查询参数,再判断是接口问题还是组件问题。

列表数据:用响应式数据和 mounted 跑通流程 ​

为什么需要:当前开发模式是数据驱动视图,表格渲染依赖响应式数据。没有把接口返回的数据放进 data(),表格就没有数据源。

怎么想:转写稿把列表实现拆成几步:封装接口、准备响应式列表数据、封装请求方法、在生命周期里调用、把数据绑定到组件。列表数据一般是数组,因为后端返回的是多条记录;初始值给 [],请求成功后再赋值。

怎么做:在组件中导入接口,在 data() 中准备请求参数 params 和 cardList,在 mounted() 中调用 getCardList()。请求方法里 await 接口,按笔记中的返回结构取 res.data.rows:

vue
<script>
  import { getCardListAPI } from '@/apis/card'
  export default {
    data() {
      return {
        // 请求参数
        params: {
          page: 1,
          pageSize: 10
        },
        // 月卡列表
        cardList: []
      }
    },
    mounted() {
      this.getCardList()
    },
    methods: {
      async getCardList() {
        const res = await getCardListAPI(this.params)
        this.cardList = res.data.rows
      }
    }
  }
</script

易错点:把 cardList 初始值写成对象,表格却要求数组;请求方法忘记 async/await 导致拿不到返回值;返回结构写错,比如直接写 res.rows;在 created 和 mounted 之间纠结。转写稿说明,发请求场景下两者本质差别很小,网络返回时间远大于生命周期间隔,按项目习惯选择即可。调试时先看 Network 是否成功,再看 res.data.rows 是否有值。

表格渲染:data 绑定与列 prop 对齐 ​

为什么需要:接口数据只有绑定到表格组件上,才能显示成用户看到的列表。列字段必须与后端返回字段一一对应。

怎么想:表格组件通常通过 data 属性接收数组;每一列通过 prop 指定从行对象中取哪个字段。笔记里车主名称、联系方式、车牌号码等分别对应 personName、phoneNumber、carNumber 等,不能凭中文猜字段。

怎么做:使用 el-table,把 :data="cardList" 绑定到响应式数组;序号列用 type="index";普通列写 label 和 prop:

vue
<template>
  <el-table style="width: 100%" :data="cardList">
    <el-table-column type="index" label="序号" />
    <el-table-column label="车主名称" prop="personName" />
    <el-table-column label="联系方式" prop="phoneNumber" />
    <el-table-column label="车牌号码" prop="carNumber" />
    <el-table-column label="车辆品牌" prop="carBrand" />
    <el-table-column label="剩余有效天数" prop="totalEffectiveDate" />
    <el-table-column label="操作" fixed="right" width="180">
      <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>
</template>

易错点:prop 拼写与接口字段不一致,页面显示空白;data 传成对象,组件报类型不符合;把中文 label 当成字段名。调试时先看 cardList 展开后的字段名,再逐列核对 prop。

操作列:插槽拿当前行,为后续功能留入口 ​

为什么需要:列表不仅要展示,还要承载续费、查看、编辑、删除等操作。操作按钮需要知道“当前点的是哪一行”,否则无法携带该行 ID 做后续跳转或请求。

怎么想:操作列使用作用域插槽,笔记中 #default="scope" 的 scope.row 就是当前行对象。后续做编辑时,可从 scope.row.id 取 ID;做删除时也可从当前行取唯一标识。当前笔记只放了按钮,没有绑定事件,这是为下一步功能留入口。

怎么做:在操作列中写 template #default="scope",按钮先按笔记保留。后续扩展时在按钮上绑定点击事件,把 scope.row.id 传给方法。若接分页,点击页码后应修改 params.page,再重新调用 getCardList(),让表格按新页码重新渲染。

易错点:在插槽外直接写 scope.row 取不到;误以为按钮会自动带当前行数据;后续接分页时只改页码不重新请求。调试时可在方法里先 console.log(scope.row),确认当前行对象和 ID 是否存在,再继续写跳转或请求逻辑。

先明确分页要解决什么问题 ​

列表页数据一多,不能一次性全铺在页面上,所以要分页。分页的核心不是“把数据切成几份”这么简单,而是:前端把总条数、每页条数交给分页组件,分页组件算出页数并展示;用户点第几页,前端拿到当前页码,再带着新的 page 去请求接口,最后把新数据重新渲染到表格里。

转写稿里反复强调一个公式:

页数 = 总条数 / 每页条数

这里的“页数”就是分页组件帮你算出来的。组件只负责根据你传的 total 和 page-size 把页码分好,真正“点哪页请求哪页数据”的逻辑,仍然要我们在父组件里写。

笔记中的基础分页代码 ​

先保留本节笔记里的代码。它完成了两件基础事:分页组件接收 page-size 和 total;列表请求拿到 rows 和 total 后赋值。

vue
<el-pagination
  layout="total, prev, pager, next"
  :page-size="params.pageSize"
  :total="total"
/>

<script>
import { getCardListAPI } from '@/apis/card'
export default {
  data() {
    return {
      // 请求参数
      params: {
        page: 1,
        pageSize: 2
      },
      total: 0,
      // 月卡列表
      cardList: []
    }
  },
  mounted() {
    this.getCardList()
  },
  methods: {
    async getCardList() {
      const res = await getCardListAPI(this.params)
      this.cardList = res.data.rows
      this.total = res.data.total
    }
  }
}
</script>

为什么需要这样写?
因为分页组件必须知道“总共有多少条”和“每页显示多少条”,否则它算不出页数。total 不能拿当前列表长度凑,必须用接口返回的总条数;pageSize 也要和请求参数、接口要求保持一致。

怎么想?
把 params 当成请求参数对象,page 表示当前页,pageSize 表示每页条数。请求回来后,res.data.rows 给表格,res.data.total 给分页组件。

易错点:
total 要求是数字类型;pageSize 不要和接口文档不一致;page 初始值一般是 1,不要从 0 开始;不要把 total 写成 cardList.length,那只是当前页条数。

点击页码后要拿到当前页并重新请求 ​

转写稿里讲得很关键:页码数据在分页组件内部,父组件想拿它,本质上是一个“子传父”。分页组件在用户点击页码时会触发事件,并把当前页码传出来。我们在父组件绑定回调,拿到页码后修改 params.page,再重新调用 getCardList。

在 Element UI 里,这个“当前页变化”事件通常写成 @current-change。在笔记代码基础上补交互,可以写成下面这样:

vue
<el-pagination
  layout="total, prev, pager, next"
  :page-size="params.pageSize"
  :total="total"
  @current-change="handleCurrentChange"
/>
js
methods: {
  async getCardList() {
    const res = await getCardListAPI(this.params)
    this.cardList = res.data.rows
    this.total = res.data.total
  },
  handleCurrentChange(page) {
    this.params.page = page
    this.getCardList()
  }
}

为什么需要?
只展示页码还不够。用户点第 2 页时,如果不改 params.page 也不重新请求,表格仍然停在第一页数据。

怎么想?
回调参数 page 就是当前点击的页码。先改请求参数,再发请求。顺序不要反:如果先请求后改参数,拿到的还是旧页码。

怎么做?
在 methods 中声明回调,把 page 赋给 this.params.page,然后调用 this.getCardList()。请求成功后,cardList 和 total 会重新赋值,页面自然刷新。

易错点:
事件名要看组件库文档,不要死记;回调参数不要漏接;修改页码后不要忘记重新请求;如果接口要求 pageSize 字段名不同,要按接口文档改。调试时打开 Network 面板,点第二页看请求参数里的 page 是否变成 2,再看响应里的 total 是否正常赋值。这样就能确认“分页公式”和“点击请求”这条链路是否真正跑通。

先明确“点击切换”要解决什么问题 ​

为什么需要:分页组件只负责把总条数按每页条数“分”成若干页,并展示页码;它不会替我们向后端要某一页的数据。用户点击第 2 页、第 3 页时,列表必须换成对应页的数据,所以需要把“当前点击第几页”变成请求参数,再重新请求列表。

怎么想:把这件事拆成两步,正好对应笔记里的实现步骤:第一步,拿到当前点击了第几页;第二步,使用当前点击的页数去和后端要数据。不要一上来就改 total,total 是总条数,不是当前页。

怎么做:分页组件继续绑定 page-size 和 total,点击页码时通过 @current-change 触发自己的回调。核心代码就是笔记中的结构:

vue
<el-pagination
  layout="total, prev, pager, next"
  :page-size="params.pageSize"
  :total="total"
  @current-change="pageChange"
/>

易错点:total 要传数字类型,表示总条数;page-size 是每页条数;事件名不要写错。分页组件内部知道当前页,但父组件要用这个页数,所以本质上是“子组件把内部数据通过事件传给父组件”。

从分页组件拿到当前点击页 ​

为什么需要:请求参数里的 page 必须是最新点击的页码。如果拿不到页码,就无法知道该请求第几页。

怎么想:转写稿里讲得很清楚:页码本身存在分页组件内部,父组件想拿子组件内部的数据,就要靠子组件抛出事件。分页组件已经帮我们触发了一半,我们只需要在父组件把事件绑上,并接收它传下来的页码。

怎么做:在 methods 中声明 pageChange(page),这个 page 就是当前点击的页码。可以先打印或观察它,确认拿到的是 1、2、3 这类页码,而不是总条数。

易错点:不要把回调参数理解成 total;不要自己猜页码;事件绑定后要确认方法名一致。调试时可以临时打印 page,确认点击不同页码时参数随之变化。

用当前页改写请求参数 ​

为什么需要:列表请求方法 getCardList 使用的是 this.params。如果只拿到 page 却不写回请求参数,请求仍然会按旧页码发出去,列表不会切换。

怎么想:把“当前点击页”同步到“请求参数页”。转写稿中的说法是:先修改请求参数,把我们传下来的页码赋值给请求参数里的页数,然后重新发送获取列表的方法。

怎么做:在 pageChange 中先赋值,再调用列表方法:

vue
methods: {
  async getCardList() {
    const res = await getCardListAPI(this.params)
    this.cardList = res.data.rows
    this.total = res.data.total
  },
  pageChange(page) {
    // 把点击的页数赋值给请求参数页数
    this.params.page = page
    // 使用最新的请求参数获取列表数据
    this.getCardList()
  }
}

易错点:赋值和请求的先后顺序不能反;参数名要和接口文档、params 中已有字段保持一致;不要直接改 total 来代替 page。

重新请求列表并完成重新渲染 ​

为什么需要:页码变了只是参数变了,页面上的表格数据还没有变。必须重新调用列表接口,拿到新页的 rows 和 total,再赋值给响应式数据,视图才会更新。

怎么想:getCardList 里已经封装了“请求接口 + 给 cardList 和 total 赋值”。所以点击切换时不需要重写一套请求逻辑,只要复用这个方法。

怎么做:pageChange 最后调用 this.getCardList()。接口返回后,this.cardList = res.data.rows 负责当前页列表,this.total = res.data.total 负责总条数。调试时打开 Network 面板,点击不同页码,观察请求参数里的 page 是否变化,以及返回的 rows 是否对应新页。

易错点:如果页码变了但列表没变,先看请求有没有发出、params.page 有没有变;如果请求发了但页面不刷新,检查赋值字段是否写错;如果总页数异常,检查 total 和 pageSize 是否正确。

联调时的检查顺序 ​

为什么需要:点击切换涉及组件事件、请求参数、接口返回、响应式赋值四段,任何一段出错都表现为“点了没反应”。

怎么想:按顺序排查:先确认 @current-change 是否触发,再确认 pageChange 收到的 page 是否正确,再确认 this.params.page 是否被改写,最后看请求和返回。

怎么做:可以临时在 pageChange 中打印 page,在请求前打印 this.params;打开浏览器调试面板看 Network 请求参数;对照接口文档检查字段名和类型。

易错点:不要调完接口才打开 Network;不要用中文或随意字段做判断;页码、每页条数、总条数三者要分清。这样实现后,点击分页就会用最新页码请求数据,并重新渲染列表。

先明确本节目标:把查询条件收进 params ​

为什么需要:搜索区域最终要发列表请求,用户输入的车牌、姓名、状态必须被收集成请求参数。转写稿里反复强调接口三要素:URL、method、请求参数;其中参数最容易错,既要求拼写和接口文档一致,也要求类型匹配、传递位置正确。这里的 params 就是查询请求参数的容器。

怎么想:不要给每个输入框单独建变量,而是用一个响应式对象统一收。分页参数 page、pageSize 也放在同一个对象里,查询字段 carNumber、personName、cardStatus 也放进去。初始值里 page: 1、pageSize: 5 是数字,查询字段先给 null,表示“暂不筛选”。

怎么做:先保留笔记中的模板与数据代码:

vue
<!-- 搜索区域 -->
<div class="search-container">
  <span class="search-label">车牌号码:</span>
  <el-input v-model="params.carNumber" clearable placeholder="请输入内容" class="search-main" />
  <span class="search-label">车主姓名:</span>
  <el-input v-model="params.personName" clearable placeholder="请输入内容" class="search-main" />
  <span class="search-label">状态:</span>
  <el-select v-model="params.cardStatus">
    <el-option
      v-for="item in cardStatusList"
      :key="item.id"
      :value="item.id"
      :label="item.name"
    />
  </el-select>
  <el-button type="primary" class="search-btn" @click="doSearch">查询</el-button>
</div
js
data() {
    return {
      // 请求参数
      params: {
        page: 1,
        pageSize: 5,
        carNumber: null,
        personName: null,
        cardStatus: null
      },
      // 月卡状态
      cardStatusList: [
        {
          id: null,
          name: '全部'
        },
        {
          id: 0,
          name: '可用'
        },
        {
          id: 1,
          name: '已过期'
        }
      ]
    }
  }

易错点:模板末尾笔记里是截断的 </div,实际代码要闭合;v-model 路径必须写全 params.carNumber,不能只写 carNumber;字段名要和接口文档完全对应,不能多也不能少,类型也要匹配。转写稿里“parents”应纠正为 params,“mes”应纠正为 methods,“交山改查”即增删改查。

模板绑定:输入框和下拉框如何落到 params ​

为什么需要:搜索条件要随用户操作实时进入 params,点击查询时才有数据可提交。v-model 的作用就是把输入框、下拉框的值同步到 params 对应字段。

怎么想:车牌号码是文本输入,绑 params.carNumber;车主姓名是文本输入,绑 params.personName;状态是下拉选择,绑 params.cardStatus。clearable 让输入框可清空,清空后要观察 params 里的实际值,按接口约定决定是否转成 null。

怎么做:按上面模板写即可。调试时按转写稿的要求打开 DevTools,观察输入车牌、姓名、切换状态时 params 是否同步变化;如果数据没收集到,提交就是“白提”。

易错点:不要把 v-model 绑到 item.name 或显示文字上;下拉框提交的是 value,不是 label;清空输入后要检查字段值,避免接口收到不符合约定的空值。

状态下拉:id 是提交值,name 是显示值 ​

为什么需要:页面要显示“全部/可用/已过期”的中文,但接口通常要状态码。转写稿里讲状态适配:后端往往返回状态码,不一定直接返回中文,需要前端做显示转换。这里用 cardStatusList 做选项映射。

怎么想:id 作为提交值,name 作为显示文字。null 表示全部,0 表示可用,1 表示已过期。v-for 渲染选项时,:value="item.id" 决定选中后写入 params.cardStatus 的值,:label="item.name" 决定用户看到的中文。

怎么做:el-option 上保留 :key="item.id"、:value="item.id"、:label="item.name"。这样选中“可用”时,params.cardStatus 是 0;选中“全部”时是 null。

易错点:0 是有效状态值,不能用 if (!params.cardStatus) 判断“没选”,否则会把“可用”误判为空;key 最好唯一,笔记中“全部”的 id 为 null,只有一个 null 时可用,但扩展选项时要注意唯一性;value 类型要和接口一致,接口要数字就不要传字符串。

查询按钮与 doSearch:收集之后交给请求 ​

为什么需要:只把数据收进 params 还不够,点击“查询”要触发列表请求,把当前查询条件带给后端。转写稿里分页部分也强调:点击页码时先修改请求参数里的 page,再重新调用获取数据的方法。

怎么想:doSearch 应该负责带着 params 调用列表接口;page、pageSize 已经在 params 里,请求时一起带上。查询条件变化后是否把 page 重置为 1,要按业务和接口约定处理;本节笔记没有展开 doSearch 实现,不能凭空补逻辑,但要知道按钮只绑事件不等于查询完成。

怎么做:模板中保留 @click="doSearch",并在 methods 中声明 doSearch。具体请求方法按项目已有列表接口调用。

易错点:只写按钮不写方法;请求参数拼写、类型、位置错;查询后页码仍停在旧页导致空页,需按业务确认是否重置;接口字段要“不多不少、名称完全对应、类型完全匹配”。

调试与收尾 ​

按转写稿的调试习惯:先打开 DevTools,再操作搜索区域,检查 params.carNumber、params.personName、params.cardStatus 是否随输入和选择变化;再检查 page、pageSize 是否为数字;最后对照接口文档核对字段名和类型。转写稿里的“dake”应理解为 data,“吊起来”是“调起来”,“调度接口”是“调用接口”。本节完成的是查询字段收集,下一步才是点击查询后带 params 请求列表。

查询按钮为什么要单独封装 doSearch ​

为什么需要:查询按钮不是简单地“再请求一次列表”。用户可能已经在第 3 页,此时修改查询条件再点查询,如果直接调用 getCardList,请求仍会带着 page=3,新条件的结果可能只有 1 页,于是出现空列表或页码越界。转写稿里反复强调:列表请求要关注 URL、method、参数三要素,其中参数最容易错,字段名、类型、位置都要和接口文档一致。查询动作要先处理分页参数,再发请求。

怎么想:把“查询”理解成一次新的检索:查询条件变化后,结果集变了,原来的页码就失效了,所以页码要回到第 1 页。doSearch 的职责就是做查询前的参数整理,然后复用已经封装好的列表请求方法。

怎么做:在 methods 中写 doSearch,先 this.params.page = 1,再 this.getCardList()。模板按钮用 @click="doSearch" 绑定。

vue
doSearch() {
  // 调用接口之前把页数参数重置为1
  this.params.page = 1
  this.getCardList()
}

<el-button type="primary" class="search-btn" @click="doSearch">查询</el-button>

易错点:不要写成先请求再重置;不要直接把按钮绑成 getCardList 而漏掉重置;params.page 的字段名要和接口文档一致;getCardList 内部要真正把 params 传给接口,否则重置了也没用。转写稿里“parents”“dake”是口误/同音,实际应关注 params 与 data 的传参位置,别把参数放错地方。

getCardList 为什么要封装成独立方法 ​

为什么需要:初始化列表、点击分页、点击查询都要拉取列表。如果每处都复制一遍请求代码,参数一改就容易漏改。转写稿里讲列表渲染标准步骤时提到:先封装接口,再准备响应式列表数据,然后在 methods 中封装请求方法,最后在生命周期里调用并绑定到组件上。

怎么想:getCardList 负责“调用接口 + 给列表数据赋值”,doSearch 只负责“查询前的页码重置”。职责分开,查询和分页都能复用同一个请求方法。

怎么做:在 methods 里维护 getCardList,内部使用 this.params 调接口,把返回的列表赋给响应式数据;doSearch 只改 page 后调用它。调试时先打开 Network 面板,点查询看请求参数里的 page 是否为 1。

易错点:列表数据初始值类型要按后端返回准备,通常是数组;接口返回后赋值时注意字段名;不要把 page 改成字符串而接口要求数字;转写稿里“mes”应纠正为 methods,“cre”“monkey”分别对应 created、mounted,当前场景下两者都能发起请求,不必纠结先后。

查询与分页交互如何配合 ​

为什么需要:分页组件只负责把页数分出来,真正请求哪一页的数据仍要前端拿到当前页码后改请求参数。转写稿里说,分页要传 total 和 page-size,点击页码时通过子传父事件拿到当前页,再修改 params.page 并重新调用列表方法。

怎么想:分页点击是“同一查询条件下翻页”,所以只改 page;查询按钮是“查询条件变化后重新检索”,所以要把 page 重置为 1。两者都最终调用 getCardList,但前置处理不同。

怎么做:分页事件里拿到页码后执行类似 this.params.page = 当前页码; this.getCardList();查询按钮则执行笔记里的 doSearch。这样点第 2 页请求第 2 页,点查询请求第 1 页。

易错点:分页事件里不要再把 page 重置为 1,否则永远翻不了页;查询时也不要忘记重置;取参数和传参数要对应,转写稿提醒“传参方式和取参方式要对应”,否则页码拿不到。

调试与常见错误 ​

为什么需要:接口调用错时,先看 URL、method、参数三要素。转写稿提到 404 多半是 URL 错,method 错也不用再查参数,参数则最容易因拼写、类型、位置出错。

怎么想:点查询前先看 this.params.page,再在 Network 里看请求载荷;如果列表不更新,检查 getCardList 是否被调用、返回数据是否赋值、表格绑定字段是否正确。

怎么做:在 doSearch 里可临时 console.log(this.params.page);打开 Network 确认 page=1;确认 getCardList 内部使用的是最新 params。

易错点:忘记 this;把 page 写成其他字段;请求参数放在 data 而不是 params(或反之);只重置页码却没有重新请求;查询按钮事件写成未定义方法。按笔记代码,最小正确闭环就是:按钮触发 doSearch,doSearch 重置 page 后调用 getCardList。

代码示例 ​

示例一:接口封装 + 月卡列表渲染 + 分页切换 + 条件搜索 ​

js
// @/apis/card.js —— 1. 接口封装(重点看 url / method / 参数)
import request from '@/utils/request'

/**
 * 获取月卡列表
 * @param { page, pageSize, carNumber, personName, cardStatus } params
 * @returns
 */
export function getCardListAPI(params) {
  return request({
    url: '/parking/card/list',
    params
  })
}
vue
<!-- @/views/card/index.vue -->
<template>
  <div class="card-container">
    <!-- 搜索区域 -->
    <div class="search-container">
      <span class="search-label">车牌号码:</span>
      <el-input v-model="params.carNumber" clearable placeholder="请输入内容" class="search-main" />
      <span class="search-label">车主姓名:</span>
      <el-input v-model="params.personName" clearable placeholder="请输入内容" class="search-main" />
      <span class="search-label">状态:</span>
      <el-select v-model="params.cardStatus">
        <el-option
          v-for="item in cardStatusList"
          :key="item.id"
          :value="item.id"
          :label="item.name"
        />
      </el-select>
      <el-button type="primary" class="search-btn" @click="doSearch">查询</el-button>
    </div>

    <!-- 表格区域 -->
    <el-table style="width: 100%" :data="cardList">
      <el-table-column type="index" label="序号" />
      <el-table-column label="车主名称" prop="personName" />
      <el-table-column label="联系方式" prop="phoneNumber" />
      <el-table-column label="车牌号码" prop="carNumber" />
      <el-table-column label="车辆品牌" prop="carBrand" />
      <el-table-column label="剩余有效天数" prop="totalEffectiveDate" />
      <el-table-column label="操作" fixed="right" width="180">
        <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" @click="toEdit(scope.row.id)">编辑</el-button>
          <el-button size="mini" type="text">删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分页区域 -->
    <el-pagination
      layout="total, prev, pager, next"
      :page-size="params.pageSize"
      :total="total"
      @current-change="pageChange"
    />
  </div>
</template>

<script>
import { getCardListAPI } from '@/apis/card'

export default {
  data() {
    return {
      // 请求参数
      params: {
        page: 1,
        pageSize: 5,
        carNumber: null,
        personName: null,
        cardStatus: null
      },
      total: 0,
      // 月卡列表
      cardList: [],
      // 月卡状态
      cardStatusList: [
        { id: null, name: '全部' },
        { id: 0, name: '可用' },
        { id: 1, name: '已过期' }
      ]
    }
  },
  mounted() {
    this.getCardList()
  },
  methods: {
    async getCardList() {
      const res = await getCardListAPI(this.params)
      this.cardList = res.data.rows
      this.total = res.data.total
    },
    // 分页点击:拿到当前页 -> 赋值给请求参数 -> 重新请求
    pageChange(page) {
      this.params.page = page
      this.getCardList()
    },
    // 查询:重置页码为 1 再请求
    doSearch() {
      this.params.page = 1
      this.getCardList()
    },
    // 编辑:携带 id 跳转到同一个表单页
    toEdit(id) {
      this.$router.push({ path: '/card/add', query: { id } })
    }
  }
}
</script>

<style scoped>
.search-container {
  display: flex;
  align-items: center;
  padding: 10px 0;
}
.search-label {
  margin-right: 8px;
  white-space: nowrap;
}
.search-main {
  width: 180px;
  margin-right: 16px;
}
</style>

关键行与易错点

  • getCardListAPI(params) 里的 params 是 GET 的 query 参数,必须和接口文档的字段名、类型、位置完全一致,写错位置(放到 data 里)就是 404 / 400。
  • :data="cardList" 的初值必须是数组,表格组件只接受数组;prop 要和后端返回的字段名一一对应,写错就是空白列。
  • 分页公式:页数 = total / pageSize,所以 total 必须从 res.data.total 取,只给 page-size 不给 total 分页不会出现页码。
  • @current-change="pageChange" 是“子传父”:子组件内部 $emit 已写好,我们只负责绑事件 + 接收参数;参数就是当前点击的页码。
  • 搜索时最容易漏 this.params.page = 1,否则你停在第 5 页去搜索,结果为空会误以为接口坏了。
  • :key="item.id" 里 null 也是合法 key,但同一列表里 key 必须唯一,不要重复用 name。

示例二:双表单串行校验 + 参数二次处理 + 提交 ​

vue
<!-- @/views/card/add.vue -->
<template>
  <div class="card-add">
    <!-- 第一个表单:车辆信息 -->
    <el-form ref="carInfoForm" :model="ruleForm" :rules="carRules" label-width="100px">
      <el-form-item label="车主姓名" prop="personName">
        <el-input v-model="ruleForm.personName" />
      </el-form-item>
      <el-form-item label="车牌号码" prop="carNumber">
        <el-input v-model="ruleForm.carNumber" />
      </el-form-item>
    </el-form>

    <!-- 第二个表单:缴费信息 -->
    <el-form ref="payInfoForm" :model="payForm" :rules="payRules" label-width="100px">
      <el-form-item label="有效日期" prop="payTime">
        <el-date-picker
          v-model="payForm.payTime"
          type="daterange"
          value-format="YYYY-MM-DD"
          start-placeholder="开始日期"
          end-placeholder="结束日期"
        />
      </el-form-item>
      <el-form-item label="支付金额" prop="paymentAmount">
        <el-input v-model="payForm.paymentAmount" />
      </el-form-item>
      <el-form-item label="支付方式" prop="paymentMethod">
        <el-select v-model="payForm.paymentMethod">
          <el-option
            v-for="item in payMethodList"
            :key="item.id"
            :value="item.id"
            :label="item.name"
          />
        </el-select>
      </el-form-item>
    </el-form>

    <el-button type="primary" @click="submitForm">确定</el-button>
  </div>
</template>

<script>
import { addCardAPI } from '@/apis/card'

export default {
  data() {
    return {
      ruleForm: {
        personName: '',
        carNumber: ''
      },
      carRules: {
        personName: [{ required: true, message: '请输入车主姓名', trigger: 'blur' }],
        carNumber: [{ required: true, message: '请输入车牌号码', trigger: 'blur' }]
      },
      payForm: {
        payTime: [],
        paymentAmount: '',
        paymentMethod: null
      },
      payRules: {
        payTime: [{ required: true, message: '请选择有效日期', trigger: 'change' }],
        paymentAmount: [{ required: true, message: '请输入支付金额', trigger: 'blur' }],
        paymentMethod: [{ required: true, message: '请选择支付方式', trigger: 'change' }]
      },
      // 支付方式下拉列表
      payMethodList: [
        { id: 1, name: '支付宝' },
        { id: 2, name: '微信' },
        { id: 3, name: '线下' }
      ]
    }
  },
  methods: {
    // 串行校验:第一个表单通过后,才校验第二个
    submitForm() {
      this.$refs.carInfoForm.validate((valid) => {
        if (!valid) return
        this.$refs.payInfoForm.validate(async (valid2) => {
          if (!valid2) return
          await this.submitData()
        })
      })
    },
    async submitData() {
      // 二次处理请求参数:合并两个表单对象
      const reqData = {
        ...this.ruleForm,
        ...this.payForm
      }
      // 接口要 startDate / endDate,不要 payTime
      reqData.startDate = this.payForm.payTime[0]
      reqData.endDate = this.payForm.payTime[1]
      delete reqData.payTime
      // 金额要求 number 类型
      reqData.paymentAmount = Number(reqData.paymentAmount)

      await addCardAPI(reqData)
      // 提交成功的后续逻辑
      this.$message.success('新增成功')
      this.$router.back()
    }
  }
}
</script>

关键行与易错点

  • ref="carInfoForm" / ref="payInfoForm" 是拿到表单实例的唯一途径,靠 this.$refs.xxx.validate() 触发统一校验;ref 名字写错直接 undefined 报错。
  • 串行校验就是回调里再嵌一个 validate:第一个不通过 return,第二个不通过也 return,两个都通过才走提交,顺序不能乱。
  • value-format="YYYY-MM-DD" 不能省:不写的话 payTime 里是 Date 对象,展开到请求参数里是一堆 {},后端类型直接对不上。
  • payTime 是数组,[0] 是开始日期、[1] 是结束日期,取完必须 delete reqData.payTime,否则接口多一个字段(严格校验的接口会直接 400)。
  • paymentAmount 从输入框拿到的是字符串,接口要 number,必须 Number() 转一次——“字段名对、类型不对”是最常见的提交失败原因。
  • 提交前先开 Network 面板看请求载荷,确认“不多不少、名称对应、类型匹配”,不要提交完再回头找问题。
  • 成功后 this.$router.back() 回到列表页,因为两个页面是不同路由,回去时组件会重新初始化并重新拉取列表,不需要手动刷新。

示例三:编辑携带 id 跳转 + 目标页取参回填 ​

js
// @/router/index.js —— 用 query 传参不需要在路由上写 :id,用 params 才需要
const routes = [
  {
    path: '/card/list',
    component: () => import('@/views/card/index.vue')
  },
  {
    path: '/card/add',
    component: () => import('@/views/card/add.vue')
  }
]
vue
<!-- 列表页:编辑按钮,通过作用域插槽拿到当前行数据 -->
<el-table-column label="操作" fixed="right" width="180">
  <template #default="scope">
    <el-button size="mini" type="text" @click="toEdit(scope.row.id)">编辑</el-button>
  </template>
</el-table-column>

<script>
export default {
  methods: {
    toEdit(id) {
      // query 传参:URL 上会拼接 ?id=265
      this.$router.push({ path: '/card/add', query: { id } })
    }
  }
}
</script>
vue
<!-- 编辑页:同一个表单页,mounted 里取 id 判断是新增还是编辑 -->
<script>
import { getCardDetailAPI } from '@/apis/card'

export default {
  data() {
    return {
      id: null
    }
  },
  mounted() {
    // 取参方式必须和传参方式对应:query 传 -> query 取
    this.id = this.$route.query.id
    if (this.id) {
      this.getDetail()
    }
  },
  methods: {
    async getDetail() {
      const res = await getCardDetailAPI(this.id)
      // 回填:用后端返回的数据覆盖表单对象
      this.ruleForm = res.data
      this.payForm = res.data
    },
    async submitData() {
      const reqData = { ...this.ruleForm, ...this.payForm }
      reqData.startDate = this.payForm.payTime[0]
      reqData.endDate = this.payForm.payTime[1]
      delete reqData.payTime

      // 有 id 走更新,没 id 走新增
      if (this.id) {
        await updateCardAPI(reqData)
      } else {
        await addCardAPI(reqData)
      }
      this.$message.success(this.id ? '编辑成功' : '新增成功')
      this.$router.back()
    }
  }
}
</script>

关键行与易错点

  • scope.row 是当前行的完整数据对象,组件内部已经把它挂到 row 属性上了,scope.row.id 就能拿到这一行的主键。
  • 传参方式必须与取参方式对应:query 传就用 this.$route.query.id 取,params 传就用 this.$route.params.id 取;混用一定取到 undefined。
  • 用 params 传参必须在路由配置里写 path: '/card/add/:id',用 query 则不用改路由,URL 上会多一个 ?id=xxx,所以推荐用 query。
  • 取参用 this.$route(不带 r),跳转用 this.$router(带 r),写反了不报错但拿不到数据。
  • 判断新增/编辑依据是 id 是否存在,不要用中文文案或路由名去判断,否则后续维护极易出错。
  • 回填要在接口返回之后赋值,直接改 data 里的初始值不会触发视图更新;回填后记得检查日期字段的类型是否能被 el-date-picker 正常识别。

常见错误 ​

  1. 表格一行都不渲染,控制台报"期望数组、实际拿到对象"

    • 现象:el-table 空白,data 绑定的变量打印出来是 {rows: [...], total: 20}
    • 原因:把整个 res.data 赋给了列表变量,没取 rows
    • 修法:this.cardList = res.data.rows、this.total = res.data.total;赋值前先 console.log(res) 确认层级,不要凭印象写
  2. 查询之后列表空了,分页还停在第 3 页

    • 现象:翻到第 3 页再输车牌号查询,结果一条都没有
    • 原因:doSearch 只更新了查询字段,没重置 params.page,拿着第 3 页去请求一个只有 1 页的结果
    • 修法:任何"改变筛选条件"的操作,请求前一律 this.params.page = 1;把"重置页码"写进 doSearch 而不是散落在各处
  3. 新增提交接口 400,或后端提示字段缺失、类型不符

    • 现象:请求发出去了但失败;或 startDate 传成了 [object Object]
    • 原因:直接把表单对象原样提交——payTime 是数组、日期组件默认产出 Date 对象、还带着接口不需要的字段
    • 修法:提交前做二次处理(展开合并两个表单对象、拆 payTime[0]/[1]、日期组件加 value-format、delete 多余字段、金额 Number() 转换),并在 Network 面板逐字段对照接口文档,不多不少
  4. 两个表单的串行校验"点了确定但没拦住"

    • 现象:第一个表单已报红,接口照样被调用
    • 原因:validate() 没有 await,或没判断它的返回值就继续往下执行
    • 修法:await 第一个表单的 validate,用 if 把第二个表单校验和提交包在里面;第二个也通过才发请求
  5. 自定义校验器写完,表单一直"校验中",validate 的 then/catch 都不走

    • 现象:点确定无反应,也没报错
    • 原因:validator 里某个分支忘记调 callback
    • 修法:每条出口都必须调一次——不通过 callback(new Error('...')),通过 callback();写完先把通过/不通过两条路径都手动走一遍
  6. 编辑页 this.$route.query.id 拿到 undefined

    • 现象:回填接口参数为空,或页面 404
    • 原因:跳转用 params 传、取参用 query 取(或反之);或 path 名字写错
    • 修法:传参与取参方式必须成对;优先用 query + path(不用额外配路由);跳过去第一件事先打印 $route 确认
  7. 下拉框选中了,但提交上去是空

    • 现象:页面显示已选中,参数里对应字段为 null
    • 原因:v-model 绑错字段,或 el-option 的 value 绑成了 name 而不是 id
    • 修法:value 绑唯一标识、label 绑展示文案;用 Vue Devtools 检查选中瞬间数据是否真的变了

练习 ​

  1. 分页增强:给月卡列表的分页加上每页条数切换(sizes)和跳页(jumper)。要求切换每页条数后从第 1 页重新请求,且 page-size 与请求参数保持同步。

    • 验收:改成每页 2 条后,列表立刻变为 2 条且页码回到 1。
  2. 防重复提交:给查询按钮和新增提交按钮加 loading,请求期间按钮禁用,请求结束(成功或失败)都要恢复。

    • 验收:连点 5 次查询,Network 里只出现 1 次请求。
  3. 删除的边界处理:给"删除"加二次确认;删除成功后如果当前页只剩最后一条,页码自动 -1 再请求,避免停留在空页。

    • 验收:把某页最后一条删掉,页面自动跳回上一页并显示数据。
  4. 串行改并行:把两个表单的统一校验改成 Promise.all 并行校验,再和串行版对比:报错提示的呈现有什么不同?产品要求"先填完车辆信息才轮到缴费信息"时该选哪种?

    • 验收:写 3 句话结论,说明两种方案各自适用场景。
  5. 编辑回填闭环:从列表点编辑携带 id 跳到表单页,用 id 拉详情回填两个表单(注意日期字段要转成组件能接收的格式),提交时根据 id 有无分别调用更新/新增接口。

    • 验收:编辑后返回列表,该行数据已更新;不带 id 直接进页面时走新增流程且不报错。

课堂画面 ​

课堂画面 01

课堂画面 02

课堂画面 03

课堂画面 04

课堂画面 05

课堂画面 06

原视频对照 ​

#视频时长
1重点内容回顾26:37
2缴费信息表单准备08:58
3补充-看组件库文档09:34
4表单统一校验-串行校验实现06:39
5收集表单数据提交接口16:49
6提交成功之后的后续逻辑处理01:27
7编辑月卡-携带id跳转路由09:11

小结 ​

  1. 列表页的稳定套路只有四步:封装接口 → 准备响应式数据 → 封装取数方法 → 在钩子里调用;任何一步顺序乱了都会出现"数据不显示"这类现象。
  2. 报错先定位层级:404 查 URL、405/400 查 method、参数错查拼写与类型、渲染错查绑定字段名。按这个顺序排查比逐行读代码快得多。
  3. 分页与查询是一体的:只要筛选条件变了,页码就必须归 1,这条规则写死在 doSearch 里最不容易漏。
  4. 组件收集到的数据几乎不可能直接满足接口要求,"提交前二次处理"不是特殊情况而是常态,处理完必须用 Network 面板逐字段核对。
  5. 时间类组件默认产出的是 Date 对象,只要接口要字符串,就必须显式配置格式,不能靠"看起来对了"。
  6. 多个表单的统一校验,串行还是并行由交互需求决定,不由代码方便程度决定;两种写法都必须以 validate 的返回值作为是否继续的依据。
  7. 新增与编辑共用一个页面时,id 是唯一的分支开关——判断依据用 id 的有无,不要用中文状态或按钮文案。
  8. 前端校验只是前置拦截,后端同样会校验;两边规则不一致时会表现为"前端过了接口不过",遇到这种情况先回头和接口文档对齐规则,而不是反复改自己的正则。