介绍
V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,基于 Vue3、TypeScript、Element Plus、Pinia 和 Vite 等主流技术。另外还有:
- Vue-Cli 5.x 版: v3-admin
- Electron 桌面版: v3-electron-vite
目录结构
| 1 | # v3-admin-vite | 
建立目录结构
- @/src/api目录下的- login文件夹(没有的话就需要新建一个)即代表了登录模块。
- 在 login文件夹里面再建立一个types文件夹(专门放置和登录模块相关的TS 类型)和index.ts。
假如一个模块叫系统管理
system,里面有两个子模块,分别叫用户管理user、角色管理role,那么我们建立的目录大致就应该长这个样子:
编写 TS 类型
编写接口的 TS 类型,需要根据后端同事提供的接口文档,拿到接口的请求参数和响应数据的格式。
- 请求数据类型 ILoginRequestData:1 
 2
 3
 4
 5
 6
 7
 8export interface ILoginRequestData { 
 /** admin 或 editor */
 username: "admin" | "editor"
 /** 密码 */
 password: string
 /** 验证码 */
 code: string
 }
- 响应数据类型 LoginResponseData:1 export type LoginResponseData = IApiResponseData<{ token: string }> 
- IApiResponseData这个类型作为一个全局类型,被定义在- @/types/api.d.ts文件里:- 1 
 2
 3
 4
 5
 6- /** 所有 api 接口的响应数据都应该准守该格式 */ 
 interface IApiResponseData<T> {
 code: number
 data: T
 message: string
 }
- 所以最终响应数据类型 LoginResponseData就相当于:1 
 2
 3
 4
 5{ 
 code: number
 data: { token: string }
 message: string
 }
- 最终效果如下:
编写接口
- 发送请求是通过封装好的 Axios,所以第一步就是导入相关的方法:1 import { request } from "@/utils/service" 
- 将上文写好的登录接口的类型导入进来:1 import type * as Login from "./types/login" 
- 登录接口的函数名为 loginApi,它接受一个参数data,类型为ILoginRequestData。1 
 2
 3
 4
 5
 6
 7
 8/** 登录并返回 Token */ 
 export function loginApi(data: Login.ILoginRequestData) {
 return request<Login.LoginResponseData>({
 url: "users/login",
 method: "post",
 data
 })
 }
- request<Login.LoginResponseData>则表示的是待会接口响应成功的- data数据类型为- LoginResponseData。
- url代表接口地址,- method代表接口方法(get/post/put/delete),- data表示请求体数据(如果是 get 请求,则要换成- params)。
- 接口写好后如下图:
调用登录接口
首先点击登录按钮将调用的函数是 handleLogin:
| 1 | const handleLogin = () => { | 
- loginFormRef.value?.validate是校验登录表单。
- useUserStore()是状态管理器 Pinia 的- Store,调用该 Store 的- login action,并传入用户名、密码、验证码三个参数即可。
- login action返回值是一个 Promise,所以后面链式跟一个- .then、- .catch和- .finally,接口调用成功则会执行- .then(跳转到首页),如果途中发生错误,则会执行- .catch,而无论什么情况都会执行- .finally。
状态管理
- 由于点击登录按钮触发了 useUserStore的loginaction,然后在loginaction 中调用这个loginApi并传入对应参数(如果这里参数传递错误,那么 TS 就会报错提醒我们,因为我们在上文中定义接口的时候已经约束了类型)。
- 调用登录接口成功时,我们将接口返回的响应数据 res中的token分别保存到cookie(对应语句setToken(res.data.token))和当前 Store(对应语句token.value = res.data.token) 中,如果接口失败,则直接reject。
- 如果这里执行了 .then那么登录页面也将执行.then,也就会开始跳转路由到首页,那么就会触发路由守卫。
路由守卫
- @/src/router/permission.ts包含了路由守卫全部的代码:- 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71- import router from "@/router" 
 import { useUserStoreHook } from "@/store/modules/user"
 import { usePermissionStoreHook } from "@/store/modules/permission"
 import { ElMessage } from "element-plus"
 import { whiteList } from "@/config/white-list"
 import { getToken } from "@/utils/cache/cookies"
 import asyncRouteSettings from "@/config/async-route"
 import NProgress from "nprogress"
 import "nprogress/nprogress.css"
 NProgress.configure({ showSpinner: false })
 router.beforeEach(async (to, _from, next) => {
 NProgress.start()
 const userStore = useUserStoreHook()
 const permissionStore = usePermissionStoreHook()
 // 判断该用户是否登录
 if (getToken()) {
 if (to.path === "/login") {
 // 如果已经登录,并准备进入 Login 页面,则重定向到主页
 next({ path: "/" })
 NProgress.done()
 } else {
 // 检查用户是否已获得其权限角色
 if (userStore.roles.length === 0) {
 try {
 if (asyncRouteSettings.open) {
 // 注意:角色必须是一个数组! 例如: ['admin'] 或 ['developer', 'editor']
 await userStore.getInfo()
 const roles = userStore.roles
 // 根据角色生成可访问的 Routes(可访问路由 = 常驻路由 + 有访问权限的动态路由)
 permissionStore.setRoutes(roles)
 } else {
 // 没有开启动态路由功能,则启用默认角色
 userStore.setRoles(asyncRouteSettings.defaultRoles)
 permissionStore.setRoutes(asyncRouteSettings.defaultRoles)
 }
 // 将'有访问权限的动态路由' 添加到 Router 中
 permissionStore.dynamicRoutes.forEach((route) => {
 router.addRoute(route)
 })
 // 确保添加路由已完成
 // 设置 replace: true, 因此导航将不会留下历史记录
 next({ ...to, replace: true })
 } catch (err: any) {
 // 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
 userStore.resetToken()
 ElMessage.error(err.message || "路由守卫过程发生错误")
 next("/login")
 NProgress.done()
 }
 } else {
 next()
 }
 }
 } else {
 // 如果没有 Token
 if (whiteList.indexOf(to.path) !== -1) {
 // 如果在免登录的白名单中,则直接进入
 next()
 } else {
 // 其他没有访问权限的页面将被重定向到登录页面
 next("/login")
 NProgress.done()
 }
 }
 })
 router.afterEach(() => {
 NProgress.done()
 })
- 判断用户是否登录,没登录则只能进入白名单页面,比如登录页。
- 如果已经登录,将不允许进入登录页。
- 如果已经登录,还要检查是否拿到用户角色,如果没有,并且开启了动态路由功能,则要调用用户详情接口。
- 如果没有开启动态路由功能,则启用默认角色。
- 一旦发生错误,就重置 Token,并重定向到登录页。
- 如果通过路由守卫的检查后,就能正常跳转到首页了。
鉴权
- 后续所有的操作,都将携带保存在前端的 token去调用接口,token将是后端服务判断当前请求合不合法的依据,项目本身已经写在 Axios 的封装里面了:
- 假如 token已经过期后,理论上接口会抛出一个http code 401的错误,我们只需要在响应拦截器里重定向到登录页即可:
参考资料
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 张坤的博客!
 评论