使用vue3从0开发一个后台管理系统
项目简介
源码地址
https://gitee.com/szxio/zx-vue-next
描述
本项目是使用 vue3 来开发的后台管理系统模板。页面简单大方,使用悬浮式的风格,将菜单栏,顶部面包屑,中间操作区域等合理划分,功能丰富,支持主题颜色自定义,一键开启黑色主题,浅色、深色菜单动态切换等。路由采用动态路由,依托若依后端接口,拥有强大的权限管理功能。对若依感兴趣的点此跳转,希望各位小伙伴能够在学习本项目的过程中或多或少的有所收获。
如果感觉对你有所帮助,请点击 Star,感谢支持。
本文档同步至以下网站:
- https://songzx0106.github.io/
- https://blog.csdn.net/SongZhengxing_?type=blog
技术栈
- vue3
- element-plus
- Pinia
- vue-router
- js-cookie
- sass
- …
页面截图




本地运行
本项目后端借用了若依的后台框架,在她的基础上稍作了修改。
可以在本地启动本项目中的 java 代码。再启动前端查看效果。
若依启动成功截图

前端项目运行
npm installnpm run dev
输入默认的账号密码
账号:admin
密码:admin123
创建项目
使用 vite 来创建我们的工程
npm create vite@latest
或者
yarn create vite
然后按照提示操作即可!
elementui-plus
安装
官方文档:https://element-plus.gitee.io/zh-CN/
安装
cnpm install element-plus --save
引入
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'const app = createApp(App)app.use(ElementPlus)
app.mount('#app')
组件自动导入
实现组件自动导入
npm install -D unplugin-vue-components unplugin-auto-import
然后把下列代码插入到你的 Vite 的配置文件中
// vite.config.ts
import { defineConfig } from 'vite'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'export default defineConfig({// ...plugins: [// ...AutoImport({resolvers: [ElementPlusResolver()],}),Components({resolvers: [ElementPlusResolver()],}),],
})
注册所有图标
安装
# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue
# pnpm
$ pnpm install @element-plus/icons-vue
注册所有组件
// main.tsimport * as ElementPlusIconsVue from '@element-plus/icons-vue'const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {app.component(key, component)
}
解决默认是英文的问题
引入中文包:import lang from 'element-plus/lib/locale/lang/zh-cn',然后设置全局语言即可
import lang from 'element-plus/lib/locale/lang/zh-cn'
import ElementPlus from 'element-plus'const app = createApp(App)
app.use(ElementPlus, {locale: lang,
})
app.mount('#app')
添加Router
官方文档:https://router.vuejs.org/zh/
安装
npm install vue-router@4
新建测试路由
// 1.从vue-router导出两个方法使用
import {createRouter, createWebHashHistory} from 'vue-router';// 2.声明菜单数组
const routes = [{path: '/',component: import("../view/home/home.vue")},{path: '/about',component: import("../view/about/about.vue")},
]// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = createRouter({// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。history: createWebHashHistory(),routes, // `routes: routes` 的缩写
})// 导出路由
export default router;
引入
// main.ts
import router from "./router/index"const app = createApp(App)
app.use(router)
app.mount('#app')
修改App.vue
<template><router-view/>
template>
配置@路径别名
首先安装依赖
npm install @types/node
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'//设置路径别名
const alias = {'@': resolve(__dirname, './src'),'*': resolve(''),
}// https://vitejs.dev/config/
export default defineConfig({plugins: [vue()],resolve: {alias},
})
安装prettier代码格式化插件
安装
npm i --save-dev prettier
然后再根目录新建 .prettierrc 文件,内容如下
{"printWidth": 180,"tabWidth": 4,"semi": false,"singleQuote": true,"trailingComma": "es5"
}
然后以 webStorm 工具为例,使用 prettier

格式效果

Pinia
安装
npm install pinia
添加 src/stores/index.ts
import { createPinia } from 'pinia'
// 创建
const pinia = createPinia()
// 导出
export default pinia
引入
// main.tsimport {createApp} from 'vue'
import App from './App.vue'
// ...
import pinia from "./stores/index"const app = createApp(App)
// ...
app.use(pinia)
app.mount('#app')
使用pinia保存路由信息
新建 src/store/routesList.ts
import { defineStore } from 'pinia'// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {state: () => ({routesList: [],}),actions: {// 设置路由集合async setRouterList(data: any) {this.routesList = data},},
})
然后再前置路由守卫中调用方法保存
import { routesList } from '../stores/routesList'// 路由加载前
router.beforeEach(async (to, from, next) => {const routerList = routesList()await routerList.setRouterList(routes[0].children)next()
})
递归显示多级菜单
新建 src/layout/menu/Menu.vue
<template><el-menu default-active="2" class="el-menu-vertical-demo" @open="handleOpen" @close="handleClose"><template v-for="item in state.routerList" :key="item.path"><el-sub-menu :index="item.path" v-if="item.children && item.children.length > 0" :key="item.path"><template #title><span>{{ item.name }}span>template><sub-menu :chil="item.children" />el-sub-menu><template v-else><el-menu-item :index="item.path" :key="item.path"><span>{{ item.name }}span>el-menu-item>template>template>el-menu>
template><script lang="ts" setup>
import { useRouter, useRoute } from 'vue-router'
import { Document, Menu as IconMenu, Location, Setting } from '@element-plus/icons-vue'
import { onMounted, reactive, ref } from 'vue'
import { routesList } from '../../stores/routesList'
import SubMenu from './SubMenu.vue'const state = reactive({router: useRouter(),routerList: routesList().routesList,
})
const handleOpen = (key: string, keyPath: string[]) => {// console.log(key, keyPath)
}
const handleClose = (key: string, keyPath: string[]) => {// console.log(key, keyPath)
}
script><style>
.el-menu {border-right: 0;width: 200px;
}
style>
新建 src/layout/menu/SubMenu.vue
<template><template v-for="val in props.chil"><el-sub-menu :index="val.path" :key="val.path" v-if="val.children && val.children.length > 0"><template #title><span>{{ val.name }}span>template><sub-menu :chil="val.children" />el-sub-menu><template v-else><el-menu-item :index="val.path" :key="val.path"><span>{{ val.name }}span>el-menu-item>template>template>
template><script lang="ts" setup>
import { defineProps } from 'vue'const props = defineProps(['chil'])
script>
效果展示

