一、组件的静态页面
1、组件的静态页面
//src\layout\index.vue
<template>
<div class="layout_container">
<!-- 左侧菜单 -->
<div class="layout_slider"></div>
<!-- 顶部导航 -->
<div class="layout_tabbar"></div>
<!-- 内容展示区域 -->
<div class="layout_main">
<p style="height: 1000000px"></p>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.layout_container {
width: 100%;
height: 100vh;
.layout_slider {
width: $base-menu-width;
height: 100vh;
background: $base-menu-background;
}
.layout_tabbar {
position: fixed;
width: calc(100% - $base-menu-width);
height: $base-tabbar-height;
background: cyan;
top: 0;
left: $base-menu-width;
}
.layout_main {
position: absolute;
width: calc(100% - $base-menu-width);
height: calc(100vh - $base-tabbar-height);
background-color: yellowgreen;
left: $base-menu-width;
top: $base-tabbar-height;
padding: 20px;
overflow: auto;
}
}
</style>
2、定义部分全局变量&滚动条
scss全局变量
//src\styles\variable.scss
//左侧菜单宽度
$base-menu-width :260px;
//左侧菜单背景颜色
$base-menu-background: #001529;
//顶部导航的高度
$base-tabbar-height:50px;
滚动条
//src\styles\index.scss
//滚动条外观设置
::-webkit-scrollbar{
width: 10px;
}
::-webkit-scrollbar-track{
background: $base-menu-background;
}
::-webkit-scrollbar-thumb{
width: 10px;
background-color: yellowgreen;
border-radius: 10px;
}
二、 Logo子组件的搭建
页面左上角的这部分,我们将它做成子组件,并且封装方便维护以及修改。
1、Logo子组件
在这里我们引用了封装好的setting
//src\layout\logo\index.vue
<template>
<div class="logo" v-if="setting.logoHidden">
<img :src="setting.logo" alt="" />
<p>{{ setting.title }}</p>
</div>
</template>
<script setup lang="ts">
//引入设置标题与logo配置文件
import setting from '@/setting'
</script>
<style lang="scss" scoped>
.logo {
width: 100%;
height: $base-menu-logo-height;
color: white;
display: flex;
align-items: center;
padding: 20px;
img {
width: 40px;
height: 40px;
}
p {
font-size: $base-logo-title-fontSize;
margin-left: 10px;
}
}
</style>
2 、封装setting
为了方便我们以后对logo以及标题的修改。
//src\setting.ts
//用于项目logo|标题配置
export default {
title: '硅谷甄选运营平台', //项目的标题
logo: '/public/logo.png', //项目logo设置
logoHidden: true, //logo组件是否隐藏
}
3、 使用
在layout组件中引入并使用
<logo></logo>
//引入logo组件
import Logo from './logo/index.vue';
三、左侧菜单组件
1、静态页面(未封装)
主要使用到了element-plus的menu组件。附带使用了滚动组件
//src\layout\index.vue
<!-- 左侧菜单 -->
<div class="layout_slider">
<Logo></Logo>
<!-- 展示菜单 -->
<!-- 滚动组件 -->
<el-scrollbar class="scrollbar">
<!-- 菜单组件 -->
<el-menu background-color="#001529" text-color="white">
<el-menu-item index="1">首页</el-menu-item>
<el-menu-item index="2">数据大屏</el-menu-item>
<!-- 折叠菜单 -->
<el-sub-menu index="3">
<template #title>
<span>权限管理</span>
</template>
<el-menu-item index="3-1">用户管理</el-menu-item>
<el-menu-item index="3-2">角色管理</el-menu-item>
<el-menu-item index="3-3">菜单管理</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
2、递归组件生成动态菜单
在这一部分,我们要根据路由生成左侧的菜单栏
1)将父组件中写好的子组件结构提取出去
//src\layout\index.vue
<!-- 展示菜单 -->
<!-- 滚动组件 -->
<el-scrollbar class="scrollbar">
<!-- 菜单组件 -->
<el-menu background-color="#001529" text-color="white">
<!-- 更具路由动态生成菜单 -->
<Menu></Menu>
</el-menu>
</el-scrollbar>
2)动态菜单子组件:src\layout\menu\index.vue
3)处理路由
因为我们要根据路由以及其子路由作为我们菜单的一级|二级标题。因此我们要获取路由信息。
给路由中加入了路由元信息meta:它包含了2个属性:title以及hidden。
//src\router\routes.ts
{
//登录路由
path: '/login',
component: () => import('@/views/login/index.vue'),
name: 'login', //命名路由
meta: {
title: '登录', //菜单标题
hidden: true, //路由的标题在菜单中是否隐藏
},
}
//加入子路由
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue')
}
]
},
4)仓库引入路由并对路由信息类型声明(vue-router有对应函数)
//src\store\modules\user.ts
//引入路由(常量路由)
import { constantRoute } from '@/router/routes'
。。。。。
//小仓库存储数据地方
state: (): UserState => {
return {
token: GET_TOKEN(), //用户唯一标识token
menuRoutes: constantRoute, //仓库存储生成菜单需要数组(路由)
}
//src/store/moudles/types/type.ts
import type { RouteRecordRaw } from "vue-router"
//定义小仓库数据state类型
export interface UserState {
token: string | null,
menuRoutes: RouteRecordRaw[]
}
5)父组件拿到仓库路由信息并传递给子组件
//src\layout\index.vue
<el-scrollbar class="scrollbar">
<el-menu
class="el-menu-vertical-demo"
>
<Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
</el-scrollbar>
//获取用户小仓库
import useUserStore from '@/store/moudles/user';
let userStore = useUserStore();
6)子组件prps接收并且处理结构
//src/layout/menu/index.vue
<template>
<template v-for="(item, index) in menuList" :key="item.path">
<!-- 没有子路由 -->
<template v-if="!item.children">
<el-menu-item v-if="!item.meta.hidden" :index="item.path" @click="goRoute">
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有且只有一个子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item
:index="item.children[0].path"
v-if="!item.children[0].meta.hidden"
@click="goRoute"
>
<el-icon>
<component :is="item.children[0].meta.icon"></component>
</el-icon>
<template #title>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
<!-- 有子路由且个数大于一个 -->
<el-sub-menu
:index="item.path"
v-if="item.children && item.children.length >= 2"
>
<template #title>
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<span>{{ item.meta.title }}</span>
</template>
<Menu :menuList="item.children"></Menu>
</el-sub-menu>
</template>
</template>
<script setup lang="ts">
import { useRouter } from 'vue-router';
//获取父组件传递过来的全部路由数组
defineProps(['menuList']);
//获取路由器对象
let $router = useRouter();
//点击菜单的回调
const goRoute = (vc: any) => {
//路由跳转
$router.push(vc.index);
}
</script>
<script lang="ts">
export default {
name: 'Menu',
}
</script>
<style lang="scss" scoped></style>
注意:
1:因为每一个项我们要判断俩次(是否要隐藏,以及子组件个数),所以在el-menu-item外面又套了一层模板
2:当子路由个数大于等于一个时,并且或许子路由还有后代路由时。这里我们使用了递归组件。递归组件需要命名(另外使用一个script标签,vue2格式)。
3、菜单图标
1)注册图标组件
因为我们要根据路由配置对应的图标,也要为了后续方便更改。因此我们将所有的图标注册为全局组件。(使用之前将分页器以及矢量图注册全局组件的自定义插件)(所有图标全局注册的方法element-plus文档中已给出)
//src\components\index.ts
。。。。。。
//引入element-plus提供全部图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
。。。。。。
//对外暴露插件对象
export default {
//必须叫做install方法
//会接收我们的app
。。。。。。
//将element-plus提供全部图标注册为全局组件
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
},
}
2)给路由元信息添加属性:icon
以laytou和其子组件为例:首先在element-puls找到你要使用的图标的名字。将它添加到路由元信息的icon属性上
//src\router\routes.ts
{
//登录成功以后展示数据的路由
path: '/',
component: () => import('@/layout/index.vue'),
name: 'layout',
meta: {
title: 'layout',
hidden: false,
icon: 'Avatar',
},
children: [
{
path: '/home',
component: () => import('@/views/home/index.vue'),
meta: {
title: '首页',
hidden: false,
icon: 'HomeFilled',
},
},
],
},
3)菜单组件使用
以只有一个子路由的组件为例:
//src\layout\menu\index.vue
<!-- 有且只有一个子路由 -->
<template v-if="item.children && item.children.length == 1">
<el-menu-item
index="item.children[0].path"
v-if="!item.children[0].meta.hidden"
>
<template #title>
<el-icon>
<component :is="item.children[0].meta.icon"></component>
</el-icon>
<span>{{ item.children[0].meta.title }}</span>
</template>
</el-menu-item>
</template>
4)项目全部路由配置
全部路由配置(以权限管理为例)
//src/router/routers.ts
//全部路由配置(以权限管理为例)
{
path: '/acl',
component: () => import('@/layout/index.vue'),
name: 'Acl',
meta: {
hidden: false,
title: '权限管理',
icon: 'Lock',
},
children: [
{
path: '/acl/user',
component: () => import('@/views/acl/user/index.vue'),
name: 'User',
meta: {
hidden: false,
title: '用户管理',
icon: 'User',
},
},
{
path: '/acl/role',
component: () => import('@/views/acl/role/index.vue'),
name: 'Role',
meta: {
hidden: false,
title: '角色管理',
icon: 'UserFilled',
},
},
{
path: '/acl/permission',
component: () => import('@/views/acl/permission/index.vue'),
name: 'Permission',
meta: {
hidden: false,
title: '菜单管理',
icon: 'Monitor',
},
},
],
},
5)main组件
//src/layout/main/index.vue
<template>
<router-view></router-view>
</template>
5)动画 && 自动展示
- 将router-link封装成单独的文件并且添加一些动画
//src/layout/main/index.vue
<template>
<!-- 路由组件出口的位置 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一级路由的子路由 -->
<component :is="Component" />
</transition>
</router-view>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.fade-enter-from {
opacity: 0;
}
.fade-enter-active {
transition: all 0.3s;
}
.fade-enter-to {
opacity: 1;
}
</style>
- 自动展示
当页面刷新时,菜单会自动收起。我们使用element-plus的default-active 处理。$router.path为当前路由。
//src\layout\index.vue
<el-menu
class="el-menu-vertical-demo"
:default-active = "$route.path"
>
import { useRoute } from 'vue-router';
//获取路由器对象
let $route = useRoute();
四、顶部tabbar组件
1、静态页面
element-plus:breadcrumb el-button el-dropdown
//src/layout/tabbar/index.vue
<template>
<div class="tabbar">
<div class="tabbar_left">
<!-- 顶部左侧的图标 -->
<el-icon style="margin-right: 10px">
<Expand></Expand>
</el-icon>
<!-- 左侧的面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<el-breadcrumb-item>权限挂历</el-breadcrumb-item>
<el-breadcrumb-item>用户管理</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="tabbar_right">
<el-button size="small" icon="Refresh" circle></el-button>
<el-button size="small" icon="FullScreen" circle></el-button>
<el-button size="small" icon="Setting" circle></el-button>
<img
src="../../../public/logo.png"
style="width: 24px; height: 24px; margin: 0px 10px"
/>
<!-- 下拉菜单 -->
<el-dropdown>
<span class="el-dropdown-link">
admin
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>退出登陆</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup lang="ts"></script>
<style lang="scss" scoped>
.tabbar {
width: 100%;
height: 100%;
display: flex;
justify-content: space-between;
background-image: linear-gradient(
to right,
rgb(236, 229, 229),
rgb(151, 136, 136),
rgb(240, 234, 234)
);
.tabbar_left {
display: flex;
align-items: center;
margin-left: 20px;
}
.tabbar_right {
display: flex;
align-items: center;
}
}
</style>
2、菜单折叠
1)折叠变量
定义一个折叠变量来判断现在的状态是否折叠。因为这个变量同时给breadcrumb组件以及祖先组件layout使用,因此将这个变量定义在pinia中。
//src/store/modules/setting.ts
//小仓库:layout组件相关配置仓库
import { defineStore } from 'pinia'
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起的控制
}
},
})
export default useLayOutSettingStore
2)面包屑组件点击图标切换状态
//src/layout/tabbar/breadcrumb/index.vue
<template>
<!-- 顶部左侧静态 -->
<el-icon style="margin-right:10px" @click="changeIcon">
<component :is="LayOutSettingStore.fold ? 'Fold' : 'Expand'"></component>
</el-icon>
<!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<!-- 面包动态展示路由名字与标题 -->
<el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path">
<!-- 图标 -->
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 面包屑展示匹配路由的标题 -->
<span>{{ item.meta.title }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
import useLayOutSettingStore from '@/store/modules/setting';
//获取layout配置相关的仓库
let LayOutSettingStore = useLayOutSettingStore();
//获取路由对象
let $route = useRoute();
//点击图标的方法
const changeIcon = () => {
//图标进行切换
LayOutSettingStore.fold = !LayOutSettingStore.fold
}
</script>
<script lang="ts">
export default {
name: "Breadcrumb"
}
</script>
<style scoped></style>
3)layout组件根据fold状态来修改个子组件的样式(以左侧菜单为例)
//src/layout/index.vue
<!-- 左侧菜单 -->
<div class="layout_slider" :class="{ flod: LayOutSettingStore.fold ? true : false }">
<Logo></Logo>
<el-scrollbar class="scrollbar">
<el-menu :default-active="$route.path" background-color="#001529" text-color="white"
active-text-color="yellowgreen">
<!--根据路由动态生成菜单-->
<Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
</el-scrollbar>
</div>
绑定动态样式修改scss
.layout_slider {
width: $base-menu-width;
height: 100vh;
background: $base-menu-background;
transition: all 0.3s;
.scrollbar {
width: 100%;
height: calc(100vh - $base-menu-logo-height);
}
&.fold {
width: $base-menu-min-width;
}
}
4)左侧菜单使用element-plus折叠collapse属性
<el-menu :default-active="$route.path"
:collapse="LayOutSettingStore.fold"
background-color="#001529" text-color="white"
active-text-color="yellowgreen">
<!--根据路由动态生成菜单-->
<Menu :menuList="userStore.menuRoutes"></Menu>
</el-menu>
效果图:
注意:折叠文字的时候会把图标也折叠起来。在menu组件中吧图标放到template外面就可以。
3、顶部面包屑动态展示
//src/layout/tabbar/breadcrumb/index.vue
<template>
<!-- 左侧面包屑 -->
<el-breadcrumb separator-icon="ArrowRight">
<!-- 面包动态展示路由名字与标题 -->
<el-breadcrumb-item v-for="(item, index) in $route.matched" :key="index" v-show="item.meta.title" :to="item.path">
<!-- 图标 -->
<el-icon>
<component :is="item.meta.icon"></component>
</el-icon>
<!-- 面包屑展示匹配路由的标题 -->
<span>{{ item.meta.title }}</span>
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { useRoute } from 'vue-router';
//获取路由对象
let $route = useRoute();
</script>
<script lang="ts">
export default {
name: "Breadcrumb"
}
</script>
<style scoped></style>
4、刷新业务的实现
1)使用pinia定义一个变量作为标记
//src/store/modules/setting.ts
//小仓库:layout组件相关配置仓库
import { Refresh } from '@element-plus/icons-vue'
import { defineStore } from 'pinia'
let useLayOutSettingStore = defineStore('SettingStore', {
state: () => {
return {
fold: false, //用户控制菜单折叠还是收起的控制
refsh: false//刷新
}
},
})
export default useLayOutSettingStore
2)点击刷新按钮,修改标记
//src/layout/tabbar/setting/index.vue
//获取骨架的小仓库
import useLayOutSettingStore from '@/store/modules/setting';
let layoutSettingStore = useLayOutSettingStore();
//刷新按钮点击回调
const updateRefsh = () => {
layoutSettingStore.refsh = !layoutSettingStore.refsh;
};
3)main组件检测标记销毁&重加载组件(nextTick)
//src/layout/main/index.vue
<template>
<!-- 路由组件出口的位置 -->
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 渲染layout一级路由的子路由 -->
<component :is="Component" v-if="flag"/>
</transition>
</router-view>
</template>
<script setup lang="ts">
import { watch, ref, nextTick } from 'vue'
//使用layout的小仓库
import useLayOutSettingStore from '@/store/modules/setting'
let layOutSettingStore = useLayOutSettingStore()
//控制当前组件是否销毁重建
let flag = ref(true)
//监听仓库内部的数据是否发生变化,如果发生变化,说明用户点击过刷新按钮
watch(
() => layOutSettingStore.refsh,
() => {
//点击刷新按钮:路由组件销毁
flag.value = false
nextTick(() => {
flag.value = true
})
},
)
</script>
4、全屏模式的实现
(利用docment根节点的方法)
//src/layout/tabbar/setting/index.vue
//全屏按钮点击的回调
const fullScreen = () => {
//DOM对象的一个属性:可以用来判断当前是不是全屏模式[全屏:true,不是全屏:false]
let full = document.fullscreenElement;
//切换为全屏模式
if (!full) {
//文档根节点的方法requestFullscreen,实现全屏模式
document.documentElement.requestFullscreen();
} else {
//变为不是全屏模式->退出全屏模式
document.exitFullscreen();
}
}