当前位置: 首页 > article >正文

使用AI一步一步实现若依前端(12)

功能12:折叠/展开侧边栏

功能11:实现面包屑功能
功能10:添加首页菜单项
功能9:退出登录功能
功能8:页面权限控制
功能7:路由全局前置守卫
功能6:动态添加路由记录
功能5:侧边栏菜单动态显示
功能4:首页使用Layout布局
功能3:点击登录按钮实现页面跳转
功能2:静态登录界面
功能1:创建前端项目

前言

el-menu提供了侧边栏的折叠属性collapse,可以水平折叠收起菜单。

一.操作步骤

1.新建appStore

定义appStore,保存控制侧边栏状态的相关信息。
showSidebar :false侧边栏展开,true侧边栏折叠。
sidebarWidth :展开时的组件宽度。

import { defineStore } from 'pinia'
import { ref } from 'vue'

const useAppStore = defineStore('app', () => {
  const showSidebar = ref(false)
  const sidebarWidth = ref(180)

  const setSidebar = () => {
    showSidebar.value = !showSidebar.value
    if (sidebarWidth.value === 180) {
      sidebarWidth.value = 64
    } else {
      sidebarWidth.value = 180
    }
  }

  return {
    showSidebar,
    sidebarWidth,
    setSidebar
  }
})

export default useAppStore

2.修改Navbar.vue

在面包屑前,增加一个图标,点击图标会触发appStore里的状态变化,实现侧边栏折叠和展开。

<template>
  <el-header class="navbar">
    <div class="parent">
      <el-icon :size="20" class="hamburger-container" @click="toggleSideBar">
        <template v-if="appStore.showSidebar"><Expand /></template>
        <template v-else><Fold /></template>
      </el-icon>
      <el-breadcrumb separator="/">
        <el-breadcrumb-item v-for="(item, index) in breadcrumbItems" :key="item.path">
          <router-link v-if="index === 0" :to="item.path">
            {{ item.meta.title }}
          </router-link>
          <span v-else>{{ item.meta.title }}</span>
        </el-breadcrumb-item>
      </el-breadcrumb>
    </div>
    <div class="user-menu">
      <el-dropdown trigger="hover">
        <div class="avatar-container">
          <img :src="avatarUrl" alt="用户头像" class="avatar-image" />
        </div>
        <template #dropdown>
          <el-dropdown-menu class="dropdown-menu">
            <el-dropdown-item @click="handleSettings" class="menu-item">
              <span>个人设置</span>
            </el-dropdown-item>
            <el-dropdown-item divided @click="handleLogout" class="menu-item">
              <span>退出登录</span>
            </el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </el-header>
</template>

<script setup>
import { ElMessageBox } from 'element-plus'
import { computed } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()

const breadcrumbItems = computed(() => {
  // 获取当前路由的匹配数组,过滤无meta的路由
  const matched = route.matched.filter(item => item.meta?.title);

  // 确保首页始终在首位(如果当前路由不是首页)
  if (route.path !== '/' && matched[0]?.path !== '/') {
    matched.unshift({ path: '/index', meta: { title: '首页' } });
  }

  if (route.path === '/' || route.path === '/index') {
    matched.splice(0, matched.length - 1)
  }
  return matched;
})

// 请确保在 assets 目录下存在 user-avatar.png 图片文件
const avatarUrl = new URL('@/assets/images/profile.jpg', import.meta.url).href

import useUserStore from '@/stores/user'
const userStore = useUserStore()

const handleLogout = () => {
  // 处理退出登录逻辑
  ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
    confirmButtonText: '确定',
    cancelButtonText: '取消',
    type: 'warning'
  }).then(() => {
    userStore.logout().then(() => {
      location.href = '/index';
    })
  }).catch(() => { });
}

const handleSettings = () => {
  // 处理设置逻辑
  console.log('打开设置页面')
}

import useAppStore from '@/stores/app'
const appStore = useAppStore()
const toggleSideBar = () => {
  appStore.setSidebar()
}
</script>

<style scoped lang="scss">
.parent {
  display: flex; /* 启用 Flex 布局 */
  gap: 16px; /* 子元素间距(可选) */
  justify-content: flex-start; /* 水平对齐方式(可选) */
  align-items: center; /* 垂直对齐方式(可选) */

  .hamburger-container {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background 0.3s;
    -webkit-tap-highlight-color: transparent;

    &:hover {
      background: rgba(0, 0, 0, 0.025);
    }
  }
}
/* 导航栏整体样式 */
.navbar {
  background-color: #ffffff;
  border-bottom-width: 1px;
  border-bottom-style: solid;
  border-bottom-color: #e5e7eb;
  display: flex;
  align-items: center;
  justify-content: space-between;
  height: 64px;
  padding: 0 24px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}

/* 用户菜单容器 */
.user-menu {
  position: relative;
  z-index: 1000;
}

/* 头像容器 */
.avatar-container {
  position: relative;
  width: 40px;
  height: 40px;
  transition: all 0.3s ease;
}

