V3 Admin Vite
[!IMPORTANT]
欢迎体验全新的 5.0 版本,目前正在 beta 阶段,它将是一次匠心之作!
预热
在保持和 4.x 功能一致的情况下,5.0 版本主要做到了如下特性:
- 配置更少
- 架构更合理
- 性能更优秀
后面会有专门介绍文件详解 5.0 的特性!欢迎大家吃第一口 🦀
依然手膜手
如果你是新手,那你可以选择阅读针对新手编写的零基础手膜手教程:V3 Admin Vite 专栏
当然,专栏里现存的教程都是 4.x 版本的,5.0 的教程我会尽快更新
简介
V3 Admin Vite 是一个免费开源的中后台管理系统基础解决方案,基于 Vue3、Vite、TypeScript、Element Plus 等主流技术
使用
推荐环境
- 新版
Visual Studio Code
- 安装
.vscode/extensions.json
文件中推荐的插件 node
20.x 或 22+pnpm
9+
本地开发
bash
# 克隆项目
git clone https://github.com/un-pany/v3-admin-vite.git
# 进入项目目录
cd v3-admin-vite
# 安装依赖
pnpm i
# 启动服务
pnpm dev
打包构建
bash
# 打包构建预发布环境
pnpm build:staging
# 打包构建生产环境
pnpm build
本地预览
bash
# 先执行打包构建命令生成 dist 目录后再执行以下预览命令
pnpm preview
代码检查
bash
# 代码校验与格式化
pnpm lint
# 单元测试
pnpm test
代码提交规范
feat
新功能
fix
修复错误
perf
优化
refactor
重构代码
docs
文档和注释
types
类型相关
test
单测相关
ci
持续集成、工作流
revert
撤销更改
chore
琐事(更新依赖、修改配置等)
特性
结构精简:没有复杂的封装,没有复杂的类型体操,刚好够用
详细的注释:各个配置项都写有尽可能详细的注释
最新的依赖: 及时更新所有三方依赖至最新版
有一点规范: 代码风格统一、命名风格统一、注释风格统一
内置功能
用户管理:登录、登出演示
权限管理:页面级权限(动态路由)、按钮级权限(指令权限、权限函数)、路由守卫
多环境:开发环境(development)、预发布环境(staging)、生产环境(production)
多主题:普通、黑暗、深蓝, 三种主题模式
多布局:左侧、顶部、混合, 三种布局模式
首页:根据不同用户显示不同的 Dashboard 页面
错误页: 403、404
兼容移动端: 布局兼容移动端页面分辨率
其他:SVG 雪碧图、动态侧边栏、动态面包屑、标签页快捷导航、内容区放大与全屏、组合式函数
技术栈
Vue3:采用 Vue3 + script setup 最新的 Vue3 组合式 API
Element Plus:Element UI 的 Vue3 版本
Pinia: 传说中的 Vuex5
Vite:真的很快
Vue Router:路由路由
TypeScript:JavaScript 语言的超集
pnpm:更快速的,节省磁盘空间的包管理工具
Scss:和 Element Plus 保持一致
CSS 变量:主要控制项目的布局和颜色
ESlint:代码校验与格式化
Axios:发送网络请求(已封装好)
UnoCSS:具有高性能且极具灵活性的即时原子化 CSS 引擎
目录结构
sh
# v3-admin-vite
├─ .husky # commit 时进行代码校验和格式化
├─ .vscode # vscode 配置和插件
├─ public
│ ├─ favicon.ico # 网站头像
│ ├─ app-loading.css # 首屏 loading 动画
│ └─ detect-ie.js # 检测 ie
├─ src
│ ├─ common # 通用目录
│ │ ├─ apis # 通用目录 - 接口
│ │ ├─ assets # 通用目录 - 静态资源
│ │ ├─ components # 通用目录 - 组件
│ │ ├─ composables # 通用目录 - 组合式函数
│ │ ├─ constants # 通用目录 - 常量
│ │ └─ utils # 通用目录 - 工具函数
│ ├─ http # 网络请求
│ ├─ layouts # 布局
│ ├─ pages # 页面
│ │ └─ login # 登录模块
│ │ ├─ apis # 登录模块 - 私有接口
│ │ ├─ components # 登录模块 - 私有组件
│ │ ├─ composables # 登录模块 - 私有组合式函数
│ │ ├─ images # 登录模块 - 私有图片
│ │ └─ index.vue # 登录模块 - 页面
│ ├─ pinia # 状态管理
│ ├─ plugins # 插件(全局组件、自定义指令等)
│ ├─ router # 路由
│ ├─ App.vue # 入口页面
│ └─ main.ts # 入口文件
├─ tests # 单元测试
├─ types # 类型声明
├─ .editorconfig # 编辑器配置
├─ .env # 所有环境
├─ .env.development # 开发环境
├─ .env.production # 正式环境
├─ .env.staging # 预发布环境
├─ eslint.config.js # eslint 配置
├─ tsconfig.json # ts 配置
├─ unocss.config.ts # unocss 配置
└─ vite.config.ts # vite 配置
基础
路由
配置项
ts
/**
* @description 设置为 noRedirect 的时候该路由在面包屑导航中不可被点击
*/
redirect: "noRedirect"
/**
* @description 动态路由必须设定路由的 name,不然重置路由可能会出问题
* @description 如果要在标签栏中展示,也必须填 name
*/
name: "router-name"
meta: {
/**
* @description 设置该路由在侧边栏和面包屑中展示的名字
*/
title?: string
/**
* @description 设置该路由的图标,记得将 svg 导入 src/common/assets/icons
*/
svgIcon?: SvgName
/**
* @description 设置该路由的图标,直接使用 Element Plus 的 Icon(与 svgIcon 同时设置时,svgIcon 将优先生效)
*/
elIcon?: ElementPlusIconsName
/**
* @description 默认 false,设置 true 的时候该路由不会在侧边栏出现
*/
hidden?: boolean
/**
* @description 设置能进入该路由的角色,支持多个角色叠加
*/
roles?: string[]
/**
* @description 默认 true,如果设置为 false,则不会在面包屑中显示
*/
breadcrumb?: boolean
/**
* @description 默认 false,如果设置为 true,它则会固定在 tags-view 中
*/
affix?: boolean
/**
* @description 当一个路由的 children 属性中声明的非隐藏子路由只有 1 个且该子路由为叶子节点时,会将这个子路由当做父路由显示在侧边栏
* @description 当大于 1 个时,会恢复成嵌套模式
* @description 如果想不管个数总是显示父路由,可以在父路由上设置 alwaysShow: true
*/
alwaysShow?: boolean
/**
* @description 示例: activeMenu: "/xxx/xxx",
* @description 当设置了该属性进入路由时,则会高亮 activeMenu 属性对应的侧边栏
* @description 该属性适合使用在有 hidden: true 属性的路由上
*/
activeMenu?: string
/**
* @description 是否缓存该路由页面
* @description 默认为 false,为 true 时代表需要缓存,此时该路由和该页面都需要设置一致的 Name
*/
keepAlive?: boolean
}
动态路由
constantRoutes
:把不需要判断权限的路由放置在常驻路由里面,如 /login
、/dashboard
dynamicRoutes
:放置需要动态判断权限并通过 addRoute
动态添加的路由
注意:动态路由必须配置 name 属性,不然重置路由时,会漏掉没有该属性的动态路由,可能会导致业务 BUG
侧边栏和面包屑
侧边栏
侧边栏 @/layouts/components/Sidebar
是通过读取路由并结合权限判断而动态生成的(换句话说就是常驻路由 + 有权限的动态路由)
三种主题模式下截图
侧边栏外链
可以在侧边栏中配置一个外链,只要你在 path
中填写了合法的 url
路径,当你点击侧边栏的时候就会帮你新开这个页面
ts
{
path: "/link",
meta: {
title: "文档链接",
elIcon: "Link"
},
children: [
{
path: "https://juejin.cn/post/7089377403717287972",
component: () => {},
name: "Link1",
meta: {
title: "中文文档"
}
},
{
path: "https://juejin.cn/column/7207659644487139387",
component: () => {},
name: "Link2",
meta: {
title: "新手教程"
}
}
]
}
面包屑
面包屑 @/layouts/components/BreadCrumb
也是根据路由动态生成的,为路由设置 breadcrumb: false
时该路由将不会出现在面包屑中,设置 redirect: "noRedirect"
时该路由在面包屑中不能被点击
权限
登录时通过获取当前用户的权限(角色)去比对路由表,生成当前用户具有的权限可访问的路由表,通过 addRoute
动态挂载到 router
上
页面权限
控制代码都在路由守卫 @/router/guard.ts
中,这里可根据具体的业务做响应的修改:
ts
import type { Router } from "vue-router"
import { usePermissionStore } from "@/pinia/stores/permission"
import { useUserStore } from "@/pinia/stores/user"
import { routerConfig } from "@/router/config"
import { isWhiteList } from "@/router/whitelist"
import { setRouteChange } from "@@/composables/useRouteListener"
import { useTitle } from "@@/composables/useTitle"
import { getToken } from "@@/utils/cache/cookies"
import NProgress from "nprogress"
NProgress.configure({ showSpinner: false })
const { setTitle } = useTitle()
export function registerNavigationGuard(router: Router) {
// 全局前置守卫
router.beforeEach(async (to, _from) => {
NProgress.start()
const userStore = useUserStore()
const permissionStore = usePermissionStore()
// 如果没有登陆
if (!getToken()) {
// 如果在免登录的白名单中,则直接进入
if (isWhiteList(to)) return true
// 其他没有访问权限的页面将被重定向到登录页面
return "/login"
}
// 如果已经登录,并准备进入 Login 页面,则重定向到主页
if (to.path === "/login") return "/"
// 如果用户已经获得其权限角色
if (userStore.roles.length !== 0) return true
// 否则要重新获取权限角色
try {
await userStore.getInfo()
// 注意:角色必须是一个数组! 例如: ["admin"] 或 ["developer", "editor"]
const roles = userStore.roles
// 生成可访问的 Routes
routerConfig.dynamic ? permissionStore.setRoutes(roles) : permissionStore.setAllRoutes()
// 将 "有访问权限的动态路由" 添加到 Router 中
permissionStore.addRoutes.forEach(route => router.addRoute(route))
// 设置 replace: true, 因此导航将不会留下历史记录
return { ...to, replace: true }
} catch (error) {
// 过程中发生任何错误,都直接重置 Token,并重定向到登录页面
userStore.resetToken()
ElMessage.error((error as Error).message || "路由守卫发生错误")
return "/login"
}
})
// 全局后置钩子
router.afterEach((to) => {
setRouteChange(to)
setTitle(to.meta.title)
NProgress.done()
})
}
取消页面权限
假如你的业务场景中没有 动态路由
的概念,那么在 @/route/config.ts
里可以关闭该功能,关闭后系统将启用默认角色,每个登录的用户都可见所有路由
ts
/** 路由配置 */
interface RouterConfig {
/**
* @name 路由模式
* @description hash 模式和 html5 模式
*/
history: RouterHistory
/**
* @name 是否开启动态路由功能
* @description 1. 开启后需要后端配合,在查询用户详情接口返回当前用户可以用来判断并加载动态路由的字段(该项目用的是角色 roles 字段)
* @description 2. 假如项目不需要根据不同的用户来显示不同的页面,则应该将 dynamic: false
*/
dynamic: boolean
/**
* @name 默认角色
* @description 当动态路由功能关闭时:
* @description 1. 应该将所有路由都写到常驻路由里面(表明所有登录的用户能访问的页面都是一样的)
* @description 2. 系统自动给当前登录用户赋值一个没有任何作用的默认角色
*/
defaultRoles: Array<string>
/**
* @name 是否开启三级及其以上路由缓存功能
* @description 1. 开启后会进行路由降级(把三级及其以上的路由转化为二级路由)
* @description 2. 由于都会转成二级路由,所以二级及其以上路由有内嵌子路由将会失效
*/
thirdLevelRouteCache: boolean
}
权限指令
简单快速的实现按钮级别的权限判断(v-permission
已注册到全局,可直接使用):
html
<el-button v-permission="['admin', 'editor']">
admin 和 editor
</el-button>
但 Element Plus 的 el-tab-pane 和 el-table-column 以及其它动态渲染 DOM 的场景不适合使用 v-permission
这种情况下你可以通过 v-if
+ checkPermission
来实现
权限函数
ts
import { checkPermission } from "@@/utils/permission"
html
<el-tab-pane v-if="checkPermission(['admin', 'editor'])" label="admin 和 editor">
<el-tag size="large">
v-if="checkPermission(['admin', 'editor'])"
</el-tag>
</el-tab-pane>
发送 HTTP 请求
大致的流程如下:
通用 API 模块
src/common/apis
目录存放通用的接口,而非某个页面固定使用的接口
私有 API 模块
某个页面固定使用的接口,应该在当前页面目录下建立一个 apis
文件夹,用来存放私有接口
参考登录页 src/pages/login/apis
封装的 Axios
src/http/axios.ts
是基于 axios 的封装,封装了全局 request 拦截器、response 拦截器、统一的错误处理、统一的超时处理、baseURL 设置等
多环境
打包构建
项目开发完成,打包构建代码时,内置两种环境:
sh
# 打包构建预发布环境
pnpm build:staging
# 打包构建生产环境
pnpm build
环境变量
在 .env.production
等形如 .env.xxx
文件中,配置了该环境对应的一些环境变量,例如:
sh
## 后端接口地址(如果解决跨域问题采用反向代理就只需写相对路径)
VITE_BASE_URL = /api/v1
## 开发环境域名和静态资源公共路径(一般 / 或 ./ 都可以)
VITE_PUBLIC_PATH = /
使用方式:
ts
console.log(import.meta.env.VITE_BASE_API)
进阶
ESLint
规范代码很重要!
- 配置项在
eslint.config.js
文件中 - 推荐安装 VSCode 的
ESlint
插件,它可在写代码时,将不符合规范的代码标红,并且在你保存代码时自动修复一些简单的标红的代码 - 手动校验和格式化命令
pnpm lint
(提交代码前可以执行该命令)
代码提交校验
项目采用 husky
+ lint-staged
的方式,在提交代码的时候,进行全局 ts
类型检查和 eslint
校验
husky
会自动初始化,如果发现没有正常初始化,也可以通过命令 pnpm prepare
初始化 husky
跨域
反向代理
vite.config
里有 proxy
进行反向代理,与之对应的生产环境,则可以使用 nginx
来做反向代理
ts
proxy: {
"/api/v1": {
target: "https://xxxxxx",
// 是否为 WebSocket
ws: false,
// 是否允许跨域
changeOrigin: true
}
}
CORS
这种方案对于前端来说没有什么工作量,和正常发送请求写法上没有任何区别,工作量基本都在后端这里
实现 CORS 之后,不管是开发环境还是生产环境,都能方便的调用接口
SVG
使用全局 SvgIcon 组件
unplugin-svg-component
插件提供的能力!
把下载好的 SVG 图标存放在 src/common/assets/icons
目录下,无需在页面中引入 SvgIcon
组件,即可直接使用:
html
<!-- name 为 svg 文件名 -->
<!-- 通过 class 修改默认样式 -->
<SvgIcon name="search" class="svg-icon" />
这种方式一般用来处理将 svg 当做 icon 的场景,比如侧边栏导航菜单
将 SVG 文件导入为 Vue 组件
vite-svg-loader
插件提供的的能力!
比如 404 页面:
ts
<script lang="ts" setup>
import Layout from "./components/Layout.vue"
import Svg404 from "./images/404.svg?component" // vite-svg-loader 插件的功能
</script>
<template>
<Layout>
<Svg404 />
</Layout>
</template>
这种方式一般用来处理将 svg 当做图片展示的场景,比如 404 页面的大图
下载 svg icon
推荐 iconfont
常见问题
报错
- Google 一下可以解决
99%
的报错 - 尝试删除
node_modules
和.lock
文件后再次依赖 - 检查环境是否和作者推荐的一致
- 重启一下?
依赖超时
- 国内用户可以设置最新的淘宝源加快依赖速度
https://registry.npmmirror.com
热更新失效
- 检查配置路由时填写的路径是否正确(特别是字母大小写问题)
页面出现空白
控制台出现警告:Component inside <Transition> renders non-element root node that cannot be animated
解决办法:页面只保留一个根元素节点(注意:根元素外的注释也要删)