admin 管理员组

文章数量: 1087131

Vue3+Vite+TS后台项目 ~ 6.用户登录校验


一、登录

1. 登录静态页

编辑 src / views / login / index.vue 文件

<template><div class="login-container"><el-formclass="login-form":rules="rules"ref="form":model="user"size="medium"@submit.prevent="handleSubmit"><div class="login-form__header"><imgclass="login-logo"src="@/assets/login_logo.png"alt="拉勾心选"></div><el-form-item prop="account"><el-inputv-model="user.account"placeholder="请输入用户名"><template #prefix><i class="el-input__icon el-icon-user" /></template></el-input></el-form-item><el-form-item prop="pwd"><el-inputv-model="user.pwd"type="password"placeholder="请输入密码"><template #prefix><i class="el-input__icon el-icon-lock" /></template></el-input></el-form-item><el-form-item prop="imgcode"><div class="imgcode-wrap"><el-inputv-model="user.imgcode"placeholder="请输入验证码"><template #prefix><i class="el-input__icon el-icon-key" /></template></el-input><imgclass="imgcode"alt="验证码"src=""></div></el-form-item><el-form-item><el-buttonclass="submit-button"type="primary":loading="loading"native-type="submit">登录</el-button></el-form-item></el-form></div>
</template><script lang="ts" setup>
import { reactive, ref } from 'vue'const user = reactive({account: 'admin',pwd: '123456',imgcode: ''
})
const loading = ref(false)
const rules = ref({account: [{ required: true, message: '请输入账号', trigger: 'change' }],pwd: [{ required: true, message: '请输入密码', trigger: 'change' }],imgcode: [{ required: true, message: '请输入验证码', trigger: 'change' }]
})const handleSubmit = async () => {console.log('handleSubmit')
}</script><style lang="scss" scoped>
.login-container {min-width: 400px;height: 100vh;display: flex;justify-content: center;align-items: center;background-color: #2d3a4b;
}.login-form {padding: 30px;border-radius: 6px;background: #fff;min-width: 350px;.login-form__header {display: flex;justify-content: center;align-items: center;padding-bottom: 30px;}.el-form-item:last-child {margin-bottom: 0;}.login__form-title {display: flex;justify-content: center;color: #fff;}.submit-button {width: 100%;}.login-logo {width: 271px;height: 74px;}.imgcode-wrap {display: flex;align-items: center;.imgcode {height: 37px;}}
}
</style>

2. 动态验证码

⑴、 封装请求接口

编辑 src / api / common.ts 文件