根据权限显示菜单
首先设置两个角色:admin、common,分别表示管理员和普通用户。
创建 src/stores/userInfo.ts,暂时写死一个用户数据
import { defineStore } from 'pinia'// 第一个参数是应用程序中 store 的唯一 id
export const userInfo = defineStore('userInfo', {state: () => ({// 用户名称userName: 'admin',// 用户iduserId: 'zx-001',// 用户权限 admin:管理员,common:普通用户roles: ['common'],// 用户头像portrait:'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',}),actions: {// 设置用户信息setUserInfo(info: any) {},},
})
创建 src/stores/routesList.ts 文件,存放路由集合信息
import { defineStore } from 'pinia'// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {state: () => ({routesList: [],}),actions: {// 设置路由集合async setRouterList(data: any) {this.routesList = data},},
})
创建 src/router/routes.ts 菜单数据文件,其中 meta 里面有 roles 数组,表示只要用户的角色在这里就显示当前菜单
// src/router/routes.ts
import Layout from '../layout/index.vue'
import Parent from '../layout/routerview/Parent.vue'/*** meta 属性意义* roles 设置那些权限可见。admin:管理员,common:普通职工*/export default [{path: '/',name: 'router.home',component: Layout,redirect: '/home',children: [{path: '/home',meta: {roles: ['admin', 'common'],},name: 'router.home',component: () => import('../view/home/home.vue'),},{path: '/about',meta: {roles: ['admin', 'common'],},name: 'router.about',component: () => import('../view/about/about.vue'),},{path: '/order',meta: {roles: ['admin', 'common'],},name: 'router.order',component: Parent,children: [{path: 'list',meta: {roles: ['admin', 'common'],},name: 'router.order_list',component: () => import('../view/order/list.vue'),},{path: 'stock',meta: {roles: ['admin', 'common'],},name: 'router.order_stock',component: Parent,children: [{path: 'price',meta: {roles: ['admin', 'common'],},name: 'router.order_stock_price',component: () => import('../view/order/price.vue'),},],},],},{path: '/system',name: 'router.system',meta: {roles: ['admin'],},component: Parent,children: [{path: 'menu',meta: {roles: ['admin'],},name: 'router.system_menu',component: () => import('../view/system/menu.vue'),},{path: 'role',meta: {roles: ['admin'],},name: 'router.system_role',component: () => import('../view/system/role.vue'),},{path: 'user',meta: {roles: ['admin'],},name: 'router.system_user',component: () => import('../view/system/user.vue'),},{path: 'dept',meta: {roles: ['admin'],},name: 'router.system_dept',component: () => import('../view/system/dept.vue'),},],},],},
]
修改 src/router/index.ts 文件如下
// 1.从vue-router导出两个方法使用
import { createRouter, createWebHashHistory } from 'vue-router'
import { routesList } from '../stores/routesList'
import routes from './routes'// 3. 创建路由实例并传递 `routes` 配置
// 你可以在这里输入更多的配置,但我们在这里
// 暂时保持简单
const router = createRouter({// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。history: createWebHashHistory(),routes, // `routes: routes` 的缩写
})// 路由加载前
router.beforeEach(async (to, from, next) => {const routerList = routesList()await routerList.setRouterList(routes[0].children)next()
})// 导出路由
export default router
添加 src/router/filterRouter.ts 文件,待会会用到这里面的方法
/*** 判断路由 `meta.roles` 中是否包含当前登录用户权限字段* @param roles 用户权限标识,在 userInfos(用户信息)的 roles(登录页登录时缓存到浏览器)数组* @param route 当前循环时的路由项* @returns 返回对比后有权限的路由项*/
export function hasRoles(roles: any, route: any) {if (route.meta && route.meta.roles)return roles.some((role: any) => route.meta.roles.includes(role))else return true
}/*** 获取当前用户权限标识去比对路由表,设置递归过滤有权限的路由* @param routes 当前路由 children* @param roles 用户权限标识,在 userInfos(用户信息)的 roles(登录页登录时缓存到浏览器)数组* @returns 返回有权限的路由数组 `meta.roles` 中控制*/
export function setFilterHasRolesMenu(routes: any, roles: any) {const menu: any = []routes.forEach((route: any) => {const item = { ...route }if (hasRoles(roles, item)) {if (item.children) {item.children = setFilterHasRolesMenu(item.children, roles)}menu.push(item)}})return menu
}/*** 路由扁平化方法* @param routes*/
export function flatten(routes: any) {return routes.reduce((arr: any, old: any) => arr.concat([old], flatten(old.children || [])),[])
}
在 src/layout/menu/Menu.vue 组件中添加如下逻辑,首先进入页面触发 onBeforeMount,在该生命周期中调用 getRouterListByRole 方法获取菜单数据,getRouterListByRole 方法中又调用 setFilterHasRolesMenu 方法,来实现根据权限获取不同菜单
import { onBeforeRouteUpdate, useRoute } from 'vue-router'
import { onBeforeMount, reactive } from 'vue'
import { routesList } from '../../stores/routesList'
import { userInfo } from '../../stores/userInfo'
import SubMenu from './SubMenu.vue'
import { Menu as IconMenu } from '@element-plus/icons-vue'
import { setFilterHasRolesMenu } from '../../router/filterRouter'
import { useI18n } from 'vue-i18n'const { t } = useI18n()const state = reactive({routerList: [], // 路由数据active: useRoute().path, // 根据路由默认选中菜单
})onBeforeMount(() => {// 获取当前的组件信息let routerList = getRouterListByRole()routerList.forEach((item: any) => {if (item.children) {resolvePath(item.path, item.children)}})// 赋值state.routerList = routerList
})
// 递归遍历深层菜单的路径
const resolvePath = (parentPath: string, children: Array<any>) => {children.forEach((item: any) => {item.path = parentPath + '/' + item.pathif (item.children) {resolvePath(item.path, item.children)}})
}
// 路由更新时更新菜单选中
onBeforeRouteUpdate((to) => {state.active = to.path
})
// 根据用户权限获取菜单数据
const getRouterListByRole = () => {const roles = userInfo().rolesconst routerList = JSON.parse(JSON.stringify(routesList().routesList))return setFilterHasRolesMenu(routerList, roles)
}
效果显示
普通用户没有系统管理菜单

