vue3实战五、面包学,收缩菜单,暗黑高亮主题切换,全屏非全屏实现 入口

面包屑,收缩菜单,黑夜白夜样式,全屏功能实现

收缩菜单按钮结合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 组件中实现:通过onMountedonBeforeRouteUpdate钩子获取

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.htmlhtml标签上添加一个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进行高亮/暗黑模式动态切换

建议使用 useDark | 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>

效果

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax