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