主题
路由与通用组件封装(二)
本页对应 路由与通用组件封装,共 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 目录)维护,登录页只负责“调用 + 处理结果”,不要把请求地址散落在组件里。
常见错误
点「登录」按钮毫无反应,两个输入框都不报错 现象:输入为空点登录,页面无任何校验提示。 原因:按钮是
type="button",不会触发表单提交,submit事件根本不触发。 修法:给van-button加native-type="submit",把校验逻辑挂到form的@submit上。rules.ts里写规则时没有代码提示,pattern拼错也不报错 现象:规则对象里required、pattern全无补全,运行期才发现校验不生效。 原因:导出的是裸数组,没有声明FieldRule[]类型,TS 推不出结构。 修法:从 vant 以import type { FieldRule } from 'vant'引入类型,规则变量标注FieldRule[]。发送验证码时手机号为空/格式错误,请求照样发出去 现象:没填手机号点「发送验证码」,接口被调用,后台报错或发错号。 原因:只判断了倒计时,忘了单独校验手机号项;或
van-field没写name,form.validate()找不到目标项。 修法:给手机号van-field加name="mobile",在onSend里await form.value?.validate('mobile'),失败即return。倒计时数字乱跳、点击多次后越减越快,或离开页面仍在跑 现象:连点几次发送按钮,秒数一次掉好几;切换路由后定时器仍在执行。 原因:重复
setInterval未先清理;组件卸载未clearInterval。 修法:开启前先clearInterval(timer),用let timer: number保存 id,并在onUnmounted里清理。登录成功后跳转报类型错误,或跳到
[object Object]之类的地址 现象:router.replace(route.query.returnUrl)处 TS 飘红;带参数时跳转异常。 原因:query的值类型是string | string[] | null,不能直接当字符串用。 修法:断言为string,并做兜底:有值replace(returnUrl as string),无值replace('/user')。切到短信登录后,点登录仍走密码接口 / 提示密码错误 现象:短信验证码填对了,登录仍失败。 原因:
onSubmit里没有按isPass分支,始终调用loginByPassword。 修法:用三元或if/else按isPass分发到loginByPassword与loginByMobile。<CpIcon name="login-eye-on" />渲染不出图标 现象:页面空白,控制台无报错;或图标显示但尺寸巨大。 原因:name拼的路径与src/icons下的目录/文件名不一致;main.ts未引入虚拟注册模块;未设1em宽高。 修法:按「目录名-文件名」核对name,确认已import 'virtual:svg-icons-register',给组件加width/height: 1em。点眼睛图标后图标变了,但密码仍是一串圆点 现象:图标在睁/闭之间切换正常,输入框内容始终不可见。 原因:只切换了图标,
van-field的type仍写死为password。 修法::type="isShow ? 'text' : 'password'",与图标共用同一个布尔值。
练习
给登录页的「密码 / 短信验证码」两个表单项都加上
name,把onSubmit里的整体校验改成显式调用form.value?.validate(),并验证:任一字段不合法时接口不会被调用。把
rules.ts里的三条规则(手机号、密码、验证码)统一加上FieldRule[]类型,然后故意把pattern写错一个字符,观察编辑器是否提前飘红,并说明类型声明在这里起到了什么作用。给发送验证码按钮补全健壮性:先
clearInterval再开启,把timer存成number,并在组件卸载时清理。测试方式——连续快速点击 3 次按钮,确认秒数仍按 1 秒递减。模拟一次 401 回跳:在地址栏手动拼上
?returnUrl=/user/profile,登录成功后确认跳转到该地址;再清空参数登录,确认落到/user。同时用 TS 检查route.query.returnUrl不加断言时是否报错。用
CpIcon实现密码可见切换的完整交互,并额外要求:密码框为空时不显示眼睛图标(用v-if控制),输入内容后才出现。
课件图示






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