【Vue3+Element Plus】从0-1搭建后台管理系统(02)-SideBar
这一章主要完成SideBar的静态渲染。
SideBar
-
首先先在
src/layout目录下创建一个SideBar文件夹,用来存放SideBar相关的组件等,然后在SideBar下创建index.vue以及创建SideBarItem.vue,index.vue是SideBar侧边栏菜单组件,SideBarItem.vue中的内容是侧边栏菜单的菜单项以及下拉菜单,也就是说index.vue与SideBarItem.vue是父子关系,到时候在index.vue中会引入使用SideBarItem.vue。
初始化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>
让侧边栏看起来是撑满视口高度
接下来就是修饰环节了,首先让侧边栏看起来是撑满视口高度的:
- 去掉
Menu组件本身自带的有边框// SideBar/index.vue <style lang="scss" scoped> .el-menu { border-right: 0; } </style> - 给
layout/index.vue下面的el-aside组件添加边框$border-color是一个全局scss变量,是边框的颜色,它的值为:// layout/index.vue <style lang="scss" scoped> .layout { .el-aside { border-right: solid 1px $border-color; } } </style>#dcdfe6。
可以从上图看出侧边栏菜单的高度已经撑满了整个视口,这样我们想要的效果就达到啦~
侧边栏logo
一般情况下,侧边栏最顶部都是展示平台的logo,所以接下来就是来完成侧边栏logo的渲染。
侧边栏的logo我打算创建一个单独的组件,然后再在侧边栏组件中引入。
-
在
src/layout/sidebar目录下创建一个Logo.vue文件: -
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用来放工具函数。
在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目录下:
示例:返回images/logo.png文件的绝对路径:
getImageUrl('logo.png') // http://127.0.0.1:5173/src/assets/images/logo.jpg
效果:
修改侧边栏的背景色
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;
}
}
}
效果:
🆗到这,侧边栏的初步实现就完成啦~
样式穿透(: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>
上图中的data-v-5b9576ba就是scoped为SideBarItem.vue生成的唯一的标记。
可以从上图看出,当我们为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__title,el-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的标记,所以会导致样式不生效,因为命中不了。
假如现在我们直接去修改el-sub-menu__title的样式,因为el-sub-menu__title被el-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的子元素的字体颜色修改为白色。
效果:
可以看到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>
效果:
可以看到,加上了:deep()样式穿透后,设置的样式就生效了。
为什么这样就会生效呢?
因为:deep(.el-sub-menu__title)将.el-sub-menu__title末尾的标记移到了.el-sub-menu__title的前面的选择器.el-sub-menu.is-active末尾了,同时拥有el-sub-menu和is-active类名的元素又是直接出现在SideBarItem.vue的,所以可以命中到同时拥有el-sub-menu和is-active类名的元素,并且编译后的.el-sub-menu__title又确实是同时拥有el-sub-menu和is-active类名的元素的子元素,那么这样一来样式就可以生效了。