主题
记住功能与月卡管理(三)
本页对应 记住功能与月卡管理,共 5 段视频,约 31 分钟。
学习目标
- 能够对照 Element Plus 官方文档,说明
el-select与el-option的嵌套结构,并解释v-model、label、value三个属性各自的数据流向与用途(显示值 vs 提交给后端的值)。 - 能够定位并修复“翻页后搜索无结果”的缺陷:在查询方法中先将请求参数
page重置为 1,再调用列表请求,并通过接口请求参数验证修复结果。 - 能够为新增月卡页面注册一个不显示在左侧菜单、无嵌套的独立路由,并使用
router.push/ 内联点击跳转从列表页进入该页面,同时用router.back()实现返回。 - 能够为车辆信息表单完成
:model、:rules绑定,为每个el-form-item指定prop,并实现四个字段的非空校验,且能通过控制台确认双向绑定数据同步正常。 - 能够依据官方文档为车牌号字段编写
validator自定义校验规则,正确声明(rule, value, callback)三个固定参数,用正则test判断并在失败分支callback(new Error(...))、通过分支直接callback()。 - 能够说明多条校验规则的执行顺序(按数组顺序,前一条不通过则后续不执行),并独立完成“非空 + 正则”双重校验的编写与调试验证。
知识点
先说明这一节的边界
本节笔记只有三条对照:存储空间大小、后端能否操作、是否跟随接口发送。笔记里没有配图,也没有代码块,所以下面的正文不补造截图和示例代码,只把这三条讲透。另外要交代一句:本节的转写稿记录的是同一课程里下拉框属性、搜索分页 bug、新增路由、表单校验等内容,并没有出现 cookie 与 localStorage 的直接推导。因此凡涉及“调试习惯”的部分,我只沿用课程里反复强调的那条做法——改一处就测一处,不要全写完再回头找问题——不虚构本节不存在的调试过程。
三条对照其实对应三个选型问题
把这三条列出来,不是为了背结论,而是因为实际开发里选存储方式时,问的就是这三件事:能装多少、谁能读写、会不会自己跟着请求跑。任何一条答错,功能可能当场能跑,上线后却变成性能或安全问题。下面逐条拆。
存储空间:5M 和 KB 差了两个数量级
为什么需要:先知道容量上限,才知道什么数据能往里放。笔记给的对照是 localStorage 约 5M,cookie 只有 KB 级。
怎么想:5M 与 KB 的差距不是“稍微小一点”,而是两个数量级。这意味着 cookie 天生不适合放“数据”,只适合放“标识”。而 localStorage 可以放得下一份列表缓存、一段表单草稿、一个较大的 JSON 配置。
怎么做:需要缓存列表数据、页面草稿、用户偏好设置这类体积可能较大的内容,走 localStorage;只放会话标识、令牌这类几十到几百字节的小值,走 cookie。
易错点:一是把整份 JSON 塞进 cookie,结果每次请求的请求头都被撑大,接口本身没变慢,带宽先被吃掉;二是以为 localStorage 的 5M 是“无限”,写满之后写入会直接失败,代码里没有兜底就会静默丢数据;三是 cookie 存中文等非 ASCII 内容需要编码,直接塞原文容易出现取值乱码。
后端能不能操作:ls 纯前端,cookie 前后端都能碰
为什么需要:有些数据必须由服务端说了算,比如登录态是否有效;有些数据只是浏览器本地的偏好,服务端根本不关心。
怎么想:localStorage 是浏览器提供给前端 JS 的仓库,服务端完全看不见它,也改不了它。cookie 不一样,它是 HTTP 协议自带的机制,浏览器负责维护,服务端可以在响应里写、在请求里读——笔记里“后端也可操作(占多数)”说的就是这个意思:真正决定登录态的 cookie,通常是服务端种下的,不是前端自己写的。
怎么做:需要服务端下发、服务端校验的数据,交给 cookie;纯前端的界面偏好、临时缓存,交给 localStorage。
易错点:第一,被标记为 HttpOnly 的 cookie,前端用 JS 是读不到的,这不是 bug,是安全设计,别在前端死磕;第二,前端手动改写了 cookie,并不代表服务端会认可,服务端只认自己签发和校验过的那份;第三,localStorage 里的内容用户随时可以在控制台改,绝不能拿它当权限凭证。
是否跟随接口发送:cookie 会自动带上,ls 不会
为什么需要:这一条直接决定“要不要写额外代码”,也决定安全和带宽。
怎么想:cookie 是 HTTP 的一部分,同源请求发出时浏览器会自动把它塞进请求头;localStorage 只是浏览器里的一块存储,浏览器不会替你把它放进任何请求。
怎么做:用 cookie 存登录标识,前端几乎不用写“带 token”的代码,请求自动就有;用 localStorage 存令牌,则每次请求都要自己取出来,手动塞进请求头或参数里。
易错点:一是 cookie 自动携带意味着连图片、静态资源请求都会带上,无谓消耗;二是用 localStorage 手动带令牌时,取值的时机和位置容易漏,比如某个请求封装分支忘了加;三是既然 localStorage 能被 JS 随意读取,一旦页面有 XSS,令牌就直接暴露,这也是很多项目宁可把关键凭证放 HttpOnly cookie 的原因。
三条合起来怎么选
判断顺序可以固定成三问:这份数据大不大(大走 ls,小走 cookie)、这份数据要不要服务端参与(要就走 cookie)、这份数据需不需要每次请求自动带上(要就走 cookie)。三问里只要有一问答“要”,基本就落到 cookie;三问都答“不要”,才轮到 localStorage。调试时也照课程里的老规矩:先把一条对照验证清楚,比如先在控制台确认 localStorage 写入成功、再发一个请求看请求头里有没有 cookie,确认一条再往下走,别三条一起改,出了问题分不清是哪一条。
说明:转写稿与本节主题不匹配
提供的转写稿主要讲下拉框属性、月卡搜索 bug、新增月卡路由、表单基础校验和自定义校验规则;其中“飞空/教验/靠 back”等应纠正为“非空/校验/callback”。这些内容与本节“编写清除用户信息 mutation”不直接对应,因此下面不把无关段落硬套进来,推导与自检只依据本节笔记和代码,不虚构项目中的其他字段或方法。
本节目标与代码位置
本节要完成的是:在 Vuex 的 mutations 中新增 clearUserInfo,用于清除用户登录信息。笔记给出的代码如下:
javascript
mutations: {
clearUserInfo(state) {
// 清除Token
state.token = ''
removeToken(TOKEN_KEY)
}
},为什么需要:用户退出登录或需要清理登录态时,不能只改页面显示,还要把 Vuex 中的 token 和本地持久化的 token 一起清掉。
怎么想:state.token 是内存状态,removeToken(TOKEN_KEY) 是本地持久化清理,两者要同时处理。
怎么做:在 mutations 中声明 clearUserInfo(state),先置空 state.token,再调用已有的 removeToken。
易错点:不要只写 state.token = '' 而漏掉 removeToken;也不要在 mutation 里发异步请求。
为什么需要清除用户信息 mutation
为什么需要:登录态通常同时存在于 Vuex 和本地存储中。只清 Vuex,刷新页面后本地 token 可能还在;只删本地存储,当前页面内存里的 state.token 仍可能被使用。
怎么想:把“清除用户信息”视为一个明确的状态变更动作,交给 mutation 统一处理,组件或 action 只负责触发。
怎么做:在已有 Vuex store 的 mutations 对象里增加 clearUserInfo。笔记中已经给出了完整结构。
易错点:Vuex 要求 mutation 同步执行;如果 removeToken 是项目已有同步工具方法,可以直接调用,不要随意改成异步流程。
怎么想:状态与持久化要一起清
为什么需要:state.token = '' 解决当前运行时的状态;removeToken(TOKEN_KEY) 解决本地保存的 token。两者职责不同。
怎么想:先清内存状态,再清本地存储,代码阅读上更直观;顺序不是本节重点,关键是两件事都做。
怎么做:保持笔记写法,TOKEN_KEY 必须与保存 token 时使用的 key 一致,removeToken 必须是当前文件可访问的方法。
易错点:TOKEN_KEY 拼错、没有导入 removeToken、保存时用的 key 和删除时不一致,都会导致“看起来退了,实际 token 还在”。
怎么做:在登出流程中提交 mutation
为什么需要:mutation 只是定义,必须被 commit 才会执行。
怎么想:在项目已有的登出逻辑中调用 commit('clearUserInfo'),不要在组件里直接改 state.token。
怎么做:如果登出逻辑在 action 中,就写 commit('clearUserInfo');如果在组件方法中,就通过 this.$store.commit('clearUserInfo') 触发。具体调用位置以项目已有登出流程为准,本节不虚构额外代码。
易错点:不要写成 this.$store.state.token = '' 绕过 mutation;也不要漏写 commit 后误以为 mutation 会自动执行。
调试与自检
为什么需要:清除用户信息涉及内存状态和本地存储,必须两边都验证。
怎么想:触发登出后,先看 Vuex 中 token 是否变为空字符串,再看浏览器本地存储中对应 token 是否被删除,最后刷新页面确认不会恢复登录态。
怎么做:可用 Vue Devtools 观察 mutation 记录和 state.token;在浏览器 Application/Storage 面板检查 TOKEN_KEY 对应项。
易错点:只刷新页面看界面,可能漏掉本地存储仍在的情况;只检查本地存储,也可能漏掉当前内存状态未清。
小结
本节的核心是:在 mutations 中新增 clearUserInfo,通过 state.token = '' 清除 Vuex 中的 token,通过 removeToken(TOKEN_KEY) 清除本地持久化 token。写的时候牢记 mutation 同步、key 一致、方法可访问,并在真实登出流程中 commit 触发。笔记只展示了 token 清理;若项目 state 中还有用户资料字段,应按实际代码补充,不能凭空编造字段名。
先看目标:退出登录要做两件事
本节笔记只给了一个 logout 方法,但它包含了退出登录最核心的两个动作:第一,把 Vuex 中保存的用户信息清掉;第二,用路由跳回登录页。代码不长,关键在顺序和写法。
javascript
methods: {
// 退出登录
logout() {
this.$store.commit('user/clearUserInfo')
this.$router.push('/login')
}
}这里没有图片,因此不额外插入图片。下面按“为什么需要 → 怎么想 → 怎么做 → 易错点”拆开讲。
为什么需要先提交 mutation 再跳转
退出登录不是单纯换一个页面。用户信息通常存在 Vuex 里,比如 token、用户名、权限等。如果只跳转到 /login,而 Vuex 里的用户信息还在,那么登录页或后续页面可能仍然读到旧状态,出现“已经退出但页面还认为已登录”的问题。
所以这里先执行:
javascript
this.$store.commit('user/clearUserInfo')commit 是提交 mutation 的固定写法,user/clearUserInfo 表示调用 user 模块下的 clearUserInfo。它的职责是清空用户信息。清完之后再执行:
javascript
this.$router.push('/login')push 是编程式导航,把地址跳到登录页。这样“状态清理”和“页面跳转”各自负责一件事,逻辑清楚。
怎么想:把“清状态”和“导航”分开
写退出登录时,不要把它想成“点一下按钮就跳走”。要拆成两步想:
- 当前登录状态存在哪里?在 Vuex 的
user模块里。 - 清状态用什么?用
commit提交 mutation。 - 清完之后去哪里?去
/login。 - 用哪种导航?用
this.$router.push('/login'),因为目标是固定登录页,不是“返回上一页”。
转写稿里新增月卡部分提到过“点击按钮跳到一个全新的路由”,也提到 router.back() 是“路由的回退,回退到上一级”。这两个概念要区分:back() 是回退历史记录,push('/login') 是明确跳到登录页。退出登录不应依赖“上一页是谁”,否则用户从不同页面退出时行为会不稳定。
怎么做:在 methods 中写 logout
在组件 methods 中声明 logout,方法体按笔记写:
javascript
methods: {
// 退出登录
logout() {
this.$store.commit('user/clearUserInfo')
this.$router.push('/login')
}
}调用时,模板里的退出按钮绑定 logout 即可。验证时看两点:第一,提交后 Vuex 中 user 模块的用户信息是否被清空;第二,地址是否变成 /login。转写稿中新增月卡路由时,先“手动输入一个路径”测试路由是否配置正确,再绑定点击跳转。退出登录也可以先用同样思路确认 /login 路由存在,再检查按钮调用。
易错点
第一,commit 的名字必须和 mutation 注册名一致。user/clearUserInfo 带命名空间,少写 user/ 或拼错 clearUserInfo,都会提交失败。
第二,clearUserInfo 必须已经在 user 模块的 mutations 中定义。本节只写调用,不写 mutation 本身,不能误以为 commit 会自动清空。
第三,跳转路径要和路由表一致。笔记写的是 /login,如果路由表里不是这个路径,页面不会按预期进入登录页。
第四,顺序不要反。先 push('/login') 再清用户信息,登录页可能短暂读到旧状态;按笔记先 commit 再 push 更稳。
第五,不要把这里的 push('/login') 写成 back()。转写稿里的 router.back() 是回退上一级,适合“返回列表”这类场景;退出登录的目标是登录页,应使用明确路径。
本节目标与代码位置
这一节要完成的是“路由级权限控制”:在用户进入页面前先判断有没有登录凭证 token。没有 token 的用户只能访问登录页、404 等白名单页面,其他页面统一跳回登录页;有 token 的用户正常放行。
本节笔记中的核心代码如下,先保留原始代码:
javascript
// 权限控制
import router from './router'
import { getToken } from './utils/auth'
const WHITE_LIST = ['/login', '/404']
router.beforeEach((to, from, next) => {
const token = getToken()
// 有token
if (token) {
next()
} else {
// 没有token
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})注意:这段代码只有“守卫逻辑”,它要生效,必须确保该文件被项目入口引入执行。如果写完发现跳转完全没反应,先查这个文件有没有被 import,而不是先怀疑 beforeEach 写错。
为什么需要路由级权限控制
如果只在前端页面上“隐藏按钮”,用户仍然可以直接在地址栏输入路径进入页面。路由守卫解决的是“进入某个路由之前先拦一道”的问题。它不替代后端鉴权,但能避免未登录用户看到业务页面结构,也能统一处理跳转。
怎么想:把权限判断放在全局前置守卫 router.beforeEach 中,因为它会在每次路由跳转前执行,适合做统一拦截。判断依据是 token 是否存在,判断目标是 to.path,即用户准备进入的路径。
怎么做:先拿到 token,再分两支:有 token 直接 next();没有 token 时看目标路径是否在白名单中,白名单放行,否则 next('/login')。
易错点:白名单必须写实际路径,比如 /login、/404,不要写成组件名;includes(to.path) 是精确匹配,路径多一个斜杠或少一个字符都可能失效。
怎么想:先放行登录页,再拦截业务页
没有 token 时不能所有页面都跳登录页,否则登录页本身也会被拦截,形成循环。所以需要 WHITE_LIST。白名单的本质是“无需登录即可访问的路径集合”。
怎么做:把登录页和 404 放进数组,用 WHITE_LIST.includes(to.path) 判断。若命中,调用 next() 放行;若未命中,调用 next('/login') 重定向。
易错点:如果登录页路径不是 /login,白名单和重定向地址都要同步改;如果项目有注册页、忘记密码页,也需要按实际需求加入白名单,但不要把所有页面都加进去,否则权限控制形同虚设。
怎么做:token 判断与 next 放行
有 token 时直接 next(),表示本次导航继续。这里代码没有继续校验 token 是否过期、是否合法,这是当前笔记的范围。实际项目中 token 失效通常还要结合接口 401 处理,但本节不扩展。
易错点:next() 在一次守卫中只能调用一次,不能既 next() 又 next('/login');next('/login') 后也不要再写后续放行逻辑。否则会出现跳转异常或死循环。
调试过程:从最小路径验证
转写稿里反复强调“写完一个点就测一个点”,这个调试习惯同样适用于权限控制。可以先手动输入 /login,确认白名单能进;再清除 token 后输入业务路径,确认会跳到 /login;最后写入 token,再访问业务路径,确认能正常进入。
如果跳转不对,按顺序查:第一,守卫文件是否被入口引入;第二,getToken() 是否真的能取到值;第三,to.path 打印出来是否与白名单完全一致;第四,重定向地址是否写错。转写稿中出现的“飞空校验”“call back”“海对象”等说法,实际对应非空校验、callback 回调、Error 对象;这类同音转写要以代码和文档为准,不要照抄口误。
易错点汇总
第一,白名单只写登录页,漏掉 404,导致未登录访问 404 也被踢回登录页。第二,to.path 与白名单大小写、斜杠不一致。第三,把 next('/login') 写成 next({ path: '/login' }) 虽也可用,但当前笔记是字符串写法,保持统一即可。第四,忘记引入该权限文件,守卫不执行。第五,token 存在但已过期时,本逻辑仍会放行;这属于后续接口层处理,不在本节代码范围内,不能误以为本节已经完成完整鉴权。
小结
本节权限控制逻辑可以概括为:进入路由前取 token;有 token 放行;无 token 时判断白名单;白名单放行,否则跳登录页。代码不长,但关键是路径精确、next 只调用一次、守卫文件确实被加载。调试时不要一次改很多,按“登录页—白名单页—业务页”逐条验证,才能快速定位问题。
先明确本节要解决的问题
本节笔记只有一句核心要求:在入口文件里引入权限模块,让它生效。
javascript
// 在router引入之后引入
import './permission'需要说明:转写稿主要记录的是下拉框属性、搜索分页 bug、新增月卡路由绑定、表单校验等内容,和“入口文件引入 permission”不是同一主题。因此下面不把无关内容硬套进来,只借用转写稿里反复出现的调试思路:先验证一个最小点,确认没问题再继续往下写,避免全部写完才发现入口根本没执行。
为什么需要:permission 这类模块通常承担路由权限判断、登录状态检查、守卫注册等启动逻辑。它不应该等到某个页面组件加载后才执行,而应该在应用入口启动阶段就执行。
怎么想:import './permission' 是“副作用引入”,不是引入一个函数再手动调用。只要入口文件执行到这一行,permission 文件内部代码就会执行。
怎么做:在入口文件里,先引入 router,再写 import './permission',顺序保持笔记注释的要求。
易错点:不要把它放到 router 引入之前;不要误以为 import 会自动导出某个方法;路径大小写、相对路径要跟项目实际文件一致。
为什么必须放在 router 引入之后
为什么需要:权限模块往往要依赖路由实例、路由表或路由守卫。如果 permission 先执行,而 router 还没准备好,就可能出现“拿不到 router”“守卫注册不生效”等问题。
怎么想:入口文件的执行顺序就是模块执行顺序。先创建/引入 router,再执行权限模块,才能保证权限模块运行时依赖已经存在。
怎么做:保留笔记中的顺序,不要被格式化工具或手动整理打乱:
javascript
// 在router引入之后引入
import './permission'易错点:最常见的是“代码写了但没生效”,实际原因是 import './permission' 被放到了 router 之前,或者被放进了某个页面组件里。入口文件的引入是启动时执行,页面组件里的引入是进入页面后才执行,二者时机不同。
怎么验证它真的生效
为什么需要:import './permission' 没有显式调用,很容易出现“我以为它执行了,其实没有”的情况。转写稿里调试搜索 bug 时,是通过看接口请求参数发现页码仍然是 3;这里同理,不能只看页面表面,要看执行时机和实际行为。
怎么想:先做最小验证:确认入口文件确实加载了 permission,再验证路由跳转、权限拦截是否符合预期。不要一上来就改一堆逻辑。
怎么做:可以在 permission 文件里临时加一行打印或断点,确认入口启动时执行到它;然后测试一个需要权限判断的路由跳转。调试完成后删除临时打印。转写稿里也强调“写完第一个点就测试一下,别都写完了才发现没出来”,这里同样适用。
易错点:开发环境热更新可能让模块重复执行,调试时要区分首次加载和热更新;生产环境不要保留调试输出;不要只看控制台有没有报错,还要看路由行为是否真的被处理。
入口引入和权限内部逻辑的边界
为什么需要:本节笔记只负责“引入生效”,并没有给出 permission 内部实现。如果把内部逻辑也凭空补出来,就容易编造。
怎么想:入口文件的职责是保证权限模块在正确时机执行;具体守卫、白名单、token 判断应放在 permission 文件内部,按项目实际代码为准。
怎么做:本节只确认入口文件中有且仅有这行引入,并保证位置正确。后续排查权限问题时,再进入 permission 文件看具体逻辑。
易错点:不要把大段权限判断写进入口文件;不要重复引入多个权限模块;不要因为“页面能打开”就认为权限生效,必须测试未登录、无权限、正常登录等实际场景。
小结
这一节的关键不是写复杂代码,而是保证 permission 在入口文件、router 引入之后执行。记住三点:副作用引入会直接执行、顺序不能反、写完先做最小验证。这样后续权限逻辑才有稳定的执行入口。
代码示例
示例 1:新增月卡 —— 车辆信息表单校验(基础校验 + 自定义 validator)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>新增月卡 - 车辆信息表单校验</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
</head>
<body>
<div id="app" style="padding: 20px">
<!-- 外层 form 绑定 model 与 rules,内层 form-item 用 prop 指定规则 -->
<el-form ref="carForm" :model="carInfo" :rules="carInfoRules" label-width="100px">
<el-form-item label="车主姓名" prop="personName">
<el-input v-model="carInfo.personName" placeholder="请输入车主姓名"></el-input>
</el-form-item>
<el-form-item label="联系方式" prop="phoneNumber">
<el-input v-model="carInfo.phoneNumber" placeholder="请输入联系方式"></el-input>
</el-form-item>
<el-form-item label="车辆号码" prop="carNumber">
<el-input v-model="carInfo.carNumber" placeholder="请输入车辆号码"></el-input>
</el-form-item>
<el-form-item label="车辆品牌" prop="carBrand">
<el-input v-model="carInfo.carBrand" placeholder="请输入车辆品牌"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submit">确定</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
// 车牌号正则:1 个汉字 + 5 位数字/字母(转写稿中的规则)
const CAR_NUMBER_REG = /^[\u4e00-\u9fa5][A-Za-z0-9]{5}$/
new Vue({
el: '#app',
data() {
return {
// 表单对象
carInfo: {
personName: '',
phoneNumber: '',
carNumber: '',
carBrand: ''
},
// 规则对象:key 必须和 carInfo 里的字段名一一对应
carInfoRules: {
personName: [{ required: true, message: '请输入车主姓名', trigger: 'blur' }],
phoneNumber: [{ required: true, message: '请输入联系方式', trigger: 'blur' }],
carNumber: [
{ required: true, message: '请输入车辆号码', trigger: 'blur' },
// 自定义校验规则:validator 是固定属性名
{
validator: (rule, value, callback) => this.checkCarNumber(rule, value, callback),
trigger: 'blur'
}
],
carBrand: [{ required: true, message: '请输入车辆品牌', trigger: 'blur' }]
}
}
},
methods: {
// rule: 当前规则对象(一般不用)
// value: 用户当前输入的数据(要校验的就是它)
// callback: 放行函数,必须调用
checkCarNumber(rule, value, callback) {
if (!CAR_NUMBER_REG.test(value)) {
// 校验不通过:callback 里 new 一个 Error
callback(new Error('请输入符合规范的车牌号'))
} else {
// 校验通过:直接 callback()
callback()
}
},
submit() {
this.$refs.carForm.validate(valid => {
if (!valid) return
this.$message.success('校验通过,可以提交')
})
},
reset() {
this.$refs.carForm.resetFields()
}
}
})
</script>
</body>
</html>关键行与易错点
:model="carInfo"、:rules="carInfoRules"绑定在外层 form 上,prop="carNumber"写在内层 form-item 上,二者缺一个校验就失效。prop的值、carInfo里的字段名、carInfoRules里的 key,三处名字必须完全一致,写错一个字母校验就“悄悄不生效”。validator是固定属性名,后面跟函数;参数顺序(rule, value, callback)固定不能乱。callback必须调用:通过就callback(),不通过就callback(new Error('提示语')),不调用则表单永远卡在“校验中”。data()里直接用this.checkCarNumber拿到的是undefined(此时 methods 还没挂载),所以要用箭头函数包一层(rule, value, callback) => this.checkCarNumber(...)。- 多条规则按数组顺序执行,第一条
required卡住后不会走到validator,所以空值先提示“请输入车辆号码”。
示例 2:搜索 bug 修复 —— 查询前把页码重置为 1
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>月卡管理 - 搜索重置页码</title>
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css" />
</head>
<body>
<div id="app" style="padding: 20px">
<div style="margin-bottom: 12px">
<el-input
v-model="params.carNumber"
placeholder="车牌号码"
style="width: 220px; margin-right: 8px"
></el-input>
<el-button type="primary" @click="handleSearch">查询</el-button>
<el-button @click="showAll">显示全部</el-button>
</div>
<el-table :data="list" border>
<el-table-column prop="carNumber" label="车牌号码" />
<el-table-column prop="personName" label="车主姓名" />
</el-table>
<el-pagination
style="margin-top: 12px"
layout="prev, pager, next, total"
:current-page.sync="params.page"
:page-size="params.pageSize"
:total="total"
@current-change="getList"
/>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/element-ui/lib/index.js"></script>
<script>
// ===== 模拟后端数据 =====
const DB = Array.from({ length: 23 }, (_, i) => ({
id: i + 1,
// 只有一条是“京A12345”,模拟真实业务里搜索结果很少的场景
carNumber: i === 5 ? '京A12345' : '津B' + String(10000 + i),
personName: '车主' + (i + 1)
}))
// ===== 模拟接口:按车牌号过滤 + 分页 =====
function mockGetList({ page, pageSize, carNumber }) {
const filtered = carNumber
? DB.filter(item => item.carNumber.includes(carNumber))
: DB
const start = (page - 1) * pageSize
return new Promise(resolve => {
setTimeout(() => {
resolve({ rows: filtered.slice(start, start + pageSize), total: filtered.length })
}, 100)
})
}
new Vue({
el: '#app',
data() {
return {
// 请求参数
params: {
page: 1,
pageSize: 10,
carNumber: ''
},
list: [],
total: 0
}
},
created() {
this.getList()
},
methods: {
async getList() {
const { rows, total } = await mockGetList(this.params)
this.list = rows
this.total = total
},
// 查询:先重置页码,再取数据
handleSearch() {
this.params.page = 1
this.getList()
},
showAll() {
this.params.carNumber = ''
this.params.page = 1
this.getList()
}
}
})
</script>
</body>
</html>关键行与易错点
- bug 表现:先翻到第 3 页,再输车牌号查询,接口参数里
page=3,而结果只有 1 条,后端返回空数组 → 页面“数据消失”。 - 修复核心就一行:
this.params.page = 1,且必须在this.getList()之前执行,否则发出去的还是旧页码。 getList内部不要再偷偷改page,页码的“重置”只放在查询/重置动作里,职责才清晰。:current-page.sync="params.page"保证点分页时params.page同步更新;如果写成:current-page(不加.sync)又不监听current-change赋值,翻页请求会一直取第 1 页。- 清空搜索条件(显示全部)同样要重置页码,否则会停在一个不存在的页上。
示例 3:退出登录 + 路由守卫权限控制(对应 permission.js)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>权限控制 - 路由守卫</title>
</head>
<body>
<div id="app">
<router-view></router-view>
</div>
<script src="https://unpkg.com/vue@2/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router@3/dist/vue-router.js"></script>
<script>
// ===== utils/auth.js(原笔记用 js-cookie,这里用 localStorage 等价实现) =====
const TOKEN_KEY = 'hrsaas_token'
const getToken = () => localStorage.getItem(TOKEN_KEY)
const setToken = token => localStorage.setItem(TOKEN_KEY, token)
const removeToken = () => localStorage.removeItem(TOKEN_KEY)
// ===== 页面组件 =====
const Login = {
template: `<div style="padding:20px">
<h3>登录页</h3>
<button @click="login">登录</button>
</div>`,
methods: {
login() {
setToken('mock-token-123')
this.$router.push('/home')
}
}
}
const Home = {
template: `<div style="padding:20px">
<h3>首页(需要 token 才能访问)</h3>
<button @click="logout">退出登录</button>
</div>`,
methods: {
// 等价于 this.$store.commit('user/clearUserInfo')
logout() {
removeToken(TOKEN_KEY) // 清掉本地 token
this.$router.push('/login') // 跳回登录页
}
}
}
const NotFound = { template: `<div style="padding:20px"><h3>404</h3></div>` }
// ===== router/index.js =====
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/home', component: Home },
{ path: '/404', component: NotFound },
{ path: '*', redirect: '/404' }
]
})
// ===== permission.js:权限控制 =====
const WHITE_LIST = ['/login', '/404']
router.beforeEach((to, from, next) => {
const token = getToken()
if (token) {
// 有 token,放行
next()
} else {
// 没有 token:白名单直接放行,否则踢回登录页
if (WHITE_LIST.includes(to.path)) {
next()
} else {
next('/login')
}
}
})
new Vue({ el: '#app', router })
</script>
</body>
</html>关键行与易错点
router.beforeEach((to, from, next) => {})三个参数顺序固定;to是即将进入的路由,用to.path做白名单判断。- 每个分支都必须调用且只调用一次
next,漏调页面白屏,重复调会报错。 - 白名单
WHITE_LIST里必须包含/login,否则没有 token 时跳登录页会被守卫再拦一次,形成死循环。 next('/login')与next({ path: '/login' })都可以,但不要写成next()后再router.push('/login')。- 退出登录的顺序:先清 token(
removeToken),再$router.push('/login');如果只跳转不清 token,守卫会发现仍有 token 而直接放行。 - 使用 Vuex 时对应写法是
this.$store.commit('user/clearUserInfo'),mutation 内部先state.token = ''再removeToken(TOKEN_KEY);直接改state.token而不删本地存储,刷新后守卫依旧能拿到 token。 permission.js要在入口文件中import router from './router'之后引入(import './permission'),否则 router 实例还没创建。
常见错误
1. 下拉框能展开,选项却是空白或显示成 [object Object]
- 现象:点开 select 有若干行,但看不到中文,或者只看到一串对象字符。
- 原因:
el-option的:label/:value绑错了层级,写成了:label="item";或者忘了加:,把它当成普通字符串。 - 修法:
:label="item.label"、:value="item.value"(字段名以后端返回为准),并保留:key。
2. 选中某一项后,页面没变化,取到的值始终是初始值
- 现象:点选项有高亮,但 data 里的变量不变,提交时拿到空值。
- 原因:
el-select上漏写v-model,或v-model绑的字段没在 data 中声明。 - 修法:先在 data 中声明变量,再在
el-select上写v-model="xxx",用 devtools 确认值随选择变化。
3. 翻到第 3 页再搜索,明明有数据却查不出来
- 现象:在列表第一页搜索正常,切到第 3 页再查,结果为空。
- 原因:查询请求沿用了当前的
page,等于向后端要“第 3 页 + 该车牌号”,而后端只有 1 条数据。 - 修法:查询方法里先
this.params.page = 1,再调getList()。
4. 加了 rules 但输入框前不出现星号,提交也不拦截
- 现象:规则对象写好了,页面上没有任何校验表现。
- 原因:
el-form的:model、el-form-item的prop、rules里的键名三者中有一个字母对不上(大小写、下划线都算)。 - 修法:写完第一个字段先只测这一个,星号出现后再往下写其余字段,避免一次性写完再排查。
5. 输入框敲了字,但表单对象里的字段不更新
- 现象:校验规则本身没问题,但提交时字段全是初始值。
- 原因:
el-input漏写v-model="表单对象.字段",只写了prop。 - 修法:补齐双向绑定,改完在 devtools 里点开组件看对应字段是否随输入变化。
6. 自定义校验写完后,表单永远提交不了(或点确定毫无反应)
- 现象:输入合法车牌号,红字不消失,确定按钮点了没动静。
- 原因:
validator函数里某个分支忘了调用callback;或者new Error(...)后忘了return,导致继续往下走又调了一次。 - 修法:通读函数,保证每条路径都以
callback()或return callback(new Error('提示'))结尾。
7. 车牌号正则“怎么输都不通过”
- 现象:输入
京A12345仍提示不符合规范。 - 原因:
reg.test()里传的是rule(规则对象)而不是value;或复用了带g标志的正则,lastIndex残留导致时对时错。 - 修法:改成
reg.test(value);自定义校验用的正则不要带g。
8. 新增页跳过去后,左侧菜单多了一项 / 页面套了两层侧边栏
- 现象:点“添加月卡”能进页面,但布局不对,或菜单里冒出一个不该有的入口。
- 原因:把新增路由挂进了 layout 的
children里。 - 修法:参照 404 的写法,作为与 layout 同级的顶层路由配置,
path: '/addCard',组件直接指向新增页。
9. 退出登录后手动输地址还能进业务页 / 打开就无限跳登录页
- 现象:清完 token 仍能访问列表页;或一进项目就在
/login打转。 - 原因:
permission.js在router之前引入(守卫拿不到实例);或白名单漏了/login,next('/login')被自己又拦一次。 - 修法:
import './permission'放在 router 引入之后;白名单至少含/login、/404。
练习
练习 1|下拉框取值 用后端返回的状态码数组渲染 el-select,选项显示中文、选中后把码值存进查询参数。要求:切换选项后,在 devtools 中能看到 params.status 变化,且首次进入有默认选中项。
验收:选项文案是中文、
params.status是码值而非中文。
练习 2|搜索重置页码 给列表页的“查询”按钮实现完整逻辑:重置页码 → 重置列表数据 → 重新请求。分别在“第一页搜索”和“先翻到第 3 页再搜索”两种场景下验证。
验收:Network 面板中两次请求的
page都是 1。
练习 3|新增页路由 新增一个“添加月卡”页面路由并完成按钮跳转,页面顶部有返回按钮。要求该路由不出现在左侧菜单,也不套 layout。
验收:点击按钮跳转正常;左侧菜单无新增项;点返回回到列表页且列表数据仍在。
练习 4|表单基础校验 给车辆信息表单的 4 个字段加必填校验,其中“车辆号码”在必填之外还要加正则校验。先只写第一个字段并测试星号,再补齐其余字段。
验收:4 个字段都有星号;空提交全部报红;输入
123时车牌号单独报“请输入符合规范的车牌号”。
练习 5|校验规则排错 故意把 rules 里某个键名改错一个字母,观察现象,然后把正确的排查步骤写下来(先看哪、再看哪、最后看哪)。
验收:能说清
:model/prop/rules三者的对齐关系,并能在 1 分钟内定位问题字段。
课堂画面






