Skip to content

路由与通用组件封装(二) ​

本页对应 路由与通用组件封装,共 7 段视频,约 1 小时 21 分。

学习目标 ​

  • 能够使用 Vue 的 ref 与 rules 为手机号、密码、短信验证码配置必填和格式校验,并将校验规则抽离到独立模块中复用。
  • 能够将登录按钮改为 submit 类型触发整体表单校验,并在提交事件中校验协议勾选状态,未勾选时阻止提交并给出轻提示。
  • 能够封装密码登录、短信登录和发送验证码 API 函数,并在登录页根据当前登录模式调用对应接口,成功后存储用户信息、提示成功并处理 returnUrl 回跳。
  • 能够通过状态变量切换密码登录与短信登录的标题、按钮和表单项,并使用 button 插槽添加发送验证码按钮。
  • 能够实现发送验证码前的倒计时与手机号校验,完成 60 秒倒计时、按钮文案与样式切换,并在倒计时结束及组件卸载时清理定时器。
  • 能够配置 vite-plugin-svg-icons 打包 SVG 地图,封装可复用的 SVG 图标组件,并使用该组件完成密码可见与不可见切换功能。

知识点 ​

先明确本节目标 ​

本节只做登录页的表单校验:手机号必填且格式正确、密码必填且为 8–24 个字符、点击登录时整体校验两个表单项,并在提交前校验“是否勾选协议”。后面的密码登录、短信登录不属于本节范围。


手机号校验:先绑定数据,再加规则 ​

为什么需要:手机号是登录账号,既不能为空,也不能随便输入一串数字,否则请求发到后台也会失败。

怎么想:Vant 的表单校验和 Element Plus 类似,表单项通过 rules 属性接收规则数组。先把输入框和响应式数据绑定,再写规则。规则先写“必填”,验证生效后再补“格式”。

怎么做:创建 mobile 响应式数据,用 v-model 绑定到 van-field;在 rules 中写 required: true 和 pattern 正则。手机号正则按“1 开头、第二位 3–9、后面 9 个数字”写为 /^1[3-9]\d{9}$/。

vue
<van-field
  v-model="mobile"
  name="mobile"
  :rules="mobileRules"
  placeholder="请输入手机号"
/>
ts
export const mobileRules = [
  { required: true, message: '请输入手机号' },
  { pattern: /^1[3-9]\d{9}$/, message: '手机号不正确' }
]

易错点:只写 required 只能校验空值,格式错误仍会通过;pattern 要写完整首尾锚点,否则可能只匹配部分字符串。调试时先让输入框失焦,看必填提示是否出现;再输入 13212345678 看是否通过,输入错误号码看“手机号不正确”是否出现。


把校验规则提取到外部模块 ​

为什么需要:规则直接写在模板里会塞进大量 JS 代码,不好维护;而且手机号规则在很多页面都会复用,写在当前组件里无法共享。

怎么想:把规则抽到独立的 rules.ts 中,登录页只负责导入和使用。这样以后密码、验证码等规则也能统一维护。

怎么做:新建 rules.ts,定义并导出 mobileRules;在登录页导入后绑定到 :rules。转写稿中先验证了模板内规则可用,再提取到外部,刷新浏览器后必填和格式校验仍然正常,说明提取成功。

ts
// rules.ts
import type { FieldRules } from 'vant'

export const mobileRules: FieldRules = [
  { required: true, message: '请输入手机号' },
  { pattern: /^1[3-9]\d{9}$/, message: '手机号不正确' }
]

易错点:外部模块中的规则数组最好加上 FieldRules 类型,否则写 required、pattern 时没有类型提示;导入路径要按项目别名配置写,不要凭感觉写相对路径。


密码校验:同样的套路,别忘了类型提示 ​

为什么需要:密码不能为空,且长度要符合前后端约定,否则提交后后台仍会报错。

怎么想:和手机号一样,先准备 password 数据并绑定,再在 rules.ts 中新增 passwordRules。密码规则是“必填 + 8 到 24 个字符”,正则可写为 /^\w{8,24}$/。

怎么做:

vue
<van-field
  v-model="password"
  type="password"
  name="password"
  :rules="passwordRules"
  placeholder="请输入密码"
/>
ts
export const passwordRules: FieldRules = [
  { required: true, message: '请输入密码' },
  { pattern: /^\w{8,24}$/, message: '密码必须是8到24个字符' }
]

