
面包屑,收缩菜单,黑夜白夜样式,全屏功能实现
收缩菜单按钮结合pinia功能实现
菜单收缩只要动态切换el-menu
组件上的 collapse Prop
值即可。因为 collapse
状态值在 layoutAside/verticalMenu.vue
中获取后,需要与头部组件layoutHeader/index.vue
共享,多组件共享同一状态值使用 pinia
状态管理。
第一步、定义布局配置的数据类型
创建类型接口文件 src/types/pinia.d.ts
,定义布局配置的数据类型
javascript
/**
* pinia状态类型定义
*/
declare interface layoutConfigState{
isCollapse:boolean; // 是否展开菜单
globalTitle:string; // 网站主标题
}

第二步、创建布局状态管理文件
创建布局状态管理文件: src/stores/layoutConfig.ts
javascript
import {defineStore} from 'pinia'
export const useLayoutConfigStore = defineStore('layoutConfig', {
state:():layoutConfigState=>{
return {
isCollapse:false, // 菜单是否折叠
globalTitle:"手撸管理后台", // 网站主标题
}
},
getters:{
},
actions:{
}
})
第三步、使用布局配置状态
在layoutAside/verticalMenu.vue
中使用布局配置状态模板中使用: :collapse="isCollapse"
javascript
<script lang="ts" setup>
import {useRoute} from 'vue-router'
import { storeToRefs } from 'pinia';
import {useLayoutConfigStore} from '@/stores/layoutConfig'
const route = useRoute()
const layoutConfigStore = useLayoutConfigStore()
const { isCollapse } = storeToRefs(layoutConfigStore)
</script>
<template>
<el-menu :collapse="isCollapse" :default-openeds="['/system']" :default-active="route.path" :router="true" background-color="transparent" class="el-menu-vertical-demo">
</template>
第四步、进行展开/收起左侧菜单逻辑
在layoutHeader/breadcrumb.vue
中实现点击切换图标,进行展开/收起左侧菜单逻辑
javascript
<template>
<div class="layout-header-breadcrumb">
<!-- 收缩图标 -->
<SvgIcon
:name="layoutConfig.isCollapse ? 'ele-Expand' : 'ele-Fold'"
@click="handleChangeCollapse"
class="layout-header-expand-icon"
/>
<!-- 面包屑 -->
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">系统管理</el-breadcrumb-item>
<el-breadcrumb-item>菜单管理</el-breadcrumb-item>
<el-breadcrumb-item>promotion detail</el-breadcrumb-item>
</el-breadcrumb>
</div>
</template>
<script lang="ts" setup>
import { useLayoutConfigStore } from "@/stores/layoutConfig";
import { Expand } from "@element-plus/icons-vue";
const layoutConfig = useLayoutConfigStore();
// 点击展开或收起左侧菜单
function handleChangeCollapse() {
layoutConfig.isCollapse = !layoutConfig.isCollapse;
}
</script>
<style>
</style>
第五步、动态切换左侧菜单宽度样式
动态切换左侧菜单宽度样式,在 src\layout\layoutAside\index.vue
实现:
javascript
<template>
<!-- 左侧菜单栏 -->
<div class="h100">
<el-aside
class="layout-container layout-aside layout-aside-menu-200"
:class="
layoutConfig.isCollapse
? 'layout-aside-menu-60'
: 'layout-aside-menu-200'
"
>
<logo />
<VerticalMenu />
</el-aside>
</div>
</template>
<script setup lang="ts" name="LayoutAside">
import { useLayoutConfigStore } from "@/stores/layoutConfig";
import { defineAsyncComponent } from "vue";
const Logo = defineAsyncComponent(() => import("./logo.vue"));
const VerticalMenu = defineAsyncComponent(() => import("./verticalMenu.vue"));
const layoutConfig = useLayoutConfigStore();
</script>
<style>
</style>
第六步、动态显示系统标题
当收起左侧菜单,在 layout/layoutAside/logo.vue
隐藏菜单上方的系统标题,并动态显示系统标题
javascript
<template>
<div class="layout-logo">
<img class="layout-logo-img" src="@/assets/logo(1).png" alt="logo" />
<span v-if="!layoutConfig.isCollapse">{{ layoutConfig.globalTitle }}</span>
</div>
</template>
<script lang="ts" setup name="LayoutLogo">
import { useLayoutConfigStore } from "@/stores/layoutConfig";
const layoutConfig = useLayoutConfigStore();
</script>
<style></style>
效果