...
export const getCaptcha = () => {return request<Blob>({method: 'GET',url: '/admin//captcha_pro',params: {stamp: Date.now()},responseType: 'blob' // 请求获取图片数据})
}

⑵、 登录页调用
<template>
...<imgclass="imgcode"alt="验证码":src="captchaSrc"@click="loadCaptcha"></div></el-form-item>
...
</template><script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { getCaptcha } from '@/api/common'...
const captchaSrc = ref('')onMounted(() => {loadCaptcha()
})const loadCaptcha = async () => { // 获取图片验证码const data = await getCaptcha()captchaSrc.value = URL.createObjectURL(data)
}
...
</script>

⑶、 请求优化

编辑 src / utils / request.ts 文件

...
// 接口返回数据处理
export default <T = any>(config: AxiosRequestConfig) => {return request(config).then(res => {return (res.data.data || res.data) as T})
}

3. 登录基础流程

⑴、 封装请求接口

编辑 src / api / common.ts 文件

...
export const login = (data: {account: string,pwd: string,imgcode: string
}) => { // 用户登录请求return request<ILoginResponse>({method: 'POST',url: '/admin/login',data})
}

编辑 src / api / types / common.ts 文件

// 登录请求
export interface IUserInfo {id: numberaccount: stringhead_pic: string
}export interface IMenu {path: stringtitle: stringicon: stringheader: stringis_header: numberchildren?: IMenu[]
}export interface ILoginResponse {token: stringexpires_time: numbermenus: IMenu[]unique_auth: string[]user_info: IUserInfologo: stringlogo_square: stringversion: stringnewOrderAudioLink: string
}

⑵、 登录页调用

编辑 src / views / login / index.vue 文件

...
<script lang="ts" setup>
import { onMounted, reactive, ref } from 'vue'
import { getCaptcha, login } from '@/api/common'
import { ElForm } from 'element-plus'
import { useRouter } from 'vue-router'const router = useRouter()const user = reactive({account: 'admin',pwd: '123456',imgcode: ''
})
const loading = ref(false)
const rules = ref({account: [{ required: true, message: '请输入账号', trigger: 'change' }],pwd: [{ required: true, message: '请输入密码', trigger: 'change' }],imgcode: [{ required: true, message: '请输入验证码', trigger: 'change' }]
})
const captchaSrc = ref('')
const form = ref<InstanceType<typeof ElForm> | null>(null)onMounted(() => {loadCaptcha()
})const loadCaptcha = async () => { // 获取图片验证码const data = await getCaptcha()captchaSrc.value = URL.createObjectURL(data)
}const handleSubmit = async () => { // 登录请求// 表单验证const valid = await form.value?.validate()if (!valid) {return false}// 验证通过, 展示loadingloading.value = true// 请求提交const data = await login(user).finally(() => {loading.value = false})console.log('data=>', data)router.replace({name: 'home'})
}
</script>
...

⑶、 错误请求统一处理

编辑 src / utils / request.ts 文件

// 响应拦截器
request.interceptors.response.use(function (response) {// 统一设置接口相应错误, 比如 token 过期失效, 服务端异常if (response.data.status && response.data.status !== 200) { // 后端返回访问失败ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')// 手动返回一个 Promise 异常return Promise.reject(response.data)}return response
}, function (error) {// Do something with response errorreturn Promise.reject(error)
})

⑷、 页面样式



4. elementPlus 类型封装

⑴、 类型封装

新建 src / types / element-plus.ts 文件

import { ElForm } from 'element-plus'
import type { FormItemRule } from 'element-plus/es/components/form/src/form.type'export type IElForm = InstanceType<typeof ElForm>
export type IFormRule = Record<string, FormItemRule[]>

⑵、 页面调用

编辑 src / views / login / index.vue 文件

...
import type { IElForm, IFormRule } from '@/types/element-plus'// const rules = ref({
const rules = ref<IFormRule>({// const form = ref<InstanceType<typeof ElForm> | null>(null)
const form = ref<IElForm | null>(null)
...


二、用户信息展示

1. 获取用户信息

编辑 src / views / login / index.vue 文件

<script lang="ts" setup>
...// 请求提交const data = await login(user).finally(() => {loading.value = false})storemit('setUser', data.user_info)console.log('data =>', data)
...
</script>

2. 存储用户信息

编辑 src / store / index.vue 文件

import { IUserInfo } from '@/api/types/common'const state = {count: 1,isCollapse: false,user: JSON.parse(window.localStorage.getItem('user') || 'null') as IUserInfo | null
}
...mutations: {
...setUser (state, payload) {state.user = payload// 本地存储window.localStorage.setItem('user', JSON.stringify(state.user))
...

3. 页面调用

新建 src / layout / components / AppHeader / components / UserInfo.vue 文件

<template><el-dropdown><span class="el-dropdown-link">{{ $store.state.user?.account }}<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item>退出登录</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template><script lang="ts" setup>
import { ArrowDown } from '@element-plus/icons-vue'
</script>
<style lang="scss" scoped></style>

编辑 src / layout / components / AppHeader / index.vue 文件

<template><el-space><ToggleSidebar /><BreadcrumbVue /></el-space><el-space><FullScreen /><UserInfoVue /></el-space>
</template>
<script lang="ts" setup>
import ToggleSidebar from './components/ToggleSidebar.vue'
import BreadcrumbVue from './components/Breadcrumb.vue'
import UserInfoVue from './components/UserInfo.vue'
import FullScreen from './components/FullScreen.vue'
</script>
<style lang="scss" scoped></style>

4. 页面展示



5. 封装 localstorage 方法

⑴、 自定义方法

新建 src / utils / storage.ts 文件

export const getItem = <T>(key: string) => {const data = window.localStorage.getItem(key)if (!data) return nulltry {return JSON.parse(data) as T} catch (err) {return null}
}export const setItem = (key: string, value: object | string | null) => {if (typeof value === 'object') {value = JSON.stringify(value)}window.localStorage.setItem(key, value)
}export const removeItem = (key: string) => {window.localStorage.removeItem(key)
}

⑵、 组件调用

编辑 src / store / index.ts 文件

  import { getItem, setItem } from '@/utils/storage'// user: JSON.parse(window.localStorage.getItem('user') || 'null') as IUserInfo | nulluser: getItem<IUserInfo>('user')// window.localStorage.setItem('user', JSON.stringify(state.user))setItem('user', state.user)

6. 固话常量

⑴、 自定义常量

新建 src / utils / constants.ts 文件

export const USER = 'USER'

⑵、 组件调用

编辑 src / store / index.ts 文件

import { USER } from '@/utils/constants'// user: getItem<IUserInfo>('user')
user: getItem<IUserInfo>(USER)// setItem('user', state.user)
setItem(USER, state.user)


三、退出登录

1. 封装请求

编辑 src / api / common.ts 文件

...
export const logout = () => { // 管理员退出return request<ILoginResponse>({method: 'GET',url: '/setting/admin/logout'})
}

2. 页面调用

编辑 src / layout / components / AppHeader / components / UserInfo.vue 文件

<template><el-dropdown><span class="el-dropdown-link">{{ $store.state.user?.account }}<el-icon class="el-icon--right"><arrow-down /></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item>个人中心</el-dropdown-item><el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item></el-dropdown-menu></template></el-dropdown>
</template><script lang="ts" setup>
import { logout } from '@/api/common'
import { ArrowDown } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { useRouter } from 'vue-router'
import { store } from '@/store'const router = useRouter()const handleLogout = () => {// 确认提示ElMessageBox.confirm('是否确认退出?','提示',{confirmButtonText: '确认',cancelButtonText: '取消',type: 'warning'}).then(async () => {// 退出请求await logout()// 跳转登录页router.push({name: 'login'})// 清除用户信息storemit('setUser', null)ElMessage({type: 'success',message: '退出成功'})}).catch(() => {ElMessage({type: 'info',message: '已取消退出'})})
}
</script>
<style lang="scss" scoped></style>

3. token 存储

编辑 src / views / login / index.vue 文件

const handleSubmit = async () => { // 登录请求
...storemit('setUser', {...data.user_info,token: data.token})
...

编辑 src / utils / request.ts 文件

// 根据不同环境 切换不同路径
const request = axios.create({baseURL: import.meta.env.VITE_API_BASEURL
})// 请求拦截器
request.interceptors.request.use(function (config: any) {// 统一设置用户身份 tokenconst user = store.state.userif (user && user.token) {config.headers.Authorization = `Bearer ${user.token}`}return config
}, function (error) {return Promise.reject(error)
})

4. 页面展示

这里退出并没有成功, 因为退出需要传递用户 token 作为标识



四、登录状态

1. 路由校验

路由元信息

编辑 router.d.ts 文件

import 'vue-router'declare module 'vue-router' {// eslint-disable-next-line no-unused-varsinterface RouteMeta {title?: string,requiresAuth?: boolean}
}

2. 路由配置

编辑 src / router / index.ts 文件

...path: '', // 默认子路由name: 'home',component: () => import('../views/home/index.vue'),meta: { title: '首页', requiresAuth: true }},
...// 全局前置守卫
router.beforeEach((to, form) => {nprogress.start() // 开始加载进度条if (to.meta.requiresAuth && !store.state.user) {// 此路由需要授权,请检查是否已登录// 如果没有,则重定向到登录页面return {path: '/login',// 保存我们所在的位置,以便以后再来query: { redirect: to.fullPath }}}
})
...

编辑 src / router / modules /product.ts 文件

...path: 'product',component: RouterView,meta: {title: '商品',requiresAuth: true},
...

3. 登录跳转

编辑 src / views / login / index.vue 文件

...console.log('data =>', data)// 获取当前路由对象let redirect = route.query.redirect || '/'if (typeof redirect !== 'string') {redirect = '/'}router.replace(redirect)
...

4. 登录过期

编辑 src / utils / request.ts 文件

import axios, { AxiosRequestConfig } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import { store } from '@/store'
import router from '@/router/'// 根据不同环境 切换不同路径
const request = axios.create({baseURL: import.meta.env.VITE_API_BASEURL
})// 请求拦截器
request.interceptors.request.use(function (config: any) {// 统一设置用户身份 tokenconst user = store.state.userif (user && user.token) {config.headers.Authorization = `Bearer ${user.token}`}return config
}, function (error) {return Promise.reject(error)
})// 控制登录过期的锁
let isRefreshing = false// 响应拦截器
request.interceptors.response.use(function (response) {// 统一设置接口相应错误, 比如 token 过期失效, 服务端异常// if (response.data.status && response.data.status !== 200) { // 后端返回访问失败//   ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')//   // 手动返回一个 Promise 异常//   return Promise.reject(response.data)// }// return responseconst status = response.data.statusif (!status || status === 200) { // 正常情况return response}if (status === 410000) { // 异常情况: token 过期...if (isRefreshing) return Promise.reject(response)isRefreshing = true// 提示是否跳转登录页ElMessageBox.confirm('你的登录状态已经过期, 是否前往登录页面?', '提示', {confirmButtonText: '确认',cancelButtonText: '取消'}).then(() => {// 清除本地过期登录状态storemit('setUser', null)// 跳转登录页面router.push({name: 'login',query: {redirect: router.currentRoute.value.fullPath}})// 抛出异常}).finally(() => {isRefreshing = false})// 内部这个消化业务异常return Promise.reject(response)}// 其他情况ElMessage.error(response.data.msg || '请求失败, 请刷新后重试')// 手动返回一个 Promise 异常return Promise.reject(response.data)
}, function (error) {return Promise.reject(error)
})// 接口返回数据处理
export default <T = any>(config: AxiosRequestConfig) => {return request(config).then(res => {return (res.data.data || res.data) as T})
}

5. 实现功能

  • 跳转路由时,会校验用户 token,如果为空则进入登录页
  • 会记录用户上一次登录页, 登录后,跳转相应页面
  • token 过期时,进行提示


本文标签: Vue3ViteTS后台项目6用户登录校验