管理员可以看到系统管理

实现点击菜单进行路由跳转
首先添加下面代码,作用是可以将内层的菜单设置为全路径
onBeforeMount(() => {// 获取当前的组件信息let routerList = JSON.parse(JSON.stringify(routesList().routesList))routerList.forEach((item: any) => {if (item.children) {resolvePath(item.path, item.children)}})// 赋值state.routerList = routerList
})
// 递归遍历深层菜单的路径
const resolvePath = (parentPath: string, children: Array<any>) => {children.forEach((item: any) => {item.path = parentPath + '/' + item.pathif (item.children) {resolvePath(item.path, item.children)}})
}
然后开启 Menu 组件的 router 模式即可
动态面包屑导航
完整代码
<template><el-breadcrumb separator="/"><el-breadcrumb-itemv-for="(item, index) in state.breadcrumbList":key="index"><span class="breadcrumb-text">{{ item.name }}span>el-breadcrumb-item>el-breadcrumb>
template><script lang="ts" setup>
import { watch, ref, reactive } from 'vue'
// 引入路由
import { useRoute } from 'vue-router'const state = reactive({breadcrumbList: [],route: useRoute(),
})
// 初始化面包屑
const initBreadcrumbList = () => {// route.matched 可以获取当前路由的完整路由表state.breadcrumbList = state.route.matched.slice(1)
}
// 监听路由变化
watch(state.route,() => {initBreadcrumbList()},{ deep: true, immediate: true }
)
script>
效果

页面最大化和最小化
<template><div class="full"><el-icon :size="20" @click="handleFullScreen" class="icon-color"><FullScreen />el-icon>div>
template><script setup>
import { reactive, computed } from 'vue'const state = reactive({fullscreen: false,
})
const handleFullScreen = () => {let element = document.documentElement// 判断是否已经是全屏// 如果是全屏,退出if (state.fullscreen) {if (document.exitFullscreen) {document.exitFullscreen()} else if (document.webkitCancelFullScreen) {document.webkitCancelFullScreen()} else if (document.mozCancelFullScreen) {document.mozCancelFullScreen()} else if (document.msExitFullscreen) {document.msExitFullscreen()}} else {// 否则,进入全屏if (element.requestFullscreen) {element.requestFullscreen()} else if (element.webkitRequestFullScreen) {element.webkitRequestFullScreen()} else if (element.mozRequestFullScreen) {element.mozRequestFullScreen()} else if (element.msRequestFullscreen) {// IE11element.msRequestFullscreen()}}// 改变当前全屏状态state.fullscreen = !state.fullscreen
}
script><style scoped>
.full {width: 35px;height: 35px;background: var(--el-color-primary-light-9);border-radius: 50%;display: flex;align-items: center;justify-content: center;
}.icon-color {color: var(--el-color-primary);
}
style>
主题颜色自定义
添加一个初始化配置文件 src/stores/styleconfig.ts
import { defineStore } from 'pinia'let state = {// 默认 primary 主题颜色primary: '#752bec',// 白色背景bgWhite: '#ffffff',
}// 从缓存中读取预设的样式配置
const config = localStorage.getItem('styleConfig')
if (config) {state = JSON.parse(config)
}// 第一个参数是应用程序中 store 的唯一 id
export const styleConfig = defineStore('styleConfig', {state: () => state,
})
新建一个工具文件 src/utils/theme.ts
import { ElMessage } from 'element-plus';/*** hex颜色转rgb颜色* @param str 颜色值字符串* @returns 返回处理后的颜色值*/
export function hexToRgb(str: any) {let hexs: any = '';let reg = /^\#?[0-9A-Fa-f]{6}$/;if (!reg.test(str)) return ElMessage.warning('输入错误的hex');str = str.replace('#', '');hexs = str.match(/../g);for (let i = 0; i < 3; i++) hexs[i] = parseInt(hexs[i], 16);return hexs;
}/*** rgb颜色转Hex颜色* @param r 代表红色* @param g 代表绿色* @param b 代表蓝色* @returns 返回处理后的颜色值*/
export function rgbToHex(r: any, g: any, b: any) {let reg = /^\d{1,3}$/;if (!reg.test(r) || !reg.test(g) || !reg.test(b)) return ElMessage.warning('输入错误的rgb颜色值');let hexs = [r.toString(16), g.toString(16), b.toString(16)];for (let i = 0; i < 3; i++) if (hexs[i].length == 1) hexs[i] = `0${hexs[i]}`;return `#${hexs.join('')}`;
}/*** 加深颜色值* @param color 颜色值字符串* @param level 加深的程度,限0-1之间* @returns 返回处理后的颜色值*/
export function getDarkColor(color: string, level: number) {let reg = /^\#?[0-9A-Fa-f]{6}$/;if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值');let rgb = hexToRgb(color);for (let i = 0; i < 3; i++) rgb[i] = Math.floor(rgb[i] * (1 - level));return rgbToHex(rgb[0], rgb[1], rgb[2]);
}/*** 变浅颜色值* @param color 颜色值字符串* @param level 加深的程度,限0-1之间* @returns 返回处理后的颜色值*/
export function getLightColor(color: string, level: number) {let reg = /^\#?[0-9A-Fa-f]{6}$/;if (!reg.test(color)) return ElMessage.warning('输入错误的hex颜色值');let rgb = hexToRgb(color);for (let i = 0; i < 3; i++) rgb[i] = Math.floor((255 - rgb[i]) * level + rgb[i]);return rgbToHex(rgb[0], rgb[1], rgb[2]);
}
然后再新建一个配置文件 src/config/styleSetting.ts
import { styleConfig } from '../stores/styleconfig'
import { getLightColor } from '../utils/theme'//设置主题颜色
export const setPrimaryColor = (color = '') => {const el = document.documentElement// 设置主题颜色变量el.style.setProperty('--el-color-primary', color || styleConfig().primary)// 颜色变浅for (let i = 1; i <= 9; i++) {el.style.setProperty(`--el-color-primary-light-${i}`,`${getLightColor(color || styleConfig().primary, i / 10)}`)}
}// 设置主背景颜色
export const setBgWhite = (color = '') => {const el = document.documentElementel.style.setProperty('--el-color-bg-white', color || styleConfig().bgWhite)
}// 页面加载时默认执行所有方法
const setStyle = () => {setPrimaryColor()setBgWhite()
}export default setStyle
然后在 main.ts 中引入 styleSetting.ts
import setStyle from "./config/styleSetting";app.use(setStyle)
然后在 css 中需要设置主颜色时,直接使用变量来代替颜色值,例如下面是设置菜单选中时的颜色
>>> .el-menu-item.is-active {color: var(--el-color-primary);background: var(--el-color-primary-light-9);transition: 0.5s;
}>>> .el-menu-item.is-active:after {content: '';width: 5px;height: 100%;background-color: var(--el-color-primary);position: absolute;left: 0;transition: 0.5s;
}
默认显示的主题颜色

