# 演示用例
全部代码都放上来的话太多了,所以只放了layout的部分,完整源码可以前去GitHub仓库 (opens new window) 的example目录下查看
demo的菜单
export default [
{
fullPath: '/',
meta: { title: '根菜单', icon: 'el-icon-success' },
children: [
{
fullPath: '/index',
meta: { title: '首页', icon: 'el-icon-platform-eleme', affix: true }
},
{
fullPath: '/test',
meta: { title: '测试页', icon: 'el-icon-phone' }
},
{
fullPath: '/reuse/1',
meta: { title: '复用路由1', icon: 'el-icon-phone' }
},
{
fullPath: '/reuse/2',
meta: { title: '复用路由2', icon: 'el-icon-phone' }
},
{
fullPath: '/nest',
meta: { title: '嵌套页', icon: 'el-icon-s-order' },
children: [
{
fullPath: '/nest0',
meta: { title: '嵌套页0', icon: 'el-icon-s-order' }
},
{
fullPath: '/nest0-1',
meta: { title: '嵌套页0-1', icon: 'el-icon-s-order' }
}
]
},
{
fullPath: '外链父级',
meta: { title: '外链', icon: 'el-icon-s-flag' },
children: [
{
fullPath: 'https://www.taobao.com',
meta: { title: '淘宝' }
},
{
fullPath: 'https://www.baidu.com',
meta: { title: '百度' }
}
]
},
{
fullPath: '/dsafwqewq',
meta: { title: '没有对应路由的菜单', icon: 'el-icon-platform-eleme' }
}
]
},
{
fullPath: '/iframe',
meta: { title: 'iframe', icon: 'el-icon-s-flag' },
children: [
{
fullPath: '/iframe/taobao',
meta: { title: '淘宝' }
},
{
fullPath: '/iframe/baidu',
meta: { title: '百度' }
}
]
},
{
fullPath: '/breadcrumb',
meta: { title: '面包屑', icon: 'el-icon-s-cooperation' },
children: [
{
fullPath: '/breadcrumb/simple',
meta: { title: '简单' }
},
{
fullPath: '/breadcrumb/list',
meta: { title: '列表页' }
}
]
}
]
demo的样式(个别会有自己的特殊样式)
html, body, #app {
height: 100%;
margin: 0;
}
# 基础使用
最基本的例子,只需要传入菜单即可
<template>
<el-admin-layout/>
</template>
<script>
import ElAdminLayout, { appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('基础使用')
appMutations.menus(menus)
export default {
name: 'Layout',
components: { ElAdminLayout }
}
</script>
# 从服务器加载菜单
store的数据变化会触发视图更新,利用这一点来实现异步编程。通过appStore.loadingMenu
可以控制菜单的加载情况
<template>
<div style="height: 100%">
<el-admin-layout/>
<el-button
type="primary"
style="position: fixed;top: 50%;left: 50%"
@click="loadMenu"
>
重新加载菜单
</el-button>
</div>
</template>
<script>
import ElAdminLayout, { appGetters, appMutations, asideMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('从服务器加载菜单')
appMutations.loadingMenu(true)
// 如果不设置,侧边栏在没有菜单时不会渲染
asideMutations.alwaysRender(true)
export default {
name: 'Layout',
components: { ElAdminLayout },
methods: {
loadMenu() {
if (appGetters.loadingMenu) return
appMutations.loadingMenu(true)
window.setTimeout(() => appMutations.loadingMenu(false), 2000)
}
},
mounted() {
window.setTimeout(() => {
appMutations.menus(menus)
appMutations.loadingMenu(false)
}, 2000)
}
}
</script>
# 自定义menu
导航菜单分为侧边栏菜单和顶栏菜单,前者仅在移动端或非head导航模式下渲染,后者仅在非移动端和非aside导航模式下渲染。
两者都有menu-icon
(自定义菜单图标)和menu-content
(自定义菜单内容)插槽
这两个插槽都会传入{menu: 菜单对象, depth: 菜单当前的层级深度}
<template>
<el-admin-layout>
<template v-slot:aside-menu-icon="{menu, depth}">
<i v-if="menu.meta.title === '首页'" class="menu-icon el-icon-eleme"/>
<i v-else-if="menu.children" class="menu-icon el-icon-folder"/>
<i v-else-if="depth === 1" class="menu-icon el-icon-check"/>
<i v-else class="menu-icon el-icon-close"/>
</template>
<template v-slot:aside-menu-content="{menu, depth}">
<span>我的深度:{{ depth }}</span>
</template>
<template v-slot:header-menu-content="{menu}">
<span>{{ menu.meta.title }}</span>
<span v-if="menu.meta.title === '根菜单'" class="menu-tag">new</span>
</template>
</el-admin-layout>
</template>
<script>
import ElAdminLayout, { appMutations, asideMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('自定义menu')
appMutations.menus(menus)
asideMutations.showParentOnCollapse(true)
export default {
name: 'Layout',
components: { ElAdminLayout }
}
</script>
@import '~el-admin-layout/src/style';
html, body, #app {
height: 100%;
margin: 0;
}
.menu-tag {
position: relative;
left: 12px;
padding: 0 2px 2px 2px;
font-size: 12px;
border-radius: 3px;
color: $--color-white;
background-color: $--color-danger;
}
注意
自定义侧边栏菜单的icon时,如果设置了asideStore.showParentOnCollapse
, 那么侧边栏折叠时,弹出菜单父级的深度会比未折叠时+1
# 自定义汉堡包位置
汉堡包就是控制侧边栏折叠的按钮,默认是位于侧边栏的底部(移动端时会位于顶栏左侧)
<template>
<div style="height: 100%">
<el-admin-layout>
<template v-if="position === 'header'" v-slot:header-left="defaultContent">
<header-left :default="defaultContent"/>
</template>
</el-admin-layout>
<div style="position: fixed;top: 40%;left: 40%">
<label>汉堡包位置:</label>
<el-radio-group v-model="position" @change="onPositionChange">
<el-radio label="aside">侧边栏</el-radio>
<el-radio label="header">顶栏</el-radio>
</el-radio-group>
</div>
</div>
</template>
<script>
import ElAdminLayout, { appMutations, asideMutations } from 'el-admin-layout'
import HeaderLeft from './HeaderLeft'
import menus from '@example/common/menu'
appMutations.title('自定义汉堡包位置')
appMutations.menus(menus)
export default {
name: 'Layout',
components: { ElAdminLayout, HeaderLeft },
data: () => ({ position: 'aside' }),
methods: {
onPositionChange(v) {
asideMutations.showHamburger(v === 'aside')
}
}
}
</script>
<script>
import { appGetters } from 'el-admin-layout'
import Hamburger from 'el-admin-layout/src/component/Hamburger'
export default {
name: 'HeaderLeft',
// 只有函数式组件可以返回多个节点
functional: true,
props: { default: Array },
render(h, context) {
return [
...context.props.default,
// 移动端时,顶栏已有默认的汉堡包
!appGetters.isMobile && h(Hamburger, { class: 'header-item header-icon' })
]
}
}
</script>
# 自定义页头
默认的页头会带有一个面包屑,如果不想要可以通过page页面的header
插槽来自行定制
<template>
<el-admin-layout>
<template v-slot:page-header>
<div style="width: 100%;text-align: center">我是自定义页头</div>
</template>
</el-admin-layout>
</template>
<script>
import ElAdminLayout, { appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('自定义页头')
appMutations.menus(menus)
export default {
name: 'Layout',
components: { ElAdminLayout }
}
</script>
# 自定义页脚
el-admin-layout并不提供默认的页脚组件,有需要的可以用page页面的footer
插槽来自行定制
<template>
<el-admin-layout>
<template v-slot:page-footer>
<div class="copyright">
Copyright © 2020 - <a href="https://github.com/toesbieya" target="_blank">toesbieya</a>
</div>
</template>
</el-admin-layout>
</template>
<script>
import ElAdminLayout, { appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('自定义页脚')
appMutations.menus(menus)
export default {
name: 'Layout',
components: { ElAdminLayout }
}
</script>
注意
如果需要修改页脚的高度,建议通过修改scss变量中的$page-footer-height
# 侧边栏搜索框
通过侧边栏的header
插槽将搜索框放到侧边栏顶部,使用asideStore.postMenus
确定最终需要渲染的菜单,最后通过侧边栏的menu-content
插槽实现高亮搜索词
其中有用到jsx语法和函数式组件,以及一些涉及el-admin-layout源码的代码,不过核心还是上面的三点
<template>
<el-admin-layout ref="layout">
<template v-slot:aside-header="defaultContent">
<aside-header :default="defaultContent"/>
</template>
<template v-slot:aside-menu-content="{menu}">
<aside-menu-content :title="menu.meta.title" :search-word="searchWord"/>
</template>
</el-admin-layout>
</template>
<script>
import ElAdminLayout, { appMutations, asideMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
import AsideHeader from './AsideHeader'
import AsideMenuContent from './AsideMenuContent'
import { filterMenuBySearchWord, expandAfterSearch } from './util'
appMutations.title('侧边栏搜索框')
appMutations.menus(menus)
appMutations.navMode('aside')
export default {
name: 'Layout',
components: { ElAdminLayout, AsideHeader, AsideMenuContent },
data: () => ({ searchWord: '' }),
methods: {
searchWordMutation(v) {
this.searchWord = v
},
postMenus(menus) {
const searchWord = this.searchWord
const filtered = filterMenuBySearchWord(menus, searchWord)
// 在新的菜单渲染完毕后展开sub-menu
this.$nextTick(() => {
const sidebar = this.$refs['layout'].$refs['aside'].$refs['default-sidebar']
const elMenu = sidebar.$_getElMenuInstance()
elMenu && expandAfterSearch(elMenu, searchWord, filtered)
})
return filtered
}
},
created() {
// 避免搜索结果为空时侧边栏不渲染
asideMutations.alwaysRender(true)
asideMutations.postMenus(this.postMenus)
}
}
</script>
<script>
import { appGetters, asideGetters } from 'el-admin-layout'
import MenuSearch from './MenuSearch'
export default {
name: 'AsideHeader',
functional: true,
props: { default: Object },
render(h, context) {
return [
context.props.default,
!appGetters.isMobile && (
<MenuSearch
v-show={!asideGetters.collapse}
on-search={context.parent.searchWordMutation}
/>
)
]
}
}
</script>
<script>
export default {
name: 'AsideMenuContent',
props: {
title: String,
searchWord: String
},
render() {
const { title, searchWord } = this
if (!searchWord) return <span>{title}</span>
const start = title.indexOf(searchWord)
if (start === -1) return <span>{title}</span>
const end = start + searchWord.length
return (
<span>
{title.substring(0, start)}
<span class="menu-highlight-result">{title.substring(start, end)}</span>
{title.substring(end)}
</span>
)
}
}
</script>
<template>
<div class="aside-menu-search">
<el-input
v-model="value"
size="mini"
clearable
placeholder="搜索菜单"
prefix-icon="el-icon-search"
@input="search"
/>
</div>
</template>
<script>
import { debounce } from '@example/common/util'
export default {
name: 'MenuSearch',
data: () => ({ value: '' }),
methods: {
search(v) {
this.$emit('search', v)
}
},
created() {
this.search = debounce(this.search, 300)
},
beforeDestroy() {
this.$emit('search', '')
}
}
</script>
import { isEmpty } from '@example/common/util'
// 获取高亮菜单的sub-menu
function getSubHighlightMenu(searchWord, children, parent) {
const result = []
children.forEach(child => {
if (child.meta.title.includes(searchWord)) {
parent && result.push(parent)
}
if (child.children) {
result.push(...getSubHighlightMenu(searchWord, child.children, child))
}
})
return [...new Set(result)]
}
// 根据搜索词过滤菜单
export function filterMenuBySearchWord(menus, searchWord) {
if (!menus) return
return menus
.map(menu => ({ ...menu }))
.filter(menu => {
// 如果匹配,那么其子节点无需再判断
if (menu.meta.title.includes(searchWord)) {
return true
}
const children = filterMenuBySearchWord(menu.children, searchWord)
if (children) menu.children = children
return children && children.length > 0
})
}
// 根据查找结果展开菜单
export function expandAfterSearch(elMenu, searchWord, filteredMenus) {
// 清空搜索词时还原原本展开的菜单
if (isEmpty(searchWord)) {
elMenu.openedMenus = []
return elMenu.initOpenedMenu()
}
const expandMenus = getSubHighlightMenu(searchWord, filteredMenus)
const expandMenuIndexList = [...elMenu.openedMenus]
for (const { fullPath } of expandMenus) {
const sub = elMenu.submenus[fullPath]
sub && expandMenuIndexList.push(...sub.indexPath)
}
// 不调用el-menu的open方法是为了避免uniqueOpened
elMenu.openedMenus = [...new Set(expandMenuIndexList)]
}
@import '~el-admin-layout/src/style';
html, body, #app {
height: 100%;
margin: 0;
}
// 搜索框
.aside-menu-search {
margin: 12px 8px;
}
// 暗色主题
.dark .aside-menu-search .el-input__inner {
border: none;
color: $--color-white;
background-color: lighten($menu-background-dark, 5%);
}
// 菜单的高亮关键字
.menu-highlight-result {
vertical-align: unset;
color: $--color-success;
}
# 模拟移动端
可能有时候需要让el-admin-layout在桌面端以移动端的形式渲染,可以通过Const.maxMobileWidth
和appStore.isMobile
实现
<template>
<el-admin-layout/>
</template>
<script>
import ElAdminLayout, { Const, appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('模拟移动端')
appMutations.menus(menus)
// 先将isMobile设为true
appMutations.isMobile(true)
// 然后设置maxMobileWidth,这两步最好在ElAdminLayout渲染前完成
Const.maxMobileWidth = 10000
export default {
name: 'Layout',
components: { ElAdminLayout },
mounted() {
// 如果在ElAdminLayout渲染后,那么上述两步需要调换执行顺序
/*Const.maxMobileWidth = 10000
appMutations.isMobile(true)*/
}
}
</script>
注意
这种方式会让一些css失效,比如辅助类hide-on-mobile
# 设置抽屉
el-admin-layout并不像ant-design-pro那样会有一个设置抽屉(这东东自己写更快),所以需要自己搞定
本示例只是列出了比较常用的设置项,所有的设置项请查看数据控制
<template>
<div style="height: 100%">
<el-admin-layout/>
<setting-drawer ref="setting-drawer"/>
<el-button
type="primary"
style="position: fixed;top: 50%;left: 50%"
@click="openSettingDrawer"
>
打开设置抽屉
</el-button>
</div>
</template>
<script>
import ElAdminLayout, { appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
import SettingDrawer from './SettingDrawer'
appMutations.title('设置抽屉')
appMutations.menus(menus)
export default {
name: 'Layout',
components: { ElAdminLayout, SettingDrawer },
methods: {
openSettingDrawer() {
this.$refs['setting-drawer'].visible = true
}
}
}
</script>
<template>
<el-drawer
:visible="visible"
:with-header="false"
custom-class="setting-drawer"
append-to-body
size="300px"
@close="visible = false"
>
<el-divider>app</el-divider>
<div class="setting-drawer-item">
<span>标题</span>
<el-input
:value="appGetters.title"
@input="v => onChange('appMutations', 'title', v)"
/>
</div>
<div class="setting-drawer-item">
<span>logo地址</span>
<el-input
:value="appGetters.logo"
@input="v => onChange('appMutations', 'logo', v)"
/>
</div>
<div class="setting-drawer-item">
<span>显示logo</span>
<el-switch
:value="appGetters.showLogo"
@input="v => onChange('appMutations', 'showLogo', v)"
/>
</div>
<div class="setting-drawer-item">
<span>分层结构</span>
<el-select
:value="appGetters.struct"
@input="v => onChange('appMutations', 'struct', v)"
>
<el-option value="top-bottom" label="上下"/>
<el-option value="left-right" label="左右"/>
</el-select>
</div>
<div class="setting-drawer-item">
<span>导航模式</span>
<el-select
:value="appGetters.navMode"
@input="v => onChange('appMutations', 'navMode', v)"
>
<el-option value="aside" label="侧边栏"/>
<el-option value="head" label="顶部"/>
<el-option value="mix" label="混合"/>
</el-select>
</div>
<el-divider>aside</el-divider>
<div class="setting-drawer-item">
<span>主题</span>
<el-select
:value="asideGetters.theme"
@input="v => onChange('asideMutations', 'theme', v)"
>
<el-option value="light" label="亮色"/>
<el-option value="dark" label="暗色"/>
</el-select>
</div>
<div class="setting-drawer-item">
<span>手风琴</span>
<el-switch
:value="asideGetters.uniqueOpen"
@input="v => onChange('asideMutations', 'uniqueOpen', v)"
/>
</div>
<div class="setting-drawer-item">
<span>折叠</span>
<el-switch
:value="asideGetters.collapse"
@input="v => onChange('asideMutations', 'collapse', v)"
/>
</div>
<div class="setting-drawer-item">
<span>折叠时显示上级菜单</span>
<el-switch
:value="asideGetters.showParentOnCollapse"
@input="v => onChange('asideMutations', 'showParentOnCollapse', v)"
/>
</div>
<div class="setting-drawer-item">
<span>显示汉堡包</span>
<el-switch
:value="asideGetters.showHamburger"
@input="v => onChange('asideMutations', 'showHamburger', v)"
/>
</div>
<div class="setting-drawer-item">
<span>自动隐藏</span>
<el-switch
:value="asideGetters.autoHide"
@input="v => onChange('asideMutations', 'autoHide', v)"
/>
</div>
<el-divider>header</el-divider>
<div class="setting-drawer-item">
<span>主题</span>
<el-select
:value="headerGetters.theme"
@input="v => onChange('headerMutations', 'theme', v)"
>
<el-option value="light" label="亮色"/>
<el-option value="dark" label="暗色"/>
</el-select>
</div>
<div class="setting-drawer-item">
<span>头像地址</span>
<el-input
:value="headerGetters.avatar"
@input="v => onChange('headerMutations', 'avatar', v)"
/>
</div>
<div class="setting-drawer-item">
<span>用户名称</span>
<el-input
:value="headerGetters.username"
@input="v => onChange('headerMutations', 'username', v)"
/>
</div>
<el-divider>page</el-divider>
<div class="setting-drawer-item">
<span>启用过渡动画</span>
<el-switch
:value="pageGetters.enableTransition"
@input="v => onChange('pageMutations', 'enableTransition', v)"
/>
</div>
<div class="setting-drawer-item">
<span>显示页头</span>
<el-switch
:value="pageGetters.showHeader"
@input="v => onChange('pageMutations', 'showHeader', v)"
/>
</div>
<el-divider>tagsView</el-divider>
<div class="setting-drawer-item">
<span>启用</span>
<el-switch
:value="tagsViewGetters.enabled"
@input="v => onChange('tagsViewMutations', 'enabled', v)"
/>
</div>
<div class="setting-drawer-item">
<span>启用缓存功能</span>
<el-switch
:value="tagsViewGetters.enableCache"
@input="v => onChange('tagsViewMutations', 'enableCache', v)"
/>
</div>
<div class="setting-drawer-item">
<span>根据页签顺序确定过渡动画</span>
<el-switch
:value="tagsViewGetters.enableChangeTransition"
@input="v => onChange('tagsViewMutations', 'enableChangeTransition', v)"
/>
</div>
</el-drawer>
</template>
<script>
import {
appGetters,
appMutations,
asideGetters,
asideMutations,
headerGetters,
headerMutations,
tagsViewGetters,
tagsViewMutations,
pageGetters,
pageMutations
} from 'el-admin-layout'
export default {
name: 'SettingDrawer',
// 让el-input、el-select的size为mini
provide: () => ({ elFormItem: { elFormItemSize: 'mini' } }),
data: () => ({ visible: false }),
methods: {
onChange(mutation, prop, v) {
this.getMutation(mutation)[prop](v)
},
getMutation(str) {
switch (str) {
case 'appMutations':
return appMutations
case 'asideMutations':
return asideMutations
case 'headerMutations':
return headerMutations
case 'tagsViewMutations':
return tagsViewMutations
case 'pageMutations':
return pageMutations
}
}
},
created() {
Object
.entries({ appGetters, asideGetters, headerGetters, tagsViewGetters, pageGetters })
.forEach(([k, v]) => this[k] = v)
}
}
</script>
@import '~el-admin-layout/src/style';
html, body, #app {
height: 100%;
margin: 0;
}
.setting-drawer {
overflow-y: auto;
.el-drawer__body {
padding: $--dialog-padding-primary;
}
&-item {
display: flex;
justify-content: space-between;
color: $--color-text-regular;
font-size: 14px;
padding: 12px 0;
}
.el-input, .el-select {
width: 80px;
}
}
小优化
像设置抽屉这种和其他组件基本没有关联的组件,建议自己控制数据,这样不会说每打开一次抽屉,父组件就render一次
# 持久化页签
这个功能不看源码就做的话,可能会出问题,所以出一个demo
打开几个页签然后刷新iframe即可看到效果(当然也可以直接跳转到iframe的地址)
这个demo会将页签数据存储到sessionStorage里,键是'eal-test-persist-tags'
,关闭页面即可清除
<template>
<el-admin-layout/>
</template>
<script>
import ElAdminLayout, { appMutations, tagsViewGetters, tagsViewMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
import { debounce } from '@example/common/util'
appMutations.title('持久化页签')
appMutations.menus(menus)
const STORAGE = window.sessionStorage
const KEY = 'eal-test-persist-tags'
export default {
name: 'Layout',
components: { ElAdminLayout },
methods: {
getTags() {
const data = STORAGE.getItem(KEY)
return data ? JSON.parse(data) : []
},
setTags(data) {
data ? STORAGE.setItem(KEY, JSON.stringify(data)) : STORAGE.removeItem(KEY)
},
// 只需要关心fullPath和meta属性
persist(data) {
this.setTags(data.map(v => ({ fullPath: v.fullPath, meta: v.meta })))
}
},
beforeCreate() {
// 防抖处理
this.persist = debounce(this.persist)
},
mounted() {
// 从本地存储中取数据,放到tagsViewStore中
this.getTags().forEach(({ fullPath, meta }) => {
const { route } = this.$router.resolve(fullPath)
// 需要合并路由meta,优先使用持久化的meta的属性
route && tagsViewMutations.addTagOnly({ ...route, meta: { ...route.meta, ...meta } })
})
// 页签变化时,将页签数据保存到本地存储
// 上面页签数据可能已经发生了变化,所以立即执行一次持久化
this.$watch(() => tagsViewGetters.visitedViews, this.persist, { immediate: true })
}
}
</script>
# 仿旧版七牛云侧边栏
说实话,如果真有这种需求,强烈建议自己写layout,因为这种肯定是需要去看源码的(像插槽的传递、父子组件关系等等), 如果在el-admin-layout基础上搞,你看看demo就知道有多麻烦了
<template>
<el-admin-layout>
<!--移动端时恢复成默认的侧边栏-->
<template v-if="!isMobile" v-slot:aside="props">
<v-root/>
<v-sub/>
</template>
</el-admin-layout>
</template>
<script>
import ElAdminLayout, { appGetters, appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
import VRoot from './OldQiniuSidebar/root'
import VSub from './OldQiniuSidebar/sub'
appMutations.title('仿旧版七牛云侧边栏')
appMutations.menus(menus)
// 只渲染侧边栏
appMutations.navMode('aside')
export default {
name: 'Layout',
components: { ElAdminLayout, VRoot, VSub },
computed: {
isMobile: () => appGetters.isMobile
}
}
</script>
<script>
import menuMixin from 'el-admin-layout/src/mixin/menu'
import { appGetters, appMutations, asideGetters } from 'el-admin-layout'
import Logo from 'el-admin-layout/src/component/Logo'
import { isRedirectRouter } from 'el-admin-layout/src/config/logic'
import { getMenuByFullPath } from 'el-admin-layout/src/store/app'
import { findFirstLeaf } from 'el-admin-layout/src/util'
export default {
name: 'OldQiniuSidebarRoot',
mixins: [menuMixin],
components: { Logo },
data() {
return {
// 是否折叠
collapse: true,
// 是否点击了菜单
hasSelectMenu: false
}
},
computed: {
// 是否需要显示logo
showLogo() {
return appGetters.showLogo && appGetters.struct === 'left-right'
},
sidebarClass() {
return { 'root-sidebar': true, 'collapse': this.collapse }
},
menuClass() {
return `el-menu el-menu--vertical el-menu--${asideGetters.theme}`
}
},
watch: {
// 路由变化时设置高亮菜单
$route: {
immediate: true,
handler(to) {
if (!this.setActiveRootMenuWhenRouteChange(to)) {
return
}
// 滚动至激活菜单,仅当组件已mounted时继续
this._isMounted && this.$nextTick(() => {
this.$el.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
})
}
}
},
methods: {
// 根据路由设置当前高亮的根节点
setActiveRootMenu({ matched: [root] } = this.$route) {
// 此处的path是路由定义中的原始数据,所以根路由不能使用动态匹配的方式定义(一般也不会有这种情况吧)
// 如果路由中使用了'/',那么此处的path会是''
root && appMutations.activeRootMenu(root.path || '/')
},
// 路由变化时设置高亮根节点菜单,设置成功时返回true
setActiveRootMenuWhenRouteChange(route) {
const { matched } = route
if (matched.length === 0 || isRedirectRouter(route)) {
return false
}
this.setActiveRootMenu(route)
return true
},
onSelect(index) {
this.onSelectRootMenu(index)
// 设置标识位,因为此时折叠菜单时可能会触发onMouseEnter
this.hasSelectMenu = true
// 只要点击了菜单项就收起
this.collapse = true
},
onSelectRootMenu(index) {
const root = getMenuByFullPath(index)
// vue-router中对应index的路由可能有子级且未设置redirect,此时访问index会404
const { leaf, hasOtherLeaf } = findFirstLeaf(root)
// 如果该根节点已激活且有多个叶子节点,退出
if (!leaf || appGetters.activeRootMenu === index && hasOtherLeaf) {
return
}
this.actionOnSelectMenu(leaf.fullPath)
},
onMouseLeave() {
this.collapse = true
this.hasSelectMenu = false
},
onMouseEnter() {
// 如果是onSelect导致的菜单折叠,退出
if (this.collapse && this.hasSelectMenu) {
this.hasSelectMenu = false
return
}
this.collapse = false
}
},
render() {
return (
<div class="root-sidebar-container">
<div
class={this.sidebarClass}
on-mouseleave={this.onMouseLeave}
on-mouseenter={this.onMouseEnter}
>
{this.showLogo && <logo show-title={!this.collapse}/>}
<ul class={this.menuClass}>
{appGetters.menus.map(menu => {
const { fullPath, meta: { icon, title } } = menu
const isActive = fullPath === appGetters.activeRootMenu
return (
<li class={{ 'el-menu-item': true, 'is-active': isActive }}
on-click={() => this.onSelect(fullPath)}
>
{icon && <i class={`menu-icon ${icon}`}/>}
{!this.collapse && <span>{title}</span>}
</li>
)
})}
</ul>
</div>
</div>
)
}
}
</script>
<script>
import menuMixin from 'el-admin-layout/src/mixin/menu'
import { appGetters, asideGetters } from 'el-admin-layout'
import NavMenu from 'el-admin-layout/src/component/NavMenu'
import Hamburger from 'el-admin-layout/src/component/Hamburger'
import { getRouterActiveMenu, isRedirectRouter } from 'el-admin-layout/src/config/logic'
export default {
name: 'OldQiniuSidebarSub',
mixins: [menuMixin],
components: { NavMenu, Hamburger },
computed: {
// 父级菜单
rootMenu() {
const active = appGetters.activeRootMenu
return appGetters.menus.find(i => i.fullPath === active)
},
// 侧边栏菜单
menus() {
const root = this.rootMenu
return root ? root.children || [] : []
},
// 侧边栏的折叠状态,true折叠、false展开
collapse() {
return asideGetters.collapse
},
className() {
return { 'sub-sidebar': true, 'collapse': this.collapse }
}
},
watch: {
$route: {
immediate: true,
handler(to) {
if (isRedirectRouter(to)) return
this.activeMenu = getRouterActiveMenu(this.$route)
const menu = this.$_getElMenuInstance()
if (!menu) return
const item = menu.items[this.activeMenu]
// 如果侧边栏中没有对应的激活菜单,则收起全部,退出
if (!item) return menu.openedMenus = []
// 由于elMenu的initOpenedMenu()不会触发select事件,所以选择手动触发
this.onSelect(item.index, item.indexPath, item, false)
// 滚动至激活的菜单
this.$nextTick(this.moveToActiveMenuVertically)
}
}
},
methods: {
// 模拟选中菜单
onSelect(index, indexPath, item, jump = true) {
// 开启手风琴模式时,激活没有子级的菜单时收起其它展开项
if (asideGetters.uniqueOpen && indexPath.length === 1) {
const menu = this.$_getElMenuInstance()
const opened = menu.openedMenus
opened.forEach(i => i !== index && menu.closeMenu(i))
}
jump && this.actionOnSelectMenu(index)
},
// 将当前激活的菜单移动到视窗中
moveToActiveMenuVertically() {
const menu = this.$_getElMenuInstance()
if (!menu) return
const cur = menu.activeIndex
if (!cur) return
const curInstance = menu.items[cur]
if (!curInstance) return
let el = curInstance.$el
// 当侧边栏折叠时,需要滚动至可视区域的元素是激活菜单的最顶层父节点
if (menu.collapse) {
let rootParent = curInstance
while (rootParent.$parent.$options.componentName !== 'ElMenu') {
rootParent = rootParent.$parent
}
el = rootParent.$el
}
/*
* 这里考虑了菜单展开时的200ms动画时间
* 为什么不分情况讨论?比如当subMenu已经是展开状态时,无需延时滚动
* 但这种情况无法判断,因为这时menu.openedMenus已经包含了subMenu,无论subMenu之前是否展开
* 所以统一延时300ms
* */
window.setTimeout(() => el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 300)
}
},
render() {
if (this.menus.length === 0) return
return (
<div class={this.className}>
{!this.collapse && (
<div class="sub-sidebar-title">
{this.rootMenu && this.rootMenu.meta.title}
</div>
)}
<nav-menu
ref="nav-menu"
menus={this.menus}
theme={asideGetters.theme}
collapse={this.collapse}
default-active={this.activeMenu}
unique-opened={asideGetters.uniqueOpen}
show-parent-on-collapse={asideGetters.showParentOnCollapse}
switch-transition
switch-transition-name="sidebar"
inline-indent={26}
on-select={this.onSelect}
/>
<div class="sidebar-footer">
<hamburger/>
</div>
</div>
)
}
}
</script>
@import '~el-admin-layout/src/style';
html, body, #app {
height: 100%;
margin: 0;
}
@mixin common {
height: 100%;
width: $aside-width;
transition: width 0.2s;
&.collapse {
width: $aside-collapse-width;
}
}
.root-sidebar {
position: absolute;
overflow: hidden;
z-index: 1;
border-right-width: 1px;
border-right-style: solid;
@include common;
.el-menu-item {
padding: 0 $menu-padding;
}
// 收起时
&.collapse {
.logo-container {
padding-left: ($aside-collapse-width - $logo-size) / 2;
}
// 不显示背景色和小竖条
.el-menu-item {
&::before,
&::after {
display: none;
}
}
}
&-container {
float: left;
position: relative;
width: $aside-collapse-width;
height: 100%;
}
}
.sub-sidebar {
display: flex;
flex-direction: column;
> .el-menu {
flex: 1;
}
@include common;
&-title {
height: $header-height;
line-height: $header-height;
padding-left: $menu-padding;
font-size: 16px;
}
}
// 暗色主题
.aside.dark {
.root-sidebar,
.root-sidebar > .el-menu {
background-color: $header-background-dark;
}
.root-sidebar {
border-right-color: $--border-color-dark;
}
.sub-sidebar-title {
color: $--color-white;
}
}
# 仿chrome页签
一个简易版本,样式参考了chrome-tabs (opens new window)
<template>
<el-admin-layout>
<template #tags-view-item="{key, active, on, title, close}">
<div :key="key" :class="{'tags-view-item': true, 'active': active}" v-on="on">
<div class="tags-view-item__title">{{ title }}</div>
<div v-if="close" class="tags-view-item__close" @click="close">
<i class="el-icon-close"/>
</div>
<div class="tags-view-item__divider"/>
</div>
</template>
</el-admin-layout>
</template>
<script>
import ElAdminLayout, { appMutations } from 'el-admin-layout'
import menus from '@example/common/menu'
appMutations.title('仿chrome页签')
appMutations.menus(menus)
export default {
name: 'Layout',
components: { ElAdminLayout }
}
</script>
$tags-view-background-color: #dee1e6;
// 页签分割线相对于上下的距离
$nav-item-divider-gap: 7px;
// 页签分割线颜色
$nav-item-divider-color: #a9adb0;
// 页签hover时的背景色
$nav-item-bg-hover: darken($tags-view-background-color, 5%);
// 页签上方圆角
$nav-item-border-radius-top: 8px;
// 页签下方圆角
$nav-item-border-radius-bottom: 10px;
// 页签关闭按钮大小
$nav-item-close-size: 16px;
// 页签关闭按钮hover背景色
$nav-item-close-bg-hover: rgba(0, 0, 0, .1);
// 页签关闭按钮active背景色
$nav-item-close-bg-active: rgba(0, 0, 0, .2);
$tags-view-shadow: unset;
$tags-view-item-border: unset;
$tags-view-item-border-radius: $nav-item-border-radius-top $nav-item-border-radius-top 0 0;
$tags-view-item-padding: 9px 8px;
$tags-view-item-between: $nav-item-border-radius-bottom;
@import './var';
@import '~el-admin-layout/src/style';
@import './chrome-tabs';
html, body, #app {
height: 100%;
margin: 0;
}
.tags-view-item {
position: relative;
width: 140px;
padding: $tags-view-item-padding;
margin: 0;
border-radius: $nav-item-border-radius-top $nav-item-border-radius-top 0 0;
color: unset;
background-color: transparent;
transition: background-color .2s;
&.active {
z-index: 5;
color: unset;
background-color: $page-background-color;
&::before, &::after {
box-shadow: 0 0 0 30px $page-background-color;
}
}
&::after, &::before {
position: absolute;
bottom: 0;
content: '';
width: $nav-item-border-radius-bottom * 2;
height: $nav-item-border-radius-bottom * 2;
border-radius: 100%;
box-shadow: 0 0 0 $nav-item-border-radius-bottom * 1.5 transparent;
transition: box-shadow .2s;
}
&::after {
right: -($nav-item-border-radius-bottom * 2);
clip-path: inset(50% 50% 0 -#{$nav-item-border-radius-bottom});
}
&::before {
left: -($nav-item-border-radius-bottom * 2);
clip-path: inset(50% -#{$nav-item-border-radius-bottom} 0 50%);
}
&:not(.active):hover {
z-index: 2;
background-color: $nav-item-bg-hover;
&::before, &::after {
box-shadow: 0 0 0 30px $nav-item-bg-hover;
}
}
&__title {
position: relative;
flex: 1;
vertical-align: top;
overflow: hidden;
white-space: nowrap;
mask-image: linear-gradient(90deg, black calc(100% - 24px), transparent);
}
&__close {
flex-shrink: 0;
height: $nav-item-close-size;
width: $nav-item-close-size;
text-align: center;
border-radius: 50%;
margin-left: .5em;
i {
font-size: $tags-view-item-font-size;
font-weight: bold;
margin: 0 !important;
}
&:hover {
background-color: $nav-item-close-bg-hover;
}
&:active {
background-color: $nav-item-close-bg-active;
}
}
&__divider {
position: absolute;
top: $nav-item-divider-gap;
bottom: $nav-item-divider-gap;
right: -1px;
width: 1px;
background-color: $nav-item-divider-color;
opacity: 1;
transition: opacity .2s;
}
// 激活、hover时不显示右边的分割线
&:last-child .tags-view-item__divider,
&.active .tags-view-item__divider,
&:hover .tags-view-item__divider {
opacity: 0;
}
}