主题
插槽(二)
本页对应 插槽,共 5 段视频,约 47 分钟。
学习目标
- 能封装可复用表格组件,在子组件中预留插槽,并在父组件通过默认插槽自定义表格最后一列结构。
- 能在表格循环渲染中通过作用域插槽向父组件传递行数据
id,并使用#插槽名="对象/解构"接收后完成删除功能。 - 能说明作用域插槽中多实例传值、
template包裹、解构赋值及插槽默认值等细节,并能定位和修正相关错误。 - 能区分默认插槽、具名插槽、作用域插槽的语法,能识别并改写旧版
slot-scope写法。 - 能完成
tabbar路由配置、主题色配置和二级路由配置,并搭建登录、注册静态布局及处理登录表单细节。
知识点
页面骨架与目标

本节的目标是在 layout.vue 这个“首页架子”里放一个底部 Tabbar,让面经、收藏、喜欢、我的四个入口有统一的底部导航。官方文档地址是:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/tabbar。
为什么需要:移动端页面通常需要一个固定底部导航,用户可以在几个主页面之间快速切换;如果每个页面各写一套底部按钮,结构重复、样式不统一。
怎么想:先不要急着改样式,而是按笔记要求“复制官方代码”,再把显示文本和图标换成项目需要的四项。这样能保证组件行为与 Vant 文档一致。
怎么做:在 layout.vue 中保留“首页架子 - 内容区域”,在下面放 van-tabbar 和四个 van-tabbar-item。
易错点:不要一上来重写 Tabbar 结构;笔记明确要求先复制官方代码,再只改文本和图标,避免把组件自带的结构改坏。
引入 Vant 的 Tabbar 组件
jsx
import { Button, Icon, Tabbar, TabbarItem } from 'vant'
Vue.use(Tabbar)
Vue.use(TabbarItem)为什么需要:Vant 的组件不是全局自动可用的,需要在入口或统一配置文件里注册;否则模板里写 <van-tabbar> 时组件不会被识别。
怎么想:笔记把注册集中放在 vant-ui.js,这是统一管理 UI 组件引入的位置。这里除了 Tabbar,还保留了 Button、Icon 的引入,说明项目里可能已有其他 Vant 组件。
怎么做:在 vant-ui.js 中从 vant 解构出 Tabbar、TabbarItem,然后分别 Vue.use(Tabbar)、Vue.use(TabbarItem)。
易错点:只 import 不 Vue.use,页面会提示组件未注册;或者只注册 Tabbar 忘记注册 TabbarItem,四个子项无法正常渲染。
在 layout.vue 中改造 Tabbar
jsx
<template>
<div class="layout-page">
首页架子 - 内容区域
<van-tabbar>
<van-tabbar-item icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>为什么需要:官方示例只是通用结构,项目需要的是“面经、收藏、喜欢、我的”四个入口,所以必须把文本和图标改成业务含义。
怎么想:笔记第 1 步是“复制官方代码”,第 2 步才是“修改显示文本及显示的图标”。这说明改造范围很小:保留 van-tabbar 容器,只调整每个 van-tabbar-item 的 icon 和插槽文本。
怎么做:四个子项分别使用 notes-o、star-o、like-o、user-o,文本分别写“面经”“收藏”“喜欢”“我的”。
易错点:图标名要和当前 Vant 版本匹配;文本不要漏改,否则页面还是官方示例文案;不要把 van-tabbar-item 写到 van-tabbar 外面,否则不会作为标签项渲染。
路由项目准备与调试思路
转写稿里在安排新项目时提到:创建项目要选择 Router,路由模式输入 N,也就是使用哈希模式;配置文件选择单独配置文件。这个准备过程虽然不是在讲 Tabbar 本身,但它解释了“为什么这个页面能作为路由页面被访问”。
为什么需要:Tabbar 通常挂在布局页里,而布局页需要被路由承载;如果项目创建时没有选 Router,或者路由模式、配置文件选错,后续页面访问和调试会多走弯路。
怎么想:先确认项目具备路由能力,再确认 layout.vue 被路由使用;然后回到 Vant 组件注册和模板改造。转写稿中反复强调“选 Router”“输入 N 用哈希模式”,就是先把路由底座搭好。
怎么做:按转写稿演示,在创建项目时选中 Router;路由模式问询时输入 N;配置文件选择单独配置文件。已有项目则检查 vant-ui.js 是否注册、layout.vue 是否使用 Tabbar。
易错点:路由模式不要凭感觉选;转写稿明确说输入 N 使用哈希模式。另一个易错点是只盯着 Tabbar 模板,却忘记检查组件注册和路由入口,导致页面不显示或访问不到布局页。调试时按“路由入口 → 组件注册 → layout.vue 模板”的顺序排查,比盲目改样式更稳。
本节范围与转写稿说明
本节笔记的主题是“路由配置-配置主题色”,核心是借助 Vant 的 Less 变量覆盖能力,把网站整体风格统一成橙色。参考文档:https://vant-contrib.gitee.io/vant/v2/#/zh-CN/theme 。
需要说明:提供的转写稿主要记录的是作用域插槽案例、插槽总结和作业安排,并没有直接讲“配置主题色”的推导过程。因此不能把插槽案例硬套成主题色调试,否则就是编造。转写稿中可对应的调试动作是反复出现的“重新启动服务/重启服务器”,这正好对应本节最后一句“重启服务器生效”;另外转写稿里“满婷/买T/买提保”等同音转写,应纠正为 Vant。下面只依据笔记和 Vant 主题机制展开,不虚构转写稿中没有的细节。
为什么需要配置主题色
为什么需要:项目整体视觉希望统一为橙色,而 Vant 组件默认有一套自己的主题色。如果只在业务样式里零散覆盖按钮、标签、导航等颜色,会到处补样式,维护成本高。
怎么想:主题色不应该靠一个个组件手写覆盖,而应该从 Vant 的样式变量源头改。只要让项目使用 Vant 的 Less 源码样式,再通过 Less 变量覆盖,就能让相关组件统一变色。
怎么做:先确认项目使用 Vant 2,并参考官方主题定制文档;然后准备两个配置文件:babel.config.js 负责指定 Vant 样式路径为 Less,vue.config.js 负责覆盖 Less 变量。
易错点:只改 vue.config.js 而不改样式路径,可能覆盖不到;变量名不能凭感觉写,要按 Vant 文档和笔记给出的变量来。
修改 babel.config.js 指定样式路径
为什么需要:Vant 默认引入的可能是编译后的 CSS,CSS 里没有 Less 变量可改。要让 modifyVars 生效,必须让 Vant 组件样式走 Less 入口。
怎么想:通过 babel-plugin-import 的 style 配置,把每个 Vant 组件的样式路径指定为 ${name}/style/less。这样组件按需引入时,引入的是 Less 样式,后续才能被 Less 编译器处理。
怎么做:在 babel.config.js 中配置:
jsx
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
// 指定样式路径
style: (name) => `${name}/style/less`
}, 'vant']
]
}易错点:style 函数必须返回正确的 Less 路径;插件数组里的 'vant' 不能漏;改完 Babel 配置后不要只等热更新,要重启开发服务器。
在 vue.config.js 中覆盖变量
为什么需要:样式路径改成 Less 后,还需要告诉 Less 编译器“把哪个变量改成什么颜色”。笔记要求整体网站风格是橙色,因此直接覆盖主题变量。
怎么想:Vue CLI 项目可以在 vue.config.js 的 css.loaderOptions.less.lessOptions.modifyVars 中覆盖 Less 变量。笔记里直接覆盖 blue 为 #FA6D1D。
怎么做:在 vue.config.js 中配置:
jsx
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
// 直接覆盖变量
'blue': '#FA6D1D',
},
},
},
},
},
})易错点:层级容易写错,必须是 css.loaderOptions.less.lessOptions.modifyVars;变量名和颜色值要按文档确认;如果项目里还有别的 Less 全局变量配置,注意不要互相覆盖。
重启服务器并验证生效
为什么需要:Babel 配置和 Vue CLI 的 CSS loader 配置都属于构建配置,开发服务器启动后再改,热更新不一定能完整应用新配置。
怎么想:转写稿里多次出现“重新启动服务”“重启服务器”,说明配置类改动要用重启来排除缓存和旧编译结果。本节笔记也明确写了“重启服务器生效!”。
怎么做:保存两个配置文件后,停止当前开发服务器,重新执行启动命令;打开页面检查 Vant 按钮、导航等使用主题色的组件是否变为橙色。
易错点:只刷新浏览器不重启,可能看不到变化;如果仍未生效,先检查终端是否有 Less 编译报错,再确认 babel.config.js 和 vue.config.js 是否在项目根目录、是否保存成功。
易错点汇总
- 样式路径没改成
${name}/style/less,导致变量覆盖无效。 modifyVars层级写错,或把变量写到普通less配置外层。- 改完配置不重启服务器,误以为配置无效。
- 变量名写错、颜色值格式写错,导致编译失败或颜色不变。
- 把转写稿里的插槽内容误当成主题色步骤;本节应聚焦 Vant 主题变量覆盖,转写稿只可对应“重启服务器”这一调试动作。
先看整体目标:为什么要配二级路由