然后写一个设置主题颜色的方法,来实时的更新 --el-color-primary 值,点击保存后把配置保存在缓存中
<template><el-icon :size="20" class="icon-color" @click="state.isShow = true"><Brush />el-icon><el-drawerv-model="state.isShow"title="主题设置"direction="rtl"size="380px":before-close="beforeClose"><template #default><el-form:model="state.config"label-width="100px"class="content"label-position="left"><el-form-item label="主题颜色"><el-color-pickerv-model="state.config.primary"@change="changePrimary"/>el-form-item>el-form>template><template #footer><div style="flex: auto"><el-button @click="state.isShow = false">关闭el-button><el-button type="primary" @click="confirmClick">保存el-button>div>template>el-drawer>
template><script lang="ts" setup>
import { reactive } from 'vue'
import { setPrimaryColor } from '../../config/styleSetting'
import { styleConfig } from '../../stores/styleconfig'const state = reactive({// 是否显示右侧设置框isShow: false,// 主题配置对象config: {// 默认 primary 主题颜色primary: styleConfig().primary,// 白色背景bgWhite: styleConfig().bgWhite,},
})
// 关闭设置框
const beforeClose = () => {state.isShow = false
}
//修改主题色
const changePrimary = (color: string) => {setPrimaryColor(color)
}
// 保存配置
const confirmClick = () => {localStorage.setItem('styleConfig', JSON.stringify(state.config))window.location.reload()
}
script><style scoped>
.icon-color {width: 35px;height: 35px;background: var(--el-color-primary-light-9);border-radius: 50%;display: flex;align-items: center;justify-content: center;color: var(--el-color-primary);
}.content {border-top: 1px solid var(--el-color-primary-light-6);padding-top: 15px;
}
style>
修改一个颜色后,页面整体颜色都会发生变化

