parent
82ad7d684c
commit
6f61b12934
@ -1,74 +0,0 @@ |
||||
<template> |
||||
<li |
||||
class="el-menu-item" |
||||
:style="paddingStyle" |
||||
:class="{'is-active': active,'is-disabled': disabled}" |
||||
@click="handleClick" |
||||
> |
||||
<el-tooltip |
||||
v-if="parentMenu.$options.componentName === 'ElMenu' && rootMenu.collapse && $slots.title" |
||||
effect="dark" |
||||
placement="right" |
||||
> |
||||
<template v-slot:content> |
||||
<slot name="title"/> |
||||
</template> |
||||
<div :style="iconContainerStyle"> |
||||
<slot/> |
||||
</div> |
||||
</el-tooltip> |
||||
|
||||
<template v-else> |
||||
<slot/> |
||||
<slot name="title"/> |
||||
</template> |
||||
</li> |
||||
</template> |
||||
|
||||
<script> |
||||
import MenuMixin from './mixin' |
||||
import Emitter from 'element-ui/lib/mixins/emitter' |
||||
|
||||
export default { |
||||
name: 'ElMenuItem', |
||||
|
||||
componentName: 'ElMenuItem', |
||||
|
||||
mixins: [MenuMixin, Emitter], |
||||
|
||||
props: { |
||||
index: String, |
||||
route: [String, Object], |
||||
disabled: Boolean |
||||
}, |
||||
|
||||
computed: { |
||||
active() { |
||||
return this.index === this.rootMenu.activeIndex |
||||
}, |
||||
|
||||
iconContainerStyle() { |
||||
return `position: absolute;left: 0;top: 0;height: 100%;width: 100%;display: inline-block;box-sizing: border-box;padding: 0 ${this.inlineIndent}px;` |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
handleClick() { |
||||
if (!this.disabled) { |
||||
this.dispatch('ElMenu', 'item-click', this) |
||||
this.$emit('click', this) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
this.parentMenu.addItem(this) |
||||
this.rootMenu.addItem(this) |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
this.parentMenu.removeItem(this) |
||||
this.rootMenu.removeItem(this) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,250 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import MenuMixin from './mixin' |
||||
import Emitter from 'element-ui/lib/mixins/emitter' |
||||
import Popper from 'element-ui/lib/utils/vue-popper' |
||||
|
||||
const PopperMixin = { |
||||
props: { |
||||
transformOrigin: {type: [Boolean, String], default: false}, |
||||
offset: Popper.props.offset, |
||||
boundariesPadding: Popper.props.boundariesPadding, |
||||
popperOptions: Popper.props.popperOptions |
||||
}, |
||||
data: Popper.data, |
||||
methods: Popper.methods, |
||||
beforeDestroy: Popper.beforeDestroy, |
||||
deactivated: Popper.deactivated |
||||
} |
||||
|
||||
export default { |
||||
name: 'ElSubmenu', |
||||
|
||||
componentName: 'ElSubmenu', |
||||
|
||||
mixins: [MenuMixin, Emitter, PopperMixin], |
||||
|
||||
props: { |
||||
index: String, |
||||
showTimeout: {type: Number, default: 300}, |
||||
hideTimeout: {type: Number, default: 300}, |
||||
popperClass: String, |
||||
disabled: Boolean, |
||||
popperAppendToBody: {type: Boolean, default: true} |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
popperJS: null, |
||||
timeout: null, |
||||
items: {}, |
||||
submenus: {}, |
||||
mouseInChild: false |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
appendToBody() { |
||||
return this.popperAppendToBody === undefined |
||||
? this.isFirstLevel |
||||
: this.popperAppendToBody |
||||
}, |
||||
menuTransitionName() { |
||||
return this.rootMenu.collapse ? 'el-zoom-in-left' : 'el-zoom-in-top' |
||||
}, |
||||
opened() { |
||||
return this.rootMenu.openedMenus.includes(this.index) |
||||
}, |
||||
active() { |
||||
const active = Object.values(this.items).some(v => v.active) |
||||
return active || Object.values(this.submenus).some(v => v.active) |
||||
}, |
||||
mode() { |
||||
return this.rootMenu.mode |
||||
}, |
||||
isMenuPopup() { |
||||
return this.rootMenu.isMenuPopup |
||||
}, |
||||
isFirstLevel() { |
||||
const arr = ['ElSubmenu', 'ElMenuItemGroup'] |
||||
let isFirstLevel = true |
||||
let parent = this.$parent |
||||
|
||||
while (parent && parent !== this.rootMenu) { |
||||
if (arr.includes(parent.$options.componentName)) { |
||||
isFirstLevel = false |
||||
break |
||||
} |
||||
else parent = parent.$parent |
||||
} |
||||
return isFirstLevel |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
opened() { |
||||
this.isMenuPopup && this.$nextTick(this.updatePopper) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
handleCollapseToggle(value) { |
||||
value ? this.initPopper() : this.doDestroy() |
||||
}, |
||||
|
||||
addItem(item) { |
||||
this.$set(this.items, item.index, item) |
||||
}, |
||||
removeItem(item) { |
||||
delete this.items[item.index] |
||||
}, |
||||
addSubmenu(item) { |
||||
this.$set(this.submenus, item.index, item) |
||||
}, |
||||
removeSubmenu(item) { |
||||
delete this.submenus[item.index] |
||||
}, |
||||
|
||||
handleClick() { |
||||
const {rootMenu, disabled} = this |
||||
if ( |
||||
(rootMenu.menuTrigger === 'hover' && rootMenu.mode === 'horizontal') || |
||||
(rootMenu.collapse && rootMenu.mode === 'vertical') || |
||||
disabled |
||||
) { |
||||
return |
||||
} |
||||
this.dispatch('ElMenu', 'submenu-click', this) |
||||
}, |
||||
|
||||
handleMouseenter(event, showTimeout = this.showTimeout) { |
||||
if (!('ActiveXObject' in window) && event.type === 'focus' && !event.relatedTarget) { |
||||
return |
||||
} |
||||
|
||||
const {rootMenu, disabled} = this |
||||
|
||||
if (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal' |
||||
|| !rootMenu.collapse && rootMenu.mode === 'vertical' |
||||
|| disabled) return |
||||
|
||||
this.dispatch('ElSubmenu', 'mouse-enter-child') |
||||
|
||||
window.clearTimeout(this.timeout) |
||||
this.timeout = window.setTimeout(() => this.rootMenu.openMenu(this.index, this.indexPath), showTimeout) |
||||
|
||||
this.appendToBody && this.$parent.$el.dispatchEvent(new MouseEvent('mouseenter')) |
||||
}, |
||||
|
||||
handleMouseleave(deepDispatch = false) { |
||||
const {rootMenu} = this |
||||
|
||||
if (rootMenu.menuTrigger === 'click' && rootMenu.mode === 'horizontal' |
||||
|| !rootMenu.collapse && rootMenu.mode === 'vertical') return |
||||
|
||||
this.dispatch('ElSubmenu', 'mouse-leave-child') |
||||
|
||||
window.clearTimeout(this.timeout) |
||||
this.timeout = window.setTimeout(() => !this.mouseInChild && this.rootMenu.closeMenu(this.index), this.hideTimeout) |
||||
|
||||
if (this.appendToBody && deepDispatch) { |
||||
if (this.$parent.$options.name === 'ElSubmenu') { |
||||
this.$parent.handleMouseleave(true) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
updatePlacement() { |
||||
this.placement = this.mode === 'horizontal' && this.isFirstLevel |
||||
? 'bottom-start' |
||||
: 'right-start' |
||||
}, |
||||
|
||||
initPopper() { |
||||
this.referenceElm = this.$el |
||||
this.popperElm = this.$refs.menu |
||||
this.updatePlacement() |
||||
} |
||||
}, |
||||
|
||||
created() { |
||||
this.$on('toggle-collapse', this.handleCollapseToggle) |
||||
this.$on('mouse-enter-child', () => { |
||||
this.mouseInChild = true |
||||
window.clearTimeout(this.timeout) |
||||
}) |
||||
this.$on('mouse-leave-child', () => { |
||||
this.mouseInChild = false |
||||
window.clearTimeout(this.timeout) |
||||
}) |
||||
}, |
||||
|
||||
mounted() { |
||||
this.parentMenu.addSubmenu(this) |
||||
this.rootMenu.addSubmenu(this) |
||||
this.initPopper() |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
this.parentMenu.removeSubmenu(this) |
||||
this.rootMenu.removeSubmenu(this) |
||||
}, |
||||
|
||||
render() { |
||||
const {rootMenu, $slots} = this |
||||
|
||||
const popupMenu = ( |
||||
<transition name={this.menuTransitionName}> |
||||
<div |
||||
ref="menu" |
||||
v-show={this.opened} |
||||
class={[`el-menu--${this.mode}`, this.popperClass]} |
||||
on-mouseenter={e => this.handleMouseenter(e, 100)} |
||||
on-mouseleave={() => this.handleMouseleave(true)} |
||||
on-focus={e => this.handleMouseenter(e, 100)}> |
||||
<ul class={['el-menu el-menu--popup', `el-menu--popup-${this.currentPlacement}`]}> |
||||
{$slots.default} |
||||
</ul> |
||||
</div> |
||||
</transition> |
||||
) |
||||
|
||||
const inlineMenu = ( |
||||
<el-collapse-transition> |
||||
<ul v-show={this.opened} class="el-menu el-menu--inline"> |
||||
{$slots.default} |
||||
</ul> |
||||
</el-collapse-transition> |
||||
) |
||||
|
||||
const submenuTitleIcon = ( |
||||
rootMenu.mode === 'horizontal' && this.isFirstLevel || |
||||
rootMenu.mode === 'vertical' && !rootMenu.collapse |
||||
) ? 'el-icon-arrow-down' : 'el-icon-arrow-right' |
||||
|
||||
return ( |
||||
<li |
||||
class={{ |
||||
'el-submenu': true, |
||||
'is-active': this.active, |
||||
'is-opened': this.opened, |
||||
'is-disabled': this.disabled |
||||
}} |
||||
on-mouseenter={this.handleMouseenter} |
||||
on-mouseleave={() => this.handleMouseleave(false)} |
||||
on-focus={this.handleMouseenter} |
||||
> |
||||
<div |
||||
ref="submenu-title" |
||||
class="el-submenu__title" |
||||
style={this.paddingStyle} |
||||
on-click={this.handleClick} |
||||
> |
||||
{$slots.title} |
||||
<i class={['el-submenu__icon-arrow', submenuTitleIcon]}/> |
||||
</div> |
||||
{this.isMenuPopup ? popupMenu : inlineMenu} |
||||
</li> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,53 +0,0 @@ |
||||
/** |
||||
* 在原基础上增加了 inlineIndent 属性,用于确定每级节点的单位缩进距离 |
||||
*/ |
||||
export default { |
||||
inject: ['rootMenu'], |
||||
|
||||
props: { |
||||
inlineIndent: {type: Number, default: 20} |
||||
}, |
||||
|
||||
computed: { |
||||
indexPath() { |
||||
const path = [this.index] |
||||
let parent = this.$parent |
||||
while (parent.$options.componentName !== 'ElMenu') { |
||||
if (parent.index) { |
||||
path.unshift(parent.index) |
||||
} |
||||
parent = parent.$parent |
||||
} |
||||
return path |
||||
}, |
||||
|
||||
parentMenu() { |
||||
let parent = this.$parent |
||||
while ( |
||||
parent && |
||||
['ElMenu', 'ElSubmenu'].indexOf(parent.$options.componentName) === -1 |
||||
) { |
||||
parent = parent.$parent |
||||
} |
||||
return parent |
||||
}, |
||||
|
||||
paddingStyle() { |
||||
if (this.rootMenu.mode !== 'vertical') return {} |
||||
|
||||
let padding = this.inlineIndent |
||||
|
||||
if (!this.rootMenu.collapse) { |
||||
let parent = this.$parent |
||||
while (parent && parent.$options.componentName !== 'ElMenu') { |
||||
if (parent.$options.componentName === 'ElSubmenu') { |
||||
padding += this.inlineIndent |
||||
} |
||||
parent = parent.$parent |
||||
} |
||||
} |
||||
|
||||
return {'padding-left': `${padding}px`} |
||||
} |
||||
} |
||||
} |
||||
@ -1,13 +1,6 @@ |
||||
import MenuItem from "./component/Menu/MenuItem" |
||||
import Submenu from "./component/Menu/Submenu" |
||||
import Message from "./component/Message" |
||||
|
||||
const components = [MenuItem, Submenu] |
||||
|
||||
export default function (Vue) { |
||||
components.forEach(component => { |
||||
Vue.component(component.name, component) |
||||
}) |
||||
|
||||
Vue.prototype.$message = Message |
||||
} |
||||
|
||||
@ -1,7 +1,4 @@ |
||||
/*仅覆盖element-ui中的预设变量,不会被使用*/ |
||||
|
||||
//el-menu的hover背景颜色 |
||||
$--menu-item-hover-fill: none; |
||||
|
||||
//引入element的scss时需要覆盖字体路径 |
||||
$--font-path: '~element-ui/lib/theme-chalk/fonts'; |
||||
|
||||
@ -1,5 +0,0 @@ |
||||
@media only screen and (max-width: $--sm) { |
||||
.hidden-xs { |
||||
display: none !important |
||||
} |
||||
} |
||||
@ -1,31 +0,0 @@ |
||||
.right-out, |
||||
.left-out { |
||||
&-leave-active, |
||||
&-enter-active { |
||||
transition: all .2s; |
||||
} |
||||
} |
||||
|
||||
.right-out { |
||||
&-enter { |
||||
opacity: 0; |
||||
transform: translateX(-30px); |
||||
} |
||||
|
||||
&-leave-to { |
||||
opacity: 0; |
||||
transform: translateX(30px); |
||||
} |
||||
} |
||||
|
||||
.left-out { |
||||
&-enter { |
||||
opacity: 0; |
||||
transform: translateX(30px); |
||||
} |
||||
|
||||
&-leave-to { |
||||
opacity: 0; |
||||
transform: translateX(-30px); |
||||
} |
||||
} |
||||
@ -1,30 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import common from './common' |
||||
|
||||
export default { |
||||
name: "ColorCheckBox", |
||||
|
||||
mixins: [common], |
||||
|
||||
render() { |
||||
return ( |
||||
<div class="color-checkbox" style={{backgroundColor: this.value}} on-click={this.click}> |
||||
{this.checked && <i class="el-icon-check"/>} |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style> |
||||
.color-checkbox { |
||||
width: 20px; |
||||
height: 20px; |
||||
margin: 5px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
text-align: center; |
||||
font-weight: bold; |
||||
color: #ffffff; |
||||
} |
||||
</style> |
||||
@ -1,21 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
export default { |
||||
name: "CheckboxGroup", |
||||
|
||||
props: {value: String}, |
||||
|
||||
data: () => ({children: []}), |
||||
|
||||
render() { |
||||
return <div class="checkbox-group">{this.$slots.default}</div> |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style> |
||||
.checkbox-group { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
width: 100%; |
||||
} |
||||
</style> |
||||
@ -1,43 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import common from './common' |
||||
|
||||
export default { |
||||
name: "ImgCheckBox", |
||||
|
||||
mixins: [common], |
||||
|
||||
props: { |
||||
img: String, |
||||
label: String |
||||
}, |
||||
|
||||
render() { |
||||
return ( |
||||
<div class="img-checkbox" title={this.label} on-click={this.click}> |
||||
<img src={this.img}/> |
||||
{this.checked && <i className="el-icon-check"/>} |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.img-checkbox { |
||||
width: 55px; |
||||
margin: 10px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
position: relative; |
||||
|
||||
.el-icon-check { |
||||
position: absolute; |
||||
color: $--color-primary; |
||||
font-weight: bold; |
||||
top: 25px; |
||||
right: 10px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -1,114 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
const Item = { |
||||
functional: true, |
||||
|
||||
render(h, context) { |
||||
return <li class="context-menu-item" {...context.data}>{context.children}</li> |
||||
} |
||||
} |
||||
|
||||
export default { |
||||
name: "ContextMenu", |
||||
|
||||
components: {Item}, |
||||
|
||||
props: { |
||||
value: Boolean, |
||||
items: Array, |
||||
left: Number, |
||||
top: Number |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
realLeft: '0px', |
||||
realTop: '0px' |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
value(v) { |
||||
document.body[v ? 'addEventListener' : 'removeEventListener']('click', this.close) |
||||
v && this.$nextTick(this.autoAdapt) |
||||
}, |
||||
left(v) { |
||||
v && this.autoAdaptLeft(v) |
||||
}, |
||||
top(v) { |
||||
v && this.autoAdaptTop(v) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
close() { |
||||
this.$emit('input', false) |
||||
}, |
||||
autoAdapt() { |
||||
this.autoAdaptTop(this.top) |
||||
this.autoAdaptLeft(this.left) |
||||
}, |
||||
autoAdaptTop(v) { |
||||
if (!this.value) return |
||||
|
||||
const elOffsetHeight = this.$el.offsetHeight |
||||
|
||||
if (elOffsetHeight > document.body.clientHeight - v && v > elOffsetHeight) { |
||||
this.realTop = v - elOffsetHeight + 'px' |
||||
} |
||||
else this.realTop = v + 'px' |
||||
}, |
||||
autoAdaptLeft(v) { |
||||
if (!this.value) return |
||||
|
||||
const elOffsetWidth = this.$el.offsetWidth |
||||
|
||||
if (elOffsetWidth > document.body.clientWidth - v) { |
||||
this.realLeft = v - elOffsetWidth + 'px' |
||||
} |
||||
else this.realLeft = v + 'px' |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
if (!Array.isArray(this.items)) return |
||||
|
||||
const style = {left: this.realLeft, top: this.realTop} |
||||
|
||||
return ( |
||||
<ul v-show={this.value} class="context-menu" style={style}> |
||||
{this.items.map(i => ( |
||||
i && <item on-click={i.click}>{i.content}</item> |
||||
))} |
||||
</ul> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.context-menu { |
||||
margin: 0; |
||||
background-color: $menu-background-dark; |
||||
z-index: 10; |
||||
position: fixed; |
||||
list-style-type: none; |
||||
padding: 5px 0; |
||||
border-radius: 4px; |
||||
font-size: 12px; |
||||
font-weight: 400; |
||||
color: #ffffff; |
||||
box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3); |
||||
|
||||
&-item { |
||||
margin: 0; |
||||
padding: 7px 16px; |
||||
cursor: pointer; |
||||
|
||||
&:hover { |
||||
color: $--color-primary; |
||||
} |
||||
} |
||||
} |
||||
</style> |
||||
@ -1,175 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import cssVar from '@/asset/style/var.scss' |
||||
import Item from './item' |
||||
import {isEmpty, deepClone, trim} from "@/util" |
||||
|
||||
const inlineIndent = parseFloat(cssVar.menuPadding) |
||||
|
||||
export default { |
||||
name: 'NavMenu', |
||||
|
||||
props: { |
||||
//路由配置项组成的树形数组 |
||||
menus: Array, |
||||
|
||||
//垂直还是水平,在el-menu原效果上加了样式名 |
||||
mode: {type: String, default: 'vertical'}, |
||||
|
||||
//主题,light 或 dark |
||||
theme: {type: String, default: 'light'}, |
||||
|
||||
//是否折叠 |
||||
collapse: Boolean, |
||||
|
||||
//折叠时的展开菜单是否显示父级 |
||||
showParentOnCollapse: Boolean, |
||||
|
||||
//搜索词 |
||||
searchWord: String, |
||||
|
||||
//能够显示图标的最大深度,不传 或 <0 则不作限制 |
||||
showIconMaxDepth: {type: Number, default: 1}, |
||||
|
||||
//menus变化时是否使用过渡动画 |
||||
switchTransition: Boolean, |
||||
|
||||
//menus过渡动画名称 |
||||
switchTransitionName: String |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
//去除空格后的搜索词,不使用computed是为了第三方能直接响应式地修改数据 |
||||
realSearchWord: '' |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
//实际用于渲染的菜单数组 |
||||
realMenus() { |
||||
return isEmpty(this.realSearchWord) |
||||
? this.menus |
||||
: this.filterAfterSearch(deepClone(this.menus)) |
||||
}, |
||||
|
||||
//是否使用切换动画 |
||||
useSwitchTransition() { |
||||
return this.switchTransition && !isEmpty(this.switchTransitionName) |
||||
}, |
||||
|
||||
themeClass() { |
||||
return `el-menu--${this.theme}` |
||||
}, |
||||
menuClass() { |
||||
return `el-menu--${this.mode} ${this.themeClass}` |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
//搜索词去重 |
||||
searchWord(v) { |
||||
this.realSearchWord = trim(v) |
||||
}, |
||||
|
||||
//搜索词改变时,展开高亮菜单 |
||||
realSearchWord() { |
||||
this.$nextTick(this.expandAfterSearch) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//根据搜索词过滤菜单 |
||||
filterAfterSearch(menus) { |
||||
if (!menus) return undefined |
||||
|
||||
return menus.filter(menu => { |
||||
//如果匹配,那么其子节点无需再判断 |
||||
if (menu.meta.title.includes(this.realSearchWord)) { |
||||
return true |
||||
} |
||||
|
||||
const children = this.filterAfterSearch(menu.children) |
||||
|
||||
if (children) menu.children = children |
||||
|
||||
return children && children.length > 0 |
||||
}) |
||||
}, |
||||
|
||||
//根据查找结果展开菜单 |
||||
expandAfterSearch() { |
||||
const menu = this.$refs['el-menu'] |
||||
|
||||
//清空搜索词时还原原本展开的菜单 |
||||
if (isEmpty(this.realSearchWord)) { |
||||
menu.openedMenus = [] |
||||
return this.$nextTick(menu.initOpenedMenu) |
||||
} |
||||
|
||||
const {openedMenus, submenus} = menu |
||||
const expandMenus = [...new Set(this.getHighlightMenuParent(this.realMenus))] |
||||
|
||||
//不调用el-menu的open方法是为了避免uniqueOpened |
||||
for (const {fullPath} of expandMenus) { |
||||
const sub = submenus[fullPath] |
||||
|
||||
if (!sub || openedMenus.includes(sub.index)) continue |
||||
|
||||
sub.indexPath.forEach(i => { |
||||
!openedMenus.includes(i) && openedMenus.push(i) |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
//获取高亮菜单的上一级节点 |
||||
getHighlightMenuParent(children, parent) { |
||||
const result = [] |
||||
|
||||
children.forEach(child => { |
||||
if (child.meta.title.includes(this.realSearchWord)) { |
||||
parent && result.push(parent) |
||||
} |
||||
|
||||
if (child.children) { |
||||
result.push(...this.getHighlightMenuParent(child.children, child)) |
||||
} |
||||
}) |
||||
|
||||
return result |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
let items = this.realMenus.map(m => ( |
||||
<Item |
||||
menu={m} |
||||
popper-class={this.themeClass} |
||||
highlight={this.realSearchWord} |
||||
show-parent={this.showParentOnCollapse} |
||||
collapse={this.collapse} |
||||
inline-indent={inlineIndent} |
||||
show-icon-max-depth={this.showIconMaxDepth} |
||||
/> |
||||
)) |
||||
|
||||
if (this.useSwitchTransition) { |
||||
items = <transition-group name={this.switchTransitionName}>{items}</transition-group> |
||||
} |
||||
|
||||
return ( |
||||
<el-menu |
||||
ref="el-menu" |
||||
class={this.menuClass} |
||||
mode={this.mode} |
||||
collapse={this.collapse} |
||||
collapse-transition={false} |
||||
{...{attrs: this.$attrs, on: this.$listeners}} |
||||
> |
||||
{items} |
||||
</el-menu> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -1,154 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import {isEmpty} from "@/util" |
||||
|
||||
const MenuItemContent = { |
||||
functional: true, |
||||
|
||||
props: { |
||||
icon: String, |
||||
title: String, |
||||
highlight: String |
||||
}, |
||||
|
||||
render(h, context) { |
||||
const {icon, title, highlight} = context.props |
||||
const content = isEmpty(highlight) |
||||
? title |
||||
: getHighlightContent(h, title, highlight) |
||||
|
||||
return [<v-icon icon={icon}/>, <span slot="title" class="menu-item-content">{content}</span>] |
||||
} |
||||
} |
||||
|
||||
//获得高亮的菜单内容vnode |
||||
function getHighlightContent(h, title, highlight) { |
||||
const start = title.indexOf(highlight) |
||||
|
||||
if (start === -1) return title |
||||
|
||||
const end = start + highlight.length |
||||
|
||||
return [ |
||||
title.substring(0, start), |
||||
<span class="menu-highlight-result">{title.substring(start, end)}</span>, |
||||
title.substring(end) |
||||
] |
||||
} |
||||
|
||||
//根据showIconMaxDepth、depth判断是否需要限制图标的显示 |
||||
function getIcon({icon, showIconMaxDepth, depth}) { |
||||
if (showIconMaxDepth == null || showIconMaxDepth < 0) { |
||||
return icon |
||||
} |
||||
return showIconMaxDepth < depth ? undefined : icon |
||||
} |
||||
|
||||
//获取不需要嵌套展示的菜单 |
||||
function getOnlyChild(menu) { |
||||
const {children = [], meta: {alwaysShow} = {}} = menu |
||||
|
||||
if (!children.length) return {...menu, children: undefined} |
||||
|
||||
if (children.length === 1) return alwaysShow ? null : getOnlyChild(children[0]) |
||||
|
||||
return null |
||||
} |
||||
|
||||
function renderSingleMenu(h, {index, inlineIndent, icon, title, highlight}) { |
||||
return ( |
||||
<el-menu-item key={index} index={index} inline-indent={inlineIndent}> |
||||
<MenuItemContent icon={icon} title={title} highlight={highlight}/> |
||||
</el-menu-item> |
||||
) |
||||
} |
||||
|
||||
function renderSubMenu(h, {index, inlineIndent, icon, title, popperClass, highlight, children}) { |
||||
return ( |
||||
<el-submenu key={index} index={index} inline-indent={inlineIndent} popper-class={popperClass}> |
||||
<MenuItemContent slot="title" icon={icon} title={title} highlight={highlight}/> |
||||
{children} |
||||
</el-submenu> |
||||
) |
||||
} |
||||
|
||||
function renderChildrenWithParentMenu(h, {icon, title, children}) { |
||||
return [ |
||||
<div class="popover-menu__title el-menu-item"> |
||||
<MenuItemContent icon={icon} title={title}/> |
||||
</div>, |
||||
<div class="el-menu el-menu--inline">{children}</div> |
||||
] |
||||
} |
||||
|
||||
function renderMenu(h, props) { |
||||
const {menu, inlineIndent, popperClass, highlight, showParent, collapse, showIconMaxDepth, depth = 1} = props |
||||
|
||||
const onlyOneChild = getOnlyChild(menu) |
||||
|
||||
const showSingle = onlyOneChild && !onlyOneChild.children |
||||
|
||||
if (showSingle) { |
||||
const {fullPath, meta: {icon, title}} = onlyOneChild |
||||
return renderSingleMenu(h, { |
||||
index: fullPath, |
||||
inlineIndent, |
||||
icon: getIcon({icon, showIconMaxDepth, depth}), |
||||
title, |
||||
highlight |
||||
}) |
||||
} |
||||
|
||||
const {icon, title} = menu.meta |
||||
|
||||
let children = menu.children.map(child => renderMenu(h, {...props, menu: child, depth: depth + 1})) |
||||
|
||||
//这里弹出菜单如果包裹了<el-scrollbar>,则在触发mouseleave时,不会触发父菜单的mouseleave事件 |
||||
if (collapse) { |
||||
//弹出菜单显示父级信息 |
||||
if (showParent) { |
||||
children = renderChildrenWithParentMenu(h, {icon, title, children}) |
||||
} |
||||
} |
||||
|
||||
return renderSubMenu(h, { |
||||
index: menu.fullPath, |
||||
inlineIndent, |
||||
icon: getIcon({icon, showIconMaxDepth, depth}), |
||||
title, |
||||
popperClass, |
||||
highlight, |
||||
children |
||||
}) |
||||
} |
||||
|
||||
export default { |
||||
functional: true, |
||||
|
||||
props: { |
||||
//菜单对象,即路由配置项 |
||||
menu: Object, |
||||
|
||||
//弹出菜单的自定义类名 |
||||
popperClass: String, |
||||
|
||||
//需要高亮的字符串 |
||||
highlight: String, |
||||
|
||||
//折叠时的展开菜单是否显示父级 |
||||
showParent: Boolean, |
||||
|
||||
//向左缩进的单位距离 |
||||
inlineIndent: Number, |
||||
|
||||
//是否为折叠状态 |
||||
collapse: Boolean, |
||||
|
||||
//能够显示图标的最大深度,不传 或 <0 则不作限制 |
||||
showIconMaxDepth: Number |
||||
}, |
||||
|
||||
render(h, context) { |
||||
return renderMenu(h, context.props) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,109 +0,0 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.el-menu { |
||||
//图标的大小 |
||||
.icon { |
||||
font-size: $menu-icon-size; |
||||
} |
||||
|
||||
//图标与文字的间隔 |
||||
.icon + .menu-item-content { |
||||
margin-left: $menu-icon-text-gap; |
||||
} |
||||
|
||||
//菜单的高亮关键字 |
||||
.menu-highlight-result { |
||||
vertical-align: unset; |
||||
color: $--color-success; |
||||
} |
||||
} |
||||
|
||||
//原始样式使用 '>' 可能导致侧边栏折叠时还会显示右侧箭头图标 |
||||
//此样式只对垂直状态生效 |
||||
.el-menu--collapse .el-submenu .el-submenu__title .el-submenu__icon-arrow { |
||||
display: none; |
||||
} |
||||
|
||||
//hover时,菜单左侧图标放大(弹出菜单的父级不放大) |
||||
.el-menu-item:not(.popover-menu__title), |
||||
.el-submenu__title { |
||||
> .icon, |
||||
> .el-tooltip > .icon { |
||||
transition: transform .1s; |
||||
} |
||||
|
||||
&:hover > .icon, |
||||
&:hover > .el-tooltip > .icon { |
||||
transform: scale(1.2); |
||||
} |
||||
} |
||||
|
||||
//侧边栏垂直菜单 |
||||
.el-menu--vertical { |
||||
&.el-menu { |
||||
overflow-y: auto; |
||||
} |
||||
|
||||
&.el-menu, |
||||
.el-menu { |
||||
//菜单文字 |
||||
.el-menu-item, |
||||
.el-submenu__title { |
||||
> .menu-item-content { |
||||
text-overflow: ellipsis; |
||||
overflow: hidden; |
||||
//侧边栏宽度、菜单左右padding、左侧图标宽度、距离左右的距离、右侧图标宽度 |
||||
width: $aside-width - $menu-padding * 2 - $menu-icon-size - $menu-icon-text-gap * 2 - 12px; |
||||
} |
||||
} |
||||
|
||||
//弹出菜单的父级 |
||||
&.el-menu--popup .popover-menu__title { |
||||
cursor: auto; |
||||
border-bottom-width: 1px; |
||||
border-bottom-style: solid; |
||||
} |
||||
} |
||||
} |
||||
|
||||
//顶部水平菜单 |
||||
.el-menu--horizontal { |
||||
//顶部菜单的顶级节点 |
||||
&.el-menu { |
||||
//不显示折叠展开按钮 |
||||
> .el-submenu > .el-submenu__title > .el-submenu__icon-arrow { |
||||
display: none; |
||||
} |
||||
|
||||
//顶级节点的高度以及hover颜色 |
||||
> .el-menu-item, |
||||
> .el-submenu > .el-submenu__title { |
||||
height: $nav-height; |
||||
line-height: $nav-height; |
||||
|
||||
&:hover { |
||||
border-bottom: 2px solid $--color-primary; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&.el-menu, |
||||
.el-menu { |
||||
//菜单的文字以及hover颜色 |
||||
.el-menu-item, |
||||
.el-submenu__title { |
||||
> span { |
||||
max-width: 100px; |
||||
} |
||||
} |
||||
|
||||
//submenu的右侧箭头会稍微偏上,不懂为啥 |
||||
.el-submenu__icon-arrow { |
||||
margin-top: -4px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
|
||||
@import "./theme-light"; |
||||
@import "./theme-dark"; |
||||
@ -1,60 +0,0 @@ |
||||
.el-menu--dark { |
||||
//侧边栏垂直菜单 |
||||
&.el-menu--vertical { |
||||
&.el-menu, |
||||
.el-menu { |
||||
background-color: $menu-background-dark; |
||||
|
||||
//菜单的文字以及hover颜色 |
||||
.el-menu-item, |
||||
.el-submenu__title { |
||||
color: $menu-text-color-dark; |
||||
|
||||
&:hover { |
||||
color: $menu-text-hover-color-dark; |
||||
} |
||||
} |
||||
|
||||
//子级菜单的背景颜色 |
||||
&.el-menu--inline { |
||||
background-color: $sub-menu-background-dark; |
||||
} |
||||
|
||||
//子级菜单内阴影 |
||||
&.el-menu--inline { |
||||
box-shadow: inset 0 2px 8px darken($sub-menu-background-dark, 10%) |
||||
} |
||||
|
||||
//设置了显示弹出菜单的父级 |
||||
&.el-menu--popup .popover-menu__title { |
||||
color: $menu-text-hover-color-dark; |
||||
border-bottom-color: $--border-color-dark; |
||||
} |
||||
} |
||||
} |
||||
|
||||
//顶部水平菜单 |
||||
&.el-menu--horizontal { |
||||
&.el-menu, |
||||
.el-menu { |
||||
background-color: $root-menu-background-dark; |
||||
|
||||
//菜单的文字以及hover颜色 |
||||
.el-menu-item, |
||||
.el-submenu__title { |
||||
color: $menu-text-color-dark; |
||||
|
||||
&:hover { |
||||
color: $menu-text-hover-color-dark; |
||||
} |
||||
} |
||||
} |
||||
|
||||
&.el-menu > .el-menu-item, |
||||
&.el-menu > .el-submenu > .el-submenu__title, |
||||
&.el-menu > .el-menu-item:hover, |
||||
&.el-menu > .el-submenu > .el-submenu__title:hover { |
||||
border-bottom: none; |
||||
} |
||||
} |
||||
} |
||||
@ -1,39 +0,0 @@ |
||||
.el-menu--light { |
||||
//侧边栏垂直菜单 |
||||
&.el-menu--vertical { |
||||
&.el-menu, |
||||
.el-menu { |
||||
//菜单的文字以及hover颜色 |
||||
.el-menu-item, |
||||
.el-submenu__title { |
||||
color: $menu-text-color-light; |
||||
|
||||
&:hover { |
||||
color: $menu-text-hover-color-light; |
||||
} |
||||
} |
||||
|
||||
//弹出菜单的父级 |
||||
&.el-menu--popup .popover-menu__title { |
||||
color: $menu-text-hover-color-light; |
||||
border-bottom-color: $--border-color-light; |
||||
} |
||||
} |
||||
} |
||||
|
||||
//顶部水平菜单 |
||||
&.el-menu--horizontal { |
||||
&.el-menu, |
||||
.el-menu { |
||||
//菜单的文字以及hover颜色 |
||||
.el-menu-item, |
||||
.el-submenu__title { |
||||
color: $menu-text-color-light; |
||||
|
||||
&:hover { |
||||
color: $menu-text-hover-color-light; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -1,68 +0,0 @@ |
||||
import {isEmpty, timeFormat} from '@/util' |
||||
|
||||
//时间戳格式化
|
||||
export function timestamp2Date(timestamp) { |
||||
if (isEmpty(timestamp)) return '' |
||||
return timeFormat(null, new Date(timestamp)) |
||||
} |
||||
|
||||
//秒数格式化,最低时间为分钟
|
||||
export function secondFormat(time) { |
||||
if (!time || isNaN(time)) return ' NaN' |
||||
if (time < 3600) return ~~(time / 60) + ' 分' |
||||
if (time < 86400) return ~~(time / 3600) + ' 小时' |
||||
return ~~(time / 86400) + ' 天' |
||||
} |
||||
|
||||
//格式化当前和给定的时间戳之间的秒数差
|
||||
export function secondBetweenFormat(time) { |
||||
const between = Date.now() / 1000 - Number(time) |
||||
return secondFormat(between) |
||||
} |
||||
|
||||
/** |
||||
* 数字转存储单位,1024 => KB |
||||
* |
||||
* @param num {number} |
||||
* @param digits {number} 保留几位小数 |
||||
* @return {string} |
||||
*/ |
||||
export function number2StorageUnit(num, digits = 2) { |
||||
if (isEmpty(num)) return '' |
||||
const si = [ |
||||
{value: 1024 * 1024 * 1024 * 1024, symbol: 'TB'}, |
||||
{value: 1024 * 1024 * 1024, symbol: 'GB'}, |
||||
{value: 1024 * 1024, symbol: 'MB'}, |
||||
{value: 1024, symbol: 'KB'} |
||||
] |
||||
for (let i = 0; i < si.length; i++) { |
||||
if (num >= si[i].value) { |
||||
return (num / si[i].value).toFixed(digits).replace(/\.0+$|(\.[0-9]*[1-9])0+$/, '$1') + si[i].symbol |
||||
} |
||||
} |
||||
return num.toString() |
||||
} |
||||
|
||||
/** |
||||
* 10000 => "10,000" |
||||
* @param num {number} |
||||
*/ |
||||
export function toThousandFilter(num) { |
||||
if (isEmpty(num)) return '' |
||||
return (+num || 0).toString().replace(/^-?\d+/g, m => m.replace(/(?=(?!\b)(\d{3})+$)/g, ',')) |
||||
} |
||||
|
||||
/** |
||||
* Upper case first char |
||||
* @param string {string} |
||||
*/ |
||||
export function uppercaseFirst(string) { |
||||
if (isEmpty(string)) return '' |
||||
return string.charAt(0).toUpperCase() + string.slice(1) |
||||
} |
||||
|
||||
export default function (Vue) { |
||||
Object |
||||
.entries({timestamp2Date, secondFormat, secondBetweenFormat, number2StorageUnit, toThousandFilter, uppercaseFirst}) |
||||
.forEach(([key, value]) => Vue.filter(key, value)) |
||||
} |
||||
@ -1,56 +0,0 @@ |
||||
<template> |
||||
<div class="aside-search"> |
||||
<el-input |
||||
v-model="value" |
||||
size="mini" |
||||
clearable |
||||
placeholder="搜索菜单" |
||||
prefix-icon="el-icon-search" |
||||
@input="search" |
||||
/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import {debounce} from "@/util" |
||||
|
||||
export default { |
||||
name: "MenuSearch", |
||||
|
||||
data: () => ({value: ''}), |
||||
|
||||
methods: { |
||||
search(v) { |
||||
this.$emit('search', v) |
||||
} |
||||
}, |
||||
|
||||
created() { |
||||
this.search = debounce(this.search) |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
this.$emit('search', '') |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "~@/asset/style/var"; |
||||
|
||||
//侧边栏折叠时不显示 |
||||
.collapse .aside-search { |
||||
display: none; |
||||
} |
||||
|
||||
.aside-search { |
||||
margin: 12px 8px; |
||||
} |
||||
|
||||
//暗色主题 |
||||
.dark .aside-search .el-input__inner { |
||||
border: none; |
||||
color: $--color-white; |
||||
background-color: lighten($menu-background-dark, 5%); |
||||
} |
||||
</style> |
||||
@ -1,251 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import hamburgerMixin from '@/layout/mixin/hamburger' |
||||
import menuMixin from "@/layout/mixin/menu" |
||||
import menuSearchMixin from '@/layout/mixin/menuSearch' |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside" |
||||
import {getters as pageGetters} from "@/layout/store/page" |
||||
import Logo from '@/layout/component/Logo' |
||||
import NavMenu from '@/component/menu/NavMenu' |
||||
import {getSidebarMenus, getActiveMenuByRoute} from "@/layout/util" |
||||
import {moveToActiveMenuVertically} from "@/util/element-ui/elMenu" |
||||
|
||||
export default { |
||||
name: "Sidebar", |
||||
|
||||
mixins: [hamburgerMixin, menuMixin, menuSearchMixin], |
||||
|
||||
components: {Logo, NavMenu}, |
||||
|
||||
data() { |
||||
return { |
||||
//鼠标是否在侧边栏外 |
||||
mouseOutside: true |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
isMobile: () => appGetters.isMobile, |
||||
|
||||
//侧边栏菜单 |
||||
menus: () => getSidebarMenus(), |
||||
|
||||
//当是移动端或设置了侧边栏自动隐藏时将侧边栏用抽屉包裹 |
||||
renderInDrawer() { |
||||
return this.isMobile || asideGetters.autoHide |
||||
}, |
||||
//el-drawer的自定义类名 |
||||
drawerClass() { |
||||
return this.isMobile || pageGetters.position !== 'top-bottom' |
||||
? undefined |
||||
: 'drawer-behind-header' |
||||
}, |
||||
|
||||
//侧边栏的显隐状态,true显示、false隐藏 |
||||
show() { |
||||
//store中要显示那就显示 |
||||
if (asideGetters.show) return true |
||||
|
||||
//移动端,false |
||||
if (this.isMobile) return false |
||||
|
||||
//未设置侧边栏自动隐藏,false |
||||
if (!asideGetters.autoHide) return false |
||||
|
||||
//鼠标在侧边栏内,true |
||||
if (!this.mouseOutside) return true |
||||
|
||||
//侧边栏处于折叠状态 且 存在弹出的子菜单,true |
||||
return this.collapse && this.openedMenuNum > 0 |
||||
}, |
||||
|
||||
//侧边栏的折叠状态,true折叠、false展开,仅在pc端可折叠 |
||||
collapse() { |
||||
return asideGetters.collapse && !this.isMobile |
||||
}, |
||||
|
||||
//判断是否需要绑定鼠标移动的事件 |
||||
shouldBindMouseMoveEvent() { |
||||
//如果是移动端,false |
||||
if (this.isMobile) return false |
||||
|
||||
//如果未启用侧边栏自动隐藏,false |
||||
if (!asideGetters.autoHide) return false |
||||
|
||||
//侧边栏为打开状态,false |
||||
if (this.show) return false |
||||
|
||||
//鼠标在侧边栏外部,true |
||||
return this.mouseOutside |
||||
}, |
||||
|
||||
//是否需要显示logo |
||||
showLogo() { |
||||
return pageGetters.showLogo && (this.isMobile || pageGetters.position === 'left-right') |
||||
}, |
||||
|
||||
//是否需要使用侧边栏的切换动画 |
||||
switchTransition() { |
||||
return !this.isMobile && appGetters.navMode !== 'aside' |
||||
}, |
||||
|
||||
//是否需要显示搜索框 |
||||
renderMenuSearch() { |
||||
return !this.isMobile && asideGetters.search |
||||
}, |
||||
|
||||
className() { |
||||
return {'sidebar': true, 'collapse': this.collapse} |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
'$route.path': { |
||||
immediate: true, |
||||
handler(v) { |
||||
//如果是redirect跳转,则跳过 |
||||
if (v.startsWith('/redirect')) return |
||||
|
||||
this.activeMenu = getActiveMenuByRoute(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.renderInDrawer && this.$nextTick(this.moveToCurrentMenu) |
||||
} |
||||
}, |
||||
|
||||
//切换至移动端时收起侧边栏 |
||||
isMobile: { |
||||
immediate: true, |
||||
handler(v) { |
||||
v && asideMutations.close() |
||||
} |
||||
}, |
||||
|
||||
//抽屉模式下侧边栏显示时滚动至激活菜单 |
||||
show(v) { |
||||
v && this.$nextTick(this.moveToCurrentMenu) |
||||
}, |
||||
|
||||
//添加或移除鼠标移动事件 |
||||
shouldBindMouseMoveEvent: { |
||||
immediate: true, |
||||
handler(v) { |
||||
//尝试移除之前可能添加的事件 |
||||
window.removeEventListener('mousemove', this.moveEvent) |
||||
|
||||
v && window.addEventListener('mousemove', this.moveEvent) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//开启侧边栏自动隐藏后的鼠标移动事件 |
||||
moveEvent(e) { |
||||
//鼠标移动至屏幕左侧边缘时,标识鼠标在侧边栏内部 |
||||
if (e.clientX <= 1) this.mouseOutside = false |
||||
}, |
||||
|
||||
//模拟选中菜单 |
||||
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) |
||||
|
||||
//抽屉模式下需要关闭抽屉 |
||||
if (this.renderInDrawer && this.show) { |
||||
asideMutations.close() |
||||
this.mouseOutside = true |
||||
} |
||||
}, |
||||
|
||||
//渲染el-menu时监听其展开菜单 |
||||
watchOpenedMenus() { |
||||
//尝试取消之前设置的监听 |
||||
if (this.watchOpenedMenusCallback) { |
||||
this.watchOpenedMenusCallback() |
||||
this.watchOpenedMenusCallback = null |
||||
} |
||||
|
||||
const menu = this.$_getElMenuInstance() |
||||
if (!menu) return |
||||
|
||||
this.watchOpenedMenusCallback = menu.$watch('openedMenus', v => { |
||||
this.openedMenuNum = v.length |
||||
}) |
||||
}, |
||||
|
||||
//滚动至当前激活的菜单 |
||||
moveToCurrentMenu() { |
||||
moveToActiveMenuVertically(this.$_getElMenuInstance()) |
||||
} |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
window.removeEventListener('mousemove', this.moveEvent) |
||||
}, |
||||
|
||||
render() { |
||||
if (this.menus.length <= 0) return |
||||
|
||||
const aside = ( |
||||
<div |
||||
class={this.className} |
||||
on-mouseleave={() => this.mouseOutside = true} |
||||
on-mouseenter={() => this.mouseOutside = false} |
||||
> |
||||
{this.showLogo && <logo show-title={!this.collapse}/>} |
||||
|
||||
{this.renderMenuSearch && <menu-search on-search={this.handlerSearch}/>} |
||||
|
||||
<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={this.switchTransition} |
||||
switch-transition-name="sidebar" |
||||
on={{'select': this.onSelect, 'hook:mounted': this.watchOpenedMenus}} |
||||
/> |
||||
|
||||
{!this.renderInDrawer && this.renderHamburger && <hamburger class="el-menu-item"/>} |
||||
</div> |
||||
) |
||||
|
||||
if (this.renderInDrawer) { |
||||
return ( |
||||
<el-drawer |
||||
visible={this.show} |
||||
with-header={false} |
||||
custom-class={this.drawerClass} |
||||
modal={this.isMobile} //设置自动隐藏时不使用遮罩 |
||||
direction="ltr" |
||||
size="auto" |
||||
on-close={asideMutations.close} |
||||
> |
||||
{aside} |
||||
</el-drawer> |
||||
) |
||||
} |
||||
|
||||
return aside |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,134 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import menuMixin from "@/layout/mixin/menu" |
||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
||||
import {getters as asideGetters} from "@/layout/store/aside" |
||||
import {getters as pageGetters} from "@/layout/store/page" |
||||
import Logo from '@/layout/component/Logo' |
||||
import {scrollIntoViewVertically} from "@/util/browser" |
||||
|
||||
const Item = { |
||||
functional: true, |
||||
|
||||
props: { |
||||
title: String, |
||||
icon: String, |
||||
active: Boolean |
||||
}, |
||||
|
||||
render(h, context) { |
||||
const {title, icon, active} = context.props |
||||
const {click} = context.listeners |
||||
|
||||
return ( |
||||
<li |
||||
class={{'el-menu-item': true, 'is-active': active}} |
||||
on-click={click} |
||||
> |
||||
<v-icon icon={icon}/> |
||||
<span class="menu-item-content">{title}</span> |
||||
</li> |
||||
) |
||||
} |
||||
} |
||||
|
||||
export default { |
||||
name: "RootSidebar", |
||||
|
||||
mixins: [menuMixin], |
||||
|
||||
components: {Logo, Item}, |
||||
|
||||
data() { |
||||
return { |
||||
//鼠标是否在外部,用于判断是否需要展开折叠 |
||||
mouseOutside: true |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
menus() { |
||||
return appGetters.menus.map(root => ({...root, children: undefined})) |
||||
}, |
||||
|
||||
//是否需要显示logo |
||||
showLogo() { |
||||
return pageGetters.showLogo && pageGetters.position === 'left-right' |
||||
}, |
||||
|
||||
containerClass() { |
||||
return {'root-sidebar': true, 'collapse': this.mouseOutside} |
||||
}, |
||||
|
||||
menuClass() { |
||||
return `el-menu el-menu--vertical el-menu--${asideGetters.theme}` |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
//路由变化时设置高亮菜单 |
||||
$route: { |
||||
immediate: true, |
||||
handler(to) { |
||||
const {path, matched} = to |
||||
|
||||
//使用/redirect跳转 或 无匹配路由 时跳过 |
||||
if (path.startsWith('/redirect') || matched.length === 0) { |
||||
return |
||||
} |
||||
|
||||
this.setActiveRootMenu(this.navMode, to) |
||||
|
||||
//滚动至激活菜单 |
||||
if (!this._isMounted) return |
||||
this.$nextTick(() => { |
||||
scrollIntoViewVertically( |
||||
this.$el.querySelector('.el-menu-item.is-active'), |
||||
this.$el.querySelector('.el-menu.el-menu--vertical') |
||||
) |
||||
}) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//设置当前激活的顶部菜单 |
||||
setActiveRootMenu(navMode = this.navMode, {matched} = this.$route) { |
||||
const rootPath = matched[0].path || '/' |
||||
appMutations.activeRootMenu(rootPath) |
||||
}, |
||||
onSelect(index) { |
||||
//点击相同菜单不刷新页面 |
||||
if (index !== appGetters.activeRootMenu) { |
||||
this.actionOnSelectMenu(index) |
||||
} |
||||
|
||||
//收起 |
||||
this.mouseOutside = true |
||||
}, |
||||
}, |
||||
|
||||
render() { |
||||
return ( |
||||
<div class="root-sidebar-container"> |
||||
<div |
||||
class={this.containerClass} |
||||
on-mouseleave={() => this.mouseOutside = true} |
||||
on-mouseenter={() => this.mouseOutside = false} |
||||
> |
||||
{this.showLogo && <logo show-title={!this.mouseOutside}/>} |
||||
<ul class={this.menuClass}> |
||||
{this.menus.map(({fullPath, meta: {title, icon}}) => ( |
||||
<item |
||||
title={title} |
||||
icon={icon} |
||||
active={fullPath === appGetters.activeRootMenu} |
||||
on-click={() => this.onSelect(fullPath)} |
||||
/> |
||||
))} |
||||
</ul> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,110 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import hamburgerMixin from '@/layout/mixin/hamburger' |
||||
import menuMixin from "@/layout/mixin/menu" |
||||
import menuSearchMixin from '@/layout/mixin/menuSearch' |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
import {getters as asideGetters} from "@/layout/store/aside" |
||||
import NavMenu from "@/component/menu/NavMenu" |
||||
import {getActiveMenuByRoute, getSidebarMenus} from "@/layout/util" |
||||
import {moveToActiveMenuVertically} from "@/util/element-ui/elMenu" |
||||
|
||||
export default { |
||||
name: "SubSidebar", |
||||
|
||||
mixins: [hamburgerMixin, menuMixin, menuSearchMixin], |
||||
|
||||
components: {NavMenu}, |
||||
|
||||
computed: { |
||||
//父级菜单 |
||||
rootMenu() { |
||||
const active = appGetters.activeRootMenu |
||||
return appGetters.menus.find(i => i.fullPath === active) |
||||
}, |
||||
|
||||
//侧边栏菜单 |
||||
menus: () => getSidebarMenus(), |
||||
|
||||
//侧边栏的折叠状态,true折叠、false展开,仅在pc端可折叠 |
||||
collapse() { |
||||
return asideGetters.collapse && !asideGetters.isMobile |
||||
}, |
||||
|
||||
className() { |
||||
return {'sub-sidebar': true, 'collapse': this.collapse} |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
'$route.path': { |
||||
immediate: true, |
||||
handler(v) { |
||||
//如果是redirect跳转,则跳过 |
||||
if (v.startsWith('/redirect')) return |
||||
|
||||
this.activeMenu = getActiveMenuByRoute(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.moveToCurrentMenu) |
||||
} |
||||
} |
||||
}, |
||||
|
||||
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) |
||||
}, |
||||
|
||||
//滚动至当前激活的菜单 |
||||
moveToCurrentMenu() { |
||||
moveToActiveMenuVertically(this.$_getElMenuInstance()) |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
if (this.menus.length <= 0) return |
||||
|
||||
return ( |
||||
<div class={this.className}> |
||||
<div class="sub-sidebar-title"> |
||||
{this.rootMenu && this.rootMenu.meta.title} |
||||
</div> |
||||
|
||||
{asideGetters.search && <menu-search on-search={this.handlerSearch}/>} |
||||
|
||||
<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" |
||||
on-select={this.onSelect} |
||||
/> |
||||
|
||||
{this.renderHamburger && <hamburger class="el-menu-item"/>} |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,34 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import OnePart from './component/OnePartRoot' |
||||
import TwoPartRoot from './component/TwoPartRoot' |
||||
import TwoPartSub from './component/TwoPartSub' |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
import {getters as asideGetters} from "@/layout/store/aside" |
||||
|
||||
export default { |
||||
functional: true, |
||||
|
||||
render() { |
||||
let children |
||||
|
||||
//移动端只能使用抽屉模式的单层侧边栏 |
||||
if (appGetters.isMobile) { |
||||
children = <OnePart/> |
||||
} |
||||
else { |
||||
switch (appGetters.navMode) { |
||||
case 'mix': |
||||
case 'aside': |
||||
children = <OnePart/> |
||||
break |
||||
case 'aside-two-part': |
||||
children = [<TwoPartRoot/>, <TwoPartSub/>] |
||||
} |
||||
} |
||||
|
||||
return <aside class={`aside ${asideGetters.theme}`}>{children}</aside> |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -1,101 +0,0 @@ |
||||
@import "~@/asset/style/var"; |
||||
@import "~@/asset/style/mixin.scss"; |
||||
|
||||
.aside { |
||||
display: flex; |
||||
z-index: 11; |
||||
|
||||
//移动端下侧边栏抽屉不能设置z-index,否则会被el-drawer的遮罩遮住 |
||||
@include mobile { |
||||
z-index: auto !important; |
||||
} |
||||
|
||||
//单层和双层侧边栏 |
||||
.sidebar, |
||||
.root-sidebar, |
||||
.sub-sidebar { |
||||
display: flex; |
||||
flex-direction: column; |
||||
height: 100%; |
||||
width: $aside-width; |
||||
box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35); |
||||
transition: all 0.2s; |
||||
|
||||
//折叠状态 |
||||
&.collapse { |
||||
width: $aside-collapse-width; |
||||
|
||||
//隐藏菜单文字 |
||||
.menu-item-content, |
||||
.sub-sidebar-title { |
||||
display: none; |
||||
} |
||||
} |
||||
} |
||||
|
||||
//垂直菜单占满剩余空间 |
||||
.el-menu--vertical { |
||||
flex: 1; |
||||
} |
||||
|
||||
//底部汉堡包 |
||||
.hamburger { |
||||
border-top-width: 1px; |
||||
border-top-style: solid; |
||||
} |
||||
} |
||||
|
||||
//页面为上下结构时,侧边栏的抽屉模式需要扣除导航栏的高度 |
||||
.el-drawer.drawer-behind-header { |
||||
top: $nav-height; |
||||
} |
||||
|
||||
|
||||
/* 双侧侧边栏开始 */ |
||||
|
||||
.root-sidebar-container { |
||||
position: relative; |
||||
width: $aside-collapse-width; |
||||
height: 100%; |
||||
} |
||||
|
||||
.root-sidebar { |
||||
position: absolute; |
||||
overflow: hidden; |
||||
z-index: 1; |
||||
box-shadow: none !important; |
||||
border-right-width: 1px; |
||||
border-right-style: solid; |
||||
} |
||||
|
||||
.sub-sidebar-title { |
||||
height: $nav-height; |
||||
line-height: $nav-height; |
||||
padding-left: $menu-padding; |
||||
font-size: 16px; |
||||
} |
||||
|
||||
/* 双侧侧边栏结束 */ |
||||
|
||||
|
||||
/* 侧边栏切换动画开始 */ |
||||
|
||||
.sidebar-enter-active { |
||||
transition: all 0.2s; |
||||
} |
||||
|
||||
.sidebar-enter, |
||||
.sidebar-leave-active { |
||||
opacity: 0; |
||||
transform: translateY(30px) skewY(10deg) |
||||
} |
||||
|
||||
.sidebar-leave-active { |
||||
position: absolute; |
||||
} |
||||
|
||||
/* 侧边栏切换动画结束 */ |
||||
|
||||
|
||||
@import "theme-light"; |
||||
@import "theme-dark"; |
||||
@ -1,27 +0,0 @@ |
||||
.aside.dark { |
||||
&, |
||||
> .el-drawer__wrapper .el-drawer__body { |
||||
background-color: $menu-background-dark; |
||||
} |
||||
|
||||
.root-sidebar, |
||||
.root-sidebar > .el-menu--vertical.el-menu { |
||||
background-color: $root-menu-background-dark; |
||||
} |
||||
|
||||
.root-sidebar { |
||||
border-right-color: $--border-color-dark; |
||||
} |
||||
|
||||
.sub-sidebar-title { |
||||
color: $--color-white; |
||||
} |
||||
|
||||
.hamburger { |
||||
border-top-color: $--border-color-dark; |
||||
|
||||
i { |
||||
color: $menu-text-color-dark; |
||||
} |
||||
} |
||||
} |
||||
@ -1,14 +0,0 @@ |
||||
.aside.light { |
||||
.root-sidebar { |
||||
background-color: $--color-white; |
||||
border-right-color: $--border-color-light; |
||||
} |
||||
|
||||
.hamburger { |
||||
border-top-color: $--border-color-light; |
||||
|
||||
i { |
||||
color: $menu-text-color-light; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<template functional> |
||||
<div class="copyright"> |
||||
Copyright © 2020 - <a href="https://github.com/toesbieya" target="_blank">toesbieya</a> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: "Footer" |
||||
} |
||||
</script> |
||||
@ -0,0 +1,9 @@ |
||||
.page-footer > .copyright { |
||||
color: $--color-text-secondary; |
||||
font-size: 14px; |
||||
|
||||
> a:hover { |
||||
color: $--color-primary; |
||||
text-decoration: underline; |
||||
} |
||||
} |
||||
@ -1,30 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
import {getters as asideGetters, mutations as asideMutations} from "@/layout/store/aside" |
||||
|
||||
export default { |
||||
name: 'Hamburger', |
||||
|
||||
render() { |
||||
//考虑侧边栏可能是抽屉的情况 |
||||
const collapsed = appGetters.isMobile ? asideGetters.show : !asideGetters.collapse |
||||
const className = `hamburger ${collapsed ? 'collapsed' : ''}` |
||||
|
||||
return ( |
||||
<div |
||||
class={className} |
||||
title={`${collapsed ? '折叠' : '展开'}侧边栏`} |
||||
on-click={asideMutations.switch} |
||||
> |
||||
<i class="el-icon-s-unfold"/> |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style> |
||||
.hamburger.collapsed > i { |
||||
transform: rotate(180deg); |
||||
} |
||||
</style> |
||||
@ -1,72 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import {title} from '@/config' |
||||
|
||||
export default { |
||||
name: 'Logo', |
||||
|
||||
functional: true, |
||||
|
||||
props: {showTitle: Boolean}, |
||||
|
||||
render(h, context) { |
||||
const {showTitle} = context.props |
||||
|
||||
const logoClass = {'logo-container': true, 'no-title': !showTitle} |
||||
|
||||
return ( |
||||
<div class={logoClass}> |
||||
<router-link class="logo-link" to="/"> |
||||
<img src={`${process.env.BASE_URL}static/img/logo.svg`}/> |
||||
{showTitle && <h1>{title}</h1>} |
||||
</router-link> |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "~@/asset/style/var"; |
||||
|
||||
$logoSize: 30px; |
||||
|
||||
.logo-container { |
||||
width: 100%; |
||||
height: $nav-height; |
||||
line-height: $nav-height; |
||||
padding: 0 $menu-padding; |
||||
|
||||
.logo-link { |
||||
display: block; |
||||
height: 100%; |
||||
|
||||
> img { |
||||
display: inline-block; |
||||
height: $logoSize; |
||||
vertical-align: middle; |
||||
|
||||
& + h1 { |
||||
margin-left: 12px; |
||||
} |
||||
} |
||||
|
||||
> h1 { |
||||
display: inline-block; |
||||
margin: 0; |
||||
color: $--color-primary; |
||||
font-weight: 600; |
||||
line-height: $nav-height; |
||||
font-size: 18px; |
||||
vertical-align: middle; |
||||
} |
||||
} |
||||
|
||||
&.no-title { |
||||
padding-left: ($aside-collapse-width - $logoSize) / 2; |
||||
} |
||||
} |
||||
|
||||
.dark .logo-container .logo-link > h1 { |
||||
color: #ffffff; |
||||
} |
||||
</style> |
||||
@ -1,41 +0,0 @@ |
||||
<template> |
||||
<el-badge :hidden="hidden" class="bell-badge" is-dot title="消息提醒" @click.native="jump"> |
||||
<i class="el-icon-bell navbar-icon"/> |
||||
</el-badge> |
||||
</template> |
||||
|
||||
<script> |
||||
import {refreshPage} from "@/util/route" |
||||
|
||||
export default { |
||||
name: "Bell", |
||||
|
||||
computed: { |
||||
hidden() { |
||||
return this.$store.state.message.unreadCount < 1 |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
jump() { |
||||
const target = '/message/user' |
||||
|
||||
this.$route.path === target |
||||
? refreshPage() |
||||
: this.$router.push(target) |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
this.$store.dispatch('message/refresh') |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style> |
||||
.bell-badge .el-badge__content { |
||||
top: 15px !important; |
||||
right: 10px !important; |
||||
border: none; |
||||
} |
||||
</style> |
||||
@ -1,210 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
/** |
||||
* 顶部菜单,参考了ant design的响应式设计 |
||||
*/ |
||||
import menuMixin from "@/layout/mixin/menu" |
||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
||||
import NavMenu from "@/component/menu/NavMenu" |
||||
import NavMenuItem from "@/component/menu/NavMenu/item" |
||||
import {getActiveMenuByRoute} from "@/layout/util" |
||||
import {isDom} from "@/util/browser" |
||||
|
||||
export default { |
||||
name: "HeadMenu", |
||||
|
||||
mixins: [menuMixin], |
||||
|
||||
components: {NavMenu, NavMenuItem}, |
||||
|
||||
props: { |
||||
//是否在只有一个顶部菜单时仍然渲染 |
||||
alwaysShow: Boolean, |
||||
|
||||
//主题由父组件控制 |
||||
theme: String |
||||
}, |
||||
|
||||
data() { |
||||
return { |
||||
//最后一个不被隐藏的顶部菜单的数组下标 |
||||
//为undefined时,说明不需要隐藏菜单,为-1时,说明需要隐藏全部菜单 |
||||
lastVisibleIndex: undefined, |
||||
|
||||
//用于生成el-submenu的key |
||||
seed: 0 |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
navMode: () => appGetters.navMode, |
||||
|
||||
menus() { |
||||
if (appGetters.isMobile) return [] |
||||
switch (this.navMode) { |
||||
case 'aside': |
||||
return [] |
||||
case 'head' : |
||||
return appGetters.menus |
||||
case 'mix': |
||||
return appGetters.menus.map(root => ({...root, children: undefined})) |
||||
} |
||||
}, |
||||
|
||||
//实际用于渲染的菜单数组 |
||||
realMenus() { |
||||
const {lastVisibleIndex, menus} = this |
||||
|
||||
//不需要隐藏菜单 |
||||
if (lastVisibleIndex === undefined) { |
||||
return menus |
||||
} |
||||
|
||||
const fullPath = `_${this.seed}` |
||||
|
||||
//隐藏全部菜单 |
||||
if (lastVisibleIndex === -1) { |
||||
return [{fullPath, meta: {icon: 'el-icon-menu'}, children: menus}] |
||||
} |
||||
|
||||
const visible = menus.slice(0, lastVisibleIndex + 1) |
||||
const hidden = menus.slice(lastVisibleIndex + 1) |
||||
|
||||
visible.push({fullPath, meta: {title: '...'}, children: hidden}) |
||||
|
||||
return visible |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
//路由变化时设置高亮菜单 |
||||
$route: { |
||||
immediate: true, |
||||
handler(to) { |
||||
const {path, matched} = to |
||||
//使用/redirect跳转 或 无匹配路由 时跳过 |
||||
if (path.startsWith('/redirect') || matched.length === 0) { |
||||
return |
||||
} |
||||
this.setActiveMenu(this.navMode, to) |
||||
} |
||||
}, |
||||
|
||||
//切换导航模式时重新设置高亮菜单 |
||||
navMode: { |
||||
immediate: true, |
||||
handler(mode) { |
||||
this.setActiveMenu(mode) |
||||
} |
||||
}, |
||||
|
||||
//变动时修改种子,避免组件不更新 |
||||
lastVisibleIndex() { |
||||
this.seed++ |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
setActiveMenu(navMode = this.navMode, {path, meta, matched} = this.$route) { |
||||
//设置当前激活的顶部菜单 |
||||
const rootPath = matched[0].path || '/' |
||||
appMutations.activeRootMenu(rootPath) |
||||
|
||||
//只有在混合导航模式下才将当前激活的顶部菜单认为是当前菜单 |
||||
if (navMode === 'mix') { |
||||
this.activeMenu = rootPath |
||||
} |
||||
//否则按照路由配置项设置 |
||||
else this.activeMenu = getActiveMenuByRoute({path, meta}) |
||||
}, |
||||
onSelect(index) { |
||||
//混合导航模式下,点击相同菜单不刷新页面 |
||||
if (this.navMode === 'mix' && index === appGetters.activeRootMenu) { |
||||
return |
||||
} |
||||
this.actionOnSelectMenu(index) |
||||
}, |
||||
|
||||
//获取el-menu的dom |
||||
getMenuEl() { |
||||
return this.$el |
||||
}, |
||||
//获取初始菜单的总宽度,只在mounted时调用一次 |
||||
setChildrenWidth() { |
||||
const ul = this.getMenuEl() |
||||
if (!isDom(ul)) return |
||||
|
||||
const menuItemNodes = ul.children |
||||
if (!menuItemNodes || menuItemNodes.length === 0) return |
||||
|
||||
//'更多'菜单的宽度,由于不考虑自定义,所以直接写死 |
||||
this.overflowedIndicatorWidth = 50 |
||||
this.menuItemSizes = Array.from(menuItemNodes).map(i => i.getBoundingClientRect().width) |
||||
this.originalTotalWidth = this.menuItemSizes.reduce((acc, cur) => acc + cur, 0) |
||||
}, |
||||
|
||||
resize() { |
||||
const width = this.getMenuEl().getBoundingClientRect().width |
||||
|
||||
let lastVisibleIndex = undefined |
||||
|
||||
//如果初始菜单的总宽度超出容器宽度 |
||||
if (this.originalTotalWidth > width) { |
||||
lastVisibleIndex = -1 |
||||
|
||||
//得到满足总宽度不超出容器宽度的最大菜单下标 |
||||
let currentSumWidth = 0 |
||||
for (const liWidth of this.menuItemSizes) { |
||||
currentSumWidth += liWidth |
||||
if (currentSumWidth + this.overflowedIndicatorWidth > width) { |
||||
break |
||||
} |
||||
lastVisibleIndex += 1 |
||||
} |
||||
} |
||||
|
||||
this.lastVisibleIndex = lastVisibleIndex |
||||
}, |
||||
createResizeObserver() { |
||||
//菜单未渲染时,移除之前的observer |
||||
if (!isDom(this.getMenuEl())) { |
||||
if (this.resizeObserver) { |
||||
this.resizeObserver.disconnect() |
||||
this.resizeObserver = null |
||||
} |
||||
return |
||||
} |
||||
|
||||
//如果已创建observer,则返回 |
||||
if (this.resizeObserver) return |
||||
|
||||
this.resizeObserver = new ResizeObserver(this.resize) |
||||
this.resizeObserver.observe(this.getMenuEl()) |
||||
|
||||
this.$once('hook:beforeDestroy', () => { |
||||
this.resizeObserver && this.resizeObserver.disconnect() |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
this.setChildrenWidth() |
||||
this.createResizeObserver() |
||||
}, |
||||
|
||||
render() { |
||||
if (this.menus.length <= 0 || this.menus.length === 1 && !this.alwaysShow) { |
||||
return |
||||
} |
||||
|
||||
return ( |
||||
<nav-menu |
||||
menus={this.realMenus} |
||||
mode="horizontal" |
||||
theme={this.theme} |
||||
default-active={this.activeMenu} |
||||
on-select={this.onSelect} |
||||
/> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,111 +0,0 @@ |
||||
<script type="jsx"> |
||||
import {mapState} from 'vuex' |
||||
import hamburgerMixin from '@/layout/mixin/hamburger' |
||||
import HeadMenu from "./component/HeadMenu" |
||||
import Logo from "@/layout/component/Logo" |
||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
||||
import {getters as navbarGetters} from "@/layout/store/navbar" |
||||
import {getters as pageGetters} from "@/layout/store/page" |
||||
import {elConfirm} from "@/util/message" |
||||
import {refreshPage} from "@/util/route" |
||||
|
||||
export default { |
||||
name: 'navbar', |
||||
|
||||
mixins: [hamburgerMixin], |
||||
|
||||
components: {HeadMenu, Logo}, |
||||
|
||||
computed: { |
||||
...mapState('user', ['avatar', 'name', 'prepareLogout']), |
||||
|
||||
//渲染导航栏logo的条件 |
||||
//①桌面端 |
||||
//②设置了显示logo |
||||
//③导航模式为顶部导航或页面为上下结构 |
||||
renderLogo() { |
||||
return !appGetters.isMobile |
||||
&& pageGetters.showLogo |
||||
&& (appGetters.navMode === 'head' || pageGetters.position === 'top-bottom') |
||||
}, |
||||
|
||||
//渲染顶部导航菜单的条件 |
||||
//①桌面端 |
||||
//②导航模式为顶部导航或混合导航 |
||||
renderHeadMenu() { |
||||
return !appGetters.isMobile && ['head', 'mix'].includes(appGetters.navMode) |
||||
}, |
||||
|
||||
className() { |
||||
return `navbar ${navbarGetters.theme}` |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
command(command) { |
||||
if (!this.prepareLogout) { |
||||
elConfirm('确认退出?') |
||||
.then(() => this.$store.dispatch('user/logout')) |
||||
} |
||||
}, |
||||
|
||||
clickRefreshBtn() { |
||||
refreshPage() |
||||
}, |
||||
|
||||
openSettingDrawer() { |
||||
appMutations.showSettingDrawer(true) |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
return ( |
||||
<nav class={this.className}> |
||||
{this.renderLogo && <logo show-title/>} |
||||
|
||||
{this.renderHamburger && <hamburger class="navbar-item navbar-icon"/>} |
||||
|
||||
<div style="flex: 1"> |
||||
{this.renderHeadMenu && <head-menu always-show theme={navbarGetters.theme}/>} |
||||
</div> |
||||
|
||||
<div> |
||||
<div |
||||
class="navbar-item" |
||||
title="刷新" |
||||
on-click={this.clickRefreshBtn} |
||||
> |
||||
<i class="el-icon-refresh-right navbar-icon"/> |
||||
</div> |
||||
|
||||
<div |
||||
class="setting-btn navbar-item" |
||||
title="个性设置" |
||||
on-click={this.openSettingDrawer} |
||||
> |
||||
<i class="el-icon-s-operation navbar-icon"/> |
||||
</div> |
||||
|
||||
<el-dropdown class="navbar-item" on-command={this.command}> |
||||
<div class="avatar-wrapper"> |
||||
<el-avatar size={30} src={this.avatar} icon="el-icon-user-solid"/> |
||||
<span class="hidden-xs">{this.name}</span> |
||||
</div> |
||||
<el-dropdown-menu |
||||
slot="dropdown" |
||||
class={navbarGetters.theme} |
||||
visible-arrow={false} |
||||
> |
||||
<el-dropdown-item icon="el-icon-switch-button" command="logout"> |
||||
退出登录 |
||||
</el-dropdown-item> |
||||
</el-dropdown-menu> |
||||
</el-dropdown> |
||||
</div> |
||||
</nav> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -1,50 +0,0 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.navbar { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
flex-wrap: nowrap; |
||||
height: $nav-height; |
||||
padding-left: $navbar-padding-left; |
||||
z-index: 10; |
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, .08); |
||||
transition: all .2s; |
||||
|
||||
> div { |
||||
display: flex; |
||||
flex-wrap: nowrap; |
||||
height: 100%; |
||||
} |
||||
|
||||
.navbar-icon { |
||||
font-size: 18px; |
||||
} |
||||
|
||||
.navbar-item { |
||||
display: flex; |
||||
align-items: center; |
||||
padding: 0 8px; |
||||
cursor: pointer; |
||||
} |
||||
|
||||
.logo-container { |
||||
width: auto; |
||||
min-width: $aside-width - $navbar-padding-left; |
||||
padding-left: $menu-padding - $navbar-padding-left; |
||||
} |
||||
|
||||
.avatar-wrapper { |
||||
display: flex; |
||||
align-items: center; |
||||
height: 100%; |
||||
|
||||
span { |
||||
margin-right: 5px; |
||||
font-size: 18px; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@import "theme-light"; |
||||
@import "theme-dark"; |
||||
@ -1,27 +0,0 @@ |
||||
.navbar.dark { |
||||
background-color: $root-menu-background-dark; |
||||
|
||||
.navbar-item { |
||||
color: $navbar-item-color-dark; |
||||
|
||||
&:hover { |
||||
background-color: $navbar-item-hover-color-dark; |
||||
} |
||||
} |
||||
} |
||||
|
||||
//暗色主题的下拉菜单 |
||||
.dark.el-dropdown-menu { |
||||
background-color: $root-menu-background-dark; |
||||
border: none; |
||||
|
||||
.el-dropdown-menu__item { |
||||
color: $menu-text-color-dark; |
||||
|
||||
&:hover, |
||||
&:focus { |
||||
background-color: unset; |
||||
color: $menu-text-hover-color-dark; |
||||
} |
||||
} |
||||
} |
||||
@ -1,11 +0,0 @@ |
||||
.navbar.light { |
||||
background-color: $--color-white; |
||||
|
||||
.navbar-item { |
||||
color: $navbar-item-color-light; |
||||
|
||||
&:hover { |
||||
background-color: $navbar-item-hover-color-light; |
||||
} |
||||
} |
||||
} |
||||
@ -1,13 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
export default { |
||||
functional: true, |
||||
|
||||
render() { |
||||
return ( |
||||
<el-backtop target=".page-main .scroll-container" visibility-height={400} bottom={66}> |
||||
<v-icon icon="el-icon-top"/> |
||||
</el-backtop> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,92 +0,0 @@ |
||||
<script> |
||||
import {isEmpty} from "@/util" |
||||
import {getRouterViewCacheKey} from "@/layout/util" |
||||
|
||||
const KEY = '_routerViewKey' |
||||
|
||||
function getFirstComponentChild(children) { |
||||
if (Array.isArray(children)) { |
||||
return children.find(c => !isEmpty(c) && (!isEmpty(c.componentOptions) || c.isComment && c.asyncFactory)) |
||||
} |
||||
} |
||||
|
||||
function removeCache(cache, key) { |
||||
const cached = cache[key] |
||||
cached && cached.componentInstance && cached.componentInstance.$destroy() |
||||
delete cache[key] |
||||
} |
||||
|
||||
function getCacheKey(route, componentOptions) { |
||||
if (KEY in componentOptions) return componentOptions[KEY] |
||||
|
||||
const key = getRouterViewCacheKey(route) |
||||
|
||||
if (key) componentOptions[KEY] = key |
||||
|
||||
return key |
||||
} |
||||
|
||||
export default { |
||||
name: 'keep-router-view-alive', |
||||
|
||||
abstract: true, |
||||
|
||||
props: {include: Array}, |
||||
|
||||
watch: { |
||||
include: { |
||||
deep: true, |
||||
handler(val) { |
||||
const {cache, $route} = this |
||||
for (const [k, v] of Object.entries(cache)) { |
||||
const cacheKey = getCacheKey($route, v.componentOptions) |
||||
if (!val || !val.includes(cacheKey)) { |
||||
removeCache(cache, k) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
}, |
||||
|
||||
created() { |
||||
this.cache = Object.create(null) |
||||
}, |
||||
|
||||
beforeDestroy() { |
||||
for (const key of Object.keys(this.cache)) { |
||||
removeCache(this.cache, key) |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
const slot = this.$slots.default |
||||
const vnode = getFirstComponentChild(slot) |
||||
|
||||
let componentOptions = vnode && vnode.componentOptions |
||||
|
||||
if (componentOptions) { |
||||
//忽略<transition/>组件 |
||||
const child = componentOptions.tag === 'transition' ? componentOptions.children[0] : vnode |
||||
componentOptions = child && child.componentOptions |
||||
|
||||
if (componentOptions) { |
||||
const key = getCacheKey(this.$route, componentOptions) |
||||
const {include, cache} = this |
||||
|
||||
if (include && !include.includes(key)) { |
||||
return vnode |
||||
} |
||||
|
||||
if (cache[key]) { |
||||
child.componentInstance = cache[key].componentInstance |
||||
} |
||||
else cache[key] = child |
||||
|
||||
child.data.keepAlive = true |
||||
} |
||||
} |
||||
|
||||
return vnode || (slot && slot[0]) |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,28 +0,0 @@ |
||||
<template functional> |
||||
<footer class="page-footer"> |
||||
<div class="copyright"> |
||||
Copyright © 2020 - <a href="https://github.com/toesbieya" target="_blank">toesbieya</a> |
||||
</div> |
||||
</footer> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: "PageFooter" |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.page-footer { |
||||
height: $page-footer-height; |
||||
padding: $page-view-margin; |
||||
text-align: center; |
||||
|
||||
.copyright { |
||||
color: #808695; |
||||
font-size: 14px; |
||||
} |
||||
} |
||||
</style> |
||||
@ -1,65 +0,0 @@ |
||||
<template> |
||||
<div class="page-header"> |
||||
<span class="page-header-title">{{ title }}</span> |
||||
|
||||
<el-breadcrumb> |
||||
<el-breadcrumb-item v-for="(item,index) in data" :key="item.path"> |
||||
<span :class="{'no-redirect': index !== data.length - 1}">{{ item.meta.title }}</span> |
||||
</el-breadcrumb-item> |
||||
</el-breadcrumb> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: "PageHeader", |
||||
|
||||
data: () => ({data: [], title: ''}), |
||||
|
||||
watch: { |
||||
$route: { |
||||
immediate: true, |
||||
handler(to) { |
||||
const {path, meta: {title}, matched} = to |
||||
if (!path.startsWith('/redirect')) { |
||||
this.title = title |
||||
this.data = matched.filter(item => item.meta.title) |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss"> |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.page-header { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
align-items: center; |
||||
flex-wrap: nowrap; |
||||
font-size: 14px; |
||||
height: $page-header-height; |
||||
padding: $page-header-padding $page-view-margin $page-header-padding $page-view-margin; |
||||
|
||||
> .el-breadcrumb .no-redirect { |
||||
color: $--color-text-secondary; |
||||
cursor: text; |
||||
} |
||||
|
||||
&-title { |
||||
color: $--color-text-primary; |
||||
font-weight: 600; |
||||
font-size: 18px; |
||||
overflow: hidden; |
||||
white-space: nowrap; |
||||
text-overflow: ellipsis; |
||||
} |
||||
|
||||
// 页头渲染时页面顶部margin为0 |
||||
& + .page-view { |
||||
margin-top: 0 !important; |
||||
} |
||||
} |
||||
</style> |
||||
@ -1,27 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import KeepRouterViewAlive from "./KeepRouterViewAlive" |
||||
|
||||
export default { |
||||
name: "PageView", |
||||
|
||||
functional: true, |
||||
|
||||
props: {include: Array, transitionName: String, cacheable: Boolean}, |
||||
|
||||
render(h, context) { |
||||
const {include, transitionName, cacheable} = context.props |
||||
|
||||
let view = ( |
||||
<transition name={transitionName} mode="out-in"> |
||||
<router-view/> |
||||
</transition> |
||||
) |
||||
|
||||
if (cacheable) { |
||||
view = <KeepRouterViewAlive include={include}>{view}</KeepRouterViewAlive> |
||||
} |
||||
|
||||
return <div class="page-view">{view}</div> |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,82 +0,0 @@ |
||||
<template> |
||||
<div class="tags-view-wrapper" @wheel="handleScroll"> |
||||
<slot/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import {scrollTo} from "@/util/browser" |
||||
|
||||
export default { |
||||
name: 'ScrollPanel', |
||||
|
||||
props: { |
||||
between: {type: Number, default: 4} |
||||
}, |
||||
|
||||
methods: { |
||||
handleScroll(e) { |
||||
const eventDelta = e.wheelDelta || -(e.detail || 0) * 40 |
||||
const {scrollLeft, scrollWidth, clientWidth} = this.$el |
||||
if (eventDelta > 0 && scrollLeft === 0 |
||||
|| eventDelta < 0 && scrollWidth <= scrollLeft + clientWidth) { |
||||
return |
||||
} |
||||
this.smoothScroll(-eventDelta) |
||||
}, |
||||
|
||||
smoothScroll(val) { |
||||
if (this.rAF) { |
||||
window.cancelAnimationFrame(this.rAF) |
||||
this.rAf = null |
||||
} |
||||
|
||||
this.rAf = scrollTo(this.$el, this.$el.scrollLeft + val, {direction: 'left', duration: 100}) |
||||
}, |
||||
|
||||
moveToTarget(currentTag, tagInstances) { |
||||
const {offsetWidth: containerWidth, scrollWidth, scrollLeft} = this.$el |
||||
|
||||
if (containerWidth >= scrollWidth) return |
||||
|
||||
let firstTag = null |
||||
let lastTag = null |
||||
|
||||
// find first tag and last tag |
||||
if (tagInstances.length > 0) { |
||||
firstTag = tagInstances[0] |
||||
lastTag = tagInstances[tagInstances.length - 1] |
||||
} |
||||
|
||||
if (firstTag === currentTag) { |
||||
return this.scrollLeft(0) |
||||
} |
||||
if (lastTag === currentTag) { |
||||
return this.scrollLeft(scrollWidth - containerWidth) |
||||
} |
||||
|
||||
// find preTag and nextTag |
||||
const currentIndex = tagInstances.findIndex(item => item === currentTag) |
||||
const prevTag = tagInstances[currentIndex - 1] |
||||
const nextTag = tagInstances[currentIndex + 1] |
||||
|
||||
// the tag's offsetLeft after of nextTag |
||||
const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + this.between |
||||
|
||||
// the tag's offsetLeft before of prevTag |
||||
const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - this.between |
||||
|
||||
if (afterNextTagOffsetLeft > scrollLeft + containerWidth) { |
||||
this.scrollLeft(afterNextTagOffsetLeft - containerWidth) |
||||
} |
||||
else if (beforePrevTagOffsetLeft < scrollLeft) { |
||||
this.scrollLeft(beforePrevTagOffsetLeft) |
||||
} |
||||
}, |
||||
|
||||
scrollLeft(val) { |
||||
this.$el.scrollTo({left: val, behavior: 'smooth'}) |
||||
} |
||||
} |
||||
} |
||||
</script> |
||||
@ -1,207 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import shortcutMixin from '@/layout/mixin/tagsViewShortcut' |
||||
import decideRouterTransitionMixin from '@/layout/mixin/decideRouterTransition' |
||||
import persistenceMixin from '@/layout/mixin/tagsViewPersistent' |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
import {getters as tagsViewGetters, mutations as tagsViewMutations} from "@/layout/store/tagsView" |
||||
import ContextMenu from "@/component/menu/ContextMenu" |
||||
import ScrollPanel from './ScrollPanel' |
||||
import {refreshPage} from "@/util/route" |
||||
|
||||
export default { |
||||
mixins: [shortcutMixin, decideRouterTransitionMixin, persistenceMixin], |
||||
|
||||
components: {ContextMenu, ScrollPanel}, |
||||
|
||||
data() { |
||||
return { |
||||
contextmenu: { |
||||
show: false, |
||||
top: 0, |
||||
left: 0, |
||||
}, |
||||
selectedTag: {} |
||||
} |
||||
}, |
||||
|
||||
computed: { |
||||
visitedViews: () => tagsViewGetters.visitedViews, |
||||
|
||||
menus: () => appGetters.menus, |
||||
|
||||
contextMenuItems() { |
||||
return [ |
||||
{content: '刷新', click: this.refreshSelectedTag}, |
||||
this.isAffix(this.selectedTag) |
||||
? undefined |
||||
: { |
||||
content: '关闭', |
||||
click: () => this.closeSelectedTag(this.selectedTag) |
||||
}, |
||||
{content: '关闭其他', click: this.closeOthersTags}, |
||||
{content: '关闭全部', click: this.closeAllTags} |
||||
] |
||||
} |
||||
}, |
||||
|
||||
watch: { |
||||
$route(to) { |
||||
this.addTag(to) |
||||
this.$nextTick(this.moveToCurrentTag) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//判断页签是否激活,考虑redirect刷新的情况 |
||||
isActive({path}) { |
||||
const {path: routePath} = this.$route |
||||
return routePath === path || routePath === `/redirect${path}` |
||||
}, |
||||
isAffix(tag) { |
||||
return tag.meta && tag.meta.affix |
||||
}, |
||||
|
||||
initTags() { |
||||
//获取所有需要固定显示的页签 |
||||
function getAffixTags(menus) { |
||||
const tags = [] |
||||
menus.forEach(({name, fullPath, children, meta}) => { |
||||
if (meta && meta.title && meta.affix) { |
||||
tags.push({ |
||||
//注意,此处的fullPath并不是$route.fullPath,而是路由树拼接后的全路径 |
||||
fullPath, |
||||
path: fullPath, |
||||
name, |
||||
meta: {...meta} |
||||
}) |
||||
} |
||||
if (children) { |
||||
const tempTags = getAffixTags(children) |
||||
tempTags.length && tags.push(...tempTags) |
||||
} |
||||
}) |
||||
return tags |
||||
} |
||||
|
||||
//添加所有固定显示的页签 |
||||
for (const tag of getAffixTags(this.menus)) { |
||||
tagsViewMutations.addTagOnly(tag) |
||||
} |
||||
|
||||
//将当前路由对象添加为页签 |
||||
this.addTag(this.$route) |
||||
}, |
||||
//将具有meta.title的路由对象添加为tab页 |
||||
addTag(to) { |
||||
to.meta.title && tagsViewMutations.addTagAndCache(to) |
||||
}, |
||||
|
||||
//横向滚动条移动至当前tab |
||||
moveToCurrentTag() { |
||||
//获取所有页签的componentInstance |
||||
const tagInstances = this.$refs.scrollPanel.$children |
||||
const tag = tagInstances.find(i => this.isActive(i.to)) |
||||
tag && this.$refs.scrollPanel.moveToTarget(tag, tagInstances) |
||||
}, |
||||
|
||||
/** |
||||
* 右键菜单选项 |
||||
* 刷新所选、关闭所选、关闭其他、关闭所有 |
||||
*/ |
||||
refreshSelectedTag() { |
||||
if (!this.selectedTag) return |
||||
tagsViewMutations.delCacheOnly(this.selectedTag) |
||||
this.$nextTick(() => refreshPage(this.selectedTag)) |
||||
}, |
||||
closeSelectedTag(view) { |
||||
if (this.isAffix(view)) return |
||||
tagsViewMutations.delTagAndCache(view) |
||||
this.isActive(view) && this.gotoLastView() |
||||
}, |
||||
closeOthersTags() { |
||||
tagsViewMutations.delOtherTagAndCache(this.selectedTag) |
||||
//当前选中的页签不是当前路由时,跳转到选中页签的地址 |
||||
if (this.selectedTag.path !== this.$route.path) { |
||||
return this.$router.push(this.selectedTag) |
||||
} |
||||
}, |
||||
closeAllTags() { |
||||
tagsViewMutations.delAllTagAndCache() |
||||
this.gotoLastView() |
||||
}, |
||||
|
||||
gotoLastView() { |
||||
if (this.visitedViews.length === 0) { |
||||
return this.$router.push('/') |
||||
} |
||||
const latest = this.visitedViews[this.visitedViews.length - 1] |
||||
//只有当页签路径与当前路由路径不同时才跳转,否则刷新 |
||||
if (this.$route.path !== latest.path) { |
||||
this.$router.push(latest.path) |
||||
} |
||||
else refreshPage() |
||||
}, |
||||
|
||||
openContextMenu(e, tag) { |
||||
e.preventDefault() |
||||
|
||||
const contextMenuWidth = 105 //右键菜单的假定宽度 |
||||
const {left: elLeft, width: elWidth} = this.$el.getBoundingClientRect() |
||||
const maxLeft = elWidth + elLeft - contextMenuWidth |
||||
const left = e.clientX |
||||
|
||||
this.contextmenu.left = (left > maxLeft ? maxLeft : left) + 15 |
||||
this.contextmenu.top = e.clientY |
||||
this.contextmenu.show = true |
||||
|
||||
this.selectedTag = tag |
||||
}, |
||||
|
||||
renderTags() { |
||||
return this.visitedViews.map(tag => { |
||||
const {path, query, fullPath, meta: {title}} = tag |
||||
const active = this.isActive(tag), affix = this.isAffix(tag) |
||||
const closeSelectedTag = (e, tag) => { |
||||
e.preventDefault() |
||||
this.closeSelectedTag(tag) |
||||
} |
||||
return ( |
||||
<router-link |
||||
key={path} |
||||
class={{'tags-view-item': true, active}} |
||||
to={{path, query, fullPath}} |
||||
v-on:contextmenu_native={e => this.openContextMenu(e, tag)} |
||||
v-on:dblclick_native={e => closeSelectedTag(e, tag)} |
||||
> |
||||
<span>{title}</span> |
||||
{!affix && <i class="el-icon-close" on-click={e => closeSelectedTag(e, tag)}/>} |
||||
</router-link> |
||||
) |
||||
}) |
||||
} |
||||
}, |
||||
|
||||
mounted() { |
||||
this.initTags() |
||||
}, |
||||
|
||||
render() { |
||||
return ( |
||||
<div class="tags-view-container"> |
||||
<scroll-panel ref="scrollPanel" class="tags-view-wrapper"> |
||||
{this.renderTags()} |
||||
</scroll-panel> |
||||
|
||||
<context-menu |
||||
v-model={this.contextmenu.show} |
||||
items={this.contextMenuItems} |
||||
left={this.contextmenu.left} |
||||
top={this.contextmenu.top} |
||||
/> |
||||
</div> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -1,68 +0,0 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.tags-view-container { |
||||
height: $tags-view-height; |
||||
box-shadow: inset 0 0 3px 2px hsla(0, 0%, 39.2%, .1); |
||||
|
||||
.tags-view-wrapper { |
||||
height: 100%; |
||||
white-space: nowrap; |
||||
overflow-x: auto; |
||||
|
||||
&::-webkit-scrollbar { |
||||
display: none; |
||||
} |
||||
|
||||
.tags-view-item { |
||||
$margin: 4px; |
||||
$border-width: 1px; |
||||
|
||||
display: inline-block; |
||||
cursor: pointer; |
||||
height: #{$tags-view-height - $margin * 2}; |
||||
line-height: #{$tags-view-height - $margin * 2 - $border-width * 2}; |
||||
color: $--color-text-secondary; |
||||
background-color: #ffffff; |
||||
border-radius: 3px; |
||||
border: $border-width solid $--border-color-base; |
||||
padding: 0 12px; |
||||
font-size: 12px; |
||||
margin: $margin; |
||||
user-select: none; |
||||
|
||||
&:first-of-type { |
||||
margin-left: 15px; |
||||
} |
||||
|
||||
&:last-of-type { |
||||
margin-right: 15px; |
||||
} |
||||
|
||||
&::before { |
||||
content: ''; |
||||
position: relative; |
||||
background-color: #e8eaec; |
||||
display: inline-block; |
||||
width: 12px; |
||||
height: 12px; |
||||
border-radius: 50%; |
||||
top: 1px; |
||||
margin-right: 5px; |
||||
} |
||||
|
||||
&.active { |
||||
color: $--color-primary; |
||||
|
||||
&::before { |
||||
background-color: $--color-primary; |
||||
} |
||||
} |
||||
|
||||
.el-icon-close { |
||||
border-radius: 50%; |
||||
font-size: 14px; |
||||
margin-left: 10px; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
@ -1,67 +0,0 @@ |
||||
<script type="text/jsx"> |
||||
import {getters as pageGetters} from "@/layout/store/page" |
||||
import {getters as tagsViewGetters} from "@/layout/store/tagsView" |
||||
import BackToTop from "./component/BackToTop" |
||||
import PageHeader from "./component/PageHeader" |
||||
import PageView from "./component/PageView" |
||||
import PageFooter from "./component/PageFooter" |
||||
import TagsView from './component/TagsView' |
||||
|
||||
export default { |
||||
name: 'Page', |
||||
|
||||
components: {BackToTop, PageHeader, PageView, PageFooter, TagsView}, |
||||
|
||||
computed: { |
||||
renderPageHeader() { |
||||
return pageGetters.showPageHeader && this.$route.meta.pageHeader !== false |
||||
}, |
||||
pageClass() { |
||||
return { |
||||
'scroll-container': true, |
||||
'has-page-header': this.renderPageHeader, |
||||
'has-page-footer': true |
||||
} |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
const {transition, showIframe, iframeList, currentIframe} = pageGetters |
||||
const {cachedViews, enabled} = tagsViewGetters |
||||
|
||||
return ( |
||||
<main class="page-main"> |
||||
{enabled && <tags-view/>} |
||||
|
||||
<div v-show={!showIframe} class={this.pageClass}> |
||||
{this.renderPageHeader && <page-header/>} |
||||
|
||||
<page-view |
||||
include={cachedViews} |
||||
transition-name={transition.curr} |
||||
cacheable={enabled} |
||||
/> |
||||
|
||||
<page-footer/> |
||||
</div> |
||||
|
||||
{iframeList.map(src => ( |
||||
<iframe |
||||
v-show={showIframe && src === currentIframe} |
||||
id={src} |
||||
key={src} |
||||
src={src} |
||||
frameBorder="0" |
||||
height="100%" |
||||
width="100%" |
||||
/> |
||||
))} |
||||
|
||||
{pageGetters.showBackToTop && <back-to-top/>} |
||||
</main> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
|
||||
<style lang="scss" src="./style.scss"></style> |
||||
@ -1,23 +0,0 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.page-main { |
||||
display: flex; |
||||
flex-direction: column; |
||||
flex: 1; |
||||
position: relative; |
||||
overflow: hidden; |
||||
background-color: #f0f2f5; |
||||
|
||||
> .scroll-container { |
||||
display: flex; |
||||
flex: 1; |
||||
flex-direction: column; |
||||
overflow-y: overlay; |
||||
overflow-x: inherit; |
||||
|
||||
> .page-view { |
||||
margin: $page-view-margin; |
||||
flex: 1; |
||||
} |
||||
} |
||||
} |
||||
@ -0,0 +1,15 @@ |
||||
<template> |
||||
<div class="color-checkbox" :style="{'background-color': value}" @click="click"> |
||||
<i v-if="checked" class="el-icon-check"/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import mixin from './mixin' |
||||
|
||||
export default { |
||||
name: "ColorCheckBox", |
||||
|
||||
mixins: [mixin] |
||||
} |
||||
</script> |
||||
@ -0,0 +1,15 @@ |
||||
<template> |
||||
<div class="checkbox-group"> |
||||
<slot/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
export default { |
||||
name: "CheckboxGroup", |
||||
|
||||
props: {value: String}, |
||||
|
||||
data: () => ({children: []}) |
||||
} |
||||
</script> |
||||
@ -0,0 +1,21 @@ |
||||
<template> |
||||
<div class="img-checkbox" :title="label" @click="click"> |
||||
<img :src="img"/> |
||||
<i v-if="checked" class="el-icon-check"/> |
||||
</div> |
||||
</template> |
||||
|
||||
<script> |
||||
import mixin from './mixin' |
||||
|
||||
export default { |
||||
name: "ImgCheckBox", |
||||
|
||||
mixins: [mixin], |
||||
|
||||
props: { |
||||
img: String, |
||||
label: String |
||||
} |
||||
} |
||||
</script> |
||||
@ -0,0 +1,34 @@ |
||||
@import "~@/asset/style/var"; |
||||
|
||||
.checkbox-group { |
||||
display: flex; |
||||
flex-wrap: wrap; |
||||
width: 100%; |
||||
} |
||||
|
||||
.color-checkbox { |
||||
width: 20px; |
||||
height: 20px; |
||||
margin: 5px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
text-align: center; |
||||
font-weight: bold; |
||||
color: #ffffff; |
||||
} |
||||
|
||||
.img-checkbox { |
||||
width: 55px; |
||||
margin: 10px; |
||||
border-radius: 2px; |
||||
cursor: pointer; |
||||
position: relative; |
||||
|
||||
.el-icon-check { |
||||
position: absolute; |
||||
color: $--color-primary; |
||||
font-weight: bold; |
||||
top: 25px; |
||||
right: 10px; |
||||
} |
||||
} |
||||
@ -0,0 +1,17 @@ |
||||
@import "~@/asset/style/var"; |
||||
@import "./component/checkbox/style"; |
||||
|
||||
.drawer-container { |
||||
padding: 24px; |
||||
font-size: 14px; |
||||
line-height: 1.5; |
||||
word-wrap: break-word; |
||||
|
||||
.drawer-item { |
||||
display: flex; |
||||
justify-content: space-between; |
||||
color: $--color-text-regular; |
||||
font-size: 14px; |
||||
padding: 12px 0; |
||||
} |
||||
} |
||||
@ -1,90 +1,101 @@ |
||||
<template> |
||||
<el-admin-layout :navbar-props="navbarProps" :page-props="pageProps"/> |
||||
</template> |
||||
|
||||
<script type="text/jsx"> |
||||
import VAside from './component/Aside' |
||||
import VNavbar from './component/Navbar' |
||||
import VPage from './component/Page' |
||||
import Vue from 'vue' |
||||
import config from "@/config" |
||||
import ElAdminLayout, {setIconRenderer, appMutations} from 'el-admin-layout' |
||||
import Footer from './component/Footer' |
||||
import SettingDrawer from './component/SettingDrawer' |
||||
import {getters as appGetters, mutations as appMutations} from "@/layout/store/app" |
||||
import {getters as pageGetters} from "@/layout/store/page" |
||||
import {getters as tagsViewGetters} from "@/layout/store/tagsView" |
||||
import tagsViewPersistent from './mixin/tagsViewPersistent' |
||||
import tagsViewShortcut from './mixin/tagsViewShortcut' |
||||
import {elConfirm} from "@/util/message" |
||||
|
||||
//设置一些基础信息 |
||||
appMutations.title(config.title) |
||||
appMutations.logo(config.logo) |
||||
|
||||
//设置图标渲染方式 |
||||
setIconRenderer((h, data) => <v-icon {...data}/>) |
||||
|
||||
export default { |
||||
name: 'Layout', |
||||
name: "Layout", |
||||
|
||||
mixins: [tagsViewPersistent, tagsViewShortcut], |
||||
|
||||
components: {VAside, VNavbar, VPage, SettingDrawer}, |
||||
components: {ElAdminLayout}, |
||||
|
||||
computed: { |
||||
isLeftRight() { |
||||
return pageGetters.position === 'left-right' |
||||
}, |
||||
renderAside() { |
||||
return appGetters.isMobile || ['aside', 'aside-two-part', 'mix'].includes(appGetters.navMode) |
||||
}, |
||||
wrapperClass() { |
||||
navbarProps() { |
||||
const userInfo = this.$store.state.user |
||||
return { |
||||
'app-wrapper': true, |
||||
'flex-column': !this.isLeftRight |
||||
user: { |
||||
avatar: userInfo.avatar, |
||||
name: userInfo.name |
||||
}, |
||||
userDropdownItems: [ |
||||
{ |
||||
icon: 'el-icon-switch-button', |
||||
command: 'logout', |
||||
content: '退出登录', |
||||
handler: this.logout |
||||
} |
||||
], |
||||
renderCustomActions: this.renderNavbarActions |
||||
} |
||||
}, |
||||
containerClass() { |
||||
return [ |
||||
'main-container', |
||||
'has-nav', |
||||
`nav-mode-${appGetters.navMode}`, |
||||
this.isLeftRight && 'flex-column', |
||||
tagsViewGetters.enabled && 'has-tags-view' |
||||
] |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
closeSettingDrawer() { |
||||
appMutations.showSettingDrawer(false) |
||||
pageProps() { |
||||
return { |
||||
renderFooter: () => <Footer/> |
||||
} |
||||
} |
||||
}, |
||||
|
||||
render() { |
||||
const aside = this.renderAside && <v-aside/> |
||||
|
||||
return ( |
||||
<section class={this.wrapperClass}> |
||||
{this.isLeftRight ? aside : <v-navbar/>} |
||||
methods: { |
||||
logout() { |
||||
if (this.$store.state.user.prepareLogout) { |
||||
return |
||||
} |
||||
|
||||
<section class={this.containerClass}> |
||||
{this.isLeftRight ? <v-navbar/> : aside} |
||||
<v-page/> |
||||
</section> |
||||
elConfirm('确认退出?').then(() => this.$store.dispatch('user/logout')) |
||||
}, |
||||
|
||||
<setting-drawer |
||||
value={appGetters.showSettingDrawer} |
||||
on-input={this.closeSettingDrawer} |
||||
/> |
||||
</section> |
||||
) |
||||
} |
||||
} |
||||
</script> |
||||
openSettingDrawer() { |
||||
appMutations.showSettingDrawer(true) |
||||
}, |
||||
|
||||
<style lang="scss"> |
||||
.app-wrapper.flex-column, |
||||
.main-container.flex-column { |
||||
flex-direction: column; |
||||
} |
||||
renderNavbarActions(defaultActions) { |
||||
const customActions = [ |
||||
<div |
||||
class="setting-btn navbar-item" |
||||
title="个性设置" |
||||
on-click={this.openSettingDrawer} |
||||
> |
||||
<i class="el-icon-s-operation navbar-icon"/> |
||||
</div> |
||||
] |
||||
|
||||
.app-wrapper { |
||||
display: flex; |
||||
height: 100%; |
||||
width: 100%; |
||||
return customActions.concat(defaultActions) |
||||
} |
||||
}, |
||||
|
||||
> .main-container { |
||||
display: flex; |
||||
flex: 1; |
||||
overflow: hidden; |
||||
} |
||||
//提前创建设置抽屉,避免初始化同步设置数据时导致layout重新渲染 |
||||
beforeCreate() { |
||||
const Drawer = Vue.extend(SettingDrawer) |
||||
const instance = new Drawer().$mount() |
||||
document.body.appendChild(instance.$el) |
||||
this.$_settingDrawerInstance = instance |
||||
}, |
||||
|
||||
//左右结构时,侧边栏z-index需大于导航栏 |
||||
//上下结构时,侧边栏z-index需小于导航栏 |
||||
&.flex-column .aside { |
||||
z-index: 9; |
||||
} |
||||
//销毁时清除设置抽屉 |
||||
beforeDestroy() { |
||||
if (this.$_settingDrawerInstance) { |
||||
this.$_settingDrawerInstance.$destroy() |
||||
this.$_settingDrawerInstance.$el.remove() |
||||
delete this.$_settingDrawerInstance |
||||
} |
||||
}, |
||||
} |
||||
</style> |
||||
</script> |
||||
|
||||
@ -1,29 +0,0 @@ |
||||
import {getters as pageGetters, mutations as pageViewMutations} from "@/layout/store/page" |
||||
|
||||
export default { |
||||
watch: { |
||||
$route(to, from) { |
||||
this.decideRouteTransition(to, from) |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//根据访问的tab页的左右顺序来确定路由动画
|
||||
decideRouteTransition(to, from) { |
||||
const {next, prev} = pageGetters.transition |
||||
|
||||
let transitionName = prev |
||||
|
||||
//这里认为页签数量不会太多,所以为了可读性使用两次循环查找
|
||||
const fromIndex = this.visitedViews.findIndex(i => i.path === from.path) |
||||
const toIndex = this.visitedViews.findIndex(i => i.path === to.path) |
||||
|
||||
//新开tab也认为顺序高于上一个tab
|
||||
if (toIndex === -1 || fromIndex < toIndex) { |
||||
transitionName = next |
||||
} |
||||
|
||||
pageViewMutations.transition({curr: transitionName}) |
||||
}, |
||||
} |
||||
} |
||||
@ -1,33 +0,0 @@ |
||||
import Hamburger from '@/layout/component/Hamburger' |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
import {getters as asideGetters} from "@/layout/store/aside" |
||||
import {getSidebarMenus} from "@/layout/util" |
||||
|
||||
/** |
||||
* 汉堡包的渲染条件混入 |
||||
*/ |
||||
|
||||
export default { |
||||
components: {Hamburger}, |
||||
|
||||
computed: { |
||||
//渲染汉堡包的条件
|
||||
//①侧边栏有菜单
|
||||
//②移动端 或
|
||||
//③桌面端且汉堡包位置正确(侧边栏引入则需要是aside,反之需要是head)
|
||||
//④桌面端且是双层侧边栏导航
|
||||
//⑤桌面端且是侧边栏导航或混合导航时,未设置侧边栏自动隐藏
|
||||
renderHamburger() { |
||||
if (getSidebarMenus().length <= 0) return false |
||||
|
||||
const isMobile = appGetters.isMobile, |
||||
correctPosition = |
||||
asideGetters.hamburgerPosition === (this.$options.name === 'navbar' ? 'head' : 'aside'), |
||||
correctMode = |
||||
['aside', 'mix'].includes(appGetters.navMode) && !asideGetters.autoHide |
||||
|| appGetters.navMode === 'aside-two-part' |
||||
|
||||
return isMobile || correctPosition && correctMode |
||||
} |
||||
} |
||||
} |
||||
@ -1,56 +0,0 @@ |
||||
/** |
||||
* 顶部菜单和侧边栏菜单的公共混入 |
||||
*/ |
||||
import {mutations as tagsViewMutations} from "@/layout/store/tagsView" |
||||
import {refreshPage} from "@/util/route" |
||||
import {isExternal} from "@/util/validate" |
||||
import {findComponentByTag} from "@/util/vue" |
||||
|
||||
export default { |
||||
data() { |
||||
return { |
||||
//当前激活的菜单的fullPath
|
||||
//之所以手动维护是因为el-menu在点击后就会设置activeIndex
|
||||
activeMenu: '', |
||||
|
||||
//用于判断鼠标是否在弹出菜单内
|
||||
openedMenuNum: 0 |
||||
} |
||||
}, |
||||
|
||||
methods: { |
||||
//点击菜单后的动作
|
||||
actionOnSelectMenu(fullPath, refreshWhenSame = true) { |
||||
//外部链接时打开新窗口
|
||||
if (isExternal(fullPath)) { |
||||
window.open(fullPath) |
||||
return this.resetActiveMenu() |
||||
} |
||||
|
||||
//触发的菜单路径是当前路由时,根据参数判断是否进行刷新
|
||||
if (this.$route.path === fullPath) { |
||||
if (!refreshWhenSame) return |
||||
tagsViewMutations.delCacheOnly(this.$route) |
||||
this.$nextTick(() => refreshPage()) |
||||
} |
||||
else this.$router.push(fullPath) |
||||
}, |
||||
|
||||
//由于侧边栏菜单数组更新后,el-menu不一定会更新(当数组中不存在单级菜单时)
|
||||
//所以手动更新el-menu的当前高亮菜单
|
||||
resetActiveMenu() { |
||||
const menu = this.$_getElMenuInstance() |
||||
menu && menu.updateActiveIndex(this.activeMenu) |
||||
}, |
||||
|
||||
//获取el-menu实例
|
||||
//目前被侧边栏和双层侧边栏的主菜单使用
|
||||
$_getElMenuInstance() { |
||||
if (!this.$_elMenuInstance) { |
||||
this.$_elMenuInstance = findComponentByTag(this, 'el-menu') |
||||
} |
||||
|
||||
return this.$_elMenuInstance |
||||
} |
||||
} |
||||
} |
||||
@ -1,24 +0,0 @@ |
||||
/** |
||||
* 侧边栏搜索框混入 |
||||
*/ |
||||
import MenuSearch from '@/layout/component/Aside/component/MenuSearch' |
||||
import {trim} from "@/util" |
||||
import {findComponentByTag} from "@/util/vue" |
||||
|
||||
export default { |
||||
components: {MenuSearch}, |
||||
|
||||
methods: { |
||||
$_getNavMenuInstance() { |
||||
if (!this.$_navMenuInstance) { |
||||
this.$_navMenuInstance = findComponentByTag(this, 'nav-menu') |
||||
} |
||||
|
||||
return this.$_navMenuInstance |
||||
}, |
||||
|
||||
handlerSearch(v) { |
||||
this.$_getNavMenuInstance().realSearchWord = trim(v) |
||||
} |
||||
} |
||||
} |
||||
@ -1,83 +0,0 @@ |
||||
import Vue from 'vue' |
||||
import {debounce, isEmpty} from "@/util" |
||||
import {isMobile} from "@/util/browser" |
||||
import {createGetters, createMutations} from "@/util/observable" |
||||
|
||||
const state = { |
||||
//区分pc和移动端
|
||||
isMobile: isMobile(), |
||||
|
||||
//设置抽屉的显隐
|
||||
//放这里的原因是navbar可能会重新渲染,从而导致抽屉的重新渲染
|
||||
showSettingDrawer: false, |
||||
|
||||
//当前激活的顶部菜单的fullPath
|
||||
activeRootMenu: '', |
||||
|
||||
//所有的树形菜单,每个元素为顶部菜单,顶部菜单的子级(如果有)为侧边栏菜单
|
||||
menus: [], |
||||
|
||||
//导航模式,'aside'、'aside-two-part'、'head'、'mix'
|
||||
navMode: 'mix' |
||||
} |
||||
|
||||
const store = Vue.observable(state) |
||||
|
||||
export const getters = createGetters(store) |
||||
|
||||
export const mutations = { |
||||
...createMutations(store), |
||||
|
||||
menus(v) { |
||||
sort(v) |
||||
store.menus = v |
||||
} |
||||
} |
||||
|
||||
//菜单排序
|
||||
function sort(routes) { |
||||
if (!Array.isArray(routes) || routes.length === 0) { |
||||
return |
||||
} |
||||
|
||||
//菜单排序值的空值处理
|
||||
const getSortValue = item => { |
||||
const sort = deepGetSortValue(item) |
||||
return isEmpty(sort) ? 10000 : sort |
||||
} |
||||
|
||||
//获取菜单的排序值
|
||||
const deepGetSortValue = item => { |
||||
const {children = [], meta: {hidden, sort} = {}} = item |
||||
|
||||
if (hidden) return null |
||||
|
||||
if (!isEmpty(sort)) return sort |
||||
|
||||
//如果只有一个子节点,那么取子节点的排序值
|
||||
if (children.length === 1) { |
||||
return deepGetSortValue(children[0]) |
||||
} |
||||
|
||||
return null |
||||
} |
||||
|
||||
//对根节点排序
|
||||
routes.sort((pre, next) => { |
||||
pre = getSortValue(pre) |
||||
next = getSortValue(next) |
||||
if (pre < next) return -1 |
||||
if (pre === next) return 0 |
||||
return 1 |
||||
}) |
||||
|
||||
//对每一个根节点的子级排序
|
||||
routes.forEach(route => { |
||||
const {children} = route |
||||
children && children.length && sort(children) |
||||
}) |
||||
} |
||||
|
||||
window.addEventListener('resize', debounce(() => { |
||||
!document.hidden && mutations.isMobile(isMobile()) |
||||
})) |
||||
@ -1,71 +0,0 @@ |
||||
/** |
||||
* 侧边栏的响应式数据 |
||||
*/ |
||||
import Vue from 'vue' |
||||
import {bindThis} from "@/util" |
||||
import {createGetters, createMutations} from "@/util/observable" |
||||
import {getters as appGetters} from "./app" |
||||
|
||||
const state = { |
||||
//抽屉模式时的显隐
|
||||
show: false, |
||||
|
||||
//主题,light 或 dark
|
||||
theme: 'light', |
||||
|
||||
//手风琴效果
|
||||
uniqueOpen: true, |
||||
|
||||
//是否折叠
|
||||
collapse: false, |
||||
|
||||
//折叠时显示上级
|
||||
showParentOnCollapse: false, |
||||
|
||||
//自动隐藏
|
||||
autoHide: false, |
||||
|
||||
//汉堡包的位置,aside 或 head
|
||||
hamburgerPosition: 'aside', |
||||
|
||||
//是否显示搜索框
|
||||
search: true |
||||
} |
||||
|
||||
const store = Vue.observable(state) |
||||
|
||||
export const getters = createGetters(store) |
||||
|
||||
export const mutations = bindThis({ |
||||
...createMutations(store), |
||||
|
||||
/*移动端或设置了侧边栏自动隐藏时打开关闭抽屉,否则展开折叠*/ |
||||
open() { |
||||
if (appGetters.isMobile || store.autoHide) { |
||||
store.show = true |
||||
} |
||||
else store.collapse = false |
||||
}, |
||||
close() { |
||||
if (appGetters.isMobile || store.autoHide) { |
||||
store.show = false |
||||
} |
||||
else store.collapse = true |
||||
}, |
||||
//切换侧边栏的状态
|
||||
switch(action) { |
||||
switch (action) { |
||||
case 'open': |
||||
return this.open() |
||||
case 'close': |
||||
return this.close() |
||||
default : |
||||
let open = true |
||||
if (appGetters.isMobile) { |
||||
open = !store.show |
||||
} |
||||
else open = store.collapse |
||||
return open ? this.open() : this.close() |
||||
} |
||||
} |
||||
}) |
||||
@ -1,13 +0,0 @@ |
||||
import Vue from 'vue' |
||||
import {createGetters, createMutations} from "@/util/observable" |
||||
|
||||
const state = { |
||||
//主题,light 或 dark
|
||||
theme: 'light', |
||||
} |
||||
|
||||
const store = Vue.observable(state) |
||||
|
||||
export const getters = createGetters(store) |
||||
|
||||
export const mutations = createMutations(store) |
||||
@ -1,65 +0,0 @@ |
||||
/** |
||||
* 路由页面的响应式数据 |
||||
*/ |
||||
import Vue from 'vue' |
||||
import {bindThis} from "@/util" |
||||
import {createGetters, createMutations} from "@/util/observable" |
||||
|
||||
const state = { |
||||
//路由过渡动画
|
||||
transition: { |
||||
//当未启用多页签时的路由动画
|
||||
default: 'el-fade-in-linear', |
||||
//要访问的tab顺序高于上一个访问的tab时的路由动画
|
||||
next: 'left-out', |
||||
//要访问的tab顺序不高于上一个访问的tab时的路由动画
|
||||
prev: 'right-out', |
||||
//当前使用的路由动画
|
||||
curr: 'el-fade-in-linear' |
||||
}, |
||||
|
||||
/*iframe的控制*/ |
||||
showIframe: false, |
||||
currentIframe: '', |
||||
iframeList: [], |
||||
|
||||
//是否显示侧边栏或顶部导航栏的logo
|
||||
showLogo: true, |
||||
//分层结构,上下('top-bottom')、左右('left-right')
|
||||
position: 'left-right', |
||||
//是否显示页头
|
||||
showPageHeader: true, |
||||
//是否显示返回顶部按钮
|
||||
showBackToTop: true |
||||
} |
||||
|
||||
const store = Vue.observable(state) |
||||
|
||||
export const getters = createGetters(store) |
||||
|
||||
export const mutations = bindThis({ |
||||
...createMutations(store), |
||||
|
||||
//修改transition时使用Object.assign
|
||||
transition(obj) { |
||||
Object.assign(store.transition, obj) |
||||
}, |
||||
|
||||
addIframe(src) { |
||||
!store.iframeList.includes(src) && store.iframeList.push(src) |
||||
}, |
||||
delIframe(src) { |
||||
const index = store.iframeList.findIndex(i => i === src) |
||||
index > -1 && store.iframeList.splice(index, 1) |
||||
}, |
||||
openIframe({src}) { |
||||
store.showIframe = true |
||||
store.currentIframe = src |
||||
this.addIframe(src) |
||||
}, |
||||
closeIframe({src, del}) { |
||||
store.showIframe = false |
||||
store.currentIframe = '' |
||||
del && this.delIframe(src) |
||||
} |
||||
}) |
||||
@ -1,141 +0,0 @@ |
||||
/** |
||||
* 多页签的响应式数据 |
||||
*/ |
||||
import Vue from 'vue' |
||||
import {getters as pageGetters, mutations as pageMutations} from "@/layout/store/page" |
||||
import {getRouterViewCacheKey} from "@/layout/util" |
||||
import {bindThis} from "@/util" |
||||
import {createGetters, createMutations} from "@/util/observable" |
||||
|
||||
const state = { |
||||
//是否启用
|
||||
enabled: true, |
||||
//是否启用快捷键切换功能
|
||||
shortcut: true, |
||||
//是否将页签持久化到sessionStorage
|
||||
persistent: true, |
||||
|
||||
//显示的页签,vue-router的routeConfig对象数组
|
||||
visitedViews: [], |
||||
|
||||
//缓存的页签,用于<keep-router-view-alive/>:include
|
||||
cachedViews: [] |
||||
} |
||||
|
||||
const store = Vue.observable(state) |
||||
|
||||
export const getters = createGetters(store) |
||||
|
||||
export const mutations = bindThis({ |
||||
...createMutations(store), |
||||
|
||||
/** |
||||
* 多页签的启用/停用 |
||||
* 停用时会移除所有缓存,并且重置路由过渡动画 |
||||
* @param v 启用为true,停用为false |
||||
*/ |
||||
enabled(v) { |
||||
store.enabled = v |
||||
|
||||
if (!v) { |
||||
pageMutations.transition({curr: pageGetters.transition.default}) |
||||
this.delAllTagAndCache() |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* 在页签栏上添加一个页签,path已存在的不会重复添加,调用时需要保证meta.title有值 |
||||
* @param view {routeConfig} |
||||
*/ |
||||
addTagOnly(view) { |
||||
const {name, path, fullPath, meta} = view |
||||
|
||||
if (store.visitedViews.some(v => v.path === path)) { |
||||
return |
||||
} |
||||
|
||||
store.visitedViews.push({name, path, fullPath, meta: {...meta}}) |
||||
}, |
||||
|
||||
/** |
||||
* 将传入的routeConfig加入<keep-router-view-alive>的缓存中 |
||||
* 以下调用无效:设置了不缓存、是iframe页、未设置唯一标识、已缓存 |
||||
* @param view {routeConfig} |
||||
*/ |
||||
addCacheOnly(view) { |
||||
const {noCache, iframe, usePathKey, useFullPathKey} = view.meta || {} |
||||
|
||||
if (noCache || iframe || !view.name && !usePathKey && !useFullPathKey) { |
||||
return |
||||
} |
||||
|
||||
const key = getRouterViewCacheKey(view) |
||||
|
||||
if (store.cachedViews.includes(key)) { |
||||
return |
||||
} |
||||
|
||||
store.cachedViews.push(key) |
||||
}, |
||||
|
||||
/** |
||||
* 同时调用{@link #addTagOnly}、{@link #addCacheOnly} |
||||
* @param view {routeConfig} |
||||
*/ |
||||
addTagAndCache(view) { |
||||
this.addTagOnly(view) |
||||
this.addCacheOnly(view) |
||||
}, |
||||
|
||||
/** |
||||
* 根据path从页签栏中移除一个页签 |
||||
* @param view {path},routeConfig |
||||
*/ |
||||
delTagOnly(view) { |
||||
const index = store.visitedViews.findIndex(i => i.path === view.path) |
||||
index > -1 && store.visitedViews.splice(index, 1) |
||||
}, |
||||
|
||||
/** |
||||
* 删除<keep-router-view-alive>对应的缓存 |
||||
* @param view {routeConfig} |
||||
*/ |
||||
delCacheOnly(view) { |
||||
const key = getRouterViewCacheKey(view) |
||||
const index = store.cachedViews.indexOf(key) |
||||
index > -1 && store.cachedViews.splice(index, 1) |
||||
}, |
||||
|
||||
/** |
||||
* 同时调用{@link #delTagOnly}、{@link #delCacheOnly},移除iframe页 |
||||
* @param view {routeConfig} |
||||
*/ |
||||
delTagAndCache(view) { |
||||
this.delTagOnly(view) |
||||
this.delCacheOnly(view) |
||||
|
||||
const iframe = view.meta && view.meta.iframe |
||||
iframe && pageMutations.delIframe(iframe) |
||||
}, |
||||
|
||||
/** |
||||
* 从页签栏上移除除了routeConfig以外的所有非固定页签 |
||||
* 并且从<keep-router-view-alive>中移除除了routeConfig以外的所有缓存 |
||||
* @param view {routeConfig} |
||||
*/ |
||||
delOtherTagAndCache(view) { |
||||
const visitedViews = store.visitedViews.filter(v => v.meta.affix || v.path === view.path) |
||||
const key = store.cachedViews.find(key => key === getRouterViewCacheKey(view)) |
||||
|
||||
store.visitedViews = visitedViews |
||||
store.cachedViews = key ? [key] : [] |
||||
}, |
||||
|
||||
/** |
||||
* 从页签栏上移除所有非固定页签,并且移除<keep-router-view-alive>的所有缓存 |
||||
*/ |
||||
delAllTagAndCache() { |
||||
store.visitedViews = store.visitedViews.filter(tag => tag.meta && tag.meta.affix) |
||||
store.cachedViews = [] |
||||
} |
||||
}) |
||||
@ -0,0 +1,6 @@ |
||||
@import "~el-admin-layout/src/style"; |
||||
|
||||
@import "./component/Footer/style"; |
||||
@import "./component/SettingDrawer/style"; |
||||
|
||||
|
||||
@ -1,39 +0,0 @@ |
||||
import {getters as appGetters} from "@/layout/store/app" |
||||
|
||||
//获取路由页面缓存所需的key
|
||||
export function getRouterViewCacheKey({name, path, fullPath, meta = {}}) { |
||||
const {usePathKey, useFullPathKey} = meta |
||||
return usePathKey ? path : useFullPathKey ? fullPath : name |
||||
} |
||||
|
||||
//获取侧边栏的菜单,如果是双层侧边栏导航时,获取的是子菜单
|
||||
export function getSidebarMenus() { |
||||
const menus = appGetters.menus |
||||
|
||||
if (!Array.isArray(menus)) { |
||||
return [] |
||||
} |
||||
|
||||
//移动端时,侧边栏只会按侧边栏导航模式渲染
|
||||
if (appGetters.isMobile) { |
||||
return menus |
||||
} |
||||
|
||||
switch (appGetters.navMode) { |
||||
case 'aside': |
||||
return menus |
||||
case 'head': |
||||
return [] |
||||
case 'aside-two-part': |
||||
case 'mix': |
||||
const root = menus.find(i => i.path === appGetters.activeRootMenu) |
||||
return root ? root.children || [] : [] |
||||
default: |
||||
return [] |
||||
} |
||||
} |
||||
|
||||
//根据路由获取当前激活的菜单
|
||||
export function getActiveMenuByRoute({path, meta}) { |
||||
return meta.activeMenu || path |
||||
} |
||||
@ -1,223 +0,0 @@ |
||||
import cssVar from '@/asset/style/var.scss' |
||||
|
||||
const maxMobileWidth = parseFloat(cssVar.maxMobileWidth) |
||||
|
||||
/** |
||||
* 根据body宽度判断是否为移动端,是则返回true |
||||
* |
||||
* @return {boolean} |
||||
*/ |
||||
export function isMobile() { |
||||
const rect = document.body.getBoundingClientRect() |
||||
return rect.width <= maxMobileWidth |
||||
} |
||||
|
||||
/** |
||||
* 判断是否为dom元素,是则返回true |
||||
* |
||||
* @param obj |
||||
* @return {boolean} |
||||
*/ |
||||
export function isDom(obj) { |
||||
return obj && typeof obj === 'object' && obj.nodeType === 1 && typeof obj.nodeName === 'string' |
||||
} |
||||
|
||||
/** |
||||
* 获取元素的内宽(扣除左右padding后) |
||||
* |
||||
* @param el {HTMLElement} |
||||
* @return {number} |
||||
*/ |
||||
export function getElementInnerWidth(el) { |
||||
if (!el) return 0 |
||||
|
||||
const style = window.getComputedStyle(el) |
||||
|
||||
return parseFloat(style.width) - (parseFloat(style.paddingLeft) + parseFloat(style.paddingRight)) |
||||
} |
||||
|
||||
/** |
||||
* 获取元素的真实高度 |
||||
* |
||||
* @param el {HTMLElement} |
||||
* @param style 元素的style键值对 |
||||
* @return {number} |
||||
*/ |
||||
export function getElementHeight(el, style) { |
||||
if (!el) return 0 |
||||
|
||||
const node = el.cloneNode(true) |
||||
node.style.opacity = '0' |
||||
|
||||
if (style) { |
||||
Object.keys(style).forEach(item => { |
||||
node.style[item] = style[item] |
||||
}) |
||||
} |
||||
|
||||
const id = 'temp-id-' + Date.now() |
||||
node.setAttribute('id', id) |
||||
document.body.append(node) |
||||
|
||||
const height = node.offsetHeight |
||||
document.body.removeChild(node) |
||||
|
||||
return height |
||||
} |
||||
|
||||
/** |
||||
* 获取元素距离其容器的顶部距离 |
||||
* |
||||
* @param el {HTMLElement} 目标元素 |
||||
* @param container {HTMLElement|Window} 容器元素 |
||||
* @return {number} |
||||
*/ |
||||
export function getTopDistance(el, container) { |
||||
if (!el) return 0 |
||||
|
||||
if (!el.getClientRects().length) { |
||||
return 0 |
||||
} |
||||
|
||||
const rect = el.getBoundingClientRect() |
||||
|
||||
if (rect.width || rect.height) { |
||||
if (container === window) { |
||||
container = el.ownerDocument.documentElement |
||||
return rect.top - container.clientTop |
||||
} |
||||
return rect.top - container.getBoundingClientRect().top |
||||
} |
||||
|
||||
return rect.top |
||||
} |
||||
|
||||
/** |
||||
* 加载js或css |
||||
* |
||||
* @param url {string} 资源地址 |
||||
* @param type {string} 'js'或'css' |
||||
* @return {Promise<String>} 加载成功返回url(若资源此前已加载,返回undefined) |
||||
*/ |
||||
export function loadExternalResource(url, type = 'js') { |
||||
return new Promise((resolve, reject) => { |
||||
let tag |
||||
|
||||
if (type === "css") { |
||||
const links = Array.from(document.getElementsByTagName('link')) |
||||
if (links.some(link => link.getAttribute('href') === url)) { |
||||
return resolve() |
||||
} |
||||
|
||||
tag = document.createElement("link") |
||||
tag.rel = "stylesheet" |
||||
tag.href = url |
||||
} |
||||
else if (type === "js") { |
||||
const scripts = Array.from(document.getElementsByTagName('script')) |
||||
if (scripts.some(script => script.getAttribute('src') === url)) { |
||||
return resolve() |
||||
} |
||||
|
||||
tag = document.createElement("script") |
||||
tag.src = url |
||||
} |
||||
|
||||
if (tag) { |
||||
tag.onload = () => resolve(url) |
||||
tag.onerror = () => reject(url) |
||||
document.head.appendChild(tag) |
||||
} |
||||
else reject(`没有这个东东,url:${url},type:${type}`) |
||||
}) |
||||
} |
||||
|
||||
/** |
||||
* 将dom按最小距离垂直地滚动至视窗内 |
||||
* 比如dom在视窗下,那么会滚动到视窗底部 |
||||
* |
||||
* @param child {HTMLElement} 需要滚动的dom |
||||
* @param parent {HTMLElement|Window} 包含child的容器 |
||||
*/ |
||||
export function scrollIntoViewVertically(child, parent = window) { |
||||
const {scrollTop, scrollHeight, offsetHeight: containerHeight} = parent |
||||
|
||||
//当菜单高度不足以滚动时跳过
|
||||
if (scrollHeight <= containerHeight) return |
||||
|
||||
const elHeight = child.offsetHeight, between = getTopDistance(child, parent) |
||||
|
||||
//计算需要滚动的距离,undefined说明不需要滚动
|
||||
let distance |
||||
|
||||
if (between < 0) distance = between |
||||
else if (between + elHeight > containerHeight) { |
||||
distance = between + elHeight - containerHeight |
||||
} |
||||
|
||||
if (distance !== undefined) { |
||||
parent.scrollTo({top: scrollTop + distance, behavior: 'smooth'}) |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* 平滑滚动至指定的位置 |
||||
* |
||||
* @param el {Window|HTMLElement|string|function} 滚动容器,或可用于querySelector的字符串,或一个返回DOM的函数 |
||||
* @param position {number} 滚动的目的地 |
||||
* @param options 配置项 |
||||
* @param options.callback {function} 滚动完成的回调 |
||||
* @param options.duration {number} 滚动耗时 |
||||
* @param options.direction {string} 滚动方向,top滚动至距元素顶部distance的位置,left滚动至距元素左边distance的位置 |
||||
*/ |
||||
export function scrollTo(el, position, options) { |
||||
const {callback, duration = 300, direction = 'top'} = options || {} |
||||
|
||||
if (typeof el === 'string') { |
||||
el = document.querySelector(el) |
||||
} |
||||
else if (typeof el === 'function') { |
||||
el = el() |
||||
} |
||||
|
||||
if (!isDom(el)) return |
||||
|
||||
const toTop = direction === 'top' |
||||
const elPosition = getScroll(el, toTop) |
||||
const scrollFunc = (el => { |
||||
if (el === window) { |
||||
return toTop |
||||
? y => el.scrollTo(window.pageXOffset, window.pageYOffset + y) |
||||
: x => el.scrollTo(window.pageXOffset + x, window.pageYOffset) |
||||
} |
||||
return toTop |
||||
? y => el.scrollTop += y |
||||
: x => el.scrollLeft += x |
||||
})(el) |
||||
|
||||
let times = duration / 16, distance = (position - elPosition) / (times + 1) |
||||
|
||||
const frameFunc = () => { |
||||
if (times > 0) { |
||||
scrollFunc(distance) |
||||
times-- |
||||
return window.requestAnimationFrame(frameFunc) |
||||
} |
||||
typeof callback === 'function' && callback() |
||||
} |
||||
|
||||
return window.requestAnimationFrame(frameFunc) |
||||
} |
||||
|
||||
/** |
||||
* 获取元素的滚动距离 |
||||
* |
||||
* @param el {Window|HTMLElement} |
||||
* @param top {boolean} 是否获取垂直的滚动距离 |
||||
* @return {number} |
||||
*/ |
||||
export function getScroll(el, top) { |
||||
return el === window |
||||
? el[top ? 'pageYOffset' : 'pageXOffset'] |
||||
: el[top ? 'scrollTop' : 'scrollLeft'] |
||||
} |
||||
@ -1,35 +0,0 @@ |
||||
import {scrollIntoViewVertically} from "@/util/browser" |
||||
|
||||
/** |
||||
* 将当前激活的菜单移动到视窗中(仅限垂直菜单) |
||||
* |
||||
* @param menu el-menu实例 |
||||
*/ |
||||
export function moveToActiveMenuVertically(menu) { |
||||
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(() => scrollIntoViewVertically(el, menu.$el), 300) |
||||
} |
||||
@ -1,22 +0,0 @@ |
||||
/** |
||||
* el-tree展开折叠控制 |
||||
* |
||||
* @param ref el-tree实例 |
||||
* @param action 'expand' | 'collapse' |
||||
* @param level 展开的节点最大深度,为0时匹配全部节点 |
||||
* @param func 自定义展开的函数,传入一个node,返回boolean,优先使用 |
||||
*/ |
||||
export function expandControl(ref, action = 'expand', level = 1, func) { |
||||
const handler = function () { |
||||
if (typeof func === 'function') { |
||||
return node => func(node) |
||||
} |
||||
|
||||
const expand = action === 'expand' |
||||
const forAll = level === 0 |
||||
|
||||
return node => node.expanded = forAll || node.level <= level ? expand : !expand |
||||
}() |
||||
|
||||
ref.store._getAllNodes().forEach(handler) |
||||
} |
||||
@ -1,36 +0,0 @@ |
||||
import {getInitialValue} from "@/util" |
||||
|
||||
//为Vue.observer返回的对象设置getter
|
||||
export function createGetters(store) { |
||||
const getters = Object.create({}) |
||||
Object.defineProperties( |
||||
getters, |
||||
Object.keys(store).reduce((obj, key) => { |
||||
obj[key] = { |
||||
enumerable: true, |
||||
get() { |
||||
return store[key] |
||||
} |
||||
} |
||||
return obj |
||||
}, {}) |
||||
) |
||||
return getters |
||||
} |
||||
|
||||
//设置mutation
|
||||
export function createMutations(store, all = false) { |
||||
const keys = Object.keys(store) |
||||
const obj = {} |
||||
keys.forEach(key => { |
||||
obj[key] = v => store[key] = v |
||||
}) |
||||
if (all) { |
||||
obj['$all'] = v => { |
||||
keys.forEach(key => { |
||||
store[key] = v && v[key] || getInitialValue(store[key]) |
||||
}) |
||||
} |
||||
} |
||||
return obj |
||||
} |
||||
@ -1,33 +0,0 @@ |
||||
/** |
||||
* 路由控制工具类 |
||||
*/ |
||||
|
||||
import {isEmpty} from "@/util" |
||||
import {isString} from "@/util/validate" |
||||
import router from '@/router' |
||||
import {mutations as tagsViewMutations} from "@/layout/store/tagsView" |
||||
|
||||
/** |
||||
* 路由刷新 |
||||
* |
||||
* @param route {string|route} 需要刷新的路由,不传时为当前路由,如果是字符串时请确保以'/'开头 |
||||
* @param replace {boolean} 是否使用replace进行跳转 |
||||
* @return {Promise} 返回vue-router跳转的结果 |
||||
*/ |
||||
export function refreshPage(route = router.currentRoute, replace = true) { |
||||
const target = `/redirect${isString(route) ? route : route.fullPath}` |
||||
return router[replace ? 'replace' : 'push'](target) |
||||
} |
||||
|
||||
/** |
||||
* 关闭当前页,如果传入next则跳转到next页面 |
||||
* |
||||
* @param next {string|route} 跳转的目标页面,作为第一个参数传入vue-router.replace |
||||
* @return {undefined|Promise} 仅在next有值时,返回vue-router.replace的结果 |
||||
*/ |
||||
export function closeCurrentPage(next) { |
||||
tagsViewMutations.delTagAndCache(router.currentRoute) |
||||
if (!isEmpty(next)) { |
||||
return router.replace(next) |
||||
} |
||||
} |
||||
@ -1,23 +0,0 @@ |
||||
/** |
||||
* 根据组件标签名称获取组件实例 |
||||
* 使用广度优先搜索 |
||||
* |
||||
* @param instance 从哪个组件实例开始查找,一般是this |
||||
* @param tag 要查找的组件的标签名称 |
||||
*/ |
||||
export function findComponentByTag(instance, tag) { |
||||
if (!instance || !tag) return |
||||
|
||||
const queue = [instance] |
||||
|
||||
while (queue.length > 0) { |
||||
const item = queue.shift() |
||||
const {$options, $children} = item |
||||
|
||||
if ($options._componentTag === tag) return item |
||||
|
||||
if (Array.isArray($children)) { |
||||
queue.push(...$children) |
||||
} |
||||
} |
||||
} |
||||
Loading…
Reference in new issue