element-plus 前端后台项目layout布局
2023年1月17日大约 9 分钟约 2582 字
layout布局
layout布局分为:
- 左侧的侧边栏
Sidebar
,高度为100%整个屏幕高度 - 顶部的导航栏
NavBar
- 中间的标签栏
TagsView
- 下面内容区域
Main
侧边栏sidebar布局
src/layout/components/sidebar/Sidebar.vue
<template>
<div>
<!-- 标题栏 -->
<div class="logo-container h-11 pt-2 pb-5 flex items-center justify-center box-content">
<el-avatar icon="el-icon-user-solid" :size="44" shape="square" src="https://m.imooc.com/static/wap/static/common/img/logo-small@2x.png" style="--el-avatar-bg-color: none"></el-avatar>
<span class="logo-title ml-2 font-semibold leading-[50px] text-[16px] whitespace-nowrap" v-if="settingStore.sidebarOpened">项目标题</span>
</div>
<el-scrollbar>
<!-- 一级 menu 菜单 -->
<el-menu
router
:default-active="router.path"
:uniqueOpened="true"
default-active="2"
:background-color="themeStore.mainBg"
:text-color="themeStore.mainText"
:active-text-color="themeStore.activeText"
:collapse="!settingStore.sidebarOpened"
style="--el-menu-item-font-size: 14px"
:collapse-transition = 'false'
>
<!-- 固定菜单 -->
<!--
<el-sub-menu index="1">
<template #title>
<i class="el-icon-location"></i>
<span>导航一</span>
</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-sub-menu>
<el-menu-item index="4">
<i class="el-icon-setting"></i>
<template #title>导航四</template>
</el-menu-item>
-->
<!-- 路由菜单 -->
<el-sub-menu v-for="(firstRoute, index) in menus" :key="firstRoute.path" :index="String(index+1)">
<template #title>
<el-icon><SvgIcon :icon="firstRoute.meta.icon"></SvgIcon></el-icon>
<span class="text-base">{{firstRoute.meta.title}}</span>
</template>
<el-menu-item v-for="secondRoute in firstRoute.children"
:key="secondRoute.path"
:index="secondRoute.path"
>
<el-icon><SvgIcon :icon="secondRoute.meta.icon"></SvgIcon></el-icon>
<template #title>{{secondRoute.meta.title}}</template>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</div>
</template>
<script setup>
import {} from 'vue'
import { useRouter } from 'vue-router'
import useSettingStore from '@/store/settings'
import useThemeStore from '@/store/theme'
import SvgIcon from '@/components/SvgIcon.vue'
const settingStore = useSettingStore()
const themeStore = useThemeStore()
const router = useRouter()
const routes = router.getRoutes()
// ---------------------- 通过路由获取菜单项 --------------------
// 获取所有子路由
const childrenRoutes = []
routes.forEach(route => {
JSON.stringify(route.children) !== '[]' && childrenRoutes.push(...route.children) // if简写
})
// 获取过滤出的不重复路由(去掉重复的子路由)
const filterRoutes = routes.filter(route => {
const hasChildrenRoute = childrenRoutes.find(childrenRoute => childrenRoute.path === route.path)
return !hasChildrenRoute // 如果查不到子路由则提取
})
// 提取一级菜单项
const menus = []
filterRoutes.forEach(firstRoute => {
// 存在.meta.icon说明是菜单项
if (firstRoute.meta && firstRoute.meta.title && firstRoute.meta.icon) {
// 如果该菜单项有子菜单,则提取二级菜单项
if (JSON.stringify(firstRoute.children) !== '[]') {
const children = []
firstRoute.children.forEach(secondRoute => {
// 存在.meta.icon说明是菜单项
if (secondRoute.meta && secondRoute.meta.title && secondRoute.meta.icon) children.push(secondRoute)
})
firstRoute.children = children // 将过滤的二级路由替换上去
}
menus.push(firstRoute)
}
})
// ------------------------------------------------------------
</script>
<style lang="scss" scoped>
.logo-title {
color: v-bind('themeStore.mainText')
}
</style>
导航栏布局
src/layout/components/navbar/Navbar.vue
<template>
<div class="navbar h-12 overflow-hidden relative bg-white shadow-sm shadow-slate-300">
<!-- 伸缩侧边栏的切换按钮 -->
<div class="px-4 leading-[46px] float-left cursor-pointer transition-[background] duration-500 hover:bg-slate-50" @click="toggleClick">
<el-icon :color="themeStore.mainBg" class="inline-block align-middle w-5 h-5 ">
<i-ep-DArrowLeft v-if="settingStore.sidebarOpened" />
<i-ep-DArrowRight v-if="!settingStore.sidebarOpened" />
</el-icon>
</div>
<!-- 动态面包 -->
<breadcrumb class="breadcrumb-container float-left" :color="themeStore.mainText" />
<!-- 全屏 -->
<Screenfull class=" leading-[46px] float-right inline-block pr-4 text-2xl text-[#5a5e66] cursor-pointer"></Screenfull>
<!-- 右边下拉栏 -->
<div class="right-menu flex items-center float-right pr-4">
<!-- 头像 -->
<el-dropdown class="avatar-container cursor-pointer" :style="{ '--el-color-primary': themeStore.mainBg }" trigger="click" size="large" split-button type="primary" @command="">
<div class="avatar-wrapper relative">
<el-avatar :size="35" shape="square" :src="userStore.userinfo.avatar" style="--el-avatar-bg-color: none"></el-avatar>
<i class="el-icon-s-tools"></i>
</div>
<template #dropdown>
<el-dropdown-menu class="user-dropdown">
<router-link to="/">
<el-dropdown-item>首页</el-dropdown-item>
</router-link>
<a href="" target="_blank">
<el-dropdown-item>课程主页</el-dropdown-item>
</a>
<el-dropdown-item class=" cursor-pointer" @click="themeStore.changeTheme('朱砂红')">朱砂红主题</el-dropdown-item>
<el-dropdown-item divided @click="userStore.logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import Breadcrumb from '@/layout/components/navbar/components/Breadcrumb.vue'
import Screenfull from './components/Screenfull.vue'
import {} from 'vue'
import { storeToRefs } from 'pinia'
import useSettingStore from '@/store/settings'
import useUserStore from '@/store/user.js'
import useThemeStore from '@/store/theme.js'
const themeStore = useThemeStore()
const settingStore = useSettingStore()
const userStore = useUserStore()
// const { userinfo } = storeToRefs(userStore)
// console.log(userinfo)
// 侧边栏伸缩点击
const toggleClick = () => {
settingStore.triggerSidebarOpened()
}
</script>
<style lang="scss" scoped>
// 深度查找器,可匹配组件内元素
:v-deep(.avatar-wrapper) {
.el-avatar {
--el-avatar-bg-color: none; // 头像背景透明
margin-right: 12px;
}
}
</style>
Breadcrumb.vue
新建面包屑子组件:src/layout/components/navbar/components/Breadcrumb.vue
<template>
<el-breadcrumb class="breadcrumb inline-block ml-2 text-sm" style="line-height: 50px" separator="/">
<!-- 固定面包屑 -->
<!--
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item><a href="/">活动管理</a></el-breadcrumb-item>
<el-breadcrumb-item>活动列表</el-breadcrumb-item>
<el-breadcrumb-item>
<span class="no-redirect">活动详情</span>
</el-breadcrumb-item>
-->
<!-- 动态面包屑 -->
<transition-group name="breadcrumb">
<el-breadcrumb-item v-for="(item, index) in breadcrumbData" :key="item.path">
<!-- 不可点击项目 -->
<span v-if="index === breadcrumbData.length -1" class="no-redirect">{{ item.meta.title }}</span>
<!-- 可点击项 -->
<a v-else class="redirect" @click.prevent="onLinkClick(item)">{{ item.meta.title }}</a>
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>
<script setup>
import { ref, watch } from 'vue'
import { useRoute } from 'vue-router'
import router from '../../../../router';
// 接受父组件传递属性
defineProps({
color: { type: String, default: '#97a8be'}
})
const route = useRoute()
// ---------------------- 生成数组数据 ----------------------
const breadcrumbData = ref([])
// 使用到 route.match 属性来:获取与给定路由地址匹配的标准化的路由记录数组
const getBreadcrumbData = () => {
breadcrumbData.value = route.matched.filter(
item => item.meta && item.meta.title
)
}
// 监听路由变化时触发
watch(
route,
() => {
getBreadcrumbData()
},
{
immediate: true
}
)
// ---------------- 处理点击事件 ------------------
const onLinkClick = item => router.push(item.path)
</script>
<style lang="scss" scoped>
.breadcrumb {
:v-deep(.no-redirect) {
color: v-bind('color');
cursor: text;
}
}
// 动画
.breadcrumb-enter-active,
.breadcrumb-leave-active {
transition: all 0.5s;
}
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
}
.breadcrumb-leave-active {
position: absolute;
}
</style>
Screenfull.vue
全屏子组件:src/layout/components/navbar/components/Screenfull.vue
<template>
<div @click="onToggle">
<el-icon>
<svg-icon v-if="isFullscreen" icon="Close" />
<svg-icon v-if="!isFullscreen" icon="FullScreen" />
</el-icon>
</div>
</template>
<script setup>
/**
* 全屏组件,直接可以使用,google和edge测试过了,火狐和weiket还没有测试
* 火狐和webkit的api可能不一样:`document.mozCancelFullScreen`,`document.webkitCancelFullScreen()`等等
* 如何要方便可是使用全屏框架:`npm install screenfull`
*/
import { ref } from 'vue'
import SvgIcon from '@/components/SvgIcon.vue'
// 是否全屏(图标显示使用)
const isFullscreen = ref(false)
// 切换事件
const onToggle = function () {
if (document.fullscreenElement === null ? false : true) {
document.exitFullscreen()
} else {
document.getElementById('app').requestFullscreen()
}
isFullscreen.value = document.fullscreenElement !== null ? false : true
}
</script>
<style lang="scss">
// 注意如何元素没有设置背景颜色,默认全屏时的元素背景颜色是黑色
// 这里去掉了scode,在全局设置,设置#app根元素的全屏背景,用于继承到子元素
#app:fullscreen {
background-color: rgba(255, 255, 255, 255);
}
#app:-webkit-full-screen {
background-color: rgba(255, 255, 255, 255);
}
#app:-moz-full-screen {
background-color: rgba(255, 255, 255, 255);
}
</style>
标签栏布局
src/layout/components/tags-view/TagsView.vue
<template>
<div class="tags-view-container">
<el-scrollbar class="tags-view-wrapper">
<router-link
class="tags-view-item"
:class="isActive(route) ? 'active' : ''"
:style="{
backgroundColor: isActive(route) ? themeStore.mainBg : '',
borderColor: isActive(route) ? themeStore.mainBg : ''
}"
v-for="(route, index) in systemStore.tagsViewList"
:key="index"
:to="{ path: route.fullPath }"
@contextmenu.prevent="openMenu($event, index)"
>
<!-- 标签标题内容 -->
{{ route.meta.title }}
<!-- 标签关闭图标 -->
<el-icon @click.prevent.stop="onCloseClick(index)" v-show="!isActive(route)">
<SvgIcon icon="Close" />
</el-icon>
</router-link>
</el-scrollbar>
<ContextMenu v-show="isShowContextMenu" :style="menuStyle" :index="selectIndex"></ContextMenu>
</div>
</template>
<script setup>
import { useRoute } from 'vue-router'
import useSystemStore from '@/store/system'
import useThemeStore from '@/store/theme'
import { whiteList } from '@/permission'
import ContextMenu from './components/ContextMenu.vue'
import SvgIcon from '@/components/SvgIcon.vue'
const systemStore = useSystemStore()
const themeStore = useThemeStore()
const route = useRoute()
// -------------------------- tags 标签栏 -------------------------
// 监听路由变化
watch(
route,
(newRoute, oldRoute) => {
// 如果是白名单的页面,则不添加到tags列表中
if (whiteList.includes(newRoute.path)) return
// route是响应式对象,要做一次深拷贝,新数据脱离响应式。不然推送到列表中的项都是一个数据。
// JSON拷贝会爆出警告:https://blog.csdn.net/weixin_43245095/article/details/123091515
// const copyNewRoute = JSON.parse(JSON.stringify(newRoute))
const { meta, params, path, query, fullPath, name } = newRoute
const copyNewRoute = { meta, params, path, query, fullPath, name}
systemStore.addTagsViewList(copyNewRoute)
},
{ immediate: true }
)
/**
* 是否被选中
*/
const isActive = tagRoute => {
return tagRoute.path === route.path
}
/**
* 关闭当前tag的点击事件
*/
const onCloseClick = index => {
systemStore.removeTagsView({ type: 'index', index: index })
}
// -------------------------- contextMenu 右键上下文菜单 -------------------------
// 当前右键点击的标签索引
const selectIndex = ref(0)
// 是否展示右键菜单
const isShowContextMenu = ref(false)
// 右键菜单组件的style
const menuStyle = ref({ left: 0, top: 0})
// 展示 menu
const openMenu = (e, index) => {
const { x, y } = e
menuStyle.value.left = x + 'px'
menuStyle.value.top = y + 'px'
selectIndex.value = index
isShowContextMenu.value = true
}
// 关闭右键菜单
const closeContextMenu = ()=> {
isShowContextMenu.value = false
}
// 点击过后关闭右键菜单
watch(isShowContextMenu, (newValue, oldValue) => {
// 如果打开了右键菜单,则开始监听点击事件
if (newValue) {
document.body.addEventListener('click', closeContextMenu)
} else {
// 如果关闭了右键菜单,说明监听事件已经完成了任务,则取消监听'点击事件'。
document.body.removeEventListener('click', closeContextMenu)
}
})
</script>
<style lang="scss" scoped>
.tags-view-container {
height: 34px;
width: 100%;
background: #fff;
border-bottom: 1px solid #d8dce5;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
.tags-view-item {
display: inline-block;
position: relative;
cursor: pointer;
height: 26px;
line-height: 26px;
border: 1px solid #d8dce5;
color: #495060;
background: #fff;
padding: 0 8px;
font-size: 12px;
margin-left: 5px;
margin-top: 4px;
&:first-of-type {
margin-left: 15px;
}
&:last-of-type {
margin-right: 15px;
}
&.active {
color: #fff;
&::before {
content: '';
background: #fff;
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
position: relative;
margin-right: 4px;
}
}
// close 按钮
.el-icon-close {
width: 16px;
height: 16px;
line-height: 10px;
vertical-align: 2px;
border-radius: 50%;
text-align: center;
transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
transform-origin: 100% 50%;
&:before {
transform: scale(0.6);
display: inline-block;
vertical-align: -3px;
}
&:hover {
background-color: #b4bccc;
color: #fff;
}
}
}
}
</style>
ContextMenu.vue
右键菜单组件:src/layout/components/tags-view/components/ContextMenu.vue
<template>
<ul class="context-menu-container">
<li @click="onRefreshClick">
刷新
</li>
<li @click="onCloseRightClick">
关闭右侧所有标签
</li>
<li @click="onCloseOtherClick">
关闭其他所有标签
</li>
</ul>
</template>
<script setup>
// 右键项
import { defineProps } from 'vue'
import useSystemStore from '@/store/system'
import { useRouter } from 'vue-router';
const router = useRouter()
const systemStore = useSystemStore()
const props = defineProps({
index: {
type: Number,
required: true
}
})
const onRefreshClick = () => {
router.go(0)
}
const onCloseRightClick = () => {
systemStore.removeTagsView({ type: 'right', index: props.index })
}
const onCloseOtherClick = () => {
systemStore.removeTagsView({ type: 'other', index: props.index })
}
</script>
<style lang="scss" scoped>
.context-menu-container {
position: fixed;
background: #fff;
z-index: 3000;
list-style-type: none;
padding: 5px 0;
border-radius: 4px;
font-size: 12px;
font-weight: 400;
color: #333;
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
li {
margin: 0;
padding: 7px 16px;
cursor: pointer;
&:hover {
background: #eee;
}
}
}
</style>
请注意看
TagsView.vue
监听路由变化的代码,如果是路由白名单的页面是不会添加到标签栏的
内容栏布局
src/layout/components/app-main/AppMain.vue
<template>
<div class="app-main relative w-full overflow-hidden box-border p-5 pt-[104px] min-h-[calc(100vh-50px-43px)]">
<router-view v-slot="{ Component, route }">
<transition name="fade-transform" mode="out-in">
<keep-alive>
<component :is="Component" :key="route.path" />
</keep-alive>
</transition>
</router-view>
</div>
</template>
<script setup>
// 路由动态过渡:https://router.vuejs.org/zh/guide/advanced/transitions.html#基于路由的动态过渡
import {} from 'vue'
</script>
<style lang="scss" scoped>
/* fade-transform */
.fade-transform-leave-active,
.fade-transform-enter-active {
transition: all 0.5s;
}
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.fade-transform-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
layout根页面
最后就是用一个layout页面把这些组件汇总起来
src/layout/index.vue
<template>
<div class="app-wrapper relative h-full w-full after:table after:clear-both">
<!-- 侧边栏 -->
<sidebar class="sidebar-container fixed top-0 bottom-0 left-0 z-50 h-full transition-w duration-300 overflow-hidden" :class="[settingStore.sidebarOpened ? 'w-52' : 'w-16']" />
<!-- 页面右侧 -->
<div class="main-container relative ml-52 min-h-full transition-ml duration-300" :class="[settingStore.sidebarOpened ? 'ml-52' : 'ml-16']">
<div class="fixed-header fixed top-0 right-0 z-10 transition-w duration-300" :class="[settingStore.sidebarOpened ? 'w-[calc(100%-210px)]' : 'w-[calc(100%-64px)]']">
<!-- 顶部 navbar -->
<Navbar />
<!-- tags 标签栏 -->
<tags-view />
</div>
<!-- 内容区域 -->
<app-main />
</div>
</div>
</template>
<script setup>
import Navbar from './components/navbar/Navbar.vue'
import Sidebar from './components/sidebar/Sidebar.vue'
import AppMain from './components/app-main/AppMain.vue'
import TagsView from './components/tags-view/TagsView.vue'
import useSettingStore from '@/store/settings'
import useThemeStore from '@/store/theme'
const themeStore = useThemeStore()
const settingStore = useSettingStore()
</script>
<style lang="scss" scoped>
.sidebar-container {
background-color: v-bind('themeStore.mainBg');
}
</style>
本章总结
截止此处,项目如果需要正常启动,还需要有一些依赖组件:
- src/components/SvgIcon.vue