Skip to content

插槽(二) ​

本页对应 插槽,共 5 段视频,约 47 分钟。

学习目标 ​

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

知识点 ​

页面骨架与目标 ​

Vant 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,所以用 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>

课件图示 ​

课件第18页:目标:基于底部导航,完成二级路由配置 路由设计配置 配置二级路由 配置导航链接

课件第16页:目标:阅读vant组件库文档,实现底部导航 tabbar 路由设计配置 tabb

课件第15页:目标:分析项目页面,设计路由,配置一级路由 路由设计配置 登录 注册 首页 面经

课件第17页:目标:通过 配置主题色,实现底部导航定制 路由设计配置 配置主题色: ① bab

课件第3页:项目收获 目标:明确做完本项目,能够收获哪些内容 脚手架自定义项目构建 组件库v

课件第8页:调整初始化目录 目标:将目录调整成符合企业规范的目录 1. 删除 多余的文件 2

课件第21页:目标:将 axios 请求方法,封装到 request 模块 登录 & 注册功能

课件第27页:目标:local 模块,本地存储封装 登录 & 注册功能 // 将token存入

代码示例 ​

示例一:二级路由 + 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 router
jsx
<!-- 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作用域插槽案例-传递id05:22
3作用域插槽案例-细节说明05:27
4插槽总结10:44
5作业安排16:42

小结 ​

  1. tabbar 的“选中态”本质上是路由状态的投影,只要开启 route 模式并把 to 写对,高亮就不需要额外维护——凡是需要你手动同步 UI 状态的地方,优先想想有没有现成的响应式来源。
  2. 嵌套路由的骨架只有两件事:父级有 <router-view>,子级 path 不带斜杠。二者任一出错,症状都是“空白页”,排查时应先看这两处而不是去翻组件代码。
  3. Vant 主题定制的本质是 less 变量覆盖,它依赖一条完整的编译链路(babel 插件 → less 源文件 → 变量覆盖 → 重启)。链路型问题不要靠猜,用“逐环断开”的方式定位最快。
  4. 表单三件套各司其职:v-model 管值、name 管字段名、:rules 管校验。三者混淆是表单类 bug 的高发区,尤其 name 一旦漏写,接口字段就对不上,且错误往往延后到联调才暴露。
  5. 作用域插槽解决的是“结构由父组件定、数据由子组件出”的矛盾。判断要不要用它,只需问一句:这块内容子组件能不能写死?能写死就用普通插槽,写不死才升级为作用域插槽。
  6. 父组件传给默认插槽的结构会被分发到每一个插槽实例上,“出现三份”不是 bug 而是机制。理解这一点,才能正确预判列表类组件的渲染结果。
  7. 老语法 slot / slot-scope 虽然废弃但仍在文档与第三方组件库中大量存在,看到不认识的写法先别慌,把它和 v-slot / # 做等价替换验证,往往一眼就能看懂。
  8. 工程化配置(ESLint、格式化、主题、别名)都属于“一次性投入、长期收益”的部分,配置阶段多花十分钟,后面能省下大量格式争吵与返工。