设置国际化
引入指定版本的 "vue-i18n": "^9.1.10" ,否则高版本会报错
npm install vue-i18n@9.1.10
新建文件夹 src/i18n,这个文件夹下新建如下文件:
- lang
- en.ts
- zh.ts
- router
- en.ts
- zh.ts
- index.ts
内容分别如下
// pages/en.ts
export default {login: {login: 'login',userName: 'userName',password: 'password'}
}
// pages/zh.ts
export default {login: {login: '登录',userName: '用户名',password: '密码'}
}
// router/en.ts
export default {router: {title: 'ZX-SYSTEM',home: 'home',about: 'about',order: 'mall management',order_list: 'orderList',order_stock: 'inventory',order_stock_price: 'price',system: 'system management',system_menu: 'menu',system_role: 'role',system_user: 'user',system_dept: 'department',},
}
// router/en.ts
export default {router: {title: 'ZX-管理系统',home: "首页",about: "关于我",order: "商城管理",order_list: "订单列表",order_stock: "库存管理",order_stock_price: "价格管理",system: "系统管理",system_menu: "菜单管理",system_role: "角色管理",system_user: "用户管理",system_dept: "部门管理",}
}
然后在 index.ts 里面整合
// index.ts
import {createI18n} from 'vue-i18n'
import pagesEn from "./pages/en"
import pagesZh from "./pages/zh"import layoutEn from "./router/en"
import layoutZh from "./router/zh"/*** ./pages 表示各个页面的国际化* ./router 表示左侧菜单的国际化*/const messages = {en: {...pagesEn,...layoutEn},zh: {...pagesZh,...layoutZh},
}
const language = (navigator.language || 'en').toLocaleLowerCase() // 这是获取浏览器的语言
const i18n = createI18n({legacy: false,locale: localStorage.getItem('lang') || language.split('-')[0] || 'en', // 首先从缓存里拿,没有的话就用浏览器语言,fallbackLocale: 'en', // 设置备用语言messages,
})export default i18n
在 main.ts 中引入
import i18n from "./i18n/index"const app = createApp(App)
app.use(i18n)
app.mount('#app')
使用也非常简单,根据前缀不同,会自动的显示不同的语言
在页面中使用
<template><div><div>{{ t('login.userName') }}div>div>
template><script lang="ts" setup>
import {useI18n} from 'vue-i18n'const {t} = useI18n()
script>
在菜单中使用,修改 name 的值,不能写死为固定的中文名,而是改成国际化文件对应的属性
{path: 'stock',meta: {roles: ['admin', 'common'],},name: 'router.order_stock',component: Parent,children: [{path: 'price',meta: {roles: ['admin', 'common'],},name: 'router.order_stock_price',component: () => import('../view/order/price.vue'),},],
},
然后在组件中使用 t 转义
<template v-else><el-menu-item :index="item.path" :key="item.path"><el-icon><icon-menu />el-icon><span>{{ t(item.name) }}span>el-menu-item>
template>
import { useI18n } from 'vue-i18n'const { t } = useI18n()
然后可以添加一个方法,来切换中英文显示
<template><div class="full" @click="taggerLang"><div class="icon-color">{{ state.lang === 'zh' ? '中' : 'en' }}div>div>
template><script setup>
import {reactive, computed, onMounted} from 'vue'const state = reactive({lang: 'zh',
})
onMounted(() => {const lang = localStorage.getItem("lang")if (!lang || lang === 'zh') {state.lang = 'zh'} else {state.lang = 'en'}
})const taggerLang = () => {if (state.lang === 'zh') {state.lang = 'en'} else {state.lang = 'zh'}localStorage.setItem("lang",state.lang)window.location.reload()
}
script>
英文界面

中文界面

