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用户登录校验
版权声明:本文标题:Vue3+Vite+TS后台项目 ~ 6.用户登录校验 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/p/1700351570a409212.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论