项目简介

源码地址

https://gitee.com/szxio/zx-vue-next

描述

本项目是使用 vue3 来开发的后台管理系统模板。页面简单大方,使用悬浮式的风格,将菜单栏,顶部面包屑,中间操作区域等合理划分,功能丰富,支持主题颜色自定义,一键开启黑色主题,浅色、深色菜单动态切换等。路由采用动态路由,依托若依后端接口,拥有强大的权限管理功能。对若依感兴趣的点此跳转,希望各位小伙伴能够在学习本项目的过程中或多或少的有所收获。

如果感觉对你有所帮助,请点击 Star,感谢支持。

本文档同步至以下网站:

技术栈

  • vue3
  • element-plus
  • Pinia
  • vue-router
  • js-cookie
  • sass
  • ….

页面截图

8558467.jpg

8558467.jpg

8558467.jpg

8558467.jpg

本地运行

本项目后端借用了若依的后台框架,在她的基础上稍作了修改。

可以在本地启动本项目中的 java 代码。再启动前端查看效果。

若依启动成功截图

8558467.jpg

创建项目

使用 vite 来创建我们的工程

1
npm create vite@latest

或者

1
yarn create vite

然后按照提示操作即可!

elementui-plus

安装

官方文档:https://element-plus.gitee.io/zh-CN/

安装

1
cnpm install element-plus --save

引入

1
2
3
4
5
6
7
8
9
10
// 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')

组件自动导入

实现组件自动导入

1
npm install -D unplugin-vue-components unplugin-auto-import

然后把下列代码插入到你的 Vite 的配置文件中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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()],
}),
],
})

注册所有图标

安装

1
2
3
4
5
6
# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue
# pnpm
$ pnpm install @element-plus/icons-vue

注册所有组件

1
2
3
4
5
6
7
8
// main.ts

import * 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',然后设置全局语言即可

1
2
3
4
5
6
7
8
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/

安装

1
npm install vue-router@4

新建测试路由

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
// 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;

引入

1
2
3
4
5
6
// main.ts
import router from "./router/index"

const app = createApp(App)
app.use(router)
app.mount('#app')

修改App.vue

1
2
3
<template>
<router-view/>
</template>

配置@路径别名

首先安装依赖

1
npm install @types/node
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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代码格式化插件

安装

1
npm i --save-dev prettier

然后再根目录新建 .prettierrc 文件,内容如下

1
2
3
4
5
6
7
{
"printWidth": 180,
"tabWidth": 4,
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}

然后以 webStorm 工具为例,使用 prettier

8558467.jpg

格式效果

8558467.jpg

Pinia

安装

1
npm install pinia

添加 src/stores/index.ts

1
2
3
4
5
import { createPinia } from 'pinia'
// 创建
const pinia = createPinia()
// 导出
export default pinia

引入

1
2
3
4
5
6
7
8
9
10
11
// main.ts

import {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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {
state: () => ({
routesList: [],
}),
actions: {
// 设置路由集合
async setRouterList(data: any) {
this.routesList = data
},
},
})

然后再前置路由守卫中调用方法保存

1
2
3
4
5
6
7
8
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

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
<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>
<!--<SvgIcon :name="val.meta.icon" />-->
<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">
<!--<SvgIcon :name="item.meta.icon" />-->
<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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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>

效果展示

8558467.jpg

根据权限显示菜单

首先设置两个角色:admin、common,分别表示管理员和普通用户。