导航面包屑功能逻辑实现
获取当前页面路由对象 route
,从路由对象中获取 matched
可获取当前路由的上N级路由对象,然后将数据渲染到面包屑处。
第一步、获取当前页面路由对象
在 layoutHeader/breadcrumb.vue
组件中实现:通过onMounted
和onBeforeRouteUpdate
钩子获取
javascript
// 用于第一次加载时触发
onMounted(() => {
getBreadcrumb(route);
});
// 路由更新时触发,当前目标路由对象
onBeforeRouteUpdate((to) => {
getBreadcrumb(to);
});
第二步、过滤出有meta.title且isBreadcrumb为true的路由对象
javascript
function getBreadcrumb(to: RouteLocationNormalized) {
// 过滤出当前有 meta.title 值且isBreadcrumb不为false的路由对象
const matched = to.matched.filter(
(item) => item.meta && item.meta.title && item.meta.isBreadcrumb !== false
);
breadcrumbList.value = matched || [];
}
第三步、渲染跳转面包屑
javascript
<!-- 面包屑 -->
<el-breadcrumb separator="/">
<!-- v-for过滤效果 -->
<TransitionGroup name="breadcrumb">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="item.path"
>
<!-- 最后一级路由(当前路由),不可点击跳转 -->
<span v-if="index === breadcrumbList.length - 1" class="flex-center">
<SvgIcon v-if="item.meta.icon" :name="item.meta.icon" :size="14" />
{{ item.meta.title }}
</span>
<a v-else @click.prevent="handleLink(item)" class="flex-center">
<SvgIcon v-if="item.meta.icon" :name="item.meta.icon" :size="14" />
{{ item.meta.title }}
</a>
</el-breadcrumb-item>
</TransitionGroup>
</el-breadcrumb>
第四步、编写面包屑手动跳转方法
javascript
// 点击面包屑的某标题跳转
function handleLink(_route: RouteRecordNormalized) {
const { redirect, path } = _route;
if (redirect) {
router.push(<string>redirect);
} else {
router.push(path);
}
}
第五步、面包屑过渡动画样式
css
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all .5s;
}
.breadcrumb-enter-from,.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
// 因为 TransitionGroup 不支持 mode="out-in",通过下面方式防止显示和隐藏效果同时出现。
.breadcrumb-leave-active {
position: absolute;
z-index: -1;
}
整体代码
javascript
<template>
<div class="layout-header-breadcrumb">
<!-- 收缩图标 -->
<SvgIcon
:name="layoutConfig.isCollapse ? 'ele-Expand' : 'ele-Fold'"
@click="handleChangeCollapse"
class="layout-header-expand-icon"
/>
<!-- 面包屑 -->
<el-breadcrumb separator="/">
<!-- v-for过滤效果 -->
<TransitionGroup name="breadcrumb">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbList"
:key="item.path"
>
<!-- 最后一级路由(当前路由),不可点击跳转 -->
<span v-if="index === breadcrumbList.length - 1" class="flex-center">
<SvgIcon v-if="item.meta.icon" :name="item.meta.icon" :size="14" />
{{ item.meta.title }}
</span>
<a v-else @click.prevent="handleLink(item)" class="flex-center">
<SvgIcon v-if="item.meta.icon" :name="item.meta.icon" :size="14" />
{{ item.meta.title }}
</a>
</el-breadcrumb-item>
</TransitionGroup>
</el-breadcrumb>
</div>
</template>
<script lang="ts" setup>
import { useLayoutConfigStore } from "../../stores/layoutConfig";
import { onMounted, ref } from "vue";
import { useRoute, useRouter, onBeforeRouteUpdate } from "vue-router";
import type {
RouteLocationNormalized,
RouteRecordNormalized,
} from "vue-router";
const route = useRoute();
const router = useRouter();
// 面包屑渲染数据
const breadcrumbList = ref<RouteRecordNormalized[]>([]);
// 用于第一次加载时触发
onMounted(() => {
getBreadcrumb(route);
});
// 路由更新时触发,当前目标路由对象
onBeforeRouteUpdate((to) => {
getBreadcrumb(to);
});
function getBreadcrumb(to: RouteLocationNormalized) {
// 过滤出当前有 meta.title 值且isBreadcrumb不为false的路由对象
const matched = to.matched.filter(
(item) => item.meta && item.meta.title && item.meta.isBreadcrumb !== false
);
breadcrumbList.value = matched || [];
}
const layoutConfig = useLayoutConfigStore();
// 点击展开或收起左侧菜单
function handleChangeCollapse() {
layoutConfig.isCollapse = !layoutConfig.isCollapse;
}
// 点击面包屑的某标题跳转
function handleLink(_route: RouteRecordNormalized) {
const { redirect, path } = _route;
if (redirect) {
router.push(<string>redirect);
} else {
router.push(path);
}
}
</script>
<style>
</style>
效果