/* 头像图片样式 */
.avatar-image {
  width: 100%;
  height: 100%;
  border-radius: 9999px;
  object-fit: cover;
  border: 2px solid #fff;
  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

/* 悬停效果 */
.avatar-container:hover .avatar-image {
  transform: scale(1.1);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* 下拉菜单样式 */
.dropdown-menu {
  border: none !important;
  border-radius: 8px !important;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
  margin-top: 12px !important;
  padding: 8px 0 !important;
}

/* 菜单项样式 */
.menu-item {
  font-size: 14px;
  color: #333 !important;
  transition: all 0.2s ease;
}

.menu-item:hover {
  background: #f5f7fa !important;
  color: #409eff !important;
  transform: translateX(4px);
}

/* 分割线样式 */
.el-dropdown-menu__item--divided {
  border-color: #ebeef5 !important;
}
</style>

3.修改Sidebar.vue

将el-menu的collapse属性绑定到appstore的showSidebar,响应式的控制侧边栏的展开和折叠。

<template>
  <el-menu router :default-active="activeMenu" :default-openeds="openedMenus" class="el-menu-vertical"
    :collapse="appStore.showSidebar" unique-opened>
    <template v-for="item in menuData" :key="item.path">
      <MenuItem :item="item" :level="0" />
    </template>
  </el-menu>
</template>

<script setup>
import { defineProps, computed, ref } from 'vue';
import MenuItem from './MenuItem.vue';
import { useRoute } from 'vue-router';

import useAppStore from '@/stores/app'
const appStore = useAppStore()

const route = useRoute()
defineProps({
  menuData: {
    type: Array,
    required: true
  }
});

const activeMenu = computed(() => {
  return route.path
})

const openedMenus = computed(() => {
  return route.matched
    .map(item => item.path)
    .filter(path => path !== '/')
})
</script>

<style lang="scss" scoped>
.el-menu-vertical {
  height: 100%;
  --el-menu-bg-color: #304156;
  --el-menu-text-color: #bfcbd9;
  --el-menu-active-color: #409EFF;
  --el-menu-hover-bg-color: #1e0b39;
}
</style>

4.修改MenuItem.vue

按照Element Plus的官网,调整el-menu的一些显示,让折叠的时候达到预期效果。

<template>
    <div v-if="!item.hidden">
        <template v-if="hasChildren">
            <el-sub-menu :index="item.path">
                <template #title>
                    <el-icon>
                      <component is="House" />
                    </el-icon>
                    <span>{{ item.meta?.title }}</span>
                </template>
                <template v-for="child in item.children" :key="child.path">
                    <MenuItem :item="child" :level="level + 1" :base-path="resolvePath(item.path)" />
                </template>
            </el-sub-menu>
        </template>
        <template v-else>
            <el-menu-item :index="resolvePath(item.path)">
                <el-icon>
                    <component is="House" />
                </el-icon>
                <template #title>
                    <span>{{ item.meta?.title }}</span>
                </template>
            </el-menu-item>
        </template>
    </div>
</template>

5.修改layout/index.vue

增加样式,让侧边栏的高度撑满整个屏幕。通过绑定appStore里的控制侧边栏宽度属性,响应式改变侧边栏的组件宽度。

<template>
  <el-container class="container">
    <el-aside :width="appStore.sidebarWidth + 'px'" class="aside">
      <Sidebar :menu-data="permissionStore.arrayForMenu" />
    </el-aside>
    <el-container>
      <el-header height="48px">
        <Navbar />
      </el-header>
      <AppMain />
    </el-container>
  </el-container>
</template>

<script setup>
import { ElContainer, ElAside } from 'element-plus'
import Sidebar from './components/Sidebar.vue'
import Navbar from './components/Navbar.vue'
import AppMain from './components/AppMain.vue'
import usePermissionStore from '@/stores/permission'
import useAppStore from '@/stores/app'

const appStore = useAppStore()
const permissionStore = usePermissionStore()
</script>

<style>
.container {
  height: 100%;

  .aside {
    height: 100%;
  }
}

.el-header {
  --el-header-padding: 0;
  height: auto;
}
</style>

6.修改index.html

增加全局控制样式

  <style>
    html,
    body,
    #app {
      height: 100%;
      margin: 0px;
      padding: 0px;
    }
  </style>

7.新建全局样式表src\assets\styles\index.scss

控制el-menu相关效果的属性

/* 覆盖所有折叠弹出菜单的背景色 */
.el-menu--popup {
    background-color: #304156 !important;
    /* 自定义背景色 */
}

/* 覆盖悬停色 */
.el-menu--popup .el-menu-item:hover,
.el-menu--popup .el-sub-menu__title:hover {
    background-color: #1e0b39 !important;
}

/* 默认文字颜色 */
.el-menu--popup .el-menu-item,
.el-menu--popup .el-sub-menu__title {
  color: #bfcbd9 !important;
}

/* 隐藏折叠后的菜单文字 */
.el-menu--collapse .el-sub-menu__title span {
    display: none;
}

/* 隐藏折叠后的展开箭头 */
.el-menu--collapse .el-sub-menu__icon-arrow {
    display: none;
}

在main.js文件里进行全局注册

import '@/assets/styles/index.scss'

二.功能验证

运行项目,浏览器访问http://localhost:5173/index,点击导航栏里的图标,折叠和展开的边栏
在这里插入图片描述

三.知识点拓展

1、Pinia 状态管理(核心知识点)

通俗理解:就像在项目中创建一个"共享记事本",所有组件都能读写这个记事本里的内容。

代码体现

// 定义存储侧边栏状态的"记事本"
const useAppStore = defineStore('app', () => {
  const showSidebar = ref(false) // 折叠状态
  const sidebarWidth = ref(180)  // 侧边栏宽度

  // 修改状态的"笔"
  const setSidebar = () => {
    showSidebar.value = !showSidebar.value
    sidebarWidth.value = showSidebar.value ? 64 : 180
  }

  return { showSidebar, sidebarWidth, setSidebar }
})

应用场景
• 在导航栏组件点击折叠按钮时调用 setSidebar
• 侧边栏组件通过 showSidebar 自动响应折叠状态
• 布局组件通过 sidebarWidth 调整侧边栏宽度


2、组件间通信

通俗理解:不同组件之间通过"共享记事本"(Pinia)传递信息,无需直接对话。

实现方式
• Navbar 组件:触发状态修改

<el-icon @click="toggleSideBar"></el-icon>

• Sidebar 组件:自动响应状态变化

<el-menu :collapse="appStore.showSidebar">

3、响应式设计

通俗理解:当数据变化时,页面会自动像"镜子"一样实时反映变化。

技术实现

  1. ref() 创建响应式数据
  2. computed() 计算属性
    const activeMenu = computed(() => route.path)
    

实际效果
• 点击折叠按钮 → 自动更新侧边栏宽度和菜单状态
• 路由变化 → 面包屑自动更新


4、组件化开发

通俗理解:把页面拆分成多个"积木块",每个积木独立且可复用。

组件结构
Navbar.vue:顶部导航栏
Sidebar.vue:侧边栏容器
MenuItem.vue:递归渲染菜单项(可无限嵌套)

递归组件

<!-- 菜单项组件自己调用自己 -->
<MenuItem 
  v-for="child in item.children" 
  :item="child" 
  :level="level + 1"
/>

5、Element Plus 使用技巧

关键属性

<el-menu 
  :collapse="appStore.showSidebar" 
  unique-opened
>

样式覆盖

/* 修改 Element 默认样式 */
.el-menu--popup {
  background-color: #304156 !important;
}

折叠优化
• 隐藏折叠后的文字和箭头

.el-menu--collapse .el-sub-menu__title span {
  display: none;
}

6、CSS 布局技巧

布局方案

  1. 全屏布局
    html, body, #app { height: 100% }
    
  2. Flex 布局
    .parent {
      display: flex;
      align-items: center;
    }
    
  3. 过渡动画
    .avatar-image {
      transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
    }
    

7、最佳实践总结

  1. 状态集中管理:复杂状态交给 Pinia
  2. 组件解耦:每个组件专注单一功能
  3. 响应式思维:用数据驱动视图变化
  4. 样式分层
    • 组件样式用 scoped
    • 全局样式统一管理
  5. UI 框架深度定制:通过 CSS 覆盖实现个性化效果

四.思考


http://www.kler.cn/a/583856.html

相关文章:

  • 快读模板(Java)
  • 【后端开发面试题】每日 3 题(十二)
  • 【从零开始学习计算机科学】数据库系统(九)DBMS的体系结构
  • 复变函数摘记1
  • 在微信小程序或前端开发中,picker 和 select 都是用户交互中用于选择的组件,但它们在功能、设计和使用场景上有一定的区别
  • 桂云OSG:什么是桂链?
  • 2025-03-13 学习记录--C/C++-PTA 练习2-13 求N分之一序列前N项和
  • DeepSeek R1 与 ktransformers:结合苹果 M4 Mac 的 LLM 推理深度分析
  • 计算机网络——DHCP实验
  • Easyocr图片识别小结
  • AI自动化、资本短视、三输与破局
  • 编译器视角下的 C++ 异常:探究 throw 与 catch 的编译原理
  • AI日报 - 2025年3月14日
  • iOS开发,SQLite.swift, Missing argument label ‘value:‘ in call问题
  • 计算机视觉算法实战——驾驶员玩手机检测(主页有源码)
  • 手机遥控开关,是一种能让用户通过手机远程控制电器开关
  • 基于全局分析SpringCloud各个组件所解决的问题?
  • 【AIGC】OpenAI 集成 Langchain 操作实战使用详解
  • python元组(被捆绑的列表)
  • 【毕业论文格式】word分页符后的标题段前间距消失