转写稿里有一个关键调试过程:在 rules.ts 中直接写 required 没有提示,把鼠标放到模板的 rules 属性上才发现需要 FieldRules 类型;加上类型后,required、pattern 才有提示。这说明类型不是装饰,而是能提前发现拼写和配置错误。

易错点:密码输入框要设置 type="password",否则明文显示;长度正则要写 {8,24},不要写成 {8-24};规则数组不要漏掉 message,否则用户不知道错在哪。


整体表单校验:按钮类型必须是 submit ​

为什么需要:用户点击“登录”时,要一次性校验手机号和密码,而不是只校验当前失焦的某一项。

怎么想:Vant 的 van-form 在提交时会统一校验所有表单项。但按钮默认是普通 button,不会触发表单提交,所以要把按钮的 native-type 改成 submit。

怎么做:

vue
<van-form ref="formRef" @submit="onSubmit">
  <!-- 手机号、密码表单项 -->
  <van-button native-type="submit" type="primary" block>登录</van-button>
</van-form>
ts
const onSubmit = () => {
  console.log('login')
  // 将来在这里进行登录
}

调试时先不改按钮类型,点击登录没有任何校验;改成 native-type="submit" 后,空表单点击登录,两个表单项的提示会同时出现;输入正确内容后,控制台打印 login,说明 onSubmit 是在校验成功后才触发的。

易错点:不要写成 type="submit",Vant 按钮要用 native-type;onSubmit 只会在校验通过后执行,所以不要在里面重复写“校验手机号和密码”的逻辑,除非你有特殊需求。


勾选协议校验:提交成功之后、登录之前 ​

为什么需要:手机号和密码都正确,并不代表用户同意协议;如果不勾选协议就登录,业务上是不允许的。

怎么想:onSubmit 触发时,说明表单项已经校验通过。此时再判断 agree 是否为 true;如果没勾选,用轻提示告诉用户并阻止后续登录。

怎么做:给复选框绑定 agree,在 onSubmit 里先判断:

ts
const agree = ref(false)

const onSubmit = () => {
  if (!agree.value) {
    showToast('请勾选协议')
    return
  }
  // 校验全部通过,将来在这里登录
  console.log('login')
}
vue
<van-checkbox v-model="agree">我已同意用户协议</van-checkbox>

调试过程:输入正确手机号和密码,不勾选协议点击登录,页面弹出“请勾选协议”,控制台没有打印 login;勾选后再点击,提示消失并打印 login,说明校验顺序正确。

易错点:agree 默认值要设成 false;判断时用 !agree.value,不要漏掉 .value;showToast 后要 return,否则代码会继续往下执行登录逻辑。


本节小结 ​

表单校验的顺序是:先做单字段校验(手机号、密码),再把规则提取到 rules.ts 复用;然后让按钮以 submit 方式触发整体校验;最后在 onSubmit 中补上协议勾选判断。调试时按“失焦看单项提示 → 点击登录看整体提示 → 不勾选看提示 → 勾选看通过”的顺序验证,能快速定位问题。

先明确密码登录的完整链路 ​

密码登录不是只写一个“点击登录”的函数,它包含一条完整链路:先让手机号、密码通过表单校验,再校验用户协议,校验通过后调用登录接口,拿到用户信息并存储,提示成功,最后根据地址栏里的 returnUrl 决定回跳页面。转写稿里这一节先做表单校验,再做密码登录,顺序不能乱。下面按“为什么需要 → 怎么想 → 怎么做 → 易错点”拆开。

手机号与密码的表单校验 ​

为什么需要:手机号必须填写且格式正确,密码必须填写且长度符合要求;否则用户随便输入也能点登录,接口会报错,体验很差。

怎么想:把校验规则挂在表单项上。手机号两条规则:必填、正则;密码两条规则:必填、8 到 24 个字符。规则多,先放模板里能跑,再抽取到独立模块,方便其他页面复用。

怎么做:先绑定数据,再加 rules。手机号正则可以写成 /^1[3-9]\d{9}$/,密码可写成 /^\w{8,24}$/。在 rules.ts 中维护:

ts
import type { FieldRules } from 'vant'

export const mobileRules: FieldRules = [
  { required: true, message: '请输入手机号' },
  { pattern: /^1[3-9]\d{9}$/, message: '手机号不正确' },
]

export const passwordRules: FieldRules = [
  { required: true, message: '请输入密码' },
  { pattern: /^\w{8,24}$/, message: '密码必须是8到24个字符' },
]