使用VueUse实现全屏退出全屏效果-VueUse
VueUse 基于Vue组合式API的实用工具集,官网、中文网。
第一步、安装 VueUse
javascript
npm i @vueuse/core
第二步、使用 useFullscreen
函数实现全屏效果
用法参考。
javascript
<script lang="ts" setup>
import { useFullscreen } from "@vueuse/core";
import { useLayoutConfigStore } from "../../stores/layoutConfig";
const layoutConfig = useLayoutConfigStore();
// 全屏切换
const { isFullscreen, toggle: toggleFullscreen } = useFullscreen();
// 点击切换全屏
async function handleToggleFullscreen() {
await toggleFullscreen();
// 更新状态
layoutConfig.isFullscreen = isFullscreen.value;
}
</script>
第三步、保留到pinia状态中
javascript
import { defineStore } from 'pinia'
export const useLayoutConfigStore = defineStore('layoutConfig', {
state: (): layoutConfigState => {
return {
isCollapse: false, // 菜单是否折叠
globalTitle: "手撸管理后台", // 网站主标题
isFullscreen: false, // 是否全屏
isDark: false, // 黑暗模式
}
},
getters: {},
actions: { }
})
第四步、修改pinia.d.ts
javascript
/**
* pinia状态类型定义
*/
declare interface layoutConfigState{
isCollapse:boolean; // 是否展开菜单
globalTitle:string; // 网站主标题
isFullscreen: boolean;
isDark: boolean;
}
第五步、模板中使用

javascript
<template>
<div class="layout-header-user">
<div class="layout-header-user-icon mr5" @click="handleToggleFullscreen">
<SvgIcon name="ele-FullScreen" />
</div>
</div>
</template>
效果

