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>

效果

相关推荐
风中飘爻1 分钟前
MySQL入门:数据操作CURD
前端·bootstrap·html
rocky1919 分钟前
谷歌浏览器插件 录制元素拖动事件
前端·javascript
nothingbutluck46434 分钟前
2025.4.10 html有序、无序、定义列表、音视频标签
前端·html·音视频
爱上python的猴子1 小时前
chrome中的copy xpath 与copy full xpath的区别
前端·chrome
Lysun0012 小时前
dispaly: inline-flex 和 display: flex 的区别
前端·javascript·css
山禾女鬼0012 小时前
Vue 3 自定义指令
前端·javascript·vue.js
啊卡无敌2 小时前
Vue 3 reactive 和 ref 区别及 失去响应性问题
前端·javascript·vue.js
北桥苏2 小时前
Spine动画教程:皮肤制作
前端
涵信2 小时前
第九节:React HooksReact 18+新特性-React 19的use钩子如何简化异步操作?
前端·javascript·react.js
Aaaaaaaaaaayou3 小时前
浅玩一下 Mobile Use
前端·llm