// src\api\common.ts
// 公共基础接口封装
import request from '@/utils/request'
import { DemoData, LoginResponse } from '@/api/types/common'
export const demo = () => {
return request<DemoData>({
method: 'GET',
url: '/demo'
})
}
export const login = (data: {
account: string
pwd: string
imgcode: string
}) => {
return request<LoginResponse>({
method: 'POST',
url: '/login',
data
})
}
// src\api\types\common.ts
export interface DemoData {
title: string
date: number
}
export interface Menu {
id: number,
routePath: string
routeName: string
name: string
icon: string
children?: Menu[]
}
export interface UserInfo {
id: number
account: string
realName: string
}
export interface LoginResponse {
token: string
userInfo: UserInfo
menus: Menu[]
uniqueAuth: string[]
}
<template>
<div class="login-container">
<el-form
ref="formRef"
class="login-form"
:rules="rules"
:model="user"
size="large"
@submit.prevent="handleSubmit"
>
<div class="login-form__header">
SHOP ADMIN
div>
<el-form-item prop="account">
<el-input
v-model="user.account"
placeholder="请输入用户名"
prefix-icon="user"
/>
el-form-item>
<el-form-item prop="pwd">
<el-input
v-model="user.pwd"
type="password"
placeholder="请输入密码"
prefix-icon="lock"
/>
el-form-item>
<el-form-item prop="imgcode">
<div class="imgcode-wrap">
<el-input
v-model="user.imgcode"
placeholder="请输入验证码"
prefix-icon="key"
/>
<img
class="imgcode"
alt="验证码"
:src="captchaSrc"
@click="loadCaptcha"
>
div>
el-form-item>
<el-form-item>
<el-button
class="submit-button"
type="primary"
:loading="loading"
native-type="submit"
>
登录
el-button>
el-form-item>
el-form>
div>
template>
<script lang="ts" setup>
import type { FormInstance, FormItemRule } from 'element-plus'
import { login } from '@/api/common'
const router = useRouter()
// 获取表单组件实例
const formRef = ref<FormInstance>()
// 表单数据
const user = reactive({
account: 'admin',
pwd: '123456',
imgcode: ''
})
// 表单提交状态
const loading = ref(false)
// 验证码图片地址
const captchaSrc = ref('')
// 表单验证规则
const rules = ref<Record<string, FormItemRule[]>>({
account: [
{ required: true, message: '请输入账号', trigger: 'change' }
],
pwd: [
{ required: true, message: '请输入密码', trigger: 'change' }
],
imgcode: [
{ required: true, message: '请输入验证码', trigger: 'change' }
]
})
// 获取验证码图片地址
const loadCaptcha = () => {
captchaSrc.value = import.meta.env.VITE_API_BASEURL + '/captcha?' + Date.now()
}
// 表单提交
const handleSubmit = async () => {
if (!formRef.value) return
// 表单验证
const valid = await formRef.value.validate()
if (!valid) return false
// 验证通过 展示 loading
loading.value = true
// 请求提交
const data = await login(user).finally(() => {
loading.value = false
})
console.log(data)
// 处理响应
router.replace({
name: 'home'
})
}
onMounted(() => {
loadCaptcha()
})
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;
}
.submit-button {
width: 100%;
}
.imgcode-wrap {
display: flex;
align-items: center;
.imgcode {
width: 130px;
height: 40px;
cursor: pointer;
}
}
}
style>
登录后要将用户信息等数据存储到本地,需要操作 localStorage,将其封装到单独的模块中,统一处理 JSON 转换逻辑:
// src\utils\storage.ts
export const getItem = <T>(key: string) => {
const data = window.localStorage.getItem(key)
if (!data) return null
try {
return JSON.parse(data) as T
} catch (error) {
return null
}
}
export const setItem = (key: string, value: Object | string | null) => {
if (typeof value !== 'string') {
value = JSON.stringify(value)
}
window.localStorage.setItem(key, value as string)
}
export const removeItem = (key: string) => {
window.localStorage.removeItem(key)
}
建议将本地存储的 key 设置为静态常量,防止写错:
// src\utils\constants.ts
/**
* 静态常量
*/
export const USER = 'USER'
登录后将用户信息和 token 整合在一起(或者分别)存储到浏览器和 store:
<!-- src\views\login\index.vue -->
// 表单提交
const handleSubmit = async () => {
...
// 请求提交
const data = await login(user).finally(() => {
loading.value = false
})
store.setUser({
...data.userInfo,
token: data.token
})
...
}
// src\store\index.ts
// import { defineStore } from 'pinia'
import { UserInfo } from '@/api/types/common'
import { getItem, setItem } from '@/utils/storage'
import { USER } from '@/utils/constants'
type User = ({ token: string } & UserInfo) | null
const useStore = defineStore('main', {
state: () => ({
count: 0,
isCollapse: false,
user: getItem<User>(USER)
}),
getters: {
doubleCount(state) {
return state.count * 2
}
},
actions: {
increment() {
this.count++
},
setUser(payload: User) {
this.user = payload
setItem(USER, this.user)
}
}
})
export default useStore
<template>
<el-dropdown>
<span class="el-dropdown-link">
{{ store.user?.realName }}
<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">
import useStore from '@/store'
export default defineComponent({
setup() {
const store = useStore()
return {
store
}
},
methods: {
handleLogout() {
this.$msgBox.confirm('确认退出吗?', '退出提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
.then(async () => {
// 清除用户登录信息
this.store.setUser(null)
this.$message.success('退出成功')
this.$router.push({
name: 'login'
})
})
.catch(() => {
this.$message.info('已取消退出')
})
}
}
})
script>
<style scoped>
.el-dropdown-link {
cursor: pointer;
display: flex;
align-items: center;
}
style>
当前使用的后端服务并没有存储登录状态,所以退出登录直接删除本地存储的 token 即可。
给路由添加元信息 requiresAuth
用来标识页面是否需要校验登录状态。
// src\router\index.ts
...
const routes:RouteRecordRaw[] = [
{
path: '/',
component: AppLayout,
meta: {
requiresAuth: true
},
...
},
...
]
...
添加 TS 类型:
// src\types\vue-router.d.ts
import 'vue-router'
declare module 'vue-router' {
// eslint-disable-next-line no-unused-vars
interface RouteMeta {
title?: string
requiresAuth?: boolean
}
}
以前的版本要想获取匹配到的全部路由的
meta
数据,需要遍历匹配的每个路由$route.matched
。Vue Router v4 新提供了一个$route.meta
对象,它合并了所有的meta
字段(相同字段取子路由的)。
// src\router\index.ts
...
import useStore from '@/store'
...
router.beforeEach(to => {
const store = useStore()
// 校验登录状态
if (to.meta.requiresAuth && !store.user) {
return {
name: 'login',
query: { redirect: to.fullPath }
}
}
// 开始加载进度条
nprogress.start()
})
...
<!-- src\views\login\index.vue -->
const route = useRoute()
// 表单提交
const handleSubmit = async () => {
...
// 处理响应
const redirect = route.query.redirect
if (typeof redirect !== 'string') {
router.replace({
name: 'home'
})
} else {
router.replace(redirect)
}
}
// src\utils\request.ts
import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios'
import { ElMessage } from 'element-plus'
import useStore from '@/store'
...
const store = useStore()
// 请求拦截器
request.interceptors.request.use(function (config: AxiosRequestConfig) {
// 统一设置用户身份 token
if (store.user && store.user.token) {
(config.headers as AxiosRequestHeaders).Authorization = `Bearer ${store.user.token}`
}
return config
}, function (error) {
return Promise.reject(error)
})
...
// src\utils\request.ts
import axios, { AxiosRequestConfig, AxiosRequestHeaders } from 'axios'
import { ElMessage, ElMessageBox } from 'element-plus'
import useStore from '@/store'
import router from '@/router'
...
// 控制登录过期的锁(是否正在处理登录过期)
let isRefreshing = false
// 响应拦截器
request.interceptors.response.use(function (response) {
// 统一处理接口响应错误,如 token 过期无效、服务端异常等
const status = response.data.status
/* 正确响应 */
if (!status || status === 200) {
return response
}
/* 错误响应 */
// 登录过期处理
if (status === 401) {
if (isRefreshing) return Promise.reject(response)
isRefreshing = true
ElMessageBox.confirm('登录已过期,您可以取消停留在此页面,或确认重新登录', '登录过期', {
confirmButtonText: '确认',
cancelButtonText: '取消'
})
.then(() => {
// 清除本地登录状态
store.setUser(null)
// 跳转到登录页
router.push({
name: 'login',
query: {
redirect: router.currentRoute.value.fullPath
}
})
})
.catch(() => {})
.finally(() => {
isRefreshing = false
})
// 抛出异常
return Promise.reject(response)
}
// 其他错误响应
ElMessage.error(response.data.msg || '请求失败,请稍后重试')
return Promise.reject(response)
}, function (error) {
return Promise.reject(error)
})
...