ELADMIN-02 页面布局
1.页面的总体布局
首先最高一级的页面显示是由App.vue
来引入的,通过一个<router-view />
标签根据路由的不同来显示不同的页面。这些页面就是四个一级路由页面,包括
- /login, 系统的登录页面
- /401, 为授权的页面显示
- /404,
- /, 系统的主页面Layout,对应的组件是
src/layout/index.vue
,系统的各个界面都是嵌套路由的子路由
2.主页面的布局
主页面的布局是由src/layout/index.vue
这个组件来控制的,在这个组件中又将1页面分为三个部分,其中第一个div是控制在移动设备上的菜单栏显示的时候的一个黑色背景,也就是下面这行代码。
<div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
<sidebar class="sidebar-container" /> <!-- 侧边栏 -->
<div :class="{hasTagsView:needTagsView}" class="main-container"> <!-- 主体 -->
<div :class="{'fixed-header':fixedHeader}">
<navbar /> <!-- 顶部导航栏 -->
<tags-view v-if="needTagsView" /> <!-- 打开的标签 -->
</div>
<app-main />
<right-panel v-if="showSettings"> <!-- 布局设置面板 -->
<settings />
</right-panel>
</div>
剩下两部分则控制的是页面的侧边栏sidebar
和主体部分,其中主体部分又可以划分为三个部分
- 头部
- 顶部导航栏
- 已打开的页签栏
- 主体部分
- 布局设置面板,只有在进行布局设置的时候才会显示在页面的最右侧
2.2 侧边栏的组成
侧边栏由两部分组分,即顶部的logo和下面的菜单项组成,顶部的logo也可以看作是一个特殊的菜单项,点击它会路由挑战到首页,下面来详细介绍一下各个部分。
<!--侧边栏-->
<template>
<div :class="{'has-logo':showLogo}">
<!-- 侧边栏最上方的LOGO -->
<logo v-if="showLogo" :collapse="isCollapse" />
<!-- 滚动条容器,用来包裹菜单项,当菜单超出可视区域自动出现滚动条 -->
<el-scrollbar wrap-class="scrollbar-wrapper">
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
:background-color="variables.menuBg"
:text-color="variables.menuText"
:active-text-color="variables.menuActiveText"
:collapse-transition="false"
unique-opened
mode="vertical"
><!-- :collapse控制是否水平折叠收起菜单(仅在 mode 为 vertical 时可用)
:default-active表示当前激活的菜单
mode选择垂直模式-->
<sidebar-item v-for="route in sidebarRouters" :key="route.path" :item="route" :base-path="route.path" />
</el-menu>
</el-scrollbar>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Logo from './Logo'
import SidebarItem from './SidebarItem'
import variables from '@/assets/styles/variables.scss'
export default {
components: { SidebarItem, Logo },
computed: {
...mapGetters([
'sidebarRouters',
'sidebar'
/* 在当前 Vue 组件中创建两个计算属性 sidebarRouters
和 sidebar,这两个属性的值分别对应 Vuex store 中的
sidebarRouters 和 sidebar getter 的值。当 Vuex store
中的 sidebarRouters 和 sidebar 的值发生变化时,
这两个计算属性的值也会自动更新 */
]),
activeMenu() { /* 这个函数用来返回一个路径,来让菜单组件显示正在激活的菜单项 */
const route = this.$route
const { meta, path } = route
console.log('激活路由:' + meta.activeMenu + ' ' + path)
// if set path, the sidebar will highlight the path you set
if (meta.activeMenu) {
return meta.activeMenu
}
return path
},
showLogo() {
return this.$store.state.settings.sidebarLogo // 从store中获取sidebarLogo的值,该值用于判断是否显示logo
},
variables() {
return variables
},
isCollapse() {
return !this.sidebar.opened // 从store中获取sidebar的值,该值用于判断是否折叠菜单
}
}
}
</script>
2.2.1 Logo图标(src/layout/components/Sidebar/Logo.vue)
logo比较简单,就是分为侧边栏收缩和展开两种情况下,直接看代码即可
<template>
<div class="sidebar-logo-container" :class="{'collapse':collapse}">
<transition name="sidebarLogoFade">
<router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo"><!--在侧边栏收缩的情况下,存在logo则只显示logo,否则显示标题-->
<h1 v-else class="sidebar-title">{{ title }} </h1>
</router-link>
<router-link v-else key="expand" class="sidebar-logo-link" to="/">
<img v-if="logo" :src="logo" class="sidebar-logo"><!--侧边栏展开的情况下,显示logo和标题-->
<h1 class="sidebar-title">{{ title }}</h1>
</router-link>
</transition>
</div>
</template>
<script>
import Logo from '@/assets/images/logo.png'
export default {
name: 'SidebarLogo',
props: {
collapse: {
type: Boolean,
required: true
}
},
data() {
return {
title: 'ELADMIN-后台管理',
logo: Logo
}
}
}
</script>
2.2.2 SidebarItem组件(src/layout/components/Sidebar/SidebarItem.vue)
这个组件的作用就是渲染出侧边栏中的一个个菜单项,这个组件接收三个属性:
- item:必需的,类型为 Object,表示要显示的菜单项的路由对象。
- isNest:可选的,类型为 Boolean,默认值为 false,表示当前菜单项是否是嵌套的子菜单项。
- basePath:可选的,类型为 String,默认值为空字符串,表示当前菜单项的基础路径。
在部分进行了判断,首先判断item是否需要隐藏,只有当不需要隐藏的时候才会显示这个菜单项,
- 接着判断是否一个菜单项只有一个子菜单项需要显示,且这个子菜单项没有自己的子菜单项,或者父菜单项设置了 alwaysShow(
v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow"
)这种强况下就是使用app-link
组件来判断是否是一个外连接,如果是一个外连接就渲染为标签否则就渲染为组件,然后使用el-menu-item
显示这个子菜单项。 - 如果不满足上面的条件,就使用
el-submenu
显示出所有子菜单项,每个子菜单项再递归调用SiderbarItem
进行渲染
此外这个组件还混入了 FixiOSBug 模块,用于修复在 iOS 设备上的一个 bug。这个 bug 的具体表现是,当在 iOS 设备上点击菜单时,会触发 mouseleave 事件。为了解决这个问题,FixiOSBug 在 mounted 钩子函数中调用了 fixBugIniOS 方法。
在渲染菜单项的使用使用了item
组件,这个组件的作用就是显示侧边菜单项的图标和标题
2.3 顶部导航栏
顶部导航栏比较简单,这里就不详细说明
<template>
<div class="navbar">
<!-- 控制侧边导航栏的伸缩 -->
<hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<!--面包屑导航栏-->
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" />
<!-- 头像左边的四个图标控制-->
<div class="right-menu">
<template v-if="device!=='mobile'">
<search id="header-search" class="right-menu-item" />
<el-tooltip content="项目文档" effect="dark" placement="bottom">
<Doc class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip content="全屏缩放" effect="dark" placement="bottom">
<screenfull id="screenfull" class="right-menu-item hover-effect" />
</el-tooltip>
<el-tooltip content="布局设置" effect="dark" placement="bottom">
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<!--头像已经点击头像时显示的下拉选择框-->
<el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="user.avatarName ? baseApi + '/avatar/' + user.avatarName : Avatar" class="user-avatar">
<i class="el-icon-caret-bottom" />
</div>
<el-dropdown-menu slot="dropdown">
<span style="display:block;" @click="show = true">
<el-dropdown-item>
布局设置
</el-dropdown-item>
</span>
<!-- 进入个人中心需要进行路由挑战 -->
<router-link to="/user/center">
<el-dropdown-item>
个人中心
</el-dropdown-item>
</router-link>
<span style="display:block;" @click="open">
<el-dropdown-item divided>
退出登录
</el-dropdown-item>
</span>
</el-dropdown-menu>
</el-dropdown>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
import Breadcrumb from '@/components/Breadcrumb'
import Hamburger from '@/components/Hamburger'
import Doc from '@/components/Doc'
import Screenfull from '@/components/Screenfull'
import SizeSelect from '@/components/SizeSelect'
import Search from '@/components/HeaderSearch'
import Avatar from '@/assets/images/avatar.png'
export default {
components: {
Breadcrumb,
Hamburger,
Screenfull,
SizeSelect,
Search,
Doc
},
data() {
return {
Avatar: Avatar,
dialogVisible: false
}
},
computed: {
...mapGetters([
'sidebar',
'device',
'user',
'baseApi'
]),
show: {
get() {
return this.$store.state.settings.showSettings
},
set(val) {
this.$store.dispatch('settings/changeSetting', {
key: 'showSettings',
value: val
})
}
}
},
methods: {
// 控制侧边导航栏的伸缩
toggleSideBar() {
this.$store.dispatch('app/toggleSideBar')
},
// 退出登录
open() {
this.$confirm('确定注销并退出系统吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.logout()
})
},
logout() {
this.$store.dispatch('LogOut').then(() => {
location.reload()
})
}
}
}
</script>
2.4 TagsView(src/layout/components/TagsView/index.vue)
TagsView组件用于显示已访问视图的标签。它包含了一个 scroll-pane 组件,用于在有多个标签时提供滚动功能,这个组件相对复杂,不仅要用store来记录打开的标签和路由,还要对每一个标签的点击事件进行详细的区分。
- 左键点击进行路由跳转
- 中间点击关闭当前标签
- 右键点击显示右键菜单
组件的结构比较简单就是一个scroll-pane组件包含全部的标签,除此之外还有一个标签右键显示的菜单
<template>
<div id="tags-view-container" class="tags-view-container">
<scroll-pane ref="scrollPane" class="tags-view-wrapper">
<router-link
v-for="tag in visitedViews"
ref="tag"
:key="tag.path"
:class="isActive(tag)?'active':''"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
tag="span"
class="tags-view-item"
@click.middle.native="closeSelectedTag(tag)"
@contextmenu.prevent.native="openMenu(tag,$event)"
>
<!-- 对标签的点击进行了详细的分类,首先根据router-link的默认行为来实现左键进行路由跳转切换页面,
@click.middle.native="closeSelectedTag(tag)"来实现鼠标中键点击关闭当前标签
@contextmenu.prevent.native="openMenu(tag,$event)"实现鼠标右键点击显示右键菜单栏-->
{{ tag.title }}
<!-- 标签的关闭按钮-->
<span v-if="!tag.meta.affix" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
</router-link>
</scroll-pane>
<!-- 右键菜单栏-->
<ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
<li @click="refreshSelectedTag(selectedTag)">刷新</li>
<li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">关闭</li>
<li @click="closeOthersTags">关闭其他</li>
<li @click="closeAllTags(selectedTag)">关闭全部</li>
</ul>
</div>
</template>
补充知识:
@contextmenu
是监听contextmenu
事件,也就是鼠标的右键点击事件,当右键点击的时候触发该事件,代码中使用@contextmenu.prevent.native
是在用户在元素上右键点击时,阻止默认的右键菜单显示,并且这个元素是原生的 HTML 元素,而不是 Vue 组件。
2.5 AppMain.vue组(src/layout/components/AppMain.vue)
这个组件就是显示应用的主要内容的区域,包含了一个 router-view 组件和一个 el-backtop 组件。其中el-backtop 组件:用于快速回到页面顶部。它的 bottom 和 right 属性设置了它在页面中的位置。
router-view 组件:用于显示当前路由对应的视图。它的 key 属性绑定了当前路由的路径,这样当路由改变时,可以强制重新渲染视图。router-view 组件被包裹在一个 keep-alive 组件中,keep-alive 组件的 include 属性绑定了 cachedViews,这样可以缓存已访问的视图,提高性能。