【Vue3+Element Plus】从0-1搭建后台管理系统(02)-SideBar

2,383 阅读4分钟

【Vue3+Element Plus】从0-1搭建后台管理系统(02)-SideBar

这一章主要完成SideBar的静态渲染。

SideBar

  • 首先先在src/layout目录下创建一个SideBar文件夹,用来存放SideBar相关的组件等,然后在SideBar下创建index.vue以及创建SideBarItem.vueindex.vue是SideBar侧边栏菜单组件SideBarItem.vue中的内容是侧边栏菜单的菜单项以及下拉菜单,也就是说index.vue与SideBarItem.vue是父子关系,到时候在index.vue中会引入使用SideBarItem.vue。

    image.png

初始化SideBar/index.vue以及SideBar/SideBarItem.vue的模板结构

引入SideBar所需要的Element-plus组件,然后在Element-plus官网找到Menu组件将代码复制过来:

import { ..., ElMenu, ElMenuItem, ElSubMenu } from 'element-plus'

const ElementPlusComponents = [..., ElMenu, ElMenuItem, ElSubMenu]

SideBar/index.vue

<template>
  <div class="sidebar">
    <el-menu default-active="2" class="el-menu-vertical-demo">
      <SideBarItem />
    </el-menu>
  </div>
</template>

<script setup lang="ts">
/** 引入SideBarItem.vue */
import SideBarItem from './SideBarItem.vue'
defineOptions({
  name: 'SideBar'
})
</script>

<style lang="scss" scoped>
// SideBar style
</style>

SideBar/SideBarItem.vue

<template>
    <!-- 下拉菜单 -->
    <el-sub-menu index="1">
      <template #title>
        <el-icon><location /></el-icon>
        <span>Navigator One</span>
      </template>
      <!-- 菜单项 -->
      <el-menu-item index="1-2">
          <el-icon><icon-menu /></el-icon>
          <span>Navigator Two</span>
      </el-menu-item>
      <!-- 下拉菜单 -->
      <el-sub-menu index="1-3">
        <template #title>item four</template>
        <!-- 菜单项 -->
        <el-menu-item index="1-3-1">item one</el-menu-item>
      </el-sub-menu>
    </el-sub-menu>
    <!-- 菜单项 -->
    <el-menu-item index="3">
      <el-icon><icon-menu /></el-icon>
      <span>Navigator Two</span>
    </el-menu-item>
    <el-menu-item index="4">
      <el-icon><setting /></el-icon>
      <span>Navigator Four</span>
    </el-menu-item>
</template>

<script setup lang="ts">
defineOptions({
  name: 'SideBarItem'
})
</script>

<style lang="scss" scoped>
// SideBarItem.vue style
</style>

这里需要注意的一点是,在SideBarItem.vue不要在模板的最外层包裹一层HTML标签,否则到时候侧边栏收起的时候会出现无法正常收起的问题!

这些都完成之后,下一步就可以在layout组件中中引入SideBar组件进行展示了:

<template>
  <div class="layout">
    <el-container>
      <!-- 侧边栏菜单 -->
      <el-aside width="200px">
        <SideBar />
      </el-aside>
      <el-container>
        <!-- 头部 -->
        <el-header>Header</el-header>
        <!-- 主要内容区域 -->
        <el-main>Main</el-main>
      </el-container>
    </el-container>
  </div>
</template>

<script setup lang="ts">
/** 引入SideBar.vue */
import SideBar from './SideBar/index.vue'
defineOptions({
  name: 'Layout'
})
</script>

<style lang="scss" scoped>
// layout.vue style
</style>

sidebar.gif

让侧边栏看起来是撑满视口高度

接下来就是修饰环节了,首先让侧边栏看起来是撑满视口高度的:

  • 去掉Menu组件本身自带的有边框
    // SideBar/index.vue
    
    <style lang="scss" scoped>
    .el-menu {
      border-right: 0;
    }
    </style>
    
  • layout/index.vue下面的el-aside组件添加边框
    // layout/index.vue
    
    <style lang="scss" scoped>
    .layout {
      .el-aside {
        border-right: solid 1px $border-color;
      }
    }
    </style>
    
    $border-color是一个全局scss变量,是边框的颜色,它的值为:#dcdfe6

image.png 可以从上图看出侧边栏菜单的高度已经撑满了整个视口,这样我们想要的效果就达到啦~

侧边栏logo

一般情况下,侧边栏最顶部都是展示平台的logo,所以接下来就是来完成侧边栏logo的渲染。