这一节要解决的是:登录、注册、文章详情是独立页面;而“面经、收藏、喜欢、我的”这些页面需要共用底部 tabbar。如果每个页面都复制一份 tabbar,结构会重复,后期改一个图标就要改四处。所以要把 Layout 作为“壳”,把四个 tab 页面作为它的子路由,让子页面渲染到 Layout 内部的 router-view 中。
怎么想:先分清一级路由和二级路由。/login、/register、/article/:id 是独立一级页面;/ 对应 Layout,它自己不展示具体内容,只负责布局和 tabbar;article、like、collect、user 是它的 children。怎么做:在 router/index.js 里给 Layout 配 children,并在 Layout.vue 里放路由出口和 tabbar。易错点:子路由的 path 不要随手写成 /article,按笔记写法应写 article,让它相对父路由 / 挂载;redirect 要指向真实存在的子路由。
转写稿里创建项目时提到要选 Router,并在路由模式询问处输入 N 使用 hash 模式;如果脚手架选项选错,转写稿的处理是关掉重新创建,而不是在错误项目上硬改。这是配置本节的必要前置。
在 router/index.js 中配置二级路由
为什么需要:只有把四个 tab 页面注册成 Layout 的子路由,访问 /article 时才会先进入 Layout,再把 Article 渲染到 Layout 的出口里。怎么想:一级路由表里保留登录、注册、详情;首页布局单独占一个 /,用 redirect 默认跳到 /article,再用 children 管四个 tab。怎么做:按笔记代码导入组件并配置:
jsx
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
import Like from '@/views/Like'
import Article from '@/views/Article'
import Collect from '@/views/Collect'
import User from '@/views/User'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout,
redirect: '/article',
children: [
{ path: 'article', component: Article },
{ path: 'like', component: Like },
{ path: 'collect', component: Collect },
{ path: 'user', component: User }
]
}
]
})
export default router易错点:children 里的 path 不要写成 /article、/like 这种绝对路径;redirect: '/article' 要和子路由最终访问路径对应;Detail 按这份配置放在一级路由中,不要误塞进 children,否则详情页会渲染在 Layout 内部。导入路径要检查 @/views/... 是否和真实文件名大小写一致。
在 Layout.vue 中放路由出口和 tabbar
为什么需要:父路由 Layout 只是壳,子路由组件必须有出口才能显示;tabbar 也要用路由模式跟随当前地址高亮。怎么想:Layout 模板里先放 <router-view>,再放 van-tabbar;每个 van-tabbar-item 的 to 写完整访问路径。怎么做:
jsx
<template>
<div class="layout-page">
//路由出口
<router-view></router-view>
<van-tabbar route>
<van-tabbar-item to="/article" icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item to="/collect" icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item to="/like" icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>易错点:<router-view> 必须放在 Layout 模板内,否则子路由不显示;van-tabbar 要加 route 才能按路由高亮;to 不要漏写前导 /;模板里的 //路由出口 在实际 Vue 模板中应写成 <!-- 路由出口 -->,不要保留 // 这种注释写法。
调试与验证
为什么需要:二级路由配错时,页面往往“空白但不报大错”。怎么想:先访问 / 看是否重定向到 /article,再点四个 tab 看地址和内容是否切换,最后刷新页面确认子路由仍能渲染在 Layout 内。怎么做:检查 router/index.js 的 children、Layout.vue 的 router-view、tabbar 的 to 三处是否一致。易错点:只配了 children 但父组件没写出口,会空白;tabbar 的 to 和子路由 path 对不上,会跳 404 或不高亮;创建项目时没选 Router,则没有现成路由文件,需要按转写稿建议重新创建或补装配置。
一、先明确目标:把登录页的静态结构搭出来
登录页是面经移动端最先要落地的页面之一。这一节不接接口、不做校验提交后的跳转,只做“静态布局”:把导航栏、用户名输入框、密码输入框、提交按钮摆好,并让表单具备基本的必填与格式校验能力。
为什么需要先做静态布局?因为登录页的交互依赖表单组件的行为:输入值要能被 v-model 收集,提交要能触发 @submit,校验规则要能拦住空值和格式错误。如果结构没搭对,后面接 axios 时会出现“拿不到 values”“按钮点了没反应”等问题。
怎么想?移动端页面优先使用 Vant 组件库,直接用 van-nav-bar 做顶部导航,用 van-form 包住表单,用 van-field 做输入项,用 van-button 做提交按钮。整体结构就是“导航栏 + 表单 + 跳转链接”。
最终效果如下:

二、注册 Vant 组件:模板里能写 van-xxx 的前提
为什么需要注册?Vue 模板中出现 <van-nav-bar>、<van-form> 这类标签时,Vue 需要知道它们对应哪个组件。通过 Vue.use() 全局注册后,任意组件模板都能直接使用,不必在每个页面里重复 import 和 components 注册。
怎么做?在项目的 vant-ui.js 中按需引入并注册本节用到的四个组件:
jsx
import Vue from 'vue'
import {
NavBar,
Form,
Field
} from 'vant'
Vue.use(NavBar)
Vue.use(Form)
Vue.use(Field)易错点:只注册了 Form 却漏了 Field,页面会提示组件未注册;另外注册文件需要被项目入口加载,否则这些 Vue.use() 不会生效。
三、Login.vue 的模板:导航栏、表单、输入框、按钮
为什么用 van-form 包住输入项?因为 Vant 的校验规则是挂在 van-field 上的,但触发校验和提交的入口在 van-form 的 @submit 事件里。表单提交后,onSubmit(values) 能直接拿到各字段的值,后面接接口时就不用逐个去读 data。
怎么想?两个输入框分别绑定 username 和 password;name 属性决定提交时字段的键名;rules 数组里先写必填,再写正则格式。用户名用 /^\w{5,}$/,表示字母、数字、下划线至少 5 个字符;密码用 /^\w{6,}$/,至少 6 个字符。
jsx
<template>
<div class="login-page">
<!-- 导航栏部分 -->
<van-nav-bar title="面经登录" />
<!-- 一旦form表单提交了,就会触发submit,可以在submit事件中
根据拿到的表单提交信息,发送axios请求
-->
<van-form @submit="onSubmit">
<!-- 输入框组件 -->
<!-- \w 字母数字_ \d 数字0-9 -->
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[
{ required: true, message: '请填写用户名' },
{ pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
]"
/>
<div style="margin: 16px">
<van-button block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
</div>
</template>易错点有三个:第一,按钮必须写 native-type="submit",否则只是普通按钮,不会触发表单提交;第二,rules 里的 pattern 是正则对象,不要写成字符串;第三,v-model 绑定的字段要和 data 中的名字一致,否则输入不生效。
四、data 与 onSubmit:先验证表单行为
为什么先给初始值?username: 'zhousg'、password: '123456' 方便开发时直接点提交,观察 values 的结构,不必每次手输。
jsx
<script>
export default {
name: 'LoginPage',
data () {
return {
username: 'zhousg',
password: '123456'
}
},
methods: {
onSubmit (values) {
console.log('submit', values)
}
}
}
</script>怎么做?提交后看控制台输出,确认 values 是 { username: ..., password: ... } 这样的对象。易错点是误以为 onSubmit 收到的是事件对象,实际上 Vant 已经把表单值整理好传进来了。
五、跳转注册页:用 router-link 而不是 a 标签
登录页通常还要有“注册账号”入口。项目创建时已经选了 Router,所以用 router-link 走前端路由跳转,避免整页刷新。
vue
<template>
<div class="login-page">
<van-nav-bar title="面经登录" />
<van-form @submit="onSubmit">
...
</van-form>
<router-link class="link" to="/register">注册账号</router-link>
</div>
</template>样式上把链接放到右侧:
jsx
<style lang="less" scoped>
.link {
color: #069;
font-size: 12px;
padding-right: 20px;
float: right;
}
</style>这里用到了 lang="less"。转写稿里创建面经移动端项目时,在 Vue CLI 的功能选择中勾选了 CSS 预处理并选择了 Less,所以这里可以直接写 Less 语法。易错点是:如果项目没装 Less 预处理器,lang="less" 会报错;另外 scoped 只影响当前组件,别指望它去改全局样式。
六、环境与代码风格:转写稿里容易忽略的细节
转写稿中与本节直接相关的是项目创建和 ESLint 配置部分。创建项目时选择 Router、CSS 预处理(Less)、ESLint 标准配置,并开启“保存时格式化”。ESLint 检查的不是逻辑错误,而是代码风格:缩进、空格、引号、语句末尾分号、括号是否对齐等。所以写本节代码时,模板缩进和 JS 缩进要统一;如果保存后出现大量黄线,通常是配置重复或格式不符合规则,按项目文档调整即可。
把以上结构跑通后,登录页的“壳”就完成了。下一步才是在 onSubmit 里根据 values 发请求,把静态布局变成真正的登录功能。
本节说明
本节笔记标题是“登录表单中的细节分析”,核心是登录表单里 6 个容易忽略但很关键的细节。需要先说明:你提供的转写稿主要讲的是作用域插槽案例、插槽总结和作业安排,并没有覆盖登录表单的推导与调试过程;本节笔记也没有附带图片和代码。因此下面不虚构转写稿内容,也不补写不存在的图片/代码,只依据笔记中的 6 个知识点展开。若后续补充登录表单的转写稿,可以再把调试过程补进来。
@submit 事件:提交按钮触发的是表单提交
为什么需要:登录表单不能只靠点击按钮后手动调接口,表单本身有提交语义。用户按回车、点击 type="submit" 按钮时,都应该走同一套提交逻辑。
怎么想:把“提交动作”统一交给 form 的 @submit 处理,而不是只给按钮绑 click。这样键盘回车和按钮点击行为一致。
怎么做:在 form 上监听 @submit,事件处理函数里先阻止默认刷新,再做校验和请求。示意写法是 @submit.prevent="handleSubmit"。
易错点:按钮若写成普通 button,可能不会触发 submit;不阻止默认行为时页面可能刷新;不要把提交逻辑只写在按钮 click 上,否则回车提交容易漏掉。
v-model 双向绑定:把表单值和数据对象绑在一起
为什么需要:登录表单需要实时收集用户名、密码等输入值。手写 input 事件再赋值会很啰嗦,也容易漏字段。
怎么想:把输入框绑定到表单对象的字段上,视图变则数据变,数据变则视图变。登录场景通常绑定 form.username、form.password。
怎么做:在 data 或响应式对象中先声明字段,再用 v-model 绑定。不要临时绑定一个未声明字段。
易错点:v-model 绑定的字段要先初始化;密码框仍要用 type="password";不要以为 v-model 会自动完成校验或提交,它只负责值同步。修饰符如 .trim 要按需求使用,密码一般不要随意 trim。
name 属性:收集的 key 要和接口文档对应
为什么需要:表单提交或收集数据时,name 往往决定字段名。若 name 和接口文档不一致,后端收到的 key 就对不上。
怎么想:先看接口文档需要什么字段名,再决定 name。例如接口要 username,就不要写成 userName 或 user_name。
怎么做:让 name 与接口字段保持一致;同时检查 v-model 绑定的对象字段是否也一致,避免“页面有值但提交为空”。
易错点:name 不是给人看的标题;大小写、下划线、驼峰要严格对应;不要只改 v-model 忘了 name,也不要只改 name 忘了提交对象字段。
label:输入框的标题,也是可访问性细节
为什么需要:用户需要知道每个输入框填什么;label 还能提升可访问性,点击文字可聚焦对应输入框。
怎么想:label 是字段标题,不是提示语。标题应稳定、明确,例如“用户名”“密码”。
怎么做:用 label 的 for 关联输入框的 id,或把输入框包在 label 内。
易错点:不要用 placeholder 代替 label,因为输入后提示就消失了;for 和 id 要一致;不要把“请输入密码”这类提示写成唯一标题。
:rules:表单校验规则
为什么需要:登录表单需要校验必填、长度、格式等。只靠后端校验体验差,只靠手写判断又容易散落。
怎么想:把规则集中配置到表单/表单项上,提交时统一校验,校验通过再发请求。
怎么做:常见规则包括 required、min/max、pattern、自定义 validator;提交处理里先 validate,失败则停止请求。
易错点:规则字段名要和 v-model 字段一致;trigger 配置不对会导致触发时机不符合预期;不要只写规则不调用校验;自定义校验要正确处理回调或 Promise。
placeholder:文本框提示语
为什么需要:提示用户输入格式,例如“请输入手机号”“请输入密码”,减少误填。
怎么想:placeholder 是辅助提示,不是字段标题,也不是校验规则。
怎么做:写简短、明确的格式提示;密码框提示“请输入密码”,不要写真实密码示例。
易错点:不要用 placeholder 承担 label 的职责;不要写过长说明;不要把敏感信息放进提示语;提示语不能替代 rules。
小结
登录表单的细节可以串成一条线:label 告诉用户填什么,placeholder 提示怎么填,v-model 负责收集值,name 保证字段名和接口一致,:rules 负责提交前校验,@submit 统一处理提交动作。每个点都不复杂,但漏一个就容易出现“能输入却不能提交”“有值但接口收不到”“提示有了但校验没生效”等问题。
注册静态布局的整体目标
为什么需要:注册页是面经移动端的入口页之一,先完成静态布局,才能继续接表单校验、注册接口和登录跳转。
怎么想:页面结构分成三块:顶部导航、表单主体、底部登录链接。表单用 Vant 的 van-form、van-field、van-button 组合,减少手写表单样式。
怎么做:外层用 .login-page 包住;van-nav-bar 显示“面经注册”;van-form 绑定 @submit="onSubmit";两个 van-field 分别绑定 username、password;按钮设置 native-type="submit";底部用 router-link 跳到 /login。
易错点:按钮必须写 native-type="submit",否则点击不会触发 van-form 的 submit;router-link 的 /login 必须已在路由中配置。转写稿在新建项目时明确要求选择 router,并说明路由模式按项目配置选择;注册页的登录链接依赖这一点,不能只写链接不配路由。
表单字段与校验规则
为什么需要:用户名和密码不能为空,且要有最低长度限制,先在前端做基础拦截,减少无效提交。
怎么想:van-field 的 v-model 负责收集输入,name 负责提交时生成字段名,rules 负责校验。用户名要求必填且至少 5 个字符;密码要求必填且至少 6 个字符。
怎么做:用户名规则写 { required: true, message: '请填写用户名' } 和 { pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' };密码规则写 { required: true, message: '请填写密码' } 和 { pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }。
易错点:name 不能漏,否则 onSubmit(values) 里拿不到对应字段;\w 只匹配字母、数字、下划线,不包含中文,若产品要求中文用户名要改正则;required 和 pattern 同时存在时,空值先触发必填提示。转写稿中多次强调 ESLint 检查的是代码风格,不是逻辑错误;所以正则写错、name 漏写,ESLint 不会替你发现,必须自己检查提交结果。
提交事件与数据初始值
为什么需要:静态布局要能验证表单是否收集到数据,所以先写一个 onSubmit 打印提交值。
怎么想:data 中声明 username、password 初始为空字符串,和 v-model 对应;methods 中写 onSubmit(values),values 由 van-form 汇总。
怎么做:data 返回 { username: '', password: '' };onSubmit 中写 console.log('submit', values),先观察控制台输出。
易错点:data 里漏声明字段会导致 v-model 失效或报警;onSubmit 参数名要和模板事件对应;不要误以为 values 是单个输入值,它是表单字段对象。转写稿没有出现注册页的逐行调试过程,因此这里不虚构调试细节;可确认的是项目创建时选择了 ESLint 和保存自动格式化,代码风格问题会被提示,但业务逻辑仍要自己验证。
登录链接与样式
为什么需要:注册页要提供“有账号,去登录”的入口,形成页面间跳转。
怎么想:用 router-link 生成路由跳转,比普通 a 标签更符合 Vue Router 项目;链接靠右显示,字号小一些。
怎么做:写 <router-link class="link" to="/login">有账号,去登录</router-link>;样式写 .link { color: #069; font-size: 12px; padding-right: 20px; float: right; }。
易错点:scoped 样式下类名要写在 router-link 上;float: right 后父容器高度可能受影响,后续可补清除浮动或改用 flex;转写稿在创建项目时选择了 CSS 预处理,本节使用 lang="less",若项目未装 less 会编译失败。
完整代码保留
jsx
<template>
<div class="login-page">
<van-nav-bar title="面经注册" />
<van-form @submit="onSubmit">
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[
{ required: true, message: '请填写用户名' },
{ pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
]"
/>
<div style="margin: 16px">
<van-button block type="primary" native-type="submit"
>注册</van-button
>
</div>
</van-form>
<router-link class="link" to="/login">有账号,去登录</router-link>
</div>
</template>
<script>
export default {
name: 'Register-Page',
data () {
return {
username: '',
password: ''
}
},
methods: {
onSubmit (values) {
console.log('submit', values)
}
}
}
</script>
<style lang="less" scoped>
.link {
color: #069;
font-size: 12px;
padding-right: 20px;
float: right;
}
</style>课件图示








代码示例
示例一:二级路由 + Tabbar 标签页(router/index.js + Layout.vue)
jsx
// src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Login from '@/views/Login'
import Register from '@/views/Register'
import Detail from '@/views/Detail'
import Layout from '@/views/Layout'
import Like from '@/views/Like'
import Article from '@/views/Article'
import Collect from '@/views/Collect'
import User from '@/views/User'
Vue.use(VueRouter)
const router = new VueRouter({
routes: [
{ path: '/login', component: Login },
{ path: '/register', component: Register },
{ path: '/article/:id', component: Detail },
{
path: '/',
component: Layout,
redirect: '/article',
children: [
{ path: 'article', component: Article },
{ path: 'like', component: Like },
{ path: 'collect', component: Collect },
{ path: 'user', component: User }
]
}
]
})
export default routerjsx
<!-- src/views/Layout.vue -->
<template>
<div class="layout-page">
<!-- 二级路由的出口:面经/收藏/喜欢/我的 都渲染在这里 -->
<router-view></router-view>
<!-- 加了 route 属性,tabbar 才会跟着路由自动高亮 -->
<van-tabbar route>
<van-tabbar-item to="/article" icon="notes-o">面经</van-tabbar-item>
<van-tabbar-item to="/collect" icon="star-o">收藏</van-tabbar-item>
<van-tabbar-item to="/like" icon="like-o">喜欢</van-tabbar-item>
<van-tabbar-item to="/user" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
</div>
</template>要点解释与易错点
van-tabbar必须加route属性,van-tabbar-item用to而不是href,否则点击不跳转、也不高亮。Layout.vue里必须有<router-view></router-view>,否则二级路由组件无处渲染,页面只剩一个空架子。- 子路由
path: 'article'不要写成'/article',写成绝对路径会脱离父级/,成为一级路由。 - 父路由
redirect: '/article'不能省,访问/时才会自动跳到面经页。 - 图标名必须用 Vant 内置图标(
notes-o、star-o、like-o、user-o),拼错只是不显示图标,不报错,容易被忽略。
示例二:Vant 按需引入 + 主题色覆盖(vant-ui.js / babel.config.js / vue.config.js)
jsx
// src/utils/vant-ui.js —— 统一按需注册 Vant 组件
import Vue from 'vue'
import { Button, Icon, Tabbar, TabbarItem, NavBar, Form, Field } from 'vant'
Vue.use(Button)
Vue.use(Icon)
Vue.use(Tabbar)
Vue.use(TabbarItem)
Vue.use(NavBar)
Vue.use(Form)
Vue.use(Field)jsx
// babel.config.js —— 指定引入 less 样式源文件,才能被覆盖
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
],
plugins: [
['import', {
libraryName: 'vant',
libraryDirectory: 'es',
// 指定样式路径
style: (name) => `${name}/style/less`
}, 'vant']
]
}jsx
// vue.config.js —— 覆盖 less 变量,定制主题色
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
css: {
loaderOptions: {
less: {
lessOptions: {
modifyVars: {
// 直接覆盖变量
'blue': '#FA6D1D'
}
}
}
}
}
})要点解释与易错点
- 关键在
style: (name) =>${name}/style/less``:不改成 less 源文件,modifyVars覆盖不了任何变量,页面还是默认蓝色。 modifyVars里写的是 Vant 的 less 变量名(笔记中为blue),不要自己发明变量名,写错不会报错、只是不生效。- 三处改动后必须 重启服务器(
npm run serve),热更新不会让vue.config.js/babel.config.js生效。 - 组件一定要在
vant-ui.js里Vue.use注册,否则模板里的<van-xxx>会被当成未知标签,页面空白一片。 - 按需引入时
import的组件名首字母大写(TabbarItem),与libraryName: 'vant'配合才能正确映射到es/tabbar-item。
示例三:登录静态布局(Login.vue,含跳转注册链接)
jsx
<!-- src/views/Login.vue -->
<template>
<div class="login-page">
<!-- 导航栏部分 -->
<van-nav-bar title="面经登录" />
<!-- 一旦 form 表单提交了,就会触发 submit,可以在 submit 事件中
根据拿到的表单提交信息,发送 axios 请求 -->
<van-form @submit="onSubmit">
<!-- 输入框组件 -->
<!-- \w 字母数字_ \d 数字0-9 -->
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[
{ required: true, message: '请填写用户名' },
{ pattern: /^\w{5,}$/, message: '用户名至少包含5个字符' }
]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[
{ required: true, message: '请填写密码' },
{ pattern: /^\w{6,}$/, message: '密码至少包含6个字符' }
]"
/>
<div style="margin: 16px">
<van-button block type="info" native-type="submit">提交</van-button>
</div>
</van-form>
<router-link class="link" to="/register">注册账号</router-link>
</div>
</template>
<script>
export default {
name: 'LoginPage',
data () {
return {
username: 'zhousg',
password: '123456'
}
},
methods: {
onSubmit (values) {
console.log('submit', values)
}
}
}
</script>
<style lang="less" scoped>
.link {
color: #069;
font-size: 12px;
padding-right: 20px;
float: right;
}
</style>要点解释与易错点
- 按钮必须写
native-type="submit",否则点按钮只当普通按钮,不会触发van-form的@submit,onSubmit永远不进。 name属性决定onSubmit(values)里收集到的 key,要和接口文档字段保持一致,改错会导致后端拿不到参数。v-model负责双向绑定,:rules负责校验,两者分工不同:只写v-model不写rules就不会有任何提示。- 正则
pattern要写成{ pattern: /^\w{5,}$/, message: '...' }的对象形式,直接写正则数组项无效;注意\w不含中文和特殊符号。 - 登录页按钮
type="info"、注册页type="primary",别写反;block让按钮占满一行。 <style lang="less" scoped>中.link用float: right定位,需保证父级没有overflow: hidden之类截断;scoped下不要用标签选择器穿透修改 Vant 内部样式。
常见错误
1. 点击 tabbar 能跳转,但高亮不跟随,或压根不跳
- 现象:点“收藏”页面变了,但图标还是“面经”高亮;或点了毫无反应。
- 原因:
van-tabbar漏写route属性(不走路由模式,只当普通受控组件),或van-tabbar-item用了icon却忘了to。 - 修法:
<van-tabbar route>+ 每个 item 写to="/xxx",二者缺一不可;跳转路径要与router/index.js中子路由的path完全对应。
2. Layout 里子路由页面一片空白
- 现象:地址栏是
/article,但内容区什么都没有。 - 原因:
Layout.vue里只有van-tabbar没有<router-view>;或子路由path写成了'/article'(带斜杠),被当成绝对路径,脱离了 Layout 的嵌套关系。 - 修法:模板中补
<router-view></router-view>;children 里的path一律写相对形式'article',根路径的跳转交给父级的redirect。
3. 改了主题色,页面还是橙色没变化
- 现象:
modifyVars写完了,刷新、重启都没效果。 - 原因:三处链路断了一环——
babel.config.js里style仍是默认(未指向${name}/style/less),样式根本没走 less 源文件;或改完配置没重启 dev server;或modifyVars的 key 写错(写成了@blue以外的名字)。 - 修法:先确认
style: (name) => \${name}/style/less`,再Ctrl+C重启服务;key 用 Vant 文档给出的变量名(如blue);仍不行就rm -rf node_modules/.cache` 后重启。
4. 表单提交时 onSubmit(values) 拿到的字段缺项 / 与接口对不上
- 现象:
console.log(values)只有username,没有password;或 key 是中文、是user,而后端要username。 - 原因:
van-field漏写name(name 才是收集数据的 key,label只是标题,v-model只是绑值)。 - 修法:每个
van-field都补name="xxx",且与接口文档字段名严格一致。
5. 校验规则完全不生效,空表单也能提交
- 现象:什么都没填,点提交照样触发
submit。 - 原因:
rules前漏了冒号,写成rules="[{ required: true }]",被当成字符串传给组件;或按钮用了普通van-button没有native-type="submit",表单的@submit压根没被触发。 - 修法:写成
:rules="[...]"动态绑定;提交按钮加native-type="submit";正则用字面量/^\w{5,}$/而不是字符串。
6. 作用域插槽里取 item.id 报“属性未定义”
- 现象:最后一列的删除按钮点击后报错,或
id是undefined。 - 原因:父组件直接把插槽内容写在组件标签之间,但没写
<template #default="obj">去接收,仍按普通模板变量item访问;或者子组件slot上没写:id="item.id"这个自定义属性。 - 修法:子组件
<slot :id="item.id" :row="item">;父组件用<template #default="{ id }">解构接收,删除时del(id)。
7. 插槽结构“多出一份”或“一份都没显示”
- 现象:父组件只写了一个“移除”按钮,页面上却出现三条;或父组件什么都不传时,那一列是空的。
- 原因:前者是列表循环渲染了三个组件实例,每个实例各有一个默认插槽,父组件传的结构会被分发到每一个;后者是
<slot>中间没写兜底内容。 - 修法:理解“传一份结构 = 给每个插槽实例各传一份”是正常行为,不要试图只改其中一个;需要兜底时在
<slot>默认文本/默认按钮</slot>中间写内容,父组件传了就以父组件为准。
练习
练习 1(tabbar + 二级路由) 给 tabbar 的“我的”加一个未读消息徽标(dot 或 :badge="3"),并验证:从 /user 手动改地址栏跳到 /login,再登录返回后 tabbar 高亮是否仍正确。说明为什么 tabbar 不需要你手写“当前选中项”的变量。
练习 2(主题色排查) 把主题色改成 #FA6D1D 之外的另一个颜色,然后故意注释掉 babel.config.js 里的 style 配置,观察现象;恢复后再故意不重启服务,观察现象。写出这两个实验各自的结论,并总结出“改主题色不生效”时的排查顺序。
练习 3(表单校验) 在注册页增加“确认密码”字段,要求:① 与密码不一致时给出提示;② 该字段不参与提交给接口。提示:思考 name 属性在这里应该怎么写、rules 的 validator 自定义函数如何拿到 password 的值。
练习 4(作用域插槽进阶) 让表格组件的最后一列同时放“编辑”和“查看”两个按钮,点击时都能拿到当前行的 id 和 name。要求父组件用解构方式接收,并说明如果只写 #default="obj" 不写解构,代码要怎么改。
练习 5(插槽兜底) 给表格组件最后一列的 <slot> 写默认内容“暂无操作”,然后分别测试:① 父组件不传任何结构;② 父组件传一个按钮。解释两次渲染结果的差异,并说明这个特性在组件复用中的价值。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 作用域插槽案例-渲染结构 | 09:42 |
| 2 | 作用域插槽案例-传递id | 05:22 |
| 3 | 作用域插槽案例-细节说明 | 05:27 |
| 4 | 插槽总结 | 10:44 |
| 5 | 作业安排 | 16:42 |
小结
- tabbar 的“选中态”本质上是路由状态的投影,只要开启
route模式并把to写对,高亮就不需要额外维护——凡是需要你手动同步 UI 状态的地方,优先想想有没有现成的响应式来源。 - 嵌套路由的骨架只有两件事:父级有
<router-view>,子级path不带斜杠。二者任一出错,症状都是“空白页”,排查时应先看这两处而不是去翻组件代码。 - Vant 主题定制的本质是 less 变量覆盖,它依赖一条完整的编译链路(babel 插件 → less 源文件 → 变量覆盖 → 重启)。链路型问题不要靠猜,用“逐环断开”的方式定位最快。
- 表单三件套各司其职:
v-model管值、name管字段名、:rules管校验。三者混淆是表单类 bug 的高发区,尤其name一旦漏写,接口字段就对不上,且错误往往延后到联调才暴露。 - 作用域插槽解决的是“结构由父组件定、数据由子组件出”的矛盾。判断要不要用它,只需问一句:这块内容子组件能不能写死?能写死就用普通插槽,写不死才升级为作用域插槽。
- 父组件传给默认插槽的结构会被分发到每一个插槽实例上,“出现三份”不是 bug 而是机制。理解这一点,才能正确预判列表类组件的渲染结果。
- 老语法
slot/slot-scope虽然废弃但仍在文档与第三方组件库中大量存在,看到不认识的写法先别慌,把它和v-slot/#做等价替换验证,往往一眼就能看懂。 - 工程化配置(ESLint、格式化、主题、别名)都属于“一次性投入、长期收益”的部分,配置阶段多花十分钟,后面能省下大量格式争吵与返工。