模板中使用:

vue
<van-field
  v-model="mobile"
  name="mobile"
  :rules="mobileRules"
  placeholder="请输入手机号"
/>
<van-field
  v-model="password"
  name="password"
  type="password"
  :rules="passwordRules"
  placeholder="请输入密码"
/>

调试时转写稿里先让手机号输入框失焦,出现“请输入手机号”;输入 13212345678 正常,输入错误号码提示“手机号不正确”。密码输入 8 位正常,输入 7 位提示“密码必须是8到24个字符”。

易错点:规则数组要加类型 FieldRules,否则在 rules.ts 里写 required、pattern 没有提示;正则不要漏掉首尾 ^、$;手机号第二位是 3 到 9。

整体表单校验与协议校验 ​

为什么需要:用户可能只改了手机号没改密码,或者两个都填了但没勾协议。点击登录时应该一次性校验两个表单项,并在提交前拦住未勾协议的情况。

怎么想:Vant 表单在提交时会统一校验表单项;但按钮默认是普通 button,不会触发表单提交。需要把按钮改成原生提交类型。协议不是表单项,所以放在 submit 成功事件里单独判断。

怎么做:按钮写 native-type="submit",表单监听 @submit="onSubmit"。协议用 agree 布尔值绑定复选框:

vue
<van-checkbox v-model="agree">我已同意用户协议</van-checkbox>
<van-button native-type="submit" block>登录</van-button>
ts
const agree = ref(false)

const onSubmit = async () => {
  if (!agree.value) {
    showToast('请勾选协议')
    return
  }
  // 校验已通过,继续登录
}

转写稿里验证过:未改按钮类型时点登录没有校验;改成 native-type="submit" 后,手机号和密码的错误提示同时出现。表单都通过但未勾协议时,会弹出“请勾选协议”。

易错点:Vant Button 默认 native-type 是 button,不是 submit;协议校验必须在接口调用前 return,否则仍会登录。

封装密码登录 API ​

为什么需要:页面组件不应该直接写请求地址和请求参数,接口统一放 service/user.ts,以后复用和改地址都方便。

怎么想:密码登录接口是 POST,地址 /login/password,参数 mobile、password,返回 User。用项目里的 request 工具函数发请求,并把返回类型传给泛型。

怎么做:

ts
import request from '@/utils/request'
import type { User } from '@/types/user'

export const loginByPassword = (mobile: string, password: string) => {
  return request<User>('/login/password', 'post', {
    mobile,
    password,
  })
}

登录页调用:

ts
const res = await loginByPassword(mobile.value, password.value)
console.log(res)

转写稿里用测试账号 13212345678、密码 ABC12345 登录,控制台能打印结果;密码输错时不会打印结果,并出现错误提示。测试账号是 1323 开头、1 到 100 可用,密码统一 ABC12345。

易错点:request 的三个参数顺序是地址、方式、数据;返回类型别漏 User;接口失败时 await 后面的代码不会继续执行。

登录成功后的存储、提示与回跳 ​

为什么需要:登录成功后要把用户信息存到仓库,供后续页面使用;要提示成功;还要处理 token 失效时带来的 returnUrl,让用户回到原来页面。

怎么想:成功结果在 res.data,存到用户仓库;提示用成功 toast;跳转前判断 router.currentRoute.value.query.returnUrl,有就回跳,没有就默认 /user。用 replace 是为了登录成功后回退不再看到登录页。

怎么做:

ts
const userStore = useUserStore()
const router = useRouter()

const res = await loginByPassword(mobile.value, password.value)
userStore.setUser(res.data)
showSuccessToast('登录成功')

const returnUrl = router.currentRoute.value.query.returnUrl as string
router.replace(returnUrl || '/user')

转写稿里验证:登录成功后应用存储中能看到用户信息;地址栏带 returnUrl 时会跳到该地址,不带时跳到 /user。

易错点:用户信息在 res.data,不要直接存 res;query.returnUrl 可能是 undefined,转写稿里用了类型断言 as string,实际项目也可先判空;跳转用 replace 而不是 push,避免回退到登录页。

先明确本节目标:只做“短信登录界面切换” ​

短信登录功能分成三步:切换界面、发送验证码、用验证码登录。本节只完成第一步。密码登录和短信登录并不是两个独立页面,而是共用同一个登录表单:标题、按钮文字、密码/验证码表单项会随状态切换。这样能避免复制一份页面,也方便后续把两种登录的提交逻辑合并到同一个 onSubmit 里。