侧边栏的logo我打算创建一个单独的组件,然后再在侧边栏组件中引入。

  • src/layout/sidebar目录下创建一个Logo.vue文件:

    image.png

  • Logo.vue的代码结构:

    <template>
      <div class="logo">
        <img class="logo-image" :src="getImageUrl('logo.png')" alt="logo.png" />
        <h1 class="logo-text">DaiTuAdmin</h1>
      </div>
    </template>
    
    <script setup lang="ts">
    import { getImageUrl } from '@/utils/index'
    
    defineOptions({
      name: 'Logo'
    })
    </script>
    
    <style lang="scss" scoped>
    .logo {
      display: flex; // 让logo和文字一行显示
      justify-content: center; // flex容器内子项水平居中
      align-items: center; // flex容器内子项垂直居中
      height: 50px;
    
      &-image {
        margin-right: 5px;
        width: 32px;
        height: 32px;
      }
    
      &-text {
        font-size: 16px;
        font-weight: 400;
        color: #fff;
      }
    }
    </style>
    
    
补充一下getImageUrl(imageName)

这里使用动态导入静态资源的方式引入图片资源,如果使用这种方式,那么我们就不能直接将图片的引用地址写死了,因为在打包的时候,src下面的文件都会被打包编译,那么assets目录下的静态资源也就会被编译,静态资源的名称会被编译成hash的形式,而写死的图片地址就是一个字符串,打包时不会进行编译,所以也就找不到对应地址的图片资源了。

例如,开发时会是 /assets/img2.png,在生产构建后会是 /assets/img.2d8efhg.png

解决这个问题,我们可以使用URL()+import.meta.url

标题
URL()new URL('相对路径', '基础路径(绝对路径)'),返回一个URL对象,可以通过href拿到基础路径与相对路径结合生成的绝对路径。注:这个相对路径是相对于基础路径的位置。
import.meta.url可以获得当前模块的 url地址(绝对路径)

src/utils目录下创建了index.ts用来放工具函数。

image.png

index.ts声明了一个获取图片所在绝对路径的函数getImageUrl(imageName)

// src/utils/index.ts