动态路由
添加返回模拟数据的方法
模拟实现从后端直接获取路由进行菜单展示
首先新建:src/api/testrouter/index.ts,这个文件用来模拟后端接口返回数据
// 管理员看到的菜单
export const adminRouter = () => {return {code: 200,msg: '成功',data: {rows: [{path: '/home',meta: {roles: ['admin', 'common'],title: 'router.home',},name: 'router.home',component: 'view/home/home.vue',},{path: '/about',meta: {roles: ['admin', 'common'],title: 'router.about',},name: 'router.about',component: 'view/about/about',},{path: '/order',meta: {roles: ['admin', 'common'],title: 'router.order',},name: 'router.order',component: 'layout/routerview/Parent',children: [{path: '/order/list',meta: {roles: ['admin', 'common'],title: 'router.order_list',},name: 'router.order_list',component: 'view/order/list',},{path: '/order/stock',meta: {roles: ['admin', 'common'],title: 'router.order_stock',},name: 'router.order_stock',component: 'layout/routerview/Parent',children: [{path: '/order/price',meta: {roles: ['admin', 'common'],title: 'router.order_stock_price',},name: 'router.order_stock_price',component: 'view/order/price',},],},],},{path: '/system',name: 'router.system',meta: {roles: ['admin'],title: 'router.system',},component: 'layout/routerview/Parent',children: [{path: '/system/menu',meta: {roles: ['admin'],title: 'router.system_menu',},name: 'router.system_menu',component: 'view/system/menu',},{path: '/system/role',meta: {roles: ['admin'],title: 'router.system_role',},name: 'router.system_role',component: 'view/system/role',},{path: '/system/user',meta: {roles: ['admin'],title: 'router.system_user',},name: 'router.system_user',component: 'view/system/user',},{path: '/system/dept',meta: {roles: ['admin'],title: 'router.system_dept',},name: 'router.system_dept',component: 'view/system/dept',},],},],},}
}// 普通用户看到的菜单
export const commonRouter = () => {return {code: 200,msg: '成功',data: {rows: [{path: '/home',meta: {roles: ['admin', 'common'],title: 'router.home',},name: 'router.home',component: 'view/home/home.vue',},{path: '/about',meta: {roles: ['admin', 'common'],title: 'router.about',},name: 'router.about',component: 'view/about/about',},{path: '/order',meta: {roles: ['admin', 'common'],title: 'router.order',},name: 'router.order',component: 'layout/routerview/Parent',children: [{path: '/order/list',meta: {roles: ['admin', 'common'],title: 'router.order_list',},name: 'router.order_list',component: 'view/order/list',},{path: '/order/stock',meta: {roles: ['admin', 'common'],title: 'router.order_stock',},name: 'router.order_stock',component: 'layout/routerview/Parent',children: [{path: '/order/price',meta: {roles: ['admin', 'common'],title: 'router.order_stock_price',},name: 'router.order_stock_price',component: 'view/order/price',},],},],},],},}
}
添加模拟获取菜单数据的接口
新建:src/api/menu/index.ts,这个里面来获取上面接口的返回值
import { adminRouter, commonRouter } from '../testrouter'
import { userInfo } from '../../stores/userInfo'/*** 模拟获取后端返回的路由集合* @returns {Promise}*/
export const getRouterListFun = () => {const username = userInfo().getUserInfo().userNamereturn new Promise((resolve, reject) => {if (username === 'admin') {resolve(adminRouter())} else {resolve(commonRouter())}})
}
添加模拟返回用户信息的接口
接着新建模拟登录接口:src/api/login/index.ts
export const getUerInfoFun = (parames: any) => {return new Promise((resolve, reject) => {// 获取登录表单传递过来的参数console.log(parames)resolve({code: 200,msg: '成功',data: {info: {userName: 'admin',photo:'https://img2.baidu.com/it/u=1978192862,2048448374&fm=253&fmt=auto&app=138&f=JPEG?w=504&h=500',time: new Date().getTime(),roles: ['admin'],authBtnList: ['btn.add', 'btn.del', 'btn.edit', 'btn.link'],token: '123456',},},})})
}
封装获取菜单,保存菜单的方法
新建 src/router/backEnd.ts,用于处理后端返回的数据
import { RouteRecordRaw } from 'vue-router'
import { Session } from '../utils/storage'
import { getRouterListFun } from '../api/menu'
import { useRequestOldRoutes } from '../stores/requestOldRoutes'
import { dynamicRoutes, notFoundAndNoPower } from './routes'
import { formatFlatteningRoutes, formatTwoStageRoutes, router } from './index'
import { routesList } from '../stores/routesList'
import { useTagsViewRoutes } from '../stores/tagsViewRoutes'const layouModules: any = import.meta.glob('../layout/routerView/*.{vue,tsx}')
const viewsModules: any = import.meta.glob('../view/**/*.{vue,tsx}')// 后端控制路由/*** 获取目录下的 .vue、.tsx 全部文件* key是组件的地址,value为 component 函数* @method import.meta.glob* @link 参考:https://cn.vitejs.dev/guide/features.html#json*/
const dynamicViewsModules: Record<string, Function> = Object.assign({},{ ...layouModules },{ ...viewsModules }
)/*** 后端控制路由:初始化方法,防止刷新时路由丢失* @method NextLoading 界面 loading 动画开始执行* @method useUserInfo().setUserInfos() 触发初始化用户信息 pinia* @method useRequestOldRoutes().setRequestOldRoutes() 存储接口原始路由(未处理component),根据需求选择使用* @method setAddRoute 添加动态路由* @method setFilterMenuAndCacheTagsViewRoutes 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组*/
export async function initBackEndControlRoutes() {// 无 token 停止执行下一步if (!Session.get('token')) return false// 获取路由菜单数据const res: any = await getRouterListFun()// 存储接口原始路由(未处理component),根据需求选择使用await useRequestOldRoutes().setRequestOldRoutes(JSON.parse(JSON.stringify(res.data.rows)))// 清空路由,避免出错dynamicRoutes[0].children = []// 处理路由(component),替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由dynamicRoutes[0].children = await backEndComponent(res.data.rows)// 添加动态路由await setAddRoute()// 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组await setFilterMenuAndCacheTagsViewRoutes()
}/*** 设置路由到 vuex routesList 中(已处理成多级嵌套路由)及缓存多级嵌套数组处理后的一维数组* @description 用于左侧菜单、横向菜单的显示* @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)*/
export function setFilterMenuAndCacheTagsViewRoutes() {// 保存处理后的数据routesList().setRouterList(dynamicRoutes[0].children)setCacheTagsViewRoutes()
}/*** 缓存多级嵌套数组处理后的一维数组* @description 用于 tagsView、菜单搜索中:未过滤隐藏的(isHide)*/
export function setCacheTagsViewRoutes() {const storesTagsView = useTagsViewRoutes()storesTagsView.setTagsViewRoutes(formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))[0].children)
}/*** 处理路由格式及添加捕获所有路由或 404 Not found 路由* @description 替换 dynamicRoutes(/@/router/route)第一个顶级 children 的路由* @returns 返回替换后的路由数组*/
export function setFilterRouteEnd() {let filterRouteEnd: any = formatTwoStageRoutes(formatFlatteningRoutes(dynamicRoutes))filterRouteEnd[0].children = [...filterRouteEnd[0].children,...notFoundAndNoPower,]return filterRouteEnd
}/*** 添加动态路由* @method router.addRoute* @description 此处循环为 dynamicRoutes(/@/router/route)第一个顶级 children 的路由一维数组,非多级嵌套* @link 参考:https://next.router.vuejs.org/zh/api/#addroute*/
export async function setAddRoute() {await setFilterRouteEnd().forEach((route: RouteRecordRaw) => {router.addRoute(route)})
}/*** 后端路由 component 转换* @param routes 后端返回的路由表数组* @returns 返回处理成函数后的 component*/
export function backEndComponent(routes: any) {if (!routes) returnreturn routes.map((item: any) => {if (item.component)item.component = dynamicImport(dynamicViewsModules,item.component as string)item.children && backEndComponent(item.children)return item})
}/*** 后端路由 component 转换函数* @param dynamicViewsModules 获取目录下的 .vue、.tsx 全部文件* @param component 当前要处理项 component* @returns 返回处理成函数后的 component*/
export function dynamicImport(dynamicViewsModules: Record<string, Function>,component: string
) {const keys = Object.keys(dynamicViewsModules)const matchKeys = keys.filter((key) => {const k = key.replace(/..\//, '')return k.startsWith(`${component}`) || k.startsWith(`/${component}`)})if (matchKeys?.length === 1) {const matchKey = matchKeys[0]return dynamicViewsModules[matchKey]}if (matchKeys?.length > 1) {return false}
}
添加辅助文件
上面的代码中引用了下面的文件
src/stores/requestOldRoutes.ts
import { defineStore } from 'pinia'/*** 后端返回原始路由(未处理时)* @methods setCacheKeepAlive 设置接口原始路由数据*/
export const useRequestOldRoutes = defineStore('useRequestOldRoutes', {state: () => ({requestOldRoutes: [],}),actions: {async setRequestOldRoutes(routes: any) {this.requestOldRoutes = routes},},
})
src/stores/routesList.ts
import { defineStore } from 'pinia'// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {state: () => ({routesList: [],}),actions: {// 设置路由集合setRouterList(data: any) {this.routesList = data},},
})
src/stores/tagsViewRoutes.ts
import { defineStore } from 'pinia'
import { Session } from '../utils/storage'/*** TagsView 路由列表* @methods setTagsViewRoutes 设置 TagsView 路由列表* @methods setCurrenFullscreen 设置开启/关闭全屏时的 boolean 状态*/
export const useTagsViewRoutes = defineStore('tagsViewRoutes', {state: (): any => ({tagsViewRoutes: [],isTagsViewCurrenFull: false,}),actions: {async setTagsViewRoutes(data: Array<string>) {this.tagsViewRoutes = data},setCurrenFullscreen(bool: Boolean) {Session.set('isTagsViewCurrenFull', bool)this.isTagsViewCurrenFull = bool},},
})
修改导出路由的文件
然后改写 src/router/routes.ts,分成三个部分导出
import Layout from '../layout/index.vue'// 动态路由
export const dynamicRoutes = [{path: '/',name: '/',component: Layout,redirect: '/home',meta: {isKeepAlive: true,title: '首页',},children: [],},
]// 定义404,401等路由
export const notFoundAndNoPower = [{path: '/:path(.*)*',name: 'notFound',component: () => import('@/view/error/404.vue'),meta: {title: '404',isHide: true,},},{path: '/401',name: 'noPower',component: () => import('@/view/error/401.vue'),meta: {title: '404',isHide: true,},},
]/*** 定义静态路由(默认路由)*/
export const staticRoutes = [{path: '/login',name: 'router.login',component: () => import('../view/login/index.vue'),meta: {title: 'router.login',},},
]
修改router文件
修改 src/router/index.ts,默认只加载一个 staticRoutes
// 1.从vue-router导出两个方法使用
import { createRouter, createWebHashHistory, useRouter } from 'vue-router'
import { routesList } from '../stores/routesList'
import { staticRoutes } from './routes'
import { Session } from '../utils/storage'
import { initBackEndControlRoutes } from './backEnd'export const router = createRouter({// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。history: createWebHashHistory(),routes: staticRoutes, // 加载静态路由
})/*** 路由多级嵌套数组处理成一维数组* @param arr 传入路由菜单数据数组* @returns 返回处理后的一维路由菜单数组*/
export function formatFlatteningRoutes(arr: any) {if (arr.length <= 0) return falsefor (let i = 0; i < arr.length; i++) {if (arr[i].children) {arr = arr.slice(0, i + 1).concat(arr[i].children, arr.slice(i + 1))}}return arr
}/*** 一维数组处理成多级嵌套数组(只保留二级:也就是二级以上全部处理成只有二级,keep-alive 支持二级缓存)* @description isKeepAlive 处理 `name` 值,进行缓存。顶级关闭,全部不缓存* @link 参考:https://v3.cn.vuejs.org/api/built-in-components.html#keep-alive* @param arr 处理后的一维路由菜单数组* @returns 返回将一维数组重新处理成 `定义动态路由(dynamicRoutes)` 的格式*/
export function formatTwoStageRoutes(arr: any) {if (arr.length <= 0) return falseconst newArr: any = []const cacheList: Array<string> = []arr.forEach((v: any) => {if (v.path === '/') {newArr.push({component: v.component,name: v.name,path: v.path,redirect: v.redirect,meta: v.meta,children: [],})} else {// 判断是否是动态路由(xx/:id/:name),用于 tagsView 等中使用if (v.path.indexOf('/:') > -1) {v.meta['isDynamic'] = truev.meta['isDynamicPath'] = v.path}newArr[0].children.push({ ...v })}})return newArr
}// 路由加载前
router.beforeEach(async (to, from, next) => {const token = Session.get('token')if (to.path === '/login' && !token) {next()} else {if (!token) {next(`/login?redirect=${to.path}¶ms=${JSON.stringify(to.query ? to.query : to.params)}`)Session.clear()} else if (token && to.path === '/login') {next('/home')} else {// 判断pinia中是否有路由信息if (routesList().routesList.length === 0) {// 后端控制路由:路由数据初始化,防止刷新时丢失await initBackEndControlRoutes()// 动态添加路由:防止非首页刷新时跳转回首页的问题next({ ...to, replace: true })} else {next()}}}
})// 导出路由
export default router
添加登录页面完成测试
最后添加登录页面
<template><div><el-button type="primary" @click="login">登录el-button>div>
template><script lang="ts" setup>
import { reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getUerInfoFun } from '../../api/login'
import { userInfo } from '../../stores/userInfo'
import { initBackEndControlRoutes } from '../../router/backEnd'const state = reactive({router: useRouter(),route: useRoute(),loginForm: {username: 'admin',password: '123456',code: 1234,},
})const login = () => {// 获取用户信息getUerInfoFun(state.loginForm).then(async (res: any) => {// 保存用户基本信息await userInfo().setUserInfos(res.data.info)// 获取路由信息await initBackEndControlRoutes()// 进行路由跳转siginSuccess()})
}const siginSuccess = () => {// 跳转到上次关闭的页面if (state.route.query?.redirect) {state.router.push({path: <string>state.route.query?.redirect,query:Object.keys(<string>state.route.query?.params).length > 0? JSON.parse(<string>state.route.query?.params): '',})} else {// 跳转到首页state.router.push('/')}
}
script>
实现退出登录方法
<template><span>{{ userInfo().getUserInfo().userName }}span><el-dropdown><div class="user-img"><img :src="userInfo().getUserInfo().photo" />div><template #dropdown><el-dropdown-menu><el-dropdown-item @click="logOut">{{ t('router.log_out') }}el-dropdown-item>el-dropdown-menu>template>el-dropdown>
template><script setup>
import { Session } from '@/utils/storage'
import { userInfo } from '@/stores/userInfo'
import { useI18n } from 'vue-i18n'
import { reactive } from 'vue'
import { useRouter } from 'vue-router'const { t } = useI18n()const state = reactive({router: useRouter(),
})const logOut = () => {Session.clear()state.router.push({path: '/login',})
}
script><style scoped lang="scss">
.user-img {width: 44px;height: 44px;border-radius: 50px;border: 2px var(--el-color-primary) solid;display: flex;align-items: center;justify-content: center;img {width: 100%;height: 100%;border-radius: 50px;}
}
style>
测试不同人员返回不同菜单
首先我们直接写死一个用户名为 test 的用户来登录

查看菜单,没有系统管理的菜单

然后再用 admin 登录

修改后,重新登录查看菜单

在js中使用scss变量
首先创建 scss 变量文件 primary.module.scss
$primary-color: var(--el-color-primary);:export {primaryColor: $primary-color
}
需要注意的是,在 vite 创建的项目中,如果你想在 js 里引用 scss 文件,需要在后缀前加上 .module 。
然后再 js 中引入,html 中直接使用对应的变量即可
<el-table:header-cell-style="{ background: exCss.primaryColor, color: '#fff' }":data="state.tableData"stripestyle="width: 100%"
><el-table-column prop="date" label="Date" width="180" /><el-table-column prop="name" label="Name" width="180" /><el-table-column prop="address" label="Address" />
el-table>
import exCss from '@/style/module/primary.module.scss'
我这里做了一个表头的背景色跟着主题色变化的功能


webstorm设置代码块


自动导入vue3相关Api
安装
npm install -D unplugin-vue-components unplugin-auto-import
修改配置
// vite.config.tsimport {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import {ElementPlusResolver} from 'unplugin-vue-components/resolvers'// https://vitejs.dev/config/
export default defineConfig({plugins: [vue(),AutoImport({// Auto import functions from Vue, e.g. ref, reactive, toRef...// 自动导入 Vue 相关函数,如:ref, reactive, toRef 等imports: ['vue'],// Auto import functions from Element Plus, e.g. ElMessage, ElMessageBox... (with style)// 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)resolvers: [ElementPlusResolver(),],}),Components({resolvers: [// 自动导入 Element Plus 组件ElementPlusResolver(),],}),]
})
添加自动依赖
重启项目后会自动生成两个文件

其中 auto-import.d.ts 文件里面声明了所有可以自动引入的 Api
使用
设置完成后,在页面使用 reactive,ref,onMounted 等函数时,无需从 vue 中导出,可以直接使用。示例如下
在使用过程中也通过 webStorm 可以看到改函数的来源

<template><div>count:{{ state.total }}<el-button @click="add">添加el-button>div>
template>
<script setup>const state = reactive({total: 0
})const add = () => {state.total += 1
}
script>
效果在页面中可以正常显示,控制台也没有报错

自定义全局loading
首先添加css样式文件 src/style/loading.scss
.loading-next {position: absolute;display: flex;width: 100vw;height: 100vh;z-index: 99999;&::before {content: "";width: 100%;height: 100%;background-color: black;opacity: 0.6;z-index: -1;}
}.loading-next .loading-next-box {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);
}.loading-next .loading-next-box-warp {width: 80px;height: 80px;
}.loading-next .loading-next-box-warp .loading-next-box-item {width: 33.333333%;height: 33.333333%;background: var(--el-color-primary);float: left;animation: loading-next-animation 1.2s infinite ease;border-radius: 1px;
}.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(7) {animation-delay: 0s;
}.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(4),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(8) {animation-delay: 0.1s;
}.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(1),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(5),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(9) {animation-delay: 0.2s;
}.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(2),
.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(6) {animation-delay: 0.3s;
}.loading-next .loading-next-box-warp .loading-next-box-item:nth-child(3) {animation-delay: 0.4s;
}@keyframes loading-next-animation {0%,70%,100% {transform: scale3D(1, 1, 1);}35% {transform: scale3D(0, 0, 1);}
}
添加 src/utils/loading.ts
import { nextTick } from 'vue'
import '../style/loading.scss'/*** 页面全局 Loading* @method start 创建 loading* @method done 移除 loading*/
export const zxLoading = {// 创建 loadingshow: () => {const bodys: Element = document.bodyconst div = <HTMLElement>document.createElement('div')div.setAttribute('class', 'loading-next')const htmls = ``div.innerHTML = htmlsbodys.insertBefore(div, bodys.childNodes[0])},// 移除 loadinghidden: () => {nextTick(() => {const el = <HTMLElement>document.querySelector('.loading-next')el?.parentNode?.removeChild(el)})},
}
使用
import { zxLoading } from '@/utils/loading'zxLoading.show()setTimeout(()=>{zxLoading.hidden()
},2000)
CSS设置动画
<template><div class="err-text"><div>404div>div>
template><style scoped>
.err-text {width: 100%;height: 100%;display: flex;align-items: center;justify-content: center;font-weight: 700;font-size: 70px;/*设置动画名称,多少时间内完成,infinite:无限循环播放*/animation: zoom 0.7s infinite;/*开启反向动画*/animation-direction: alternate;
}@keyframes zoom {0% {font-size: 70px;color: rgba(242, 80, 80, 0.96);}100% {font-size: 120px;color: #8935ea;}
}
style>
效果展示

本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