知识点一:用布尔状态控制标题和按钮 ​

为什么需要:界面有两种状态,如果写两套 DOM,维护成本高。
怎么想:准备一个 isPass,true 表示密码登录,false 表示短信登录。标题和按钮文字都由它决定。
怎么做:在登录页声明 const isPass = ref(true),模板中用三元表达式显示文字;点击切换按钮时取反。
易错点:按钮文字的逻辑容易写反。isPass 为真时当前是密码登录,按钮应提示“短信验证码登录”;否则提示“密码登录”。

ts
const isPass = ref(true)
html
<h3>{{ isPass ? '密码登录' : '短信登录' }}</h3>

<button @click="isPass = !isPass">
  {{ isPass ? '短信验证码登录' : '密码登录' }}
</button>

调试时刷新页面:默认显示“密码登录”,点按钮后标题和按钮同时变化,再点一次恢复,说明状态控制生效。

知识点二:用 v-if / v-else 切换表单项 ​

为什么需要:密码登录要输入密码,短信登录要输入验证码,两者不能同时展示。
怎么想:继续复用 isPass。密码表单项用 v-if="isPass",验证码表单项用 v-else。
怎么做:先声明 const code = ref(''),再写验证码输入框并绑定 v-model="code"。验证码输入框右侧的“发送验证码”按钮,通过 Vant Field 的 #button 具名插槽插入。
易错点:v-if 和 v-else 必须相邻;插槽名是 #button;验证码字段要先声明数据再绑定;发送按钮可复用已有 btn-send 样式。

ts
const code = ref('')
html
<van-field
  v-if="isPass"
  v-model="password"
  type="password"
  placeholder="请输入密码"
  :rules="passwordRules"
/>

<van-field
  v-else
  v-model="code"
  placeholder="短信验证码"
  :rules="codeRules"
>
  <template #button>
    <span class="btn-send">发送验证码</span>
  </template>
</van-field>

浏览器验证:切到短信登录后,密码框消失,验证码框出现,右侧有“发送验证码”按钮;切回密码登录,验证码框隐藏。

知识点三:验证码校验规则集中到 rules 模块 ​

为什么需要:验证码是必填,并且格式应为 6 个数字。规则如果散落在模板里,不好维护,也不利于复用。
怎么想:延续手机号、密码规则的做法,把验证码规则放到 rules 模块中统一导出,再在登录页导入使用。
怎么做:定义 codeRules: FieldRule[],第一条 required: true,第二条用 pattern 校验 6 位数字,并导出。登录页导入后绑定到验证码表单项。
易错点:规则数组要加 FieldRule[] 类型,否则 TS 提示弱;正则要锚定开头结尾;required 和 pattern 是两条规则,不要只写一条。

ts
import type { FieldRule } from 'vant'

export const codeRules: FieldRule[] = [
  { required: true, message: '请输入短信验证码' },
  { pattern: /^\d{6}$/, message: '验证码是6个数字' }
]
ts
// 从 rules 模块导入 codeRules 后
<van-field v-else v-model="code" placeholder="短信验证码" :rules="codeRules">
  <template #button>
    <span class="btn-send">发送验证码</span>
  </template>
</van-field>

调试过程:切到短信登录,验证码框失去焦点时提示“请输入短信验证码”;输入 123456 不再报错;输入不足 6 位或非数字时提示“验证码是6个数字”。到这里,短信登录的界面切换和验证码校验就完成了,下一节再实现发送验证码。

代码示例 ​

1. src/utils/rules.ts —— 校验规则统一维护(手机号 / 密码 / 验证码) ​

ts
// 校验规则统一放在外部模块维护,任何页面都能复用
import type { FieldRule } from 'vant'

// 手机号:必填 + 格式(1 开头,第二位 3-9,后面 9 位数字)
export const mobileRules: FieldRule[] = [
  { required: true, message: '请输入手机号' },
  { pattern: /^1[3-9]\d{9}$/, message: '手机号不正确' }
]

// 密码:必填 + 8-24 个字符
export const passwordRules: FieldRule[] = [
  { required: true, message: '请输入密码' },
  { pattern: /^\w{8,24}$/, message: '密码必须是8到24个字符' }
]