实现高亮和暗黑模式
Element Plus 2.2.0+
版本支持暗黑模式,导入暗黑样式文件,然后在index.html
的html
标签上添加一个class="dark"
的类名即可切换为暗黑模式。
第一步、编写dark.scss文件
javascript
/* 暗黑样式
--------------------------- */
html[class='dark'] {
--wyk-color-white: #fff;
--wyk-color-black: #0f121d;
--wyk-color-primary: #1966ff;
--wyk-border-color: #333333;
--wyk-color-hover: #3c3c3c;
--wyk-color-hover-rgba: rgba(0, 0, 0, .9);
// 左侧菜单
--wyk-bg-menuMainColor: var(--wyk-color-black) !important;
--wyk-bg-menuActiveColor: var(--wyk-color-primary) !important;
--wyk-bg-menuHoverColor: #2e436e !important;
--wyk-text-menuMainColor: var(--wyk-color-white) !important;
--wyk-text-menuActiveColor: var(--wyk-color-white) !important;
--wyk-text-menuHoverColor: var(--wyk-color-white) !important;
// 头部导航
--wyk-bg-headerBarColor: var(--wyk-color-black) !important;
// 头部右侧图标光标浮动
--wyk-color-user-hover: var(--wyk-color-hover-rgba) !important;
// 边框
--wyk-border-color-light: var(--wyk-border-color) !important;
/* wangeditor 富文本编辑器 - css vars
--------------------------- */
--w-e-toolbar-bg-color: var(--el-bg-color) !important;
--w-e-toolbar-color: var(--el-text-color-primary) !important;
// // 工具栏浮动显示
--w-e-toolbar-active-color: var(--el-text-color-primary) !important;
--w-e-toolbar-active-bg-color: var(--el-color-primary-light-9) !important;
--w-e-toolbar-border-color: var( --wyk-border-color) !important;
// // 内容区域
--w-e-textarea-bg-color: var(--el-bg-color) !important;
--w-e-textarea-color: var(--el-text-color-primary) !important;
// .el-switch {
// --el-switch-on-color: red !important;
// }
}
第二步、导入暗黑主题css变量文件
在 src\styles\index.scss
中导入暗黑主题css
变量文件
javascript
// ElementPlus 组件所有样式
@use 'element-plus/dist/index.css';
@use './app.scss';
@use './transition.scss';
// 暗黑主题-ElementPlus-CSS变量
@use 'element-plus/theme-chalk/dark/css-vars.css';
// 暗黑主题-自定义CSS样式
@use './dark.scss';
第三步、使用VueUse进行高亮/暗黑模式动态切换
javascript
<script lang="ts" setup>
import { useLayoutConfigStore } from "../../stores/layoutConfig";
import { onMounted, ref } from "vue";
import { useRoute, useRouter, onBeforeRouteUpdate } from "vue-router";
import type {
RouteLocationNormalized,
RouteRecordNormalized,
} from "vue-router";
const route = useRoute();
const router = useRouter();
// 面包屑渲染数据
const breadcrumbList = ref<RouteRecordNormalized[]>([]);
// 用于第一次加载时触发
onMounted(() => {
getBreadcrumb(route);
});
// 路由更新时触发,当前目标路由对象
onBeforeRouteUpdate((to) => {
getBreadcrumb(to);
});
function getBreadcrumb(to: RouteLocationNormalized) {
// 过滤出当前有 meta.title 值且isBreadcrumb不为false的路由对象
const matched = to.matched.filter(
(item) => item.meta && item.meta.title && item.meta.isBreadcrumb !== false
);
breadcrumbList.value = matched || [];
}
const layoutConfig = useLayoutConfigStore();
// 点击展开或收起左侧菜单
function handleChangeCollapse() {
layoutConfig.isCollapse = !layoutConfig.isCollapse;
}
// 点击面包屑的某标题跳转
function handleLink(_route: RouteRecordNormalized) {
const { redirect, path } = _route;
if (redirect) {
router.push(<string>redirect);
} else {
router.push(path);
}
}
</script>
第四步、进行pinia持久化存储
javascript
import { defineStore } from 'pinia'
export const useLayoutConfigStore = defineStore('layoutConfig', {
state: (): layoutConfigState => {
return {
isCollapse: false, // 菜单是否折叠
globalTitle: "手撸管理后台", // 网站主标题
isFullscreen: false, // 是否全屏
isDark: false, // 黑暗模式
}
},
getters: {},
actions: {}
})
效果

监听pinia状态持久化并刷新回显
面包屑,收缩菜单,高亮暗黑主题,全屏功能持久化功能实现
第一步、监听pinia更新localStorage
在 src/stores/layoutConfig.ts
最后添加对 state
的监听器,一旦state
更新,则保存到浏览器的 localStorage
中。
javascript
import { defineStore } from 'pinia'
import { Local } from '@/utils/storage'
import { nextTick } from 'vue'
export const useLayoutConfigStore = defineStore('layoutConfig', {
state: (): layoutConfigState => {
return {
isCollapse: false, // 菜单是否折叠
globalTitle: "手撸管理后台", // 网站主标题
isFullscreen: false, // 是否全屏
isDark: false, // 黑暗模式
}
},
getters: {},
actions: {
// 更新状态
updateState(state:layoutConfigState){
// 将传递的值更新到state状态中
this.$patch(state)
}
}
})
nextTick(() => {
const layoutConfig = useLayoutConfigStore()
// 监听状态变化,将状态持久化
layoutConfig.$subscribe((mutation, state) => {
// 保存到浏览器的localStorage中
Local.set('layoutConfig', state)
})
})
第二步、贾汪localStorage更新到pinia中
在 App.vue
中的 onMounted
钩子中,当应用加载则读取localStorage
中的 layoutConfig
状态值,更新到state
上。
javascript
<script setup lang="ts" name="App">
import { onMounted } from 'vue';
import { Local } from './utils/storage';
import { useLayoutConfigStore } from './stores/layoutConfig';
const layoutConfigStore = useLayoutConfigStore();
onMounted(()=>{
// 获取localStorage配置
const layoutConfig = Local.get('layoutConfig');
if(layoutConfig){
layoutConfigStore.updateState(layoutConfig);
}
})
</script>
<template>
<div class="h100">
<router-view></router-view>
</div>
</template>
<style scoped>
</style>
效果
