目录
侧边栏菜单渲染
结合ElementPlus组件库进行实现
新建的Vue3项目,引入了格式化样式normalize.css和ElementPlus,并进行了全局引入
并进行了全局引入
设置高度为100%
粘贴ElementPlus的布局容器代码,到HomeView.vue文件中,并进行简单修改
<template>
<div class="contioner">
<el-container class="layout-container-demo" style="height: 100%">
<!-- 侧边栏 START -->
<el-aside width="200px">
<el-scrollbar>
<el-menu router>
<!-- 一级菜单 -->
<el-menu-item index="1-4-1">一级菜单</el-menu-item>
<!-- 子级菜单 -->
<el-sub-menu index="1">
<template #title>
<el-icon><message /></el-icon>子级菜单
</template>
<el-menu-item index="1-4-1">孙子菜单1</el-menu-item>
<el-sub-menu index="1-4">
<template #title>孙子菜单2</template>
<el-menu-item index="1-4-1">重孙菜单</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</el-aside>
<!-- 侧边栏 END -->
<!-- 内容区 START -->
<el-container>
<el-header style="text-align: right; font-size: 12px">
<div class="toolbar">
<el-dropdown>
<el-icon style="margin-right: 8px; margin-top: 1px">
<setting />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>View</el-dropdown-item>
<el-dropdown-item>Add</el-dropdown-item>
<el-dropdown-item>Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>Tom</span>
</div>
</el-header>
<el-main>
<el-scrollbar>
<!-- 路由出口,可以展示子级菜单内容 -->
<router-view></router-view>
</el-scrollbar>
</el-main>
</el-container>
<!-- 内容区 END -->
</el-container>
</div>
</template>
<script setup lang="ts">
import { Message, Setting } from '@element-plus/icons-vue'
</script>
<style scoped>
.contioner {
width: 100%;
height: 100%;
}
.layout-container-demo .el-header {
position: relative;
background-color: var(--el-color-primary-light-7);
color: var(--el-text-color-primary);
}
.layout-container-demo .el-aside {
color: var(--el-text-color-primary);
background: var(--el-color-primary-light-8);
}
.layout-container-demo .el-menu {
border-right: none;
}
.layout-container-demo .el-main {
padding: 0;
}
.layout-container-demo .toolbar {
display: inline-flex;
align-items: center;
justify-content: center;
height: 100%;
right: 20px;
}
</style>
效果图如下,一个简单的后台架子就出来了
然后开始路由新增
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
children: [
{
path: '/test1',
name: 'test1',
component: () => import('@/views/ParentPage.vue'),
},
{
path: '/test2',
name: 'test2',
children: [
{
path: '/test3',
name: 'test3',
component: () => import('@/views/SonPage.vue'),
},
{
path: '/test4',
name: 'test4',
children: [
{
path: '/test5',
name: 'test5',
component: () => import('@/views/SonPage.vue'),
},
],
},
],
},
],
},
{
path: '/login',
name: 'login',
component: () => import('../views/AboutView.vue'),
},
],
})
export default router
在views下新增ParentPage.vue和SonPage.vue文件
路由定义好了,就可以在HomeView.vue文件中引入路由数据了
<script setup lang="ts">
import { Message, Setting } from '@element-plus/icons-vue'
import { computed } from 'vue'
import { useRouter } from 'vue-router'
const router = useRouter()
// 路由数据
const routes = computed(() => {
return router.getRoutes().filter(p => p.path === '/')[0].children
})
</script>
多级菜单渲染,需要用到递归组件,所以我们把侧边栏菜单展示进行封装,views下新建MenuTree.vue
<template>
<div>
<template v-for="i in routes" :key="i.path">
<!-- 一级菜单 -->
<el-menu-item :index="i.path" v-if="!i.children">
{{ i.path }}
</el-menu-item>
<!-- 子级菜单 -->
<el-sub-menu :index="i.path" v-else>
<template #title>
<el-icon><message /></el-icon>{{ i.path }}
</template>
<!-- 使用递归组件,传递嵌套路由数据 -->
<MenuTree :routes="i.children"></MenuTree>
</el-sub-menu>
</template>
</div>
</template>
<script setup lang="ts">
import { Message } from '@element-plus/icons-vue'
import type { RouteRecordRaw } from 'vue-router'
defineProps({
routes: {
type: Array<RouteRecordRaw>,
default: () => [],
},
})
</script>
最后在HomeView.vue中引入此组件
<!-- 侧边栏 START -->
<el-aside width="200px">
<el-scrollbar>
<el-menu router>
<MenuTree :routes="routes"></MenuTree>
</el-menu>
</el-scrollbar>
</el-aside>
<!-- 侧边栏 END -->
此时就完成了侧边栏菜单的渲染
侧边栏折叠
然后再加一个侧边栏折叠的功能
添加一个按钮到头部,修改HomeView.vue
结构处修改:
<el-header>
<el-button type="primary" size="small">折叠/展开</el-button>
<div class="toolbar">
<el-dropdown>
<el-icon style="margin-right: 8px; margin-top: 1px">
<setting />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>View</el-dropdown-item>
<el-dropdown-item>Add</el-dropdown-item>
<el-dropdown-item>Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>Tom</span>
</div>
</el-header>
样式处修改:
.layout-container-demo .el-header {
position: relative;
background-color: var(--el-color-primary-light-7);
color: var(--el-text-color-primary);
display: flex;
justify-content: space-between;
align-items: center;
}
页面效果
然后修改侧边栏代码 修改HomeView.vue
结构处修改
<!-- 侧边栏 START -->
<!-- width="collapse" 可以让侧边栏宽度正好合适!!! -->
<el-aside width="collapse">
<el-scrollbar>
<el-menu router class="el-menu-vertical-demo" :collapse="isCollapse">
<MenuTree :routes="routes"></MenuTree>
</el-menu>
</el-scrollbar>
</el-aside>
<!-- 侧边栏 END -->
逻辑处新增:
// 折叠状态
const isCollapse = ref(false)
// 折叠/展开
const collapseChange = () => {
isCollapse.value = !isCollapse.value
}
样式处新增:
/* 侧边栏宽度 */
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
}
MenuTree.vue修改
<template>
<template v-for="i in routes" :key="i.path">
<!-- 一级菜单 -->
<el-menu-item :index="i.path" v-if="!i.children">
{{ i.path }}
</el-menu-item>
<!-- 子级菜单 -->
<el-sub-menu :index="i.path" v-else>
<template #title>
<el-icon><message /></el-icon>
<!-- 标题一定要用标签包裹,不然折叠的时候会显示一部分字!!! -->
<span>{{ i.path }}</span>
</template>
<!-- 使用递归组件,传递嵌套路由数据 -->
<MenuTree :routes="i.children"></MenuTree>
</el-sub-menu>
</template>
</template>
看下效果:
- 折叠
- 展开
黑白主题
然后再加个黑白主题吧(ElementPlus本身就有此功能)暗黑模式 | Element Plus
首先在main.ts中引入暗色主题
import 'element-plus/theme-chalk/dark/css-vars.css'
然后在HomeView.vue中新增切换按钮并添加逻辑处理
结构处修改:
<el-header>
<el-button type="primary" size="small" @click="collapseChange"
>折叠/展开</el-button
>
<div class="toolbar">
<el-switch
inline-prompt
v-model="theme"
active-text="暗黑"
inactive-text="白亮"
@change="toggle"
/>
<el-dropdown>
<el-icon style="margin-right: 8px; margin-top: 1px">
<setting />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>View</el-dropdown-item>
<el-dropdown-item>Add</el-dropdown-item>
<el-dropdown-item>Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>Tom</span>
</div>
</el-header>
逻辑处修改:
// 引入切换主题的hook
import { useDark, useToggle } from '@vueuse/core'
// 主题状态
const theme = ref(localStorage.getItem('theme') === 'dark' ? true : false)
// 切换主题
const isDark = useDark({
// 存储到localStorage中的Key 根据自己的需求更改
storageKey: 'theme',
// 暗黑class名字
valueDark: 'dark',
// 高亮class名字
valueLight: 'light',
})
const toggle = useToggle(isDark)
但是这个只实现了在组件库的主题切换,我们自己写的代码也要实现主题切换,可以在assets文件夹下新建theme文件夹,再新建theme.scss文件
// 主题
$themes: (
// 白亮: 设置一些字体颜色,背景色什么的
light:
(
background: #fff,
color: #000,
textColor: #000,
),
// 暗黑
dark:
(
background: #121212,
color: #fff,
textColor: #fff,
)
);
// 当前主题
$curTheme: light;
// 混合
// @mixin useTheme() {
// html[data-theme='light'] & {
// background-color: #fff;
// color: #000;
// }
// html[data-theme='dark'] & {
// background-color: #121212;
// color: #fff;
// }
// }
// 混合优化(遍历上面的主题)
@mixin useTheme() {
@each $key, $value in $themes {
$curTheme: $key !global; // 当前的主题
html[data-theme='#{$key}'] & {
// & 表示传入什么选择器就是什么选择器
@content; // 类似于插槽,样式可以进行传入
}
}
}
// 生成对应主题的变量
@function getVar($key) {
$themeMap: map-get($themes, $curTheme);
@return map-get($themeMap, $key);
}
全局引入此scss文件
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
// 配置全局scss文件
css: {
preprocessorOptions: {
// 引入全局的scss文件
scss: {
additionalData: `@import "./src/assets/theme/theme.scss";`,
},
},
},
})
找个测试页面测试即可
<template>
<div>
<h2>父级页面</h2>
<div class="box"></div>
<div class="mb-4">
<el-button>Default</el-button>
<el-button type="primary">Primary</el-button>
<el-button type="success">Success</el-button>
<el-button type="info">Info</el-button>
<el-button type="warning">Warning</el-button>
<el-button type="danger">Danger</el-button>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.box {
width: 100%;
height: 200px;
/* 使用主题变量 */
background-color: getVar('background');
}
</style>
全屏切换
再做一个切换全屏的效果
安装一个screenfull插件
npm i screenfull
然后修改HomeView.vue的代码
结构处修改:
<el-header>
<el-button type="primary" size="small" @click="collapseChange"
>折叠/展开</el-button
>
<div class="toolbar">
<el-switch
inline-prompt
v-model="theme"
active-text="暗黑"
inactive-text="白亮"
@change="toggle"
/>
<el-button type="primary" size="small" @click="screenfullChange"
>全屏/非全屏</el-button
>
<el-dropdown>
<el-icon style="margin-right: 8px; margin-top: 1px">
<setting />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>View</el-dropdown-item>
<el-dropdown-item>Add</el-dropdown-item>
<el-dropdown-item>Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>Tom</span>
</div>
</el-header>
逻辑处修改:
import screenfull from 'screenfull'
import { ElMessage } from 'element-plus'
// 全屏/非全屏
const screenfullChange = () => {
if (!screenfull.isEnabled) {
return ElMessage({ message: '你的浏览器不支持全屏', type: 'warning' })
}
screenfull.toggle()
}
切换组件主题色
如果不喜欢ElementPlus的组件配色,也可以自己进行修改: 主题 | Element Plus
新建styles文件夹,然后再新建element文件夹,然后再新建index.scss文件,粘贴以下代码
// styles/element/index.scss
/* 只需要重写你需要的即可 */
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: (
'primary': (
'base': green,
),
),
);
// 如果只是按需导入,则可以忽略以下内容。
// 如果你想导入所有样式:
@use "element-plus/theme-chalk/src/index.scss" as *;
然后再修改main.ts,更换引入的scss文件
import ElementPlus from 'element-plus'
// import 'element-plus/dist/index.css' // 更换完下面的scss文件,因为下面的scss文件中已经引入了所有样式
import './styles/element/index.scss'
此时就已经修改成功了,组件主题色都变成了我们设置的颜色
但是如果想通过按钮修改主题色呢?比如下面的效果
我们应该这么做
新增一个处理设置组件主题颜色的函数:在utils文件夹下新增theme.ts
// import { useCssVar } from '@vueuse/core'
type RGB = {
r: number
g: number
b: number
}
const rgbWhite = {
r: 255,
g: 255,
b: 255,
}
const rgbBlack = {
r: 0,
g: 0,
b: 0,
}
function componentToHex(c: number): string {
const hex = Math.round(c).toString(16)
return hex.length === 1 ? '0' + hex : hex
}
function rgbToHex(rgb: RGB): string {
return `#${componentToHex(rgb.r)}${componentToHex(rgb.g)}${componentToHex(rgb.b)}`
}
function mix(color: RGB, mixColor: RGB, weight: number): RGB {
return {
r: color.r * (1 - weight) + mixColor.r * weight,
g: color.g * (1 - weight) + mixColor.g * weight,
b: color.b * (1 - weight) + mixColor.b * weight,
}
}
/**
* hex 转换为 rgb
* @param hex 例如 #FF0000
*/
function hexToRGB(hex: string): RGB {
if (!/^[0-9A-Fa-f]{3}$|[0-9A-Fa-f]{6}$/.test(hex)) {
throw new Error('请传入合法的16进制颜色值,eg: #FF0000')
}
// 移除可能存在的 # 符号
hex = hex.replace('#', '')
// 确保十六进制代码是有效的
// 返回 RGB 对象
return {
r: parseInt(hex.slice(0, 2), 16),
g: parseInt(hex.slice(2, 4), 16),
b: parseInt(hex.slice(4, 6), 16),
}
}
/**
* 修改 element-plus的颜色主题
*/
function updateElementPlusTheme(type: string, baseColor: string): void {
// 针对 element-plus 进行修改
const colorArray: Record<string, string>[] = [
{ className: `--el-color-${type}`, color: rgbToHex(mix(hexToRGB(baseColor), rgbBlack, 0)) },
{
className: `--el-color-${type}-dark-2`,
color: rgbToHex(mix(hexToRGB(baseColor), rgbBlack, 0.2)),
},
{
className: `--el-color-${type}-light-3`,
color: rgbToHex(mix(hexToRGB(baseColor), rgbWhite, 0.3)),
},
{
className: `--el-color-${type}-light-5`,
color: rgbToHex(mix(hexToRGB(baseColor), rgbWhite, 0.5)),
},
{
className: `--el-color-${type}-light-7`,
color: rgbToHex(mix(hexToRGB(baseColor), rgbWhite, 0.7)),
},
{
className: `--el-color-${type}-light-8`,
color: rgbToHex(mix(hexToRGB(baseColor), rgbWhite, 0.78)),
},
{
className: `--el-color-${type}-light-9`,
color: rgbToHex(mix(hexToRGB(baseColor), rgbWhite, 0.85)),
},
]
// document.documentElement 是全局变量时
// const el = document.documentElement
colorArray.forEach((item) => {
// 下面两种方式都可以
// 方法1: 需要把顶部的导入解开
// const color = useCssVar(item.className, document.documentElement)
// color.value = item.color
// 方法2:
document.documentElement.style.setProperty(item.className, item.color)
// 方法3: 把上面的el解开
// 获取 css 变量
// getComputedStyle(el).getPropertyValue(item.className)
// 设置 css 变量
// el.style.setProperty(item.className, item.color)
})
}
export { type RGB, hexToRGB, rgbToHex, updateElementPlusTheme }
修改HomeView.vue:
结构处修改:
<!-- 颜色选择器 -->
<el-color-picker
v-model="color"
:predefine="predefineColors"
@change="setThemeColor"
color-format="hex"
:show-alpha="false"
/>
逻辑处修改:
import { updateElementPlusTheme } from '@/utils/theme'
// 颜色
const color = ref(localStorage.getItem('theme-color') || '#409EFF')
// 组件预定义颜色
const predefineColors = ref([
'#ff4500',
'#ff8c00',
'#ffd700',
'#90ee90',
'#00ced1',
'#1e90ff',
'#c71585',
'rgba(255, 69, 0, 0.68)',
'rgb(255, 120, 0)',
'hsv(51, 100, 98)',
'hsva(120, 40, 94, 0.5)',
'hsl(181, 100%, 37%)',
'hsla(209, 100%, 56%, 0.73)',
'#c7158577',
])
/**
* 设置主题颜色
* @param type 类型
* @param color 颜色
*/
const setThemeColor = (color: string, type = 'primary') => {
// 存入本地主题颜色的值
if (localStorage.getItem('theme-color') !== color) {
localStorage.setItem('theme-color', color)
}
// 更新 Element Plus 主题
updateElementPlusTheme(type, color)
}
看下效果
但是刷新后还是会丢失,所以我们在App.vue中重新设置下组件的主题颜,刷新后也不会丢失
import { onMounted } from 'vue'
import { updateElementPlusTheme } from './utils/theme'
onMounted(() => {
updateElementPlusTheme(
'primary',
localStorage.getItem('theme-color') || '#409EFF',
)
})
tab快捷栏
设置快捷栏需要用到pinia持久化,所以我们要先下载一个持久化插件
npm i pinia-plugin-persistedstate
然后修改counter.ts
import { ref } from 'vue'
import { defineStore, createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import router from '@/router'
import { ElMessage } from 'element-plus'
// 创建仓库
const pinia = createPinia()
// 使用插件进行持久化存储
pinia.use(piniaPluginPersistedstate)
interface IHistoryList {
path: string
name: string
}
export const useCounterStore = defineStore(
'counter',
() => {
const historyList = ref<IHistoryList[]>([])
// 设置菜单历史记录列表的方法
const setHistoryList = ({ path, name }: IHistoryList) => {
// 没有就添加
if (historyList.value.findIndex(i => i.path === path) === -1) {
historyList.value.push({ path, name })
}
}
// 清除某个菜单历史记录列表的方法
const clearHistory = (path: string) => {
// 如果只剩下一个路由,则弹窗提示,不操作
if (historyList.value.length === 1) {
return ElMessage.warning('只剩下一个路由了,无法删除')
}
// 跳转到后一个路由
const index = historyList.value.findIndex(i => i.path === path)
// 如果是最后一个路由则跳转到前一个路由
if (index === historyList.value.length - 1) {
router.push(historyList.value[index - 1].path)
} else {
router.push(historyList.value[index + 1].path)
}
// 删除该路由
historyList.value = historyList.value.filter(i => i.path !== path)
}
return { historyList, setHistoryList, clearHistory }
},
{
// 设置持久化
persist: {
key: 'settingInfo',
storage: sessionStorage,
},
},
)
然后还需要修改一下路由配置,也就是router文件夹下的index.ts。设置了首页重定向,然后新增了一个全局路由前置守卫(监听路由跳转,好往路由历史数组里添加路由信息)
import { createRouter, createWebHistory } from 'vue-router'
import HomeView from '../views/HomeView.vue'
import { useCounterStore } from '@/stores/counter'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'home',
component: HomeView,
// 首页进来重定向到test1
redirect: '/test1',
children: [
{
path: '/test1',
name: 'test1',
component: () => import('@/views/ParentPage.vue'),
},
{
path: '/test2',
name: 'test2',
children: [
{
path: '/test3',
name: 'test3',
component: () => import('@/views/SonPage.vue'),
},
{
path: '/test4',
name: 'test4',
children: [
{
path: '/test5',
name: 'test5',
component: () => import('@/views/SonPage.vue'),
},
],
},
],
},
],
},
{
path: '/login',
name: 'login',
component: () => import('../views/AboutView.vue'),
},
],
})
// 全局前置路由守卫
router.beforeEach(to => {
const path = to.path
const name = to.name as string
useCounterStore().setHistoryList({ path, name })
return true
})
export default router
最后修改HomeView.vue文件
结构处修改:
<el-header>
<div class="header">
<el-button type="primary" size="small" @click="collapseChange">折叠/展开</el-button>
<div class="toolbar">
<el-switch inline-prompt v-model="theme" active-text="暗黑" inactive-text="白亮" @change="toggle" />
<el-button type="primary" size="small" @click="screenfullChange">全屏/非全屏</el-button>
<!-- 颜色选择器 -->
<el-color-picker v-model="color" :predefine="predefineColors" @change="setThemeColor" color-format="hex"
:show-alpha="false" />
<el-dropdown>
<el-icon style="margin-right: 8px; margin-top: 1px">
<setting />
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>View</el-dropdown-item>
<el-dropdown-item>Add</el-dropdown-item>
<el-dropdown-item>Delete</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>Tom</span>
</div>
</div>
<el-tag v-for="tag in store.historyList" :key="tag.name" closable @click="toPath(tag.path)"
@close="delHistory(tag.path)" class="ml-2">
{{ tag.name }}
</el-tag>
</el-header>
逻辑处修改:
import { useCounterStore } from '@/stores/counter'
const store = useCounterStore()
// 删除历史记录
const delHistory = (path: string) => {
store.clearHistory(path)
}
// 跳转路由
const toPath = (path: string) => {
router.push(path)
}
样式处修改:
.layout-container-demo .el-header {
position: relative;
background-color: var(--el-color-primary-light-7);
color: var(--el-text-color-primary);
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.ml-2 {
margin-left: 8px;
}
效果:
至于想实现下面的效果
可以使用ElementPlus的Dropdown组件,做出修改即可,本质就是操作pinia中存储的路由历史数组
代码
通过网盘分享的文件:test.zip
链接: https://pan.baidu.com/s/1mmGT_xjW52s9dKUVJNZE2Q?pwd=atm8 提取码: atm8
写的很糙,关注实现即可。有空闲时间了,还会加上中英文切换