// 短信验证码:必填 + 6 个数字
export const codeRules: FieldRule[] = [
  { required: true, message: '请输入短信验证码' },
  { pattern: /^\d{6}$/, message: '验证码是6个数字' }
]

关键行与易错点

  • import type { FieldRule } from 'vant':转写稿强调“导类型时前面要加 type”。不加类型注解时,required、pattern、message 全都没有提示,还容易拼错。
  • 规则数组的类型用 FieldRule[]:required 为 true 表示必填;pattern 写正则;message 是校验失败时的提示语。不要把规则写成字符串或对象,必须是数组。
  • 正则别丢首尾锚点:/^1[3-9]\d{9}$/,漏掉 ^ 或 $ 会导致“132123456789”这种超长号码也能通过校验。
  • 规则从模板里抽到独立模块后,登录页只需 import 后交给 :rules 使用;后续其它页面用到手机号输入框可以直接复用。

2. src/views/login/index.vue —— 表单校验 + 短信/密码界面切换 + 协议校验 + 提交 ​

vue
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { showToast, showSuccessToast } from 'vant'
import type { FormInstance } from 'vant'
import { mobileRules, passwordRules, codeRules } from '@/utils/rules'
import { loginByPassword, loginByMobile, sendMobileCode } from '@/service/user'
import { useUserStore } from '@/stores/user'

// 表单实例:加了 FormInstance 类型,调用 validate 才有提示
const form = ref<FormInstance>()
const mobile = ref('')
const password = ref('')
const code = ref('')
const agree = ref(false)   // 是否勾选协议
const isPass = ref(true)   // true=密码登录  false=短信登录

const router = useRouter()
const route = useRoute()
const userStore = useUserStore()

/* ---------- 提交(表单整体校验通过后才会触发) ---------- */
const onSubmit = async () => {
  // 表单项都过了,还要校验协议勾选
  if (!agree.value) return showToast('请勾选协议')

  // 根据 isPass 合并两种登录方式
  const res = isPass.value
    ? await loginByPassword(mobile.value, password.value)
    : await loginByMobile(mobile.value, code.value)

  userStore.setUser(res.data)          // 存储用户信息
  showSuccessToast('登录成功')
  // 有回跳地址就回跳,否则去个人中心
  router.replace((route.query.returnUrl as string) || '/user')
}

/* ---------- 发送短信验证码 ---------- */
const time = ref(0)                    // 倒计时秒数,>0 表示正在倒计时
let timer: number | undefined

const onSend = async () => {
  if (time.value > 0) return           // 正在倒计时,不允许重复发送
  await form.value?.validate('mobile') // 只校验手机号这一项,失败会中断后续代码
  await sendMobileCode(mobile.value, 'login')
  showToast('发送成功')

  time.value = 60
  if (timer) clearInterval(timer)      // 严谨:先清理再开启,避免重复定时器
  timer = window.setInterval(() => {
    time.value--
    if (time.value <= 0) clearInterval(timer)
  }, 1000)
}

// 组件销毁时清理定时器
onUnmounted(() => clearInterval(timer))
</script>

<template>
  <h3>{{ isPass ? '密码登录' : '短信登录' }}</h3>

  <!-- 校验通过才会触发 submit -->
  <van-form ref="form" @submit="onSubmit">
    <van-field
      v-model="mobile"
      name="mobile"
      :rules="mobileRules"
      placeholder="请输入手机号"
    />

    <!-- 密码登录表单项 -->
    <van-field
      v-if="isPass"
      v-model="password"
      name="password"
      type="password"
      :rules="passwordRules"
      placeholder="请输入密码"
    />

    <!-- 短信登录表单项 -->
    <van-field
      v-else
      v-model="code"
      name="code"
      :rules="codeRules"
      placeholder="请输入短信验证码"
    >
      <!-- button 插槽:右侧自定义按钮 -->
      <template #button>
        <span class="btn-send" :class="{ active: time > 0 }" @click="onSend">
          {{ time > 0 ? `${time}秒后再次发送` : '发送验证码' }}
        </span>
      </template>
    </van-field>

    <van-checkbox v-model="agree">我已同意用户协议</van-checkbox>

    <van-button native-type="submit">
      {{ isPass ? '密码登录' : '短信登录' }}
    </van-button>

    <!-- 切换界面 -->
    <div @click="isPass = !isPass">
      {{ isPass ? '短信验证码登录' : '密码登录' }}
    </div>
  </van-form>
</template>