export const getImageUrl (imageName: string): sting {
    return new URL('../../assets/images/${imageName}, import.meta.url).href
}

logo图片选择存放在src/assets/images目录下:

image.png

示例:返回images/logo.png文件的绝对路径:

getImageUrl('logo.png') // http://127.0.0.1:5173/src/assets/images/logo.jpg

效果:

image.png

修改侧边栏的背景色

Logo.vue:

.logo {
    background-color: $sidebar-bg-color;
}

layout/index.vue:

.el-aside {
    background-color: $sidebar-bg-color;
}

layout/SideBar/index.vue:

<el-menu
  background-color="#001428">
  <SideBarItem />
</el-menu>

$sidebar-bg-color: #001428;

修改菜单项字体颜色,激活的菜单项字体颜色、背景,鼠标悬浮的菜单项字体颜色、背景

菜单项字体颜色
<el-menu
  text-color="#BBB"
  active-text-color="#fff">
  <SideBarItem />
</el-menu>
激活的菜单项字体颜色、背景
.el-menu-item {
    &::after {
      position: absolute;
      top: 50%;
      left: 50%;
      z-index: 0;
      transform: translate(-50%, -50%) scale(0);
      transform-origin: 0 bottom;
      content: '';
      width: 95%;
      height: 80%;
      background-color: $brand-color;
      border-radius: 5px;
      transition: transform 0.3s;
    }
  }

  .el-menu-item.is-active {
    position: relative;

    span {
      position: absolute;
      z-index: 1;
    }

    &::after {
      transform: translate(-50%, -50%) scale(1);
    }
  }
鼠标悬浮的菜单项字体颜色、背景
.el-menu-item {
    &.is-active {
        &:hover {
          span {
            color: #fff;
          }
        }
    }
}

鼠标悬停、激活的下拉菜单样式

.el-sub-menu {
    &.is-active {
      > :deep(.el-sub-menu__title) {
        color: #fff !important;

        :deep(.el-icon) {
          color: #fff;
        }
      }
    }

    :deep(.el-sub-menu__title) {
      &:hover {
        color: #fff !important;
      }
    }
}

效果:

active.gif

🆗到这,侧边栏的初步实现就完成啦~

样式穿透(:deep(selector))

这里需要强调的一点是:deep()这个的使用,:deep()是做样式穿透的。

为什么要用这个来做样式穿透呢?

因为一般情况下在组件中的<style>标签上都会加上一个scoped属性。这个属性的作用就是防止样式混乱,<style>标签上加了scoped属性的组件的样式都是独立的(私有化),本组件中的样式不会在其他组件中生效,别的组件中的样式也不会影响到本组件。

scoped样式隔离的原理

style标签上添加scoped属性后之所以能够做到样式隔离,是因为scoped为每个组件生成了一个唯一不重复的标记(每个组件生成的标记都不一样),是 (:data-v-hash) 的方式。然后为组件中的DOM以及css选择器最后面添加上这个标记,(而这个工作是由PostCSS转译实现的),达到样式私有化/模块化的目的。

如果组件内部包含其他组件,只会给其他组件的最外层标签加上当前组件的唯一data自定义属性。

示例1:

例如SidebarItem.vue中的el-menu-item里面的span标签,它两都是直接在SidebarItem.vue中的,所以当style标签添加了scoped属性后,elmenu-item和它里面的span标签会添加上SidebarItem.vue组件的唯一标记,当为这些标签元素修改样式时,css选择器末尾都会加上生成的唯一的SidebarItem.vue组件的标记:

// SideBarItem.vue

<el-menu-item index="4">
  <el-icon><setting /></el-icon>
  <span>Navigator Four</span>
</el-menu-item>

image.png

上图中的data-v-5b9576ba就是scopedSideBarItem.vue生成的唯一的标记。

image.png

可以从上图看出,当我们为span元素设置或修改样式的时候,编译后的css选择器末尾会添加上SideBarItem.vue的唯一标记。

那么el-menu-item里面的span标签上的标记和span编译后的css选择器末尾的标记都是SideBarItem.vue组件的标记data-v-5b9576ba,那么这个样式就会应用到SideBarItem.vue组件中el-menu-item里面的span元素上。

示例2:

例如SidebarItem.vue中的el-sub-menu里面的el-sub-menu__titleel-sub-menu是直接在SidebarItem.vue中的,el-sub-menu__title是插槽内的,也就是说,el-sub-menu是一个组件,它提供了一个名为title的插槽,这个插槽外面包了一层类名为el-sub-menu__title标签。刚才提到过如果组件内部包含其他组件,只会给其他组件的最外层标签加上当前组件的唯一data自定义属性。 所以,el-sub-menu会被标记上SideBarItem.vue的标记,而el-sub-menu__title就不会被标记上SideBarItem.vue的标记。

那当我们直接在SideBarItem.vue组件中去修改el-sub-menu__title的样式的话,就会在css选择器末尾,也就是在el-sub-menu__title的末尾加上SideBarItem.vue的标记,但是el-sub-menu__title不是直接在SideBarItem.vue组件中的,是直接出现在el-sub-menu组件中的,所以el-sub-menu__title类的元素被标记的是el-sub-menu的标记,而不是SideBarItem.vue的标记,所以会导致样式不生效,因为命中不了。

image.png

假如现在我们直接去修改el-sub-menu__title的样式,因为el-sub-menu__titleel-sub-menu这个组件标记了唯一的标记。所以我们在SideBarItem.vue组件中直接修改的el-sub-menu__title的样式是不会生效的。

<style>
// 下拉菜单
  .el-sub-menu {
    &.is-active {
      > .el-sub-menu__title {
        color: #fff !important;

        .el-icon {
          color: #fff;
        }
      }
    }
}
</style>

上面代码的意思是,同时拥有el-sub-menu类和is-active类的元素的类名为el-sub-menu__title的子元素的字体颜色修改为白色。

效果:

image.png

可以看到el-sub-menu__title类名的元素以及子元素的字体颜色并没有任何改变。

这个时候就需要使用到:deep(selector)样式穿透选择器了。它会把selector上标记的SideBarItem.vue组件的标记移到selector前面的那个选择器上(在SideBarItem.vue中的元素的css选择器)。

<style>
.el-sub-menu {
    &.is-active {
      > :deep(.el-sub-menu__title) {
        color: #fff !important;

        :deep(.el-icon) {
          color: #fff;
        }
      }
   }
}
</style>

效果:

image.png

可以看到,加上了:deep()样式穿透后,设置的样式就生效了。

为什么这样就会生效呢?

因为:deep(.el-sub-menu__title).el-sub-menu__title末尾的标记移到了.el-sub-menu__title的前面的选择器.el-sub-menu.is-active末尾了,同时拥有el-sub-menuis-active类名的元素又是直接出现在SideBarItem.vue的,所以可以命中到同时拥有el-sub-menuis-active类名的元素,并且编译后的.el-sub-menu__title又确实是同时拥有el-sub-menuis-active类名的元素的子元素,那么这样一来样式就可以生效了。