V3 Admin Vite 5.0-beta 发布!

V3 Admin Vite

!IMPORTANT

欢迎体验全新的 5.0 版本,目前正在 beta 阶段,它将是一次匠心之作

预热

在保持和 4.x 功能一致的情况下,5.0 版本主要做到了如下特性:

  1. 配置更少
  2. 架构更合理
  3. 性能更优秀

后面会有专门介绍文件详解 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 请求

大致的流程如下:

graph LR 页面/交互 --> api --> axios --> 服务器

通用 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

解决办法:页面只保留一个根元素节点(注意:根元素外的注释也要删)

详情可见:juejin.cn/post/707444...

源码

V3 Admin Vite

相关推荐
拉不动的猪10 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程27 分钟前
ES练习册
java·前端·elasticsearch
Asthenia041234 分钟前
Netty编解码器详解与实战
前端
袁煦丞39 分钟前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
Mr.app1 小时前
vue mixin混入与hook
vue.js
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员2 小时前
layui时间范围
前端·javascript·layui
NoneCoder2 小时前
HTML响应式网页设计与跨平台适配
前端·html
凯哥19702 小时前
在 Uni-app 做的后台中使用 Howler.js 实现强大的音频播放功能
前端