主题
编辑月卡与删除(一)
本页对应 编辑月卡与删除,共 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>
</divjs
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正常识别。
常见错误
表格一行都不渲染,控制台报"期望数组、实际拿到对象"
- 现象:
el-table空白,data绑定的变量打印出来是{rows: [...], total: 20} - 原因:把整个
res.data赋给了列表变量,没取rows - 修法:
this.cardList = res.data.rows、this.total = res.data.total;赋值前先console.log(res)确认层级,不要凭印象写
- 现象:
查询之后列表空了,分页还停在第 3 页
- 现象:翻到第 3 页再输车牌号查询,结果一条都没有
- 原因:
doSearch只更新了查询字段,没重置params.page,拿着第 3 页去请求一个只有 1 页的结果 - 修法:任何"改变筛选条件"的操作,请求前一律
this.params.page = 1;把"重置页码"写进doSearch而不是散落在各处
新增提交接口 400,或后端提示字段缺失、类型不符
- 现象:请求发出去了但失败;或
startDate传成了[object Object] - 原因:直接把表单对象原样提交——
payTime是数组、日期组件默认产出 Date 对象、还带着接口不需要的字段 - 修法:提交前做二次处理(展开合并两个表单对象、拆
payTime[0]/[1]、日期组件加value-format、delete多余字段、金额Number()转换),并在 Network 面板逐字段对照接口文档,不多不少
- 现象:请求发出去了但失败;或
两个表单的串行校验"点了确定但没拦住"
- 现象:第一个表单已报红,接口照样被调用
- 原因:
validate()没有await,或没判断它的返回值就继续往下执行 - 修法:
await第一个表单的validate,用if把第二个表单校验和提交包在里面;第二个也通过才发请求
自定义校验器写完,表单一直"校验中",
validate的 then/catch 都不走- 现象:点确定无反应,也没报错
- 原因:
validator里某个分支忘记调callback - 修法:每条出口都必须调一次——不通过
callback(new Error('...')),通过callback();写完先把通过/不通过两条路径都手动走一遍
编辑页
this.$route.query.id拿到 undefined- 现象:回填接口参数为空,或页面 404
- 原因:跳转用
params传、取参用query取(或反之);或path名字写错 - 修法:传参与取参方式必须成对;优先用
query+path(不用额外配路由);跳过去第一件事先打印$route确认
下拉框选中了,但提交上去是空
- 现象:页面显示已选中,参数里对应字段为 null
- 原因:
v-model绑错字段,或el-option的value绑成了name而不是id - 修法:
value绑唯一标识、label绑展示文案;用 Vue Devtools 检查选中瞬间数据是否真的变了
练习
分页增强:给月卡列表的分页加上每页条数切换(
sizes)和跳页(jumper)。要求切换每页条数后从第 1 页重新请求,且page-size与请求参数保持同步。- 验收:改成每页 2 条后,列表立刻变为 2 条且页码回到 1。
防重复提交:给查询按钮和新增提交按钮加
loading,请求期间按钮禁用,请求结束(成功或失败)都要恢复。- 验收:连点 5 次查询,Network 里只出现 1 次请求。
删除的边界处理:给"删除"加二次确认;删除成功后如果当前页只剩最后一条,页码自动
-1再请求,避免停留在空页。- 验收:把某页最后一条删掉,页面自动跳回上一页并显示数据。
串行改并行:把两个表单的统一校验改成
Promise.all并行校验,再和串行版对比:报错提示的呈现有什么不同?产品要求"先填完车辆信息才轮到缴费信息"时该选哪种?- 验收:写 3 句话结论,说明两种方案各自适用场景。
编辑回填闭环:从列表点编辑携带 id 跳到表单页,用 id 拉详情回填两个表单(注意日期字段要转成组件能接收的格式),提交时根据 id 有无分别调用更新/新增接口。
- 验收:编辑后返回列表,该行数据已更新;不带 id 直接进页面时走新增流程且不报错。
课堂画面






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