创建 src/stores/userInfo.ts,暂时写死一个用户数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const userInfo = defineStore('userInfo', {
state: () => ({
// 用户名称
userName: 'admin',
// 用户id
userId: '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 文件,存放路由集合信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 数组,表示只要用户的角色在这里就显示当前菜单

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
// 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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 文件,待会会用到这里面的方法

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
/**
* 判断路由 `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 方法,来实现根据权限获取不同菜单

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
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.path
if (item.children) {
resolvePath(item.path, item.children)
}
})
}
// 路由更新时更新菜单选中
onBeforeRouteUpdate((to) => {
state.active = to.path
})
// 根据用户权限获取菜单数据
const getRouterListByRole = () => {
const roles = userInfo().roles
const routerList = JSON.parse(JSON.stringify(routesList().routesList))
return setFilterHasRolesMenu(routerList, roles)
}

效果显示

普通用户没有系统管理菜单

8558467.jpg

管理员可以看到系统管理

8558467.jpg

实现点击菜单进行路由跳转

首先添加下面代码,作用是可以将内层的菜单设置为全路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.path
if (item.children) {
resolvePath(item.path, item.children)
}
})
}

然后开启 Menu 组件的 router 模式即可

动态面包屑导航

完整代码

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
<template>
<el-breadcrumb separator="/">
<el-breadcrumb-item
v-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>

效果

8558467.jpg

页面最大化和最小化

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
<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) {
// IE11
element.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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

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

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
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.documentElement
el.style.setProperty('--el-color-bg-white', color || styleConfig().bgWhite)
}

// 页面加载时默认执行所有方法
const setStyle = () => {
setPrimaryColor()
setBgWhite()
}

export default setStyle

然后在 main.ts 中引入 styleSetting.ts

1
2
3
import setStyle from "./config/styleSetting";

app.use(setStyle)

然后在 css 中需要设置主颜色时,直接使用变量来代替颜色值,例如下面是设置菜单选中时的颜色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> .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;
}

默认显示的主题颜色

8558467.jpg

然后写一个设置主题颜色的方法,来实时的更新 --el-color-primary 值,点击保存后把配置保存在缓存中

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
72
73
74
75
76
77
78
79
80
81
82
83
<template>
<el-icon :size="20" class="icon-color" @click="state.isShow = true">
<Brush />
</el-icon>
<el-drawer
v-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-picker
v-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>

修改一个颜色后,页面整体颜色都会发生变化

8558467.jpg

设置国际化

引入指定版本的 "vue-i18n": "^9.1.10" ,否则高版本会报错

1
npm install vue-i18n@9.1.10

新建文件夹 src/i18n,这个文件夹下新建如下文件:

  • lang
    • en.ts
    • zh.ts
  • router
    • en.ts
    • zh.ts
  • index.ts

内容分别如下

1
2
3
4
5
6
7
8
// pages/en.ts
export default {
login: {
login: 'login',
userName: 'userName',
password: 'password'
}
}
1
2
3
4
5
6
7
8
// pages/zh.ts
export default {
login: {
login: '登录',
userName: '用户名',
password: '密码'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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',
},
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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 里面整合

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
// 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 中引入

1
2
3
4
5
import i18n from "./i18n/index"

const app = createApp(App)
app.use(i18n)
app.mount('#app')

使用也非常简单,根据前缀不同,会自动的显示不同的语言

在页面中使用

1
2
3
4
5
6
7
8
9
10
11
<template>
<div>
<div>{{ t('login.userName') }}</div>
</div>
</template>

<script lang="ts" setup>
import {useI18n} from 'vue-i18n'

const {t} = useI18n()
</script>

在菜单中使用,修改 name 的值,不能写死为固定的中文名,而是改成国际化文件对应的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
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 转义

1
2
3
4
5
6
7
8
<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>
1
2
3
import { useI18n } from 'vue-i18n'

const { t } = useI18n()

然后可以添加一个方法,来切换中英文显示

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

英文界面

8558467.jpg

中文界面

8558467.jpg

动态路由

添加返回模拟数据的方法

模拟实现从后端直接获取路由进行菜单展示

首先新建:src/api/testrouter/index.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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
// 管理员看到的菜单
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,这个里面来获取上面接口的返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { adminRouter, commonRouter } from '../testrouter'
import { userInfo } from '../../stores/userInfo'

/**
* 模拟获取后端返回的路由集合
* @returns {Promise<unknown>}
*/
export const getRouterListFun = () => {
const username = userInfo().getUserInfo().userName

return new Promise((resolve, reject) => {
if (username === 'admin') {
resolve(adminRouter())
} else {
resolve(commonRouter())
}
})
}

添加模拟返回用户信息的接口

接着新建模拟登录接口:src/api/login/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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,用于处理后端返回的数据

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
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) return
return 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { defineStore } from 'pinia'

// 第一个参数是应用程序中 store 的唯一 id
export const routesList = defineStore('routesList', {
state: () => ({
routesList: [],
}),
actions: {
// 设置路由集合
setRouterList(data: any) {
this.routesList = data
},
},
})

src/stores/tagsViewRoutes.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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,分成三个部分导出

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
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
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
// 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 false
for (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 false
const 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'] = true
v.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}&params=${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

添加登录页面完成测试

最后添加登录页面

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

实现退出登录方法

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
<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 的用户来登录

8558467.jpg

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

8558467.jpg

然后再用 admin 登录

8558467.jpg

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

8558467.jpg

在js中使用scss变量

首先创建 scss 变量文件 primary.module.scss

1
2
3
4
5
$primary-color: var(--el-color-primary);

:export {
primaryColor: $primary-color
}

需要注意的是,在 vite 创建的项目中,如果你想在 js 里引用 scss 文件,需要在后缀前加上 .module

然后再 js 中引入,html 中直接使用对应的变量即可

1
2
3
4
5
6
7
8
9
10
<el-table
:header-cell-style="{ background: exCss.primaryColor, color: '#fff' }"
:data="state.tableData"
stripe
style="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>
1
import exCss from '@/style/module/primary.module.scss'

我这里做了一个表头的背景色跟着主题色变化的功能

8558467.jpg

8558467.jpg

webstorm设置代码块

8558467.jpg

8558467.jpg

自动导入vue3相关Api

安装

1
npm install -D unplugin-vue-components unplugin-auto-import

修改配置

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
// vite.config.ts

import {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(),
],
}),
]
})

添加自动依赖

重启项目后会自动生成两个文件

8558467.jpg

其中 auto-import.d.ts 文件里面声明了所有可以自动引入的 Api

使用

设置完成后,在页面使用 reactive,ref,onMounted 等函数时,无需从 vue 中导出,可以直接使用。示例如下

在使用过程中也通过 webStorm 可以看到改函数的来源

8558467.jpg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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>

效果在页面中可以正常显示,控制台也没有报错

8558467.jpg

自定义全局loading

首先添加css样式文件 src/style/loading.scss

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
72
.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

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
import { nextTick } from 'vue'
import '../style/loading.scss'

/**
* 页面全局 Loading
* @method start 创建 loading
* @method done 移除 loading
*/
export const zxLoading = {
// 创建 loading
show: () => {
const bodys: Element = document.body
const div = <HTMLElement>document.createElement('div')
div.setAttribute('class', 'loading-next')
const htmls = `
<div class="loading-next-box">
<div class="loading-next-box-warp">
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
<div class="loading-next-box-item"></div>
</div>
</div>
`
div.innerHTML = htmls
bodys.insertBefore(div, bodys.childNodes[0])
},
// 移除 loading
hidden: () => {
nextTick(() => {
const el = <HTMLElement>document.querySelector('.loading-next')
el?.parentNode?.removeChild(el)
})
},
}

使用

1
2
3
4
5
6
7
import { zxLoading } from '@/utils/loading'

zxLoading.show()

setTimeout(()=>{
zxLoading.hidden()
},2000)

CSS设置动画

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
<template>
<div class="err-text">
<div>404</div>
</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>

效果展示

8558467.jpg