登录模块展示
添加登录路由
为后续路由模块化,将路由信息单独抽离封装
src\router\routes.ts
import type { RouteRecordRaw } from 'vue-router'
const routes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('@/views/login/index.vue'),
meta: {
title: '登录'
}
}
]
export default routes
封装nav-bar组件
支持组件的自定义返回功能,支持title、rightText属性,支持click-right事件
<script setup lang="ts">
import { useRouter } from 'vue-router'
const router = useRouter()
const props = defineProps<{
title: string
rightText?: string
back?: () => void
}>()
const emit = defineEmits<{
(e: 'click-right'): void
}>()
// 点击左侧返回按钮
const onClickLeft = () => {
// 支持自定义返回
if (props.back) {
return props.back()
}
// 判断上次访问页面是否存在
if (history.state.back) {
// 存在就返回
router.back()
} else {
// 不存在就返回首页
router.push('/')
}
}
</script>
<template>
<van-nav-bar
fixed
left-arrow
:title="title"
:right-text="rightText"
@click-left="onClickLeft"
@click-right="emit('click-right')"
></van-nav-bar>
</template>
<style lang="scss" scoped>
:deep() {
.van-nav-bar {
/* 修改 */
&__arrow {
font-size: 18px;
color: var(--cp-text1);
}
&__text {
font-size: 15px;
}
}
}
</style>
给组件添加类型,让写属性和事件可以有提示
点击VanNavBar看下vant的组件类型声明文件
// 核心代码
// 1. 导入组件实例
import NavBar from './NavBar.vue'
// 2. 声明 vue 类型模块
declare module 'vue' {
// 3. 给 vue 添加全局组件类型,interface 和之前的合并
interface GlobalComponents {
// 4. 定义具体组件类型,typeof 获取到组件实例类型
// typeof 作用是得到对应的TS类型
VanNavBar: typeof NavBar;
}
}
在types文件夹下创建components.d.ts文件
// 为项目全局组件声明类型
import CpNavBar from '@/components/cp-nav-bar.vue'
declare module 'vue' {
interface GlobalComponents {
// 添加组件类型
CpNavBar: typeof CpNavBar
}
}
页面布局
<script setup lang="ts"></script>
<template>
<div class="login-page">
<nav-bar
right-text="注册"
@click-right="$router.push('/register')"
></nav-bar>
<!-- 头部 -->
<div class="login-head">
<h3>密码登录</h3>
<a href="javascript:;">
<span>短信验证码登录</span>
<van-icon name="arrow"></van-icon>
</a>
</div>
<!-- 表单 -->
<van-form autocomplete="off">
<van-field placeholder="请输入手机号" type="tel"></van-field>
<van-field placeholder="请输入密码" type="password"></van-field>
<div class="cp-cell">
<van-checkbox>
<span>我已同意</span>
<a href="javascript:;">用户协议</a>
<span>及</span>
<a href="javascript:;">隐私条款</a>
</van-checkbox>
</div>
<div class="cp-cell">
<van-button block round type="primary">登 录</van-button>
</div>
<div class="cp-cell">
<a href="javascript:;">忘记密码?</a>
</div>
</van-form>
<!-- 底部 -->
<div class="login-other">
<van-divider>第三方登录</van-divider>
<div class="icon">
<img src="@/assets/qq.svg" alt="" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login {
&-page {
padding-top: 46px;
}
&-head {
display: flex;
padding: 30px 30px 50px;
justify-content: space-between;
align-items: flex-end;
line-height: 1;
h3 {
font-weight: normal;
font-size: 24px;
}
a {
font-size: 15px;
}
}
&-other {
margin-top: 60px;
padding: 0 30px;
.icon {
display: flex;
justify-content: center;
img {
width: 36px;
height: 36px;
padding: 4px;
}
}
}
}
.van-form {
padding: 0 14px;
.cp-cell {
height: 52px;
line-height: 24px;
padding: 14px 16px;
box-sizing: border-box;
display: flex;
align-items: center;
.van-checkbox {
a {
color: var(--cp-primary);
padding: 0 5px;
}
}
}
.btn-send {
color: var(--cp-primary);
&.active {
color: rgba(22,194,163,0.5);
}
}
}
</style>
表单校验
提取表单校验规则(为了其他页面复用)
utils/rules.ts
// 表单校验
const mobileRules = [
{ required: true, message: '请输入手机号' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' }
]
const passwordRules = [
{ required: true, message: '请输入密码' },
{ pattern: /^\w{8,24}$/, message: '密码需8-24个字符' }
]
export { mobileRules, passwordRules }
Login/index.vue
import { mobileRules, passwordRules } from '@/utils/rules'
+ <van-field v-model="mobile" :rules="mobileRules" placeholder="请输入手机号" type="tel"></van-field>
<van-field
v-model="password"
+ :rules="passwordRules"
placeholder="请输入密码"
:type="show ? 'text' : 'password'"
>
设置button组件为原生 submit 类型按钮
<van-button block round type="primary" native-type="submit"> 登 录 </van-button>
监听表单校验成功后 submit 事件
<van-form autocomplete="off" @submit="login">
// 表单提交
const login = () => {
if (!agree.value) return Toast('请勾选我已同意')
// 验证完毕,进行登录
}
密码登录
定义一个 api 接口函数
apis/user.ts
import type { User } from '@/types/user'
import { request } from '@/utils/request'
// 密码登录
export const loginByPassword = (mobile: string, password: string) =>
request<User>('/login/password', 'POST', { mobile, password })
进行登录
Login/index.vue
import { loginByPassword } from '@/services/login'
import { useUserStore } from '@/stores'
import { useRoute, useRouter } from 'vue-router'
import { showFailToast, showSuccessToast, type FormInstance } from 'vant'
const store = useUserStore()
const router = useRouter()
const route = useRoute()
// 表单提交
const login = async () => {
if (!agree.value) return Toast('请勾选我已同意')
// 验证完毕,进行登录
const res = await loginByPassword(mobile.value, password.value)
store.setUser(res.data)
// 如果有回跳地址就进行回跳,没有跳转到个人中心
router.push((route.query.returnUrl as string) || '/user')
showSuccessToast('登录成功')
}
切换短信登录
isPass默认是密码登录,为false为短信验证码登录
const isPass = ref(true)
<div class="login-head">
<h3>{{ isPass ? '密码登录' : '短信验证码登录' }}</h3>
<a href="javascript:;" @click="isPass = !isPass">
<span>{{ !isPass ? '密码登录' : '短信验证码登录' }}</span>
<van-icon name="arrow"></van-icon>
</a>
</div>
<van-field
v-if="isPass"
v-model="password"
:rules="passwordRules"
placeholder="请输入密码"
:type="show ? 'text' : 'password'"
>
<template #button>
<cp-icon @click="show = !show" :name="`login-eye-${show ? 'on' : 'off'}`"></cp-icon>
</template>
</van-field>
<van-field v-else placeholder="短信验证码">
<template #button>
<van-button size="small" type="primary" >发送验证码 </van-button>
</template>
</van-field>
添加校验规则
utils/rules.ts
const codeRules = [
{ required: true, message: '请输入验证码' },
{ pattern: /^\d{6}$/, message: '验证码6个数字' }
]
export { mobileRules, passwordRules, codeRules }
使用规则
Login/index.vue
import { mobileRules, passwordRules, codeRules } from '@/utils/rules'
const code = ref('')
<van-field v-else v-model="code" :rules="codeRules" placeholder="短信验证码">
发送短信
定义接口
services/user.ts
+import type { CodeType, User } from '@/types/user'
import { request } from '@/utils/request'
// 密码登录
export const loginByPassword = (mobile: string, password: string) =>
request<User>('/login/password', 'POST', { mobile, password })
+// 发送验证码
+export const sendMobileCode = (mobile: string, type: CodeType) =>
+ request('/code', 'GET', { mobile, type })
定义接口类型
types/user.d.ts
// 短信验证码类型,登录|注册|修改手机号|忘记密码|绑定手机号
export type CodeType = 'login' | 'register' | 'changeMobile' | 'forgetPassword' | 'bindMobile'
发送验证码按钮添加点击事件,并设置time不为0时disabled为true
<van-button size="small" type="primary" @click="send" :disabled="time !== 0">发送验证码</van-button>
发送验证码逻辑
const time = ref(0)
const send = async () => {
// 已经倒计时time的值大于0,此时不能发送验证码
if (time.value > 0) return
}
发送验证码需要校验手机号码是否准确
<van-field
v-model="mobile"
+ name="mobile"
:rules="mobileRules"
placeholder="请输入手机号"
type="tel"
></van-field>
+ const form = ref<FormInstance>()
const time = ref(0)
const send = async () => {
if (time.value > 0) return
+ // 验证不通过报错,阻止程序继续执行
+ await form.value?.validate('mobile')
}
发送短信验证码
const send = async () => {
if (time.value > 0) return
await form.value?.validate('mobile')
+ await sendMobileCode(mobile.value, 'login')
+ showSuccessToast('发送成功')
}
点击验证码会有60秒禁用时间,组件卸载时也要清除定时器
const form = ref<FormInstance>()
const time = ref(0)
+let timeId: number
const send = async () => {
if (time.value > 0) return
await form.value?.validate('mobile')
await sendMobileCode(mobile.value, 'login')
Toast.success('发送成功')
time.value = 60
+ // 倒计时
+ clearInterval(timeId)
+ timeId = window.setInterval(() => {
+ time.value--
+ if (time.value <= 0) window.clearInterval(timeId)
+ }, 1000)
}
+onUnmounted(() => {
+ window.clearInterval(timeId)
+})
进行登录
添加接口
// 短信登录
export const loginByMobile = (mobile: string, code: string) =>
request<User>('/login', 'POST', { mobile, code })
在登录函数添加判断
// 表单提交
const login = async () => {
if (!agree.value) return Toast('请勾选我已同意')
// 验证完毕,进行登录
+ const res = isPass.value
+ ? await loginByPassword(mobile.value, password.value)
+ : await loginByMobile(mobile.value, code.value)
store.setUser(res.data)
// 如果有回跳地址就进行回跳,没有跳转到个人中心,replace目的 a => login => b 变成 a => b
router.replace((route.query.returnUrl as string) || '/user')
Toast.success('登录成功')
}
全部代码
<script setup lang="ts">
import { loginByMobile, loginByPassword, sendMobileCode } from '@/api/user'
import { useUserStore } from '@/stores'
import { mobileRules, passwordRules, codeRules } from '@/utils/rules'
import { showFailToast, showSuccessToast, type FormInstance } from 'vant'
import { onUnmounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const mobile = ref('13211112222')
const password = ref('abc12345')
const agree = ref(false)
const isShowPass = ref(false)
const isPass = ref(true)
const code = ref('')
const time = ref(0)
const form = ref<FormInstance>()
// 定时器
let timeId: number
const userStore = useUserStore()
const router = useRouter()
const route = useRoute()
// 表单提交
const login = async () => {
if (!agree.value) return showFailToast('请勾选我已同意')
// 验证完毕,进行登录
try {
const res = isPass.value
? await loginByPassword(mobile.value, password.value)
: await loginByMobile(mobile.value, code.value)
if (res?.data) {
userStore.setUser(res.data)
// 如果有回跳地址就进行回跳,没有跳转到个人中心
router.push((route.query.returnUrl as string) || '/user')
showSuccessToast('登录成功')
}
} catch (error: any) {
showFailToast(error)
}
}
const send = async () => {
// 已经倒计时time的值大于0,此时不能发送验证码
if (time.value > 0) return
// 验证不通过报错,阻止程序继续执行
await form.value?.validate('mobile')
time.value = 60
// 倒计时
clearInterval(timeId)
timeId = window.setInterval(() => {
time.value--
if (time.value <= 0) window.clearInterval(timeId)
}, 1000)
try {
// 发送验证码
const res = await sendMobileCode(mobile.value, 'login')
showSuccessToast('发送成功')
if (res?.data?.code) {
code.value = res.data.code
}
} catch (error: any) {
showFailToast(error)
}
}
onUnmounted(() => {
window.clearInterval(timeId)
})
</script>
<template>
<div class="login-page">
<nav-bar
title="登录"
right-text="注册"
@click-right="$router.push('/register')"
></nav-bar>
<!-- 头部 -->
<div class="login-head">
<h3>{{ isPass ? '密码登录' : '短信验证码登录' }}</h3>
<a href="javascript:;" @click="isPass = !isPass">
<span>{{ !isPass ? '密码登录' : '短信验证码登录' }}</span>
<van-icon name="arrow"></van-icon>
</a>
</div>
<!-- 表单 -->
<van-form autocomplete="off" @submit="login">
<van-field
placeholder="请输入手机号"
type="tel"
v-model="mobile"
:rules="mobileRules"
name="mobile"
clearable
></van-field>
<!-- isPass控制切换 -->
<van-field
v-if="isPass"
placeholder="请输入密码"
v-model="password"
:type="`${isShowPass ? 'tel' : 'password'}`"
:rules="passwordRules"
>
<template #button>
<cp-icon
:name="`login-eye-${isShowPass ? 'on' : 'off'}`"
@click="isShowPass = !isShowPass"
/>
</template>
</van-field>
<van-field
v-else
placeholder="短信验证码"
v-model="code"
:rules="codeRules"
>
<template #button>
<van-button
size="small"
type="primary"
@click="send"
:disabled="time !== 0"
>
{{ time > 0 ? `${time}s后再次发送` : '发送验证码' }}
</van-button>
</template>
</van-field>
<div class="cp-cell">
<van-checkbox v-model="agree">
<span>我已同意</span>
<a href="javascript:;">用户协议</a>
<span>及</span>
<a href="javascript:;">隐私条款</a>
</van-checkbox>
</div>
<div class="cp-cell">
<van-button block round type="primary" native-type="submit"
>登 录</van-button>
</div>
<div class="cp-cell">
<a href="javascript:;">忘记密码?</a>
</div>
</van-form>
<!-- 底部 -->
<div class="login-other">
<van-divider>第三方登录</van-divider>
<div class="icon">
<img src="@/assets/qq.svg" alt="" />
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.login {
&-page {
padding-top: 46px;
}
&-head {
display: flex;
padding: 30px 30px 50px;
justify-content: space-between;
align-items: flex-end;
line-height: 1;
h3 {
font-weight: normal;
font-size: 24px;
}
a {
font-size: 15px;
}
}
&-other {
margin-top: 60px;
padding: 0 30px;
.icon {
display: flex;
justify-content: center;
img {
width: 36px;
height: 36px;
padding: 4px;
}
}
}
}
.van-form {
padding: 0 14px;
.cp-cell {
height: 52px;
line-height: 24px;
padding: 14px 16px;
box-sizing: border-box;
display: flex;
align-items: center;
.van-checkbox {
a {
color: var(--cp-primary);
padding: 0 5px;
}
}
}
.btn-send {
color: var(--cp-primary);
&.active {
color: rgba(22, 194, 163, 0.5);
}
}
}
</style>