<style scoped lang="scss">
.btn-send {
  color: var(--cp-primary);
  &.active { color: #999; }   /* 倒计时中文字变浅 */
}
</style>

关键行与易错点

  • native-type="submit" 是最容易漏的一行。按钮默认是 button 类型,不会触发表单校验;改成 submit 后点击才会对两个表单项一起校验,校验通过才触发 @submit。
  • name="mobile" / name="password" / name="code" 必须写:form.value.validate('mobile') 就是靠 name 找到要校验的那一项,不写名字就只能整体校验。
  • 切换界面只用一个 isPass 取反:标题、按钮文字、表单项(v-if / v-else)全部由它控制,不要给每个元素各写一个状态。
  • 发送验证码前有两道拦截:time.value > 0 直接 return;await form.value?.validate('mobile') 校验失败会抛错并中断,后面的发送逻辑不会执行。
  • time.value = 60 前后必须做定时器清理,否则连点会叠加多个 setInterval;onUnmounted 里再清一次,防止离开页面后定时器还在跑。
  • 协议校验放在 onSubmit 里(表单校验成功之后),!agree.value 时 showToast('请勾选协议') 并 return,阻止提交。

3. src/service/user.ts —— 登录 / 验证码接口封装 ​

ts
import request from '@/utils/request'
import type { User } from '@/types/user'

// 密码登录:post /login/password,参数 mobile + password,返回 User
export const loginByPassword = (mobile: string, password: string) =>
  request<User>('/login/password', 'post', { mobile, password })

// 短信登录:post /login,参数 mobile + code,返回同样是 User
export const loginByMobile = (mobile: string, code: string) =>
  request<User>('/login', 'post', { mobile, code })

// 发送验证码的类型:所有业务场景共用同一个接口,用 type 区分
// (转写稿只念出了 login / reg,其余场景取值以接口文档为准)
export type CodeType = 'login' | 'register'

// 发送验证码:get /code,参数 mobile + type
export const sendMobileCode = (mobile: string, type: CodeType) =>
  request('/code', 'get', { mobile, type })

关键行与易错点

  • request 三个参数顺序固定:地址 → 请求方式 → 提交数据。密码登录是 post /login/password,短信登录是 post /login,两者地址不同,别写反。
  • 泛型要传返回数据的类型:request<User>(...),短信登录和密码登录返回的都是用户信息,所以都是 User,登录成功后的处理逻辑可以完全一样。
  • type 用字面量联合类型而不是 string:这样调用 sendMobileCode(mobile.value, 'login') 时传错值会直接报错,编辑器也有提示。
  • 接口函数统一放在 service 目录(相当于以前的 api 目录)维护,登录页只负责“调用 + 处理结果”,不要把请求地址散落在组件里。

常见错误 ​

  1. 点「登录」按钮毫无反应,两个输入框都不报错 现象:输入为空点登录,页面无任何校验提示。 原因:按钮是 type="button",不会触发表单提交,submit 事件根本不触发。 修法:给 van-button 加 native-type="submit",把校验逻辑挂到 form 的 @submit 上。

  2. rules.ts 里写规则时没有代码提示,pattern 拼错也不报错 现象:规则对象里 required、pattern 全无补全,运行期才发现校验不生效。 原因:导出的是裸数组,没有声明 FieldRule[] 类型,TS 推不出结构。 修法:从 vant 以 import type { FieldRule } from 'vant' 引入类型,规则变量标注 FieldRule[]。

  3. 发送验证码时手机号为空/格式错误,请求照样发出去 现象:没填手机号点「发送验证码」,接口被调用,后台报错或发错号。 原因:只判断了倒计时,忘了单独校验手机号项;或 van-field 没写 name,form.validate() 找不到目标项。 修法:给手机号 van-field 加 name="mobile",在 onSend 里 await form.value?.validate('mobile'),失败即 return。

  4. 倒计时数字乱跳、点击多次后越减越快,或离开页面仍在跑 现象:连点几次发送按钮,秒数一次掉好几;切换路由后定时器仍在执行。 原因:重复 setInterval 未先清理;组件卸载未 clearInterval。 修法:开启前先 clearInterval(timer),用 let timer: number 保存 id,并在 onUnmounted 里清理。

  5. 登录成功后跳转报类型错误,或跳到 [object Object] 之类的地址 现象:router.replace(route.query.returnUrl) 处 TS 飘红;带参数时跳转异常。 原因:query 的值类型是 string | string[] | null,不能直接当字符串用。 修法:断言为 string,并做兜底:有值 replace(returnUrl as string),无值 replace('/user')。

  6. 切到短信登录后,点登录仍走密码接口 / 提示密码错误 现象:短信验证码填对了,登录仍失败。 原因:onSubmit 里没有按 isPass 分支,始终调用 loginByPassword。 修法:用三元或 if/else 按 isPass 分发到 loginByPassword 与 loginByMobile。

  7. <CpIcon name="login-eye-on" /> 渲染不出图标 现象:页面空白,控制台无报错;或图标显示但尺寸巨大。 原因:name 拼的路径与 src/icons 下的目录/文件名不一致;main.ts 未引入虚拟注册模块;未设 1em 宽高。 修法:按「目录名-文件名」核对 name,确认已 import 'virtual:svg-icons-register',给组件加 width/height: 1em。

  8. 点眼睛图标后图标变了,但密码仍是一串圆点 现象:图标在睁/闭之间切换正常,输入框内容始终不可见。 原因:只切换了图标,van-field 的 type 仍写死为 password。 修法::type="isShow ? 'text' : 'password'",与图标共用同一个布尔值。

练习 ​

  1. 给登录页的「密码 / 短信验证码」两个表单项都加上 name,把 onSubmit 里的整体校验改成显式调用 form.value?.validate(),并验证:任一字段不合法时接口不会被调用。

  2. 把 rules.ts 里的三条规则(手机号、密码、验证码)统一加上 FieldRule[] 类型,然后故意把 pattern 写错一个字符,观察编辑器是否提前飘红,并说明类型声明在这里起到了什么作用。

  3. 给发送验证码按钮补全健壮性:先 clearInterval 再开启,把 timer 存成 number,并在组件卸载时清理。测试方式——连续快速点击 3 次按钮,确认秒数仍按 1 秒递减。

  4. 模拟一次 401 回跳:在地址栏手动拼上 ?returnUrl=/user/profile,登录成功后确认跳转到该地址;再清空参数登录,确认落到 /user。同时用 TS 检查 route.query.returnUrl 不加断言时是否报错。

  5. 用 CpIcon 实现密码可见切换的完整交互,并额外要求:密码框为空时不显示眼睛图标(用 v-if 控制),输入内容后才出现。

课件图示 ​

课件第34页:短信登录-发送短信验证码 切换界面 发送短信 验证码 进行登录

课件第37页:短信登录 切换界面 发送短信 验证码 进行登录

课件第38页:短信登录-进行登录 切换界面 发送短信 验证码 进行登录 提示:开发中发送短信验

课件第31页:登录页面-短信登录-切换界面 标题按钮切换 表单项切换 验证码校验

课件第27页:登录页面-密码登录 登录API函数 进行登录 存储用户信息 成功提示 是否有回

课件第42页:图标组件-打包svg地图 安装:pnpm install vite-plugin

原视频对照 ​

#视频时长
1登录页面-表单校验16:55
2登录页面-密码登录11:22
3短信登录-切换界面10:23
4短信登录-发送验证码17:09
5短信登录-进行登录07:01
6图标组件-打包svg地图08:04
7图片组件-svg组件封装10:12

小结 ​

  1. 表单校验的三层是递进的:单字段规则 → submit 触发整体校验 → 提交回调里再补一次协议勾选,缺任何一层都会漏判。
  2. 校验规则一定要外置到独立模块并标注 FieldRule[] 类型,既复用到其他页面,也让 TS 在写错属性名时立刻报错。
  3. 只要按钮不是 native-type="submit",vant 表单的校验链就不会启动,这是「点了没反应」最高频的原因。
  4. 接口函数统一放 service 层,用 request 泛型带上返回类型(如 User),页面只负责「调用 → 存 store → 提示 → 跳转」。
  5. 登录后的跳转必须做 returnUrl 优先、/user 兜底的二分支处理,且要用 replace 避免回退又回到登录页。
  6. 短信登录不是新页面,而是复用同一套表单和提交回调,靠一个布尔状态分流接口;验证码在开发期从响应报文里取。
  7. 倒计时属于「有副作用的共享状态」:开前清理、卸载清理、按钮文案与禁用样式都由同一个 time 驱动。
  8. SVG 地图解决批量图标加载,CpIcon 组件解决使用体验——业务里应当只出现 <CpIcon name="...">,不再手写 <svg><use> 三段式。