原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 补充-下拉框的属性说明 | 03:01 |
| 2 | 月卡管理-搜索bug修复 | 03:41 |
| 3 | 新增月卡-路由绑定跳转 | 04:52 |
| 4 | 新增月卡-完成车辆信息基础校验 | 08:38 |
| 5 | 新增月卡-自定义校验规则 | 11:37 |
小结
el-select的取值链路是v-model→ 选中项value,label只负责给人看,发给后端的一律是value。- 分页列表的“查询”必须显式重置
page = 1,否则会出现“第一页能搜到、第三页搜不到”的假 bug。 - 查询类操作的标准顺序是:重置分页 → 重置数据 → 发请求,三步不要省。
- 新增/详情这类页面通常是顶层路由:不进 layout、不进菜单,参照 404 的写法最省事。
- 表单校验的三处命名(
:model、prop、rules键名)必须逐字一致,一个字母错整条规则就静默失效。 - 写校验的正确节奏是“写一个字段 → 测一个字段”,全部写完再测会把排错成本放大好几倍。
- 默认配置能覆盖大部分场景,只有复杂逻辑(正则、联动、异步)才上
validator;其中callback是所有分支都必须调用的。 - 权限守卫的两个硬性条件:
permission.js在 router 之后引入、白名单包含登录页,否则不是漏拦就是死循环。