43 changed files with 1793 additions and 213 deletions
@ -1 +0,0 @@ |
|||||
export { default as Menu } from './src/index.vue'; |
|
||||
@ -1,64 +0,0 @@ |
|||||
<template> |
|
||||
<ul :class="getClass" :style="getStyle"> |
|
||||
<slot></slot> |
|
||||
</ul> |
|
||||
</template> |
|
||||
|
|
||||
<script lang="ts"> |
|
||||
import { defineComponent, ref, computed, CSSProperties, unref } from 'vue'; |
|
||||
import { useDesign } from '/@/hooks/web/useDesign'; |
|
||||
import { propTypes } from '/@/utils/propTypes'; |
|
||||
export default defineComponent({ |
|
||||
props: { |
|
||||
mode: propTypes.oneOf(['horizontal', 'vertical']).def('vertical'), |
|
||||
theme: propTypes.oneOf(['light', 'dark', 'primary']).def('light'), |
|
||||
activeName: propTypes.oneOfType([propTypes.string, propTypes.number]), |
|
||||
openNames: propTypes.array.def([]), |
|
||||
accordion: propTypes.bool, |
|
||||
width: propTypes.string.def('210px'), |
|
||||
}, |
|
||||
setup(props) { |
|
||||
const currentActiveName = ref(props.activeName); |
|
||||
const openedNames = ref<string[]>(); |
|
||||
|
|
||||
const { prefixCls } = useDesign('menu'); |
|
||||
|
|
||||
const getClass = computed(() => { |
|
||||
const { theme, mode } = props; |
|
||||
let curTheme = theme; |
|
||||
if (mode === 'vertical' && theme === 'primary') { |
|
||||
curTheme = 'light'; |
|
||||
} |
|
||||
return [ |
|
||||
prefixCls, |
|
||||
`${prefixCls}-${curTheme}`, |
|
||||
{ |
|
||||
[`${prefixCls}-${mode}`]: mode, |
|
||||
}, |
|
||||
]; |
|
||||
}); |
|
||||
|
|
||||
const getStyle = computed( |
|
||||
(): CSSProperties => { |
|
||||
const { mode, width } = props; |
|
||||
if (mode === 'vertical') { |
|
||||
return { |
|
||||
width: width, |
|
||||
}; |
|
||||
} |
|
||||
return {}; |
|
||||
} |
|
||||
); |
|
||||
|
|
||||
function updateActiveName() { |
|
||||
if (unref(currentActiveName) === undefined) { |
|
||||
currentActiveName.value = -1; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
function updateOpened() {} |
|
||||
|
|
||||
return { getClass, getStyle }; |
|
||||
}, |
|
||||
}); |
|
||||
</script> |
|
||||
@ -0,0 +1 @@ |
|||||
|
export { default as SimpleMenu } from './src/SimpleMenu.vue'; |
||||
@ -0,0 +1,135 @@ |
|||||
|
<template> |
||||
|
<Menu |
||||
|
v-bind="getBindValues" |
||||
|
@select="handleSelect" |
||||
|
:activeName="activeName" |
||||
|
:openNames="openNames" |
||||
|
:class="prefixCls" |
||||
|
:activeSubMenuNames="activeSubMenuNames" |
||||
|
> |
||||
|
<template v-for="item in items" :key="item.path"> |
||||
|
<SimpleSubMenu |
||||
|
:item="item" |
||||
|
:parent="true" |
||||
|
:collapsedShowTitle="collapsedShowTitle" |
||||
|
:collapse="collapse" |
||||
|
/> |
||||
|
</template> |
||||
|
</Menu> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { PropType } from 'vue'; |
||||
|
import type { MenuState } from './types'; |
||||
|
import type { Menu as MenuType } from '/@/router/types'; |
||||
|
|
||||
|
import { defineComponent, computed, ref, unref, reactive, toRefs, watch } from 'vue'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
|
||||
|
import Menu from './components/Menu.vue'; |
||||
|
import SimpleSubMenu from './SimpleSubMenu.vue'; |
||||
|
import { listenerLastChangeTab } from '/@/logics/mitt/tabChange'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
import { REDIRECT_NAME } from '/@/router/constant'; |
||||
|
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'; |
||||
|
import { isFunction } from '/@/utils/is'; |
||||
|
|
||||
|
import { useOpenKeys } from './useOpenKeys'; |
||||
|
export default defineComponent({ |
||||
|
name: 'SimpleMenu', |
||||
|
inheritAttrs: false, |
||||
|
components: { |
||||
|
Menu, |
||||
|
SimpleSubMenu, |
||||
|
}, |
||||
|
props: { |
||||
|
items: { |
||||
|
type: Array as PropType<MenuType[]>, |
||||
|
default: () => [], |
||||
|
}, |
||||
|
collapse: propTypes.bool, |
||||
|
mixSider: propTypes.bool, |
||||
|
theme: propTypes.string, |
||||
|
accordion: propTypes.bool.def(true), |
||||
|
collapsedShowTitle: propTypes.bool, |
||||
|
beforeClickFn: { |
||||
|
type: Function as PropType<(key: string) => Promise<boolean>>, |
||||
|
}, |
||||
|
}, |
||||
|
setup(props, { attrs, emit }) { |
||||
|
const currentActiveMenu = ref(''); |
||||
|
const isClickGo = ref(false); |
||||
|
|
||||
|
const menuState = reactive<MenuState>({ |
||||
|
activeName: '', |
||||
|
openNames: [], |
||||
|
activeSubMenuNames: [], |
||||
|
}); |
||||
|
|
||||
|
const { currentRoute } = useRouter(); |
||||
|
const { prefixCls } = useDesign('simple-menu'); |
||||
|
const { items, accordion, mixSider } = toRefs(props); |
||||
|
const { setOpenKeys } = useOpenKeys(menuState, items, accordion, mixSider); |
||||
|
|
||||
|
const getBindValues = computed(() => ({ ...attrs, ...props })); |
||||
|
|
||||
|
watch( |
||||
|
() => props.collapse, |
||||
|
(collapse) => { |
||||
|
if (collapse) { |
||||
|
menuState.openNames = []; |
||||
|
} else { |
||||
|
setOpenKeys(currentRoute.value.path); |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
); |
||||
|
|
||||
|
listenerLastChangeTab((route) => { |
||||
|
if (route.name === REDIRECT_NAME) return; |
||||
|
|
||||
|
currentActiveMenu.value = route.meta?.currentActiveMenu; |
||||
|
handleMenuChange(route); |
||||
|
|
||||
|
if (unref(currentActiveMenu)) { |
||||
|
menuState.activeName = unref(currentActiveMenu); |
||||
|
setOpenKeys(unref(currentActiveMenu)); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
async function handleMenuChange(route?: RouteLocationNormalizedLoaded) { |
||||
|
if (unref(isClickGo)) { |
||||
|
isClickGo.value = false; |
||||
|
return; |
||||
|
} |
||||
|
const path = (route || unref(currentRoute)).path; |
||||
|
menuState.activeName = path; |
||||
|
|
||||
|
setOpenKeys(path); |
||||
|
// if (unref(currentActiveMenu)) return; |
||||
|
} |
||||
|
|
||||
|
async function handleSelect(key: string) { |
||||
|
const { beforeClickFn } = props; |
||||
|
if (beforeClickFn && isFunction(beforeClickFn)) { |
||||
|
const flag = await beforeClickFn(key); |
||||
|
if (!flag) return; |
||||
|
} |
||||
|
emit('menuClick', key); |
||||
|
|
||||
|
isClickGo.value = true; |
||||
|
setOpenKeys(key); |
||||
|
menuState.activeName = key; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
prefixCls, |
||||
|
getBindValues, |
||||
|
handleSelect, |
||||
|
...toRefs(menuState), |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
@import './index.less'; |
||||
|
</style> |
||||
@ -0,0 +1,70 @@ |
|||||
|
<template> |
||||
|
<span :class="getTagClass" v-if="getShowTag">{{ getContent }}</span> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { Menu } from '/@/router/types'; |
||||
|
import type { PropType } from 'vue'; |
||||
|
|
||||
|
import { defineComponent, computed } from 'vue'; |
||||
|
|
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'SimpleMenuTag', |
||||
|
props: { |
||||
|
item: { |
||||
|
type: Object as PropType<Menu>, |
||||
|
default: {}, |
||||
|
}, |
||||
|
collapseParent: { |
||||
|
type: Boolean as PropType<boolean>, |
||||
|
default: false, |
||||
|
}, |
||||
|
}, |
||||
|
setup(props) { |
||||
|
const { prefixCls } = useDesign('simple-menu'); |
||||
|
|
||||
|
const getShowTag = computed(() => { |
||||
|
const { item } = props; |
||||
|
|
||||
|
if (!item) return false; |
||||
|
|
||||
|
const { tag } = item; |
||||
|
if (!tag) return false; |
||||
|
|
||||
|
const { dot, content } = tag; |
||||
|
if (!dot && !content) return false; |
||||
|
return true; |
||||
|
}); |
||||
|
|
||||
|
const getContent = computed(() => { |
||||
|
if (!getShowTag.value) return ''; |
||||
|
const { item, collapseParent } = props; |
||||
|
const { tag } = item; |
||||
|
const { dot, content } = tag!; |
||||
|
return dot || collapseParent ? '' : content; |
||||
|
}); |
||||
|
|
||||
|
const getTagClass = computed(() => { |
||||
|
const { item, collapseParent } = props; |
||||
|
const { tag = {} } = item || {}; |
||||
|
const { dot, type = 'error' } = tag; |
||||
|
const tagCls = `${prefixCls}-tag`; |
||||
|
return [ |
||||
|
tagCls, |
||||
|
|
||||
|
[`${tagCls}--${type}`], |
||||
|
{ |
||||
|
[`${tagCls}--collapse`]: collapseParent, |
||||
|
[`${tagCls}--dot`]: dot, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
return { |
||||
|
getTagClass, |
||||
|
getShowTag, |
||||
|
getContent, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,115 @@ |
|||||
|
<template> |
||||
|
<MenuItem |
||||
|
:name="item.path" |
||||
|
v-if="!menuHasChildren(item) && getShowMenu" |
||||
|
v-bind="$props" |
||||
|
:class="getLevelClass" |
||||
|
> |
||||
|
<Icon v-if="getIcon" :icon="getIcon" :size="16" /> |
||||
|
<div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-1 collapse-title"> |
||||
|
{{ getI18nName }} |
||||
|
</div> |
||||
|
<template #title> |
||||
|
<span :class="['ml-2']"> |
||||
|
{{ getI18nName }} |
||||
|
</span> |
||||
|
<SimpleMenuTag :item="item" :collapseParent="getIsCollapseParent" /> |
||||
|
</template> |
||||
|
</MenuItem> |
||||
|
<SubMenu |
||||
|
:name="item.path" |
||||
|
v-if="menuHasChildren(item) && getShowMenu" |
||||
|
:class="[getLevelClass, theme]" |
||||
|
:collapsedShowTitle="collapsedShowTitle" |
||||
|
> |
||||
|
<template #title> |
||||
|
<Icon v-if="getIcon" :icon="getIcon" :size="16" /> |
||||
|
|
||||
|
<div v-if="collapsedShowTitle && getIsCollapseParent" class="mt-2 collapse-title"> |
||||
|
{{ getI18nName }} |
||||
|
</div> |
||||
|
|
||||
|
<span v-show="getShowSubTitle" :class="['ml-2', `${prefixCls}-sub-title`]"> |
||||
|
{{ getI18nName }} |
||||
|
</span> |
||||
|
<SimpleMenuTag :item="item" :collapseParent="!!collapse && !!parent" /> |
||||
|
</template> |
||||
|
<template v-for="childrenItem in item.children || []" :key="childrenItem.path"> |
||||
|
<SimpleSubMenu v-bind="$props" :item="childrenItem" :parent="false" /> |
||||
|
</template> |
||||
|
</SubMenu> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import type { PropType } from 'vue'; |
||||
|
import type { Menu } from '/@/router/types'; |
||||
|
|
||||
|
import { defineComponent, computed } from 'vue'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import Icon from '/@/components/Icon/index'; |
||||
|
|
||||
|
import MenuItem from './components/MenuItem.vue'; |
||||
|
import SubMenu from './components/SubMenuItem.vue'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
import { useI18n } from '/@/hooks/web/useI18n'; |
||||
|
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
||||
|
const { t } = useI18n(); |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'SimpleSubMenu', |
||||
|
components: { |
||||
|
SubMenu, |
||||
|
MenuItem, |
||||
|
SimpleMenuTag: createAsyncComponent(() => import('./SimpleMenuTag.vue')), |
||||
|
Icon, |
||||
|
}, |
||||
|
props: { |
||||
|
item: { |
||||
|
type: Object as PropType<Menu>, |
||||
|
default: {}, |
||||
|
}, |
||||
|
parent: propTypes.bool, |
||||
|
collapsedShowTitle: propTypes.bool, |
||||
|
collapse: propTypes.bool, |
||||
|
theme: propTypes.oneOf(['dark', 'light']), |
||||
|
}, |
||||
|
setup(props) { |
||||
|
const { prefixCls } = useDesign('simple-menu'); |
||||
|
|
||||
|
const getShowMenu = computed(() => { |
||||
|
return !props.item.meta?.hideMenu; |
||||
|
}); |
||||
|
|
||||
|
const getIcon = computed(() => props.item?.icon); |
||||
|
const getI18nName = computed(() => t(props.item?.name)); |
||||
|
const getShowSubTitle = computed(() => !props.collapse || !props.parent); |
||||
|
const getIsCollapseParent = computed(() => !!props.collapse && !!props.parent); |
||||
|
const getLevelClass = computed(() => { |
||||
|
return [ |
||||
|
{ |
||||
|
[`${prefixCls}__parent`]: props.parent, |
||||
|
[`${prefixCls}__children`]: !props.parent, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
|
||||
|
function menuHasChildren(menuTreeItem: Menu): boolean { |
||||
|
return ( |
||||
|
Reflect.has(menuTreeItem, 'children') && |
||||
|
!!menuTreeItem.children && |
||||
|
menuTreeItem.children.length > 0 |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
prefixCls, |
||||
|
menuHasChildren, |
||||
|
getShowMenu, |
||||
|
getIcon, |
||||
|
getI18nName, |
||||
|
getShowSubTitle, |
||||
|
getLevelClass, |
||||
|
getIsCollapseParent, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,149 @@ |
|||||
|
<template> |
||||
|
<ul :class="getClass"> |
||||
|
<slot></slot> |
||||
|
</ul> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import type { PropType } from 'vue'; |
||||
|
import type { SubMenuProvider } from './types'; |
||||
|
import { |
||||
|
defineComponent, |
||||
|
ref, |
||||
|
computed, |
||||
|
onMounted, |
||||
|
watchEffect, |
||||
|
watch, |
||||
|
nextTick, |
||||
|
getCurrentInstance, |
||||
|
provide, |
||||
|
} from 'vue'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
import { createSimpleRootMenuContext } from './useSimpleMenuContext'; |
||||
|
import Mitt from '/@/utils/mitt'; |
||||
|
import { isString } from '/@/utils/is'; |
||||
|
export default defineComponent({ |
||||
|
name: 'Menu', |
||||
|
props: { |
||||
|
theme: propTypes.oneOf(['light', 'dark']).def('light'), |
||||
|
activeName: propTypes.oneOfType([propTypes.string, propTypes.number]), |
||||
|
openNames: { |
||||
|
type: Array as PropType<string[]>, |
||||
|
default: [], |
||||
|
}, |
||||
|
accordion: propTypes.bool.def(true), |
||||
|
width: propTypes.string.def('100%'), |
||||
|
collapsedWidth: propTypes.string.def('48px'), |
||||
|
indentSize: propTypes.number.def(16), |
||||
|
collapse: propTypes.bool.def(true), |
||||
|
activeSubMenuNames: { |
||||
|
type: Array as PropType<(string | number)[]>, |
||||
|
default: [], |
||||
|
}, |
||||
|
}, |
||||
|
emits: ['select', 'open-change'], |
||||
|
setup(props, { emit }) { |
||||
|
const rootMenuEmitter = new Mitt(); |
||||
|
const instance = getCurrentInstance(); |
||||
|
|
||||
|
const currentActiveName = ref<string | number>(''); |
||||
|
const openedNames = ref<string[]>([]); |
||||
|
|
||||
|
const { prefixCls } = useDesign('menu'); |
||||
|
|
||||
|
const isRemoveAllPopup = ref(false); |
||||
|
|
||||
|
createSimpleRootMenuContext({ |
||||
|
rootMenuEmitter: rootMenuEmitter, |
||||
|
activeName: currentActiveName, |
||||
|
}); |
||||
|
|
||||
|
const getClass = computed(() => { |
||||
|
const { theme } = props; |
||||
|
return [ |
||||
|
prefixCls, |
||||
|
`${prefixCls}-${theme}`, |
||||
|
`${prefixCls}-vertical`, |
||||
|
{ |
||||
|
[`${prefixCls}-collapse`]: props.collapse, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
openedNames.value = props.openNames; |
||||
|
}); |
||||
|
|
||||
|
watchEffect(() => { |
||||
|
if (props.activeName) { |
||||
|
currentActiveName.value = props.activeName; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
watch( |
||||
|
() => props.openNames, |
||||
|
() => { |
||||
|
nextTick(() => { |
||||
|
updateOpened(); |
||||
|
}); |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
function updateOpened() { |
||||
|
rootMenuEmitter.emit('on-update-opened', openedNames.value); |
||||
|
} |
||||
|
|
||||
|
function addSubMenu(name: string) { |
||||
|
if (openedNames.value.includes(name)) return; |
||||
|
openedNames.value.push(name); |
||||
|
updateOpened(); |
||||
|
} |
||||
|
|
||||
|
function removeSubMenu(name: string) { |
||||
|
openedNames.value = openedNames.value.filter((item) => item !== name); |
||||
|
updateOpened(); |
||||
|
} |
||||
|
|
||||
|
function removeAll() { |
||||
|
openedNames.value = []; |
||||
|
updateOpened(); |
||||
|
} |
||||
|
|
||||
|
function sliceIndex(index: number) { |
||||
|
if (index === -1) return; |
||||
|
openedNames.value = openedNames.value.slice(0, index + 1); |
||||
|
updateOpened(); |
||||
|
} |
||||
|
|
||||
|
provide<SubMenuProvider>(`subMenu:${instance?.uid}`, { |
||||
|
addSubMenu, |
||||
|
removeSubMenu, |
||||
|
getOpenNames: () => openedNames.value, |
||||
|
removeAll, |
||||
|
isRemoveAllPopup, |
||||
|
sliceIndex, |
||||
|
level: 0, |
||||
|
props, |
||||
|
}); |
||||
|
|
||||
|
onMounted(() => { |
||||
|
openedNames.value = !props.collapse ? [...props.openNames] : []; |
||||
|
updateOpened(); |
||||
|
rootMenuEmitter.on('on-menu-item-select', (name: string) => { |
||||
|
currentActiveName.value = name; |
||||
|
|
||||
|
nextTick(() => { |
||||
|
props.collapse && removeAll(); |
||||
|
}); |
||||
|
emit('select', name); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
return { getClass, openedNames }; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
|
<style lang="less"> |
||||
|
@import './menu.less'; |
||||
|
</style> |
||||
@ -0,0 +1,78 @@ |
|||||
|
<template> |
||||
|
<transition mode="out-in" v-on="on"> |
||||
|
<slot></slot> |
||||
|
</transition> |
||||
|
</template> |
||||
|
<script lang="ts"> |
||||
|
import { defineComponent } from 'vue'; |
||||
|
import { addClass, removeClass } from '/@/utils/domUtils'; |
||||
|
|
||||
|
export default defineComponent({ |
||||
|
name: 'MenuCollapseTransition', |
||||
|
setup() { |
||||
|
return { |
||||
|
on: { |
||||
|
beforeEnter(el: any) { |
||||
|
addClass(el, 'collapse-transition'); |
||||
|
if (!el.dataset) el.dataset = {}; |
||||
|
|
||||
|
el.dataset.oldPaddingTop = el.style.paddingTop; |
||||
|
el.dataset.oldPaddingBottom = el.style.paddingBottom; |
||||
|
|
||||
|
el.style.height = '0'; |
||||
|
el.style.paddingTop = 0; |
||||
|
el.style.paddingBottom = 0; |
||||
|
}, |
||||
|
|
||||
|
enter(el: any) { |
||||
|
el.dataset.oldOverflow = el.style.overflow; |
||||
|
if (el.scrollHeight !== 0) { |
||||
|
el.style.height = el.scrollHeight + 'px'; |
||||
|
el.style.paddingTop = el.dataset.oldPaddingTop; |
||||
|
el.style.paddingBottom = el.dataset.oldPaddingBottom; |
||||
|
} else { |
||||
|
el.style.height = ''; |
||||
|
el.style.paddingTop = el.dataset.oldPaddingTop; |
||||
|
el.style.paddingBottom = el.dataset.oldPaddingBottom; |
||||
|
} |
||||
|
|
||||
|
el.style.overflow = 'hidden'; |
||||
|
}, |
||||
|
|
||||
|
afterEnter(el: any) { |
||||
|
removeClass(el, 'collapse-transition'); |
||||
|
el.style.height = ''; |
||||
|
el.style.overflow = el.dataset.oldOverflow; |
||||
|
}, |
||||
|
|
||||
|
beforeLeave(el: any) { |
||||
|
if (!el.dataset) el.dataset = {}; |
||||
|
el.dataset.oldPaddingTop = el.style.paddingTop; |
||||
|
el.dataset.oldPaddingBottom = el.style.paddingBottom; |
||||
|
el.dataset.oldOverflow = el.style.overflow; |
||||
|
|
||||
|
el.style.height = el.scrollHeight + 'px'; |
||||
|
el.style.overflow = 'hidden'; |
||||
|
}, |
||||
|
|
||||
|
leave(el: any) { |
||||
|
if (el.scrollHeight !== 0) { |
||||
|
addClass(el, 'collapse-transition'); |
||||
|
el.style.height = 0; |
||||
|
el.style.paddingTop = 0; |
||||
|
el.style.paddingBottom = 0; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
afterLeave(el: any) { |
||||
|
removeClass(el, 'collapse-transition'); |
||||
|
el.style.height = ''; |
||||
|
el.style.overflow = el.dataset.oldOverflow; |
||||
|
el.style.paddingTop = el.dataset.oldPaddingTop; |
||||
|
el.style.paddingBottom = el.dataset.oldPaddingBottom; |
||||
|
}, |
||||
|
}, |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,103 @@ |
|||||
|
<template> |
||||
|
<li :class="getClass" @click.stop="handleClickItem" :style="getCollapse ? {} : getItemStyle"> |
||||
|
<Tooltip placement="right" v-if="showTooptip"> |
||||
|
<template #title> |
||||
|
<slot name="title"></slot> |
||||
|
</template> |
||||
|
<div :class="`${prefixCls}-tooltip`"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</Tooltip> |
||||
|
|
||||
|
<template v-else> |
||||
|
<slot></slot> |
||||
|
<slot name="title"></slot> |
||||
|
</template> |
||||
|
</li> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import { PropType } from 'vue'; |
||||
|
import { defineComponent, ref, computed, unref, getCurrentInstance, watch } from 'vue'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
import { useMenuItem } from './useMenu'; |
||||
|
import { Tooltip } from 'ant-design-vue'; |
||||
|
import { useSimpleRootMenuContext } from './useSimpleMenuContext'; |
||||
|
export default defineComponent({ |
||||
|
name: 'MenuItem', |
||||
|
components: { Tooltip }, |
||||
|
props: { |
||||
|
name: { |
||||
|
type: [String, Number] as PropType<string | number>, |
||||
|
required: true, |
||||
|
}, |
||||
|
disabled: propTypes.bool, |
||||
|
}, |
||||
|
setup(props, { slots }) { |
||||
|
const instance = getCurrentInstance(); |
||||
|
|
||||
|
const active = ref(false); |
||||
|
|
||||
|
const { getItemStyle, getParentList, getParentMenu, getParentRootMenu } = useMenuItem( |
||||
|
instance |
||||
|
); |
||||
|
|
||||
|
const { prefixCls } = useDesign('menu'); |
||||
|
|
||||
|
const { rootMenuEmitter, activeName } = useSimpleRootMenuContext(); |
||||
|
|
||||
|
const getClass = computed(() => { |
||||
|
return [ |
||||
|
`${prefixCls}-item`, |
||||
|
{ |
||||
|
[`${prefixCls}-item-active`]: unref(active), |
||||
|
[`${prefixCls}-item-selected`]: unref(active), |
||||
|
[`${prefixCls}-item-disabled`]: !!props.disabled, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
|
||||
|
const getCollapse = computed(() => unref(getParentRootMenu)?.props.collapse); |
||||
|
|
||||
|
const showTooptip = computed(() => { |
||||
|
return unref(getParentMenu)?.type.name === 'Menu' && unref(getCollapse) && slots.title; |
||||
|
}); |
||||
|
|
||||
|
function handleClickItem() { |
||||
|
const { disabled } = props; |
||||
|
if (disabled) return; |
||||
|
|
||||
|
rootMenuEmitter.emit('on-menu-item-select', props.name); |
||||
|
if (unref(getCollapse)) return; |
||||
|
const { uidList } = getParentList(); |
||||
|
rootMenuEmitter.emit('on-update-opened', { |
||||
|
opend: false, |
||||
|
parent: instance?.parent, |
||||
|
uidList: uidList, |
||||
|
}); |
||||
|
} |
||||
|
watch( |
||||
|
() => activeName.value, |
||||
|
(name: string) => { |
||||
|
if (name === props.name) { |
||||
|
const { list, uidList } = getParentList(); |
||||
|
active.value = true; |
||||
|
list.forEach((item) => { |
||||
|
if (item.proxy) { |
||||
|
(item.proxy as any).active = true; |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
rootMenuEmitter.emit('on-update-active-name:submenu', uidList); |
||||
|
} else { |
||||
|
active.value = false; |
||||
|
} |
||||
|
}, |
||||
|
{ immediate: true } |
||||
|
); |
||||
|
|
||||
|
return { getClass, prefixCls, getItemStyle, getCollapse, handleClickItem, showTooptip }; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,329 @@ |
|||||
|
<template> |
||||
|
<li :class="getClass"> |
||||
|
<template v-if="!getCollapse"> |
||||
|
<div :class="`${prefixCls}-submenu-title`" @click.stop="handleClick" :style="getItemStyle"> |
||||
|
<slot name="title"></slot> |
||||
|
<Icon |
||||
|
icon="eva:arrow-ios-downward-outline" |
||||
|
:size="14" |
||||
|
:class="`${prefixCls}-submenu-title-icon`" |
||||
|
/> |
||||
|
</div> |
||||
|
<MenuCollapseTransition> |
||||
|
<ul :class="prefixCls" v-show="opened"> |
||||
|
<slot></slot> |
||||
|
</ul> |
||||
|
</MenuCollapseTransition> |
||||
|
</template> |
||||
|
|
||||
|
<Popover |
||||
|
placement="right" |
||||
|
:overlayClassName="`${prefixCls}-menu-popover`" |
||||
|
v-else |
||||
|
:visible="getIsOpend" |
||||
|
@visibleChange="handleVisibleChange" |
||||
|
:overlayStyle="getOverlayStyle" |
||||
|
:align="{ offset: [0, 0] }" |
||||
|
> |
||||
|
<div :class="getSubClass" v-bind="getEvents(false)"> |
||||
|
<div |
||||
|
:class="[ |
||||
|
{ |
||||
|
[`${prefixCls}-submenu-popup`]: !getParentSubMenu, |
||||
|
[`${prefixCls}-submenu-collapsed-show-tit`]: collapsedShowTitle, |
||||
|
}, |
||||
|
]" |
||||
|
> |
||||
|
<slot name="title"></slot> |
||||
|
</div> |
||||
|
<Icon |
||||
|
v-if="getParentSubMenu" |
||||
|
icon="eva:arrow-ios-downward-outline" |
||||
|
:size="14" |
||||
|
:class="`${prefixCls}-submenu-title-icon`" |
||||
|
/> |
||||
|
</div> |
||||
|
<template #content v-show="opened"> |
||||
|
<div v-bind="getEvents(true)"> |
||||
|
<ul :class="[prefixCls, `${prefixCls}-${getTheme}`, `${prefixCls}-popup`]"> |
||||
|
<slot></slot> |
||||
|
</ul> |
||||
|
</div> |
||||
|
</template> |
||||
|
</Popover> |
||||
|
</li> |
||||
|
</template> |
||||
|
|
||||
|
<script lang="ts"> |
||||
|
import type { CSSProperties, PropType } from 'vue'; |
||||
|
import type { SubMenuProvider } from './types'; |
||||
|
import { |
||||
|
defineComponent, |
||||
|
computed, |
||||
|
unref, |
||||
|
getCurrentInstance, |
||||
|
toRefs, |
||||
|
reactive, |
||||
|
provide, |
||||
|
onBeforeMount, |
||||
|
inject, |
||||
|
} from 'vue'; |
||||
|
import { useDesign } from '/@/hooks/web/useDesign'; |
||||
|
import { propTypes } from '/@/utils/propTypes'; |
||||
|
import { useMenuItem } from './useMenu'; |
||||
|
import { useSimpleRootMenuContext } from './useSimpleMenuContext'; |
||||
|
import MenuCollapseTransition from './MenuCollapseTransition.vue'; |
||||
|
import Icon from '/@/components/Icon'; |
||||
|
import { Popover } from 'ant-design-vue'; |
||||
|
import { isBoolean, isObject } from '/@/utils/is'; |
||||
|
import Mitt from '/@/utils/mitt'; |
||||
|
|
||||
|
const DELAY = 200; |
||||
|
export default defineComponent({ |
||||
|
name: 'SubMenu', |
||||
|
components: { |
||||
|
Icon, |
||||
|
MenuCollapseTransition, |
||||
|
Popover, |
||||
|
}, |
||||
|
props: { |
||||
|
name: { |
||||
|
type: [String, Number] as PropType<string | number>, |
||||
|
required: true, |
||||
|
}, |
||||
|
disabled: propTypes.bool, |
||||
|
collapsedShowTitle: propTypes.bool, |
||||
|
}, |
||||
|
setup(props) { |
||||
|
const instance = getCurrentInstance(); |
||||
|
|
||||
|
const state = reactive({ |
||||
|
active: false, |
||||
|
opened: false, |
||||
|
}); |
||||
|
|
||||
|
const data = reactive({ |
||||
|
timeout: null as TimeoutHandle | null, |
||||
|
mouseInChild: false, |
||||
|
isChild: false, |
||||
|
}); |
||||
|
|
||||
|
const { getParentSubMenu, getItemStyle, getParentMenu, getParentList } = useMenuItem( |
||||
|
instance |
||||
|
); |
||||
|
|
||||
|
const { prefixCls } = useDesign('menu'); |
||||
|
|
||||
|
const subMenuEmitter = new Mitt(); |
||||
|
|
||||
|
const { rootMenuEmitter } = useSimpleRootMenuContext(); |
||||
|
|
||||
|
const { |
||||
|
addSubMenu: parentAddSubmenu, |
||||
|
removeSubMenu: parentRemoveSubmenu, |
||||
|
removeAll: parentRemoveAll, |
||||
|
getOpenNames: parentGetOpenNames, |
||||
|
isRemoveAllPopup, |
||||
|
sliceIndex, |
||||
|
level, |
||||
|
props: rootProps, |
||||
|
handleMouseleave: parentHandleMouseleave, |
||||
|
} = inject<SubMenuProvider>(`subMenu:${getParentMenu.value?.uid}`)!; |
||||
|
|
||||
|
const getClass = computed(() => { |
||||
|
return [ |
||||
|
`${prefixCls}-submenu`, |
||||
|
{ |
||||
|
[`${prefixCls}-item-active`]: state.active, |
||||
|
[`${prefixCls}-opened`]: state.opened, |
||||
|
[`${prefixCls}-submenu-disabled`]: props.disabled, |
||||
|
[`${prefixCls}-submenu-has-parent-submenu`]: unref(getParentSubMenu), |
||||
|
[`${prefixCls}-child-item-active`]: state.active, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
|
||||
|
const getAccordion = computed(() => rootProps.accordion); |
||||
|
const getCollapse = computed(() => rootProps.collapse); |
||||
|
const getTheme = computed(() => rootProps.theme); |
||||
|
|
||||
|
const getOverlayStyle = computed( |
||||
|
(): CSSProperties => { |
||||
|
return { |
||||
|
minWidth: '200px', |
||||
|
}; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
const getIsOpend = computed(() => { |
||||
|
const name = props.name; |
||||
|
if (unref(getCollapse)) { |
||||
|
return parentGetOpenNames().includes(name); |
||||
|
} |
||||
|
return state.opened; |
||||
|
}); |
||||
|
|
||||
|
const getSubClass = computed(() => { |
||||
|
const isActive = rootProps.activeSubMenuNames.includes(props.name); |
||||
|
return [ |
||||
|
`${prefixCls}-submenu-title`, |
||||
|
{ |
||||
|
[`${prefixCls}-submenu-active`]: isActive, |
||||
|
[`${prefixCls}-submenu-active-border`]: isActive && level === 0, |
||||
|
[`${prefixCls}-submenu-collapse`]: unref(getCollapse) && level === 0, |
||||
|
}, |
||||
|
]; |
||||
|
}); |
||||
|
|
||||
|
function getEvents(deep: boolean) { |
||||
|
if (!unref(getCollapse)) { |
||||
|
return {}; |
||||
|
} |
||||
|
return { |
||||
|
onMouseenter: handleMouseenter, |
||||
|
onMouseleave: () => handleMouseleave(deep), |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function handleClick() { |
||||
|
const { disabled } = props; |
||||
|
if (disabled || unref(getCollapse)) return; |
||||
|
const opened = state.opened; |
||||
|
if (unref(getAccordion)) { |
||||
|
const { uidList } = getParentList(); |
||||
|
rootMenuEmitter.emit('on-update-opened', { |
||||
|
opend: false, |
||||
|
parent: instance?.parent, |
||||
|
uidList: uidList, |
||||
|
}); |
||||
|
} |
||||
|
state.opened = !opened; |
||||
|
} |
||||
|
|
||||
|
function handleMouseenter() { |
||||
|
const disabled = props.disabled; |
||||
|
if (disabled) return; |
||||
|
|
||||
|
subMenuEmitter.emit('submenu:mouse-enter-child'); |
||||
|
|
||||
|
const index = parentGetOpenNames().findIndex((item) => item === props.name); |
||||
|
|
||||
|
sliceIndex(index); |
||||
|
|
||||
|
const isRoot = level === 0 && parentGetOpenNames().length === 2; |
||||
|
if (isRoot) { |
||||
|
parentRemoveAll(); |
||||
|
} |
||||
|
data.isChild = parentGetOpenNames().includes(props.name); |
||||
|
clearTimeout(data.timeout!); |
||||
|
data.timeout = setTimeout(() => { |
||||
|
parentAddSubmenu(props.name); |
||||
|
}, DELAY); |
||||
|
} |
||||
|
|
||||
|
function handleMouseleave(deepDispatch = false) { |
||||
|
const parentName = getParentMenu.value?.props.name; |
||||
|
if (!parentName) { |
||||
|
isRemoveAllPopup.value = true; |
||||
|
} |
||||
|
|
||||
|
if (parentGetOpenNames().slice(-1)[0] === props.name) { |
||||
|
data.isChild = false; |
||||
|
} |
||||
|
|
||||
|
subMenuEmitter.emit('submenu:mouse-leave-child'); |
||||
|
if (data.timeout) { |
||||
|
clearTimeout(data.timeout!); |
||||
|
data.timeout = setTimeout(() => { |
||||
|
if (isRemoveAllPopup.value) { |
||||
|
parentRemoveAll(); |
||||
|
} else if (!data.mouseInChild) { |
||||
|
parentRemoveSubmenu(props.name); |
||||
|
} |
||||
|
}, DELAY); |
||||
|
} |
||||
|
if (deepDispatch) { |
||||
|
if (getParentSubMenu.value) { |
||||
|
parentHandleMouseleave?.(true); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
onBeforeMount(() => { |
||||
|
subMenuEmitter.on('submenu:mouse-enter-child', () => { |
||||
|
data.mouseInChild = true; |
||||
|
isRemoveAllPopup.value = false; |
||||
|
clearTimeout(data.timeout!); |
||||
|
}); |
||||
|
subMenuEmitter.on('submenu:mouse-leave-child', () => { |
||||
|
if (data.isChild) return; |
||||
|
data.mouseInChild = false; |
||||
|
clearTimeout(data.timeout!); |
||||
|
}); |
||||
|
|
||||
|
rootMenuEmitter.on( |
||||
|
'on-update-opened', |
||||
|
(data: boolean | (string | number)[] | Recordable) => { |
||||
|
if (unref(getCollapse)) return; |
||||
|
if (isBoolean(data)) { |
||||
|
state.opened = data; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (isObject(data)) { |
||||
|
const { opend, parent, uidList } = data as Recordable; |
||||
|
if (parent === instance?.parent) { |
||||
|
state.opened = opend; |
||||
|
} else if (!uidList.includes(instance?.uid)) { |
||||
|
state.opened = false; |
||||
|
} |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (props.name && Array.isArray(data)) { |
||||
|
state.opened = (data as (string | number)[]).includes(props.name); |
||||
|
} |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
rootMenuEmitter.on('on-update-active-name:submenu', (data: number[]) => { |
||||
|
state.active = data.includes(instance?.uid!); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
function handleVisibleChange(visible: boolean) { |
||||
|
state.opened = visible; |
||||
|
} |
||||
|
|
||||
|
// provide |
||||
|
provide<SubMenuProvider>(`subMenu:${instance?.uid}`, { |
||||
|
addSubMenu: parentAddSubmenu, |
||||
|
removeSubMenu: parentRemoveSubmenu, |
||||
|
getOpenNames: parentGetOpenNames, |
||||
|
removeAll: parentRemoveAll, |
||||
|
isRemoveAllPopup, |
||||
|
sliceIndex, |
||||
|
level: level + 1, |
||||
|
handleMouseleave, |
||||
|
props: rootProps, |
||||
|
}); |
||||
|
|
||||
|
return { |
||||
|
getClass, |
||||
|
prefixCls, |
||||
|
getCollapse, |
||||
|
getItemStyle, |
||||
|
handleClick, |
||||
|
handleVisibleChange, |
||||
|
getParentSubMenu, |
||||
|
getOverlayStyle, |
||||
|
getTheme, |
||||
|
getIsOpend, |
||||
|
getEvents, |
||||
|
getSubClass, |
||||
|
...toRefs(state), |
||||
|
...toRefs(data), |
||||
|
}; |
||||
|
}, |
||||
|
}); |
||||
|
</script> |
||||
@ -0,0 +1,332 @@ |
|||||
|
@menu-prefix-cls: ~'@{namespace}-menu'; |
||||
|
@menu-popup-prefix-cls: ~'@{namespace}-menu-popup'; |
||||
|
@submenu-popup-prefix-cls: ~'@{namespace}-menu-submenu-popup'; |
||||
|
|
||||
|
// @menu-dark: #191a23; |
||||
|
// @menu-dark-active-bg: #101117; |
||||
|
@transition-time: 0.2s; |
||||
|
@menu-dark-subsidiary-color: rgba(255, 255, 255, 0.7); |
||||
|
|
||||
|
.light-border { |
||||
|
&::after { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
right: 0; |
||||
|
bottom: 0; |
||||
|
display: block; |
||||
|
width: 2px; |
||||
|
background: @primary-color; |
||||
|
content: ''; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.@{menu-prefix-cls}-menu-popover { |
||||
|
.ant-popover-arrow { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
.ant-popover-inner-content { |
||||
|
padding: 0; |
||||
|
} |
||||
|
|
||||
|
.@{menu-prefix-cls} { |
||||
|
&-opened > * > &-submenu-title-icon { |
||||
|
transform: translateY(-50%) rotate(90deg) !important; |
||||
|
} |
||||
|
|
||||
|
&-item, |
||||
|
&-submenu-title { |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
padding: 12px 20px; |
||||
|
color: @menu-dark-subsidiary-color; |
||||
|
cursor: pointer; |
||||
|
transition: all @transition-time @ease-in-out; |
||||
|
|
||||
|
// &:hover { |
||||
|
// color: @primary-color; |
||||
|
// } |
||||
|
|
||||
|
&-icon { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
right: 18px; |
||||
|
transform: translateY(-50%) rotate(-90deg); |
||||
|
transition: transform @transition-time @ease-in-out; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-dark { |
||||
|
.@{menu-prefix-cls}-item, |
||||
|
.@{menu-prefix-cls}-submenu-title { |
||||
|
color: @menu-dark-subsidiary-color; |
||||
|
// background: @menu-dark-active-bg; |
||||
|
|
||||
|
&:hover { |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
&-selected { |
||||
|
color: #fff; |
||||
|
background: @primary-color !important; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-light { |
||||
|
.@{menu-prefix-cls}-item, |
||||
|
.@{menu-prefix-cls}-submenu-title { |
||||
|
color: @text-color-base; |
||||
|
|
||||
|
&:hover { |
||||
|
color: @primary-color; |
||||
|
} |
||||
|
|
||||
|
&-selected { |
||||
|
z-index: 2; |
||||
|
color: @primary-color; |
||||
|
background: fade(@primary-color, 8); |
||||
|
|
||||
|
.light-border(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.content(); |
||||
|
.content() { |
||||
|
.@{menu-prefix-cls} { |
||||
|
position: relative; |
||||
|
display: block; |
||||
|
width: 100%; |
||||
|
padding: 0; |
||||
|
margin: 0; |
||||
|
font-size: @font-size-base; |
||||
|
color: @text-color-base; |
||||
|
list-style: none; |
||||
|
outline: none; |
||||
|
|
||||
|
.collapse-transition { |
||||
|
transition: @transition-time height ease-in-out, @transition-time padding-top ease-in-out, |
||||
|
@transition-time padding-bottom ease-in-out; |
||||
|
} |
||||
|
|
||||
|
&-light { |
||||
|
background: #fff; |
||||
|
|
||||
|
.@{menu-prefix-cls}-submenu-active { |
||||
|
color: @primary-color !important; |
||||
|
// background: fade(@primary-color, 8); |
||||
|
|
||||
|
&-border { |
||||
|
.light-border(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-dark { |
||||
|
// background: @menu-dark; |
||||
|
|
||||
|
.@{menu-prefix-cls}-submenu-active { |
||||
|
color: #fff !important; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-item { |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
display: flex; |
||||
|
font-size: @font-size-base; |
||||
|
color: inherit; |
||||
|
list-style: none; |
||||
|
cursor: pointer; |
||||
|
outline: none; |
||||
|
align-items: center; |
||||
|
// transition: all @transition-time @ease-in-out; |
||||
|
|
||||
|
&:hover, |
||||
|
&:active { |
||||
|
color: inherit; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-item > i { |
||||
|
margin-right: 6px; |
||||
|
} |
||||
|
|
||||
|
&-submenu-title > i, |
||||
|
&-submenu-title span > i { |
||||
|
margin-right: 8px; |
||||
|
} |
||||
|
|
||||
|
// vertical |
||||
|
&-vertical &-item, |
||||
|
&-vertical &-submenu-title { |
||||
|
position: relative; |
||||
|
z-index: 1; |
||||
|
padding: 12px 24px; |
||||
|
cursor: pointer; |
||||
|
// transition: all @transition-time @ease-in-out; |
||||
|
|
||||
|
&:hover { |
||||
|
color: @primary-color; |
||||
|
} |
||||
|
|
||||
|
.@{menu-prefix-cls}-tooltip { |
||||
|
width: calc(100% - 0px); |
||||
|
padding: 12px 0; |
||||
|
text-align: center; |
||||
|
} |
||||
|
.@{menu-prefix-cls}-submenu-popup { |
||||
|
padding: 12px 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-vertical &-submenu-collapse { |
||||
|
.@{submenu-popup-prefix-cls} { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.@{menu-prefix-cls}-submenu-collapsed-show-tit { |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-vertical&-collapse &-item, |
||||
|
&-vertical&-collapse &-submenu-title { |
||||
|
padding: 0 0; |
||||
|
} |
||||
|
|
||||
|
&-vertical &-submenu-title-icon { |
||||
|
position: absolute; |
||||
|
top: 50%; |
||||
|
right: 18px; |
||||
|
transform: translateY(-50%); |
||||
|
} |
||||
|
|
||||
|
&-submenu-title-icon { |
||||
|
transition: transform @transition-time @ease-in-out; |
||||
|
} |
||||
|
|
||||
|
&-vertical &-opened > * > &-submenu-title-icon { |
||||
|
transform: translateY(-50%) rotate(180deg); |
||||
|
} |
||||
|
|
||||
|
&-vertical &-submenu { |
||||
|
&-nested { |
||||
|
padding-left: 20px; |
||||
|
} |
||||
|
.@{menu-prefix-cls}-item { |
||||
|
padding-left: 43px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-light&-vertical &-item { |
||||
|
&-active:not(.@{menu-prefix-cls}-submenu) { |
||||
|
z-index: 2; |
||||
|
color: @primary-color; |
||||
|
background: fade(@primary-color, 8); |
||||
|
|
||||
|
.light-border(); |
||||
|
} |
||||
|
&-active.@{menu-prefix-cls}-submenu { |
||||
|
color: @primary-color; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-light&-vertical&-collapse { |
||||
|
> li.@{menu-prefix-cls}-item-active, |
||||
|
.@{menu-prefix-cls}-submenu-active { |
||||
|
position: relative; |
||||
|
background: fade(@primary-color, 3); |
||||
|
|
||||
|
&::after { |
||||
|
display: none; |
||||
|
} |
||||
|
|
||||
|
&::before { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 3px; |
||||
|
height: 100%; |
||||
|
background: @primary-color; |
||||
|
content: ''; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-dark&-vertical &-item, |
||||
|
&-dark&-vertical &-submenu-title { |
||||
|
color: @menu-dark-subsidiary-color; |
||||
|
&-active:not(.@{menu-prefix-cls}-submenu) { |
||||
|
color: #fff !important; |
||||
|
background: @primary-color !important; |
||||
|
} |
||||
|
|
||||
|
&:hover { |
||||
|
color: #fff; |
||||
|
// background: @menu-dark; |
||||
|
} |
||||
|
|
||||
|
// &-active:not(.@{menu-prefix-cls}-submenu) { |
||||
|
// color: @primary-color; |
||||
|
// } |
||||
|
} |
||||
|
|
||||
|
&-dark&-vertical&-collapse { |
||||
|
> li.@{menu-prefix-cls}-item-active, |
||||
|
.@{menu-prefix-cls}-submenu-active { |
||||
|
position: relative; |
||||
|
color: #fff !important; |
||||
|
background-color: @sider-dark-darken-bg-color !important; |
||||
|
|
||||
|
&::before { |
||||
|
position: absolute; |
||||
|
top: 0; |
||||
|
left: 0; |
||||
|
width: 3px; |
||||
|
height: 100%; |
||||
|
background: @primary-color; |
||||
|
content: ''; |
||||
|
} |
||||
|
|
||||
|
.@{menu-prefix-cls}-submenu-collapse { |
||||
|
background-color: transparent; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-dark&-vertical &-submenu &-item { |
||||
|
// &:hover { |
||||
|
// color: #fff; |
||||
|
// background: transparent; |
||||
|
// } |
||||
|
|
||||
|
&-active, |
||||
|
&-active:hover { |
||||
|
color: #fff; |
||||
|
border-right: none; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-dark&-vertical &-child-item-active > &-submenu-title { |
||||
|
color: #fff; |
||||
|
} |
||||
|
|
||||
|
&-dark&-vertical &-opened { |
||||
|
// background: @menu-dark-active-bg; |
||||
|
// .@{menu-prefix-cls}-submenu-title { |
||||
|
// background: @menu-dark; |
||||
|
// } |
||||
|
|
||||
|
.@{menu-prefix-cls}-submenu-has-parent-submenu { |
||||
|
.@{menu-prefix-cls}-submenu-title { |
||||
|
background: transparent; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
import { Ref } from 'vue'; |
||||
|
|
||||
|
export interface Props { |
||||
|
theme: string; |
||||
|
activeName?: string | number | undefined; |
||||
|
openNames: string[]; |
||||
|
accordion: boolean; |
||||
|
width: string; |
||||
|
collapsedWidth: string; |
||||
|
indentSize: number; |
||||
|
collapse: boolean; |
||||
|
activeSubMenuNames: (string | number)[]; |
||||
|
} |
||||
|
|
||||
|
export interface SubMenuProvider { |
||||
|
addSubMenu: (name: string | number, update?: boolean) => void; |
||||
|
removeSubMenu: (name: string | number, update?: boolean) => void; |
||||
|
removeAll: () => void; |
||||
|
sliceIndex: (index: number) => void; |
||||
|
isRemoveAllPopup: Ref<boolean>; |
||||
|
getOpenNames: () => (string | number)[]; |
||||
|
handleMouseleave?: Fn; |
||||
|
level: number; |
||||
|
props: Props; |
||||
|
} |
||||
@ -0,0 +1,86 @@ |
|||||
|
import { computed, ComponentInternalInstance, unref } from 'vue'; |
||||
|
import type { CSSProperties } from 'vue'; |
||||
|
|
||||
|
export function useMenuItem(instance: ComponentInternalInstance | null) { |
||||
|
const getParentMenu = computed(() => { |
||||
|
return findParentMenu(['Menu', 'SubMenu']); |
||||
|
}); |
||||
|
|
||||
|
const getParentRootMenu = computed(() => { |
||||
|
return findParentMenu(['Menu']); |
||||
|
}); |
||||
|
|
||||
|
const getParentSubMenu = computed(() => { |
||||
|
return findParentMenu(['SubMenu']); |
||||
|
}); |
||||
|
|
||||
|
const getItemStyle = computed( |
||||
|
(): CSSProperties => { |
||||
|
let parent = instance?.parent; |
||||
|
if (!parent) return {}; |
||||
|
const indentSize = (unref(getParentRootMenu)?.props.indentSize as number) ?? 20; |
||||
|
let padding = indentSize; |
||||
|
|
||||
|
if (unref(getParentRootMenu)?.props.collapse) { |
||||
|
padding = indentSize; |
||||
|
} else { |
||||
|
while (parent && parent.type.name !== 'Menu') { |
||||
|
if (parent.type.name === 'SubMenu') { |
||||
|
padding += indentSize; |
||||
|
} |
||||
|
parent = parent.parent; |
||||
|
} |
||||
|
} |
||||
|
return { paddingLeft: padding + 'px' }; |
||||
|
} |
||||
|
); |
||||
|
|
||||
|
function findParentMenu(name: string[]) { |
||||
|
let parent = instance?.parent; |
||||
|
if (!parent) return null; |
||||
|
while (parent && name.indexOf(parent.type.name!) === -1) { |
||||
|
parent = parent.parent; |
||||
|
} |
||||
|
return parent; |
||||
|
} |
||||
|
|
||||
|
function getParentList() { |
||||
|
let parent = instance; |
||||
|
if (!parent) |
||||
|
return { |
||||
|
uidList: [], |
||||
|
list: [], |
||||
|
}; |
||||
|
const ret = []; |
||||
|
while (parent && parent.type.name !== 'Menu') { |
||||
|
if (parent.type.name === 'SubMenu') { |
||||
|
ret.push(parent); |
||||
|
} |
||||
|
parent = parent.parent; |
||||
|
} |
||||
|
return { |
||||
|
uidList: ret.map((item) => item.uid), |
||||
|
list: ret, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
function getParentInstance(instance: ComponentInternalInstance, name = 'SubMenu') { |
||||
|
let parent = instance.parent; |
||||
|
while (parent) { |
||||
|
if (parent.type.name !== name) { |
||||
|
return parent; |
||||
|
} |
||||
|
parent = parent.parent; |
||||
|
} |
||||
|
return parent; |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
getParentMenu, |
||||
|
getParentInstance, |
||||
|
getParentRootMenu, |
||||
|
getParentList, |
||||
|
getParentSubMenu, |
||||
|
getItemStyle, |
||||
|
}; |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
import type { InjectionKey, Ref } from 'vue'; |
||||
|
import { createContext, useContext } from '/@/hooks/core/useContext'; |
||||
|
import Mitt from '/@/utils/mitt'; |
||||
|
|
||||
|
export interface SimpleRootMenuContextProps { |
||||
|
rootMenuEmitter: Mitt; |
||||
|
activeName: Ref<string | number>; |
||||
|
} |
||||
|
|
||||
|
const key: InjectionKey<SimpleRootMenuContextProps> = Symbol(); |
||||
|
|
||||
|
export function createSimpleRootMenuContext(context: SimpleRootMenuContextProps) { |
||||
|
return createContext<SimpleRootMenuContextProps>(context, key, { readonly: false, native: true }); |
||||
|
} |
||||
|
|
||||
|
export function useSimpleRootMenuContext() { |
||||
|
return useContext<SimpleRootMenuContextProps>(key); |
||||
|
} |
||||
@ -0,0 +1,67 @@ |
|||||
|
@simple-prefix-cls: ~'@{namespace}-simple-menu'; |
||||
|
@prefix-cls: ~'@{namespace}-menu'; |
||||
|
|
||||
|
.@{prefix-cls} { |
||||
|
&-dark&-vertical .@{simple-prefix-cls}__parent { |
||||
|
background-color: @sider-dark-bg-color; |
||||
|
> .@{prefix-cls}-submenu-title { |
||||
|
background-color: @sider-dark-bg-color; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
&-dark&-vertical .@{simple-prefix-cls}__children, |
||||
|
&-dark&-popup .@{simple-prefix-cls}__children { |
||||
|
background-color: @sider-dark-lighten-1-bg-color; |
||||
|
> .@{prefix-cls}-submenu-title { |
||||
|
background-color: @sider-dark-lighten-1-bg-color; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.collapse-title { |
||||
|
font-size: 12px; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
.@{simple-prefix-cls} { |
||||
|
&-tag { |
||||
|
position: absolute; |
||||
|
top: calc(50% - 10px); |
||||
|
right: 30px; |
||||
|
display: inline-block; |
||||
|
padding: 2px 3px; |
||||
|
margin-right: 4px; |
||||
|
font-size: 10px; |
||||
|
line-height: 14px; |
||||
|
color: #fff; |
||||
|
border-radius: 2px; |
||||
|
|
||||
|
&--collapse { |
||||
|
top: 6px !important; |
||||
|
right: 2px; |
||||
|
} |
||||
|
|
||||
|
&--dot { |
||||
|
top: calc(50% - 4px); |
||||
|
width: 6px; |
||||
|
height: 6px; |
||||
|
padding: 0; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
|
||||
|
&--primary { |
||||
|
background: @primary-color; |
||||
|
} |
||||
|
|
||||
|
&--error { |
||||
|
background: @error-color; |
||||
|
} |
||||
|
|
||||
|
&--success { |
||||
|
background: @success-color; |
||||
|
} |
||||
|
|
||||
|
&--warn { |
||||
|
background: @warning-color; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
export interface MenuState { |
||||
|
activeName: string; |
||||
|
openNames: string[]; |
||||
|
activeSubMenuNames: string[]; |
||||
|
} |
||||
@ -0,0 +1,45 @@ |
|||||
|
import type { Menu as MenuType } from '/@/router/types'; |
||||
|
import type { MenuState } from './types'; |
||||
|
|
||||
|
import { Ref, toRaw } from 'vue'; |
||||
|
|
||||
|
import { unref } from 'vue'; |
||||
|
import { es6Unique } from '/@/utils'; |
||||
|
import { getAllParentPath } from '/@/router/helper/menuHelper'; |
||||
|
import { useTimeoutFn } from '/@/hooks/core/useTimeout'; |
||||
|
|
||||
|
export function useOpenKeys( |
||||
|
menuState: MenuState, |
||||
|
menus: Ref<MenuType[]>, |
||||
|
accordion: Ref<boolean>, |
||||
|
mixSider: Ref<boolean> |
||||
|
// mode: Ref<MenuModeEnum>,
|
||||
|
) { |
||||
|
async function setOpenKeys(path: string) { |
||||
|
// if (mode.value === MenuModeEnum.HORIZONTAL) {
|
||||
|
// return;
|
||||
|
// }
|
||||
|
const native = !mixSider.value; |
||||
|
useTimeoutFn( |
||||
|
() => { |
||||
|
const menuList = toRaw(menus.value); |
||||
|
if (menuList?.length === 0) { |
||||
|
menuState.activeSubMenuNames = []; |
||||
|
menuState.openNames = []; |
||||
|
return; |
||||
|
} |
||||
|
const keys = getAllParentPath(menuList, path); |
||||
|
if (!unref(accordion)) { |
||||
|
menuState.openNames = es6Unique([...menuState.openNames, ...keys]); |
||||
|
} else { |
||||
|
menuState.openNames = keys; |
||||
|
} |
||||
|
menuState.activeSubMenuNames = menuState.openNames; |
||||
|
}, |
||||
|
16, |
||||
|
native |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return { setOpenKeys }; |
||||
|
} |
||||
@ -1,16 +0,0 @@ |
|||||
import type { Ref } from 'vue'; |
|
||||
import type { TableActionType } from '../types/table'; |
|
||||
|
|
||||
import { provide, inject } from 'vue'; |
|
||||
|
|
||||
const key = Symbol('table'); |
|
||||
|
|
||||
type Instance = TableActionType & { wrapRef: Ref<Nullable<HTMLElement>> }; |
|
||||
|
|
||||
export function provideTable(instance: Instance) { |
|
||||
provide(key, instance); |
|
||||
} |
|
||||
|
|
||||
export function injectTable(): Instance { |
|
||||
return inject(key) as Instance; |
|
||||
} |
|
||||
@ -0,0 +1,5 @@ |
|||||
|
export default { |
||||
|
loadingText: 'Loading...', |
||||
|
cancelText: 'Close', |
||||
|
okText: 'Confirm', |
||||
|
}; |
||||
@ -0,0 +1,3 @@ |
|||||
|
export default { |
||||
|
search: 'Menu search', |
||||
|
}; |
||||
@ -0,0 +1,4 @@ |
|||||
|
export default { |
||||
|
cancelText: 'Close', |
||||
|
okText: 'Confirm', |
||||
|
}; |
||||
@ -0,0 +1,5 @@ |
|||||
|
export default { |
||||
|
loadingText: '加载中...', |
||||
|
cancelText: '关闭', |
||||
|
okText: '确认', |
||||
|
}; |
||||
@ -0,0 +1,3 @@ |
|||||
|
export default { |
||||
|
search: '菜单搜索', |
||||
|
}; |
||||
@ -0,0 +1,4 @@ |
|||||
|
export default { |
||||
|
cancelText: '关闭', |
||||
|
okText: '确认', |
||||
|
}; |
||||
Loading…
Reference in new issue