14 changed files with 424 additions and 364 deletions
@ -1,267 +0,0 @@ |
|||
import './index.less'; |
|||
|
|||
import type { MenuState } from './types'; |
|||
import type { Menu as MenuType } from '/@/router/types'; |
|||
|
|||
import { |
|||
computed, |
|||
defineComponent, |
|||
unref, |
|||
reactive, |
|||
watch, |
|||
toRefs, |
|||
ComputedRef, |
|||
ref, |
|||
CSSProperties, |
|||
} from 'vue'; |
|||
import { Menu } from 'ant-design-vue'; |
|||
import MenuContent from './MenuContent'; |
|||
// import { ScrollContainer } from '/@/components/Container';
|
|||
// import { BasicArrow } from '/@/components/Basic';
|
|||
|
|||
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; |
|||
import { ThemeEnum } from '/@/enums/appEnum'; |
|||
|
|||
import { appStore } from '/@/store/modules/app'; |
|||
|
|||
import { useOpenKeys } from './useOpenKeys'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { isFunction } from '/@/utils/is'; |
|||
import { getSlot } from '/@/utils/helper/tsxHelper'; |
|||
import { menuHasChildren } from './helper'; |
|||
import { getCurrentParentPath } from '/@/router/menus'; |
|||
|
|||
import { basicProps } from './props'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { REDIRECT_NAME } from '/@/router/constant'; |
|||
import { tabStore } from '/@/store/modules/tab'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
export default defineComponent({ |
|||
name: 'BasicMenu', |
|||
props: basicProps, |
|||
emits: ['menuClick'], |
|||
setup(props, { slots, emit }) { |
|||
const currentParentPath = ref(''); |
|||
const isClickGo = ref(false); |
|||
|
|||
const menuState = reactive<MenuState>({ |
|||
defaultSelectedKeys: [], |
|||
mode: props.mode, |
|||
theme: computed(() => props.theme) as ComputedRef<ThemeEnum>, |
|||
openKeys: [], |
|||
selectedKeys: [], |
|||
collapsedOpenKeys: [], |
|||
}); |
|||
|
|||
const { prefixCls } = useDesign('basic-menu'); |
|||
|
|||
const { items, mode, accordion } = toRefs(props); |
|||
|
|||
const { getCollapsed, getIsHorizontal, getTopMenuAlign, getSplit } = useMenuSetting(); |
|||
|
|||
const { currentRoute } = useRouter(); |
|||
|
|||
const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys( |
|||
menuState, |
|||
items, |
|||
mode, |
|||
accordion |
|||
); |
|||
|
|||
const getMenuClass = computed(() => { |
|||
const { type } = props; |
|||
const { mode } = menuState; |
|||
return [ |
|||
prefixCls, |
|||
`justify-${unref(getTopMenuAlign)}`, |
|||
{ |
|||
[`${prefixCls}--hide-title`]: !unref(showTitle), |
|||
[`${prefixCls}--collapsed-show-title`]: props.collapsedShowTitle, |
|||
[`${prefixCls}__second`]: |
|||
!props.isHorizontal && appStore.getProjectConfig.menuSetting.split, |
|||
[`${prefixCls}__sidebar-hor`]: |
|||
type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL, |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
const showTitle = computed(() => props.collapsedShowTitle && unref(getCollapsed)); |
|||
|
|||
const getInlineCollapseOptions = computed(() => { |
|||
const isInline = props.mode === MenuModeEnum.INLINE; |
|||
|
|||
const inlineCollapseOptions: { inlineCollapsed?: boolean } = {}; |
|||
if (isInline) { |
|||
inlineCollapseOptions.inlineCollapsed = unref(getCollapsed); |
|||
} |
|||
return inlineCollapseOptions; |
|||
}); |
|||
|
|||
const getWrapperStyle = computed( |
|||
(): CSSProperties => { |
|||
const isHorizontal = unref(getIsHorizontal) || getSplit.value; |
|||
|
|||
return { |
|||
height: isHorizontal ? '100%' : `calc(100% - ${props.showLogo ? '48px' : '0px'})`, |
|||
overflowY: isHorizontal ? 'hidden' : 'auto', |
|||
}; |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => tabStore.getCurrentTab, |
|||
() => { |
|||
if (unref(currentRoute).name === REDIRECT_NAME) return; |
|||
handleMenuChange(); |
|||
unref(getSplit) && getParentPath(); |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => props.items, |
|||
() => { |
|||
handleMenuChange(); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
} |
|||
); |
|||
|
|||
getParentPath(); |
|||
|
|||
async function getParentPath() { |
|||
const { appendClass } = props; |
|||
if (!appendClass) return ''; |
|||
const parentPath = await getCurrentParentPath(unref(currentRoute).path); |
|||
|
|||
currentParentPath.value = parentPath; |
|||
} |
|||
|
|||
async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) { |
|||
const { beforeClickFn } = props; |
|||
if (beforeClickFn && isFunction(beforeClickFn)) { |
|||
const flag = await beforeClickFn(key); |
|||
if (!flag) return; |
|||
} |
|||
emit('menuClick', key); |
|||
|
|||
isClickGo.value = true; |
|||
menuState.openKeys = keyPath; |
|||
menuState.selectedKeys = [key]; |
|||
} |
|||
|
|||
function handleMenuChange() { |
|||
if (unref(isClickGo)) { |
|||
isClickGo.value = false; |
|||
return; |
|||
} |
|||
const path = unref(currentRoute).path; |
|||
if (menuState.mode !== MenuModeEnum.HORIZONTAL) { |
|||
setOpenKeys(path); |
|||
} |
|||
menuState.selectedKeys = [path]; |
|||
} |
|||
|
|||
// function renderExpandIcon({ key }: { key: string }) {
|
|||
// const isOpen = getOpenKeys.value.includes(key);
|
|||
// const collapsed = unref(getCollapsed);
|
|||
// return (
|
|||
// <BasicArrow
|
|||
// expand={isOpen}
|
|||
// bottom
|
|||
// inset
|
|||
// class={[
|
|||
// `${prefixCls}__expand-icon`,
|
|||
// {
|
|||
// [`${prefixCls}__expand-icon--collapsed`]: collapsed,
|
|||
// },
|
|||
// ]}
|
|||
// />
|
|||
// );
|
|||
// }
|
|||
|
|||
function renderItem(menu: MenuType, level = 1) { |
|||
return !menuHasChildren(menu) ? renderMenuItem(menu, level) : renderSubMenu(menu, level); |
|||
} |
|||
|
|||
function renderMenuItem(menu: MenuType, level: number) { |
|||
const { appendClass } = props; |
|||
const isAppendActiveCls = |
|||
appendClass && level === 1 && menu.path === unref(currentParentPath); |
|||
|
|||
const levelCls = [ |
|||
`${prefixCls}-item__level${level}`, |
|||
` ${menuState.theme} `, |
|||
{ |
|||
'top-active-menu': isAppendActiveCls, |
|||
}, |
|||
]; |
|||
return ( |
|||
<Menu.Item key={menu.path} class={levelCls}> |
|||
{() => [ |
|||
<MenuContent |
|||
item={menu} |
|||
showTitle={unref(showTitle)} |
|||
isHorizontal={props.isHorizontal} |
|||
/>, |
|||
]} |
|||
</Menu.Item> |
|||
); |
|||
} |
|||
|
|||
function renderSubMenu(menu: MenuType, level: number) { |
|||
const levelCls = `${prefixCls}-item__level${level} ${menuState.theme} `; |
|||
return ( |
|||
<Menu.SubMenu key={menu.path} class={levelCls}> |
|||
{{ |
|||
title: () => [ |
|||
<MenuContent |
|||
showTitle={unref(showTitle)} |
|||
item={menu} |
|||
isHorizontal={props.isHorizontal} |
|||
/>, |
|||
], |
|||
// expandIcon: renderExpandIcon,
|
|||
default: () => (menu.children || []).map((item) => renderItem(item, level + 1)), |
|||
}} |
|||
</Menu.SubMenu> |
|||
); |
|||
} |
|||
|
|||
function renderMenu() { |
|||
const { selectedKeys, defaultSelectedKeys, mode, theme } = menuState; |
|||
|
|||
return ( |
|||
<Menu |
|||
selectedKeys={selectedKeys} |
|||
defaultSelectedKeys={defaultSelectedKeys} |
|||
mode={mode} |
|||
openKeys={unref(getOpenKeys)} |
|||
inlineIndent={props.inlineIndent} |
|||
theme={unref(theme)} |
|||
onOpenChange={handleOpenChange} |
|||
class={unref(getMenuClass)} |
|||
onClick={handleMenuClick} |
|||
subMenuOpenDelay={0.2} |
|||
{...unref(getInlineCollapseOptions)} |
|||
> |
|||
{{ |
|||
default: () => unref(items).map((item) => renderItem(item)), |
|||
}} |
|||
</Menu> |
|||
); |
|||
} |
|||
|
|||
return () => { |
|||
return ( |
|||
<> |
|||
{!unref(getIsHorizontal) && getSlot(slots, 'header')} |
|||
<div class={`${prefixCls}-wrapper`} style={unref(getWrapperStyle)}> |
|||
{renderMenu()} |
|||
</div> |
|||
</> |
|||
); |
|||
}; |
|||
}, |
|||
}); |
|||
@ -0,0 +1,209 @@ |
|||
<template> |
|||
<slot name="header" v-if="!getIsHorizontal" /> |
|||
<ScrollContainer :class="`${prefixCls}-wrapper`" :style="getWrapperStyle"> |
|||
<Menu |
|||
:selectedKeys="selectedKeys" |
|||
:defaultSelectedKeys="defaultSelectedKeys" |
|||
:mode="mode" |
|||
:openKeys="getOpenKeys" |
|||
:inlineIndent="inlineIndent" |
|||
:theme="theme" |
|||
@openChange="handleOpenChange" |
|||
:class="getMenuClass" |
|||
@click="handleMenuClick" |
|||
:subMenuOpenDelay="0.2" |
|||
v-bind="getInlineCollapseOptions" |
|||
> |
|||
<template v-for="item in items" :key="item.path"> |
|||
<BasicSubMenuItem |
|||
:item="item" |
|||
:theme="theme" |
|||
:level="1" |
|||
:appendClass="appendClass" |
|||
:parentPath="currentParentPath" |
|||
:showTitle="showTitle" |
|||
:isHorizontal="isHorizontal" |
|||
/> |
|||
</template> |
|||
</Menu> |
|||
</ScrollContainer> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { MenuState } from './types'; |
|||
|
|||
import { |
|||
computed, |
|||
defineComponent, |
|||
unref, |
|||
reactive, |
|||
watch, |
|||
toRefs, |
|||
ref, |
|||
CSSProperties, |
|||
} from 'vue'; |
|||
import { Menu } from 'ant-design-vue'; |
|||
import BasicSubMenuItem from './components/BasicSubMenuItem.vue'; |
|||
import { ScrollContainer } from '/@/components/Container'; |
|||
|
|||
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; |
|||
|
|||
import { appStore } from '/@/store/modules/app'; |
|||
|
|||
import { useOpenKeys } from './useOpenKeys'; |
|||
import { useRouter } from 'vue-router'; |
|||
|
|||
import { isFunction } from '/@/utils/is'; |
|||
import { getCurrentParentPath } from '/@/router/menus'; |
|||
|
|||
import { basicProps } from './props'; |
|||
import { useMenuSetting } from '/@/hooks/setting/useMenuSetting'; |
|||
import { REDIRECT_NAME } from '/@/router/constant'; |
|||
import { tabStore } from '/@/store/modules/tab'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicMenu', |
|||
components: { |
|||
Menu, |
|||
ScrollContainer, |
|||
BasicSubMenuItem, |
|||
// BasicSubMenuItem: createAsyncComponent(() => import('./components/BasicSubMenuItem.vue')), |
|||
}, |
|||
props: basicProps, |
|||
emits: ['menuClick'], |
|||
setup(props, { emit }) { |
|||
const currentParentPath = ref(''); |
|||
const isClickGo = ref(false); |
|||
|
|||
const menuState = reactive<MenuState>({ |
|||
defaultSelectedKeys: [], |
|||
openKeys: [], |
|||
selectedKeys: [], |
|||
collapsedOpenKeys: [], |
|||
}); |
|||
|
|||
const { prefixCls } = useDesign('basic-menu'); |
|||
const { items, mode, accordion } = toRefs(props); |
|||
|
|||
const { getCollapsed, getIsHorizontal, getTopMenuAlign, getSplit } = useMenuSetting(); |
|||
|
|||
const { currentRoute } = useRouter(); |
|||
|
|||
const { handleOpenChange, setOpenKeys, getOpenKeys } = useOpenKeys( |
|||
menuState, |
|||
items, |
|||
mode, |
|||
accordion |
|||
); |
|||
|
|||
const getMenuClass = computed(() => { |
|||
const { type, mode } = props; |
|||
return [ |
|||
prefixCls, |
|||
`justify-${unref(getTopMenuAlign)}`, |
|||
{ |
|||
[`${prefixCls}--hide-title`]: !unref(showTitle), |
|||
[`${prefixCls}--collapsed-show-title`]: props.collapsedShowTitle, |
|||
[`${prefixCls}__second`]: |
|||
!props.isHorizontal && appStore.getProjectConfig.menuSetting.split, |
|||
[`${prefixCls}__sidebar-hor`]: |
|||
type === MenuTypeEnum.TOP_MENU && mode === MenuModeEnum.HORIZONTAL, |
|||
}, |
|||
]; |
|||
}); |
|||
|
|||
const showTitle = computed(() => props.collapsedShowTitle && unref(getCollapsed)); |
|||
|
|||
const getInlineCollapseOptions = computed(() => { |
|||
const isInline = props.mode === MenuModeEnum.INLINE; |
|||
|
|||
const inlineCollapseOptions: { inlineCollapsed?: boolean } = {}; |
|||
if (isInline) { |
|||
inlineCollapseOptions.inlineCollapsed = unref(getCollapsed); |
|||
} |
|||
return inlineCollapseOptions; |
|||
}); |
|||
|
|||
const getWrapperStyle = computed( |
|||
(): CSSProperties => { |
|||
return { |
|||
height: `calc(100% - ${props.showLogo ? '48px' : '0px'})`, |
|||
overflowY: 'hidden', |
|||
}; |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => tabStore.getCurrentTab, |
|||
() => { |
|||
if (unref(currentRoute).name === REDIRECT_NAME) return; |
|||
handleMenuChange(); |
|||
unref(getSplit) && getParentPath(); |
|||
} |
|||
); |
|||
|
|||
watch( |
|||
() => props.items, |
|||
() => { |
|||
handleMenuChange(); |
|||
}, |
|||
{ |
|||
immediate: true, |
|||
} |
|||
); |
|||
|
|||
getParentPath(); |
|||
|
|||
async function getParentPath() { |
|||
const { appendClass } = props; |
|||
if (!appendClass) return ''; |
|||
const parentPath = await getCurrentParentPath(unref(currentRoute).path); |
|||
|
|||
currentParentPath.value = parentPath; |
|||
} |
|||
|
|||
async function handleMenuClick({ key, keyPath }: { key: string; keyPath: string[] }) { |
|||
const { beforeClickFn } = props; |
|||
if (beforeClickFn && isFunction(beforeClickFn)) { |
|||
const flag = await beforeClickFn(key); |
|||
if (!flag) return; |
|||
} |
|||
emit('menuClick', key); |
|||
|
|||
isClickGo.value = true; |
|||
menuState.openKeys = keyPath; |
|||
menuState.selectedKeys = [key]; |
|||
} |
|||
|
|||
function handleMenuChange() { |
|||
if (unref(isClickGo)) { |
|||
isClickGo.value = false; |
|||
return; |
|||
} |
|||
const path = unref(currentRoute).path; |
|||
if (props.mode !== MenuModeEnum.HORIZONTAL) { |
|||
setOpenKeys(path); |
|||
} |
|||
menuState.selectedKeys = [path]; |
|||
} |
|||
|
|||
return { |
|||
prefixCls, |
|||
getIsHorizontal, |
|||
getWrapperStyle, |
|||
handleMenuClick, |
|||
getInlineCollapseOptions, |
|||
getMenuClass, |
|||
handleOpenChange, |
|||
getOpenKeys, |
|||
currentParentPath, |
|||
showTitle, |
|||
...toRefs(menuState), |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
<style lang="less"> |
|||
@import './index.less'; |
|||
</style> |
|||
@ -0,0 +1,39 @@ |
|||
<template> |
|||
<MenuItem :class="getLevelClass"> |
|||
<MenuContent v-bind="$props" :item="item" /> |
|||
</MenuItem> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, computed } from 'vue'; |
|||
import { Menu } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { itemProps } from '../props'; |
|||
|
|||
import MenuContent from '../MenuContent'; |
|||
export default defineComponent({ |
|||
name: 'BasicMenuItem', |
|||
components: { MenuItem: Menu.Item, MenuContent }, |
|||
props: itemProps, |
|||
setup(props) { |
|||
const { prefixCls } = useDesign('basic-menu-item'); |
|||
|
|||
const getLevelClass = computed(() => { |
|||
const { appendClass, level, item, parentPath, theme } = props; |
|||
const isAppendActiveCls = appendClass && level === 1 && item.path === parentPath; |
|||
|
|||
const levelCls = [ |
|||
`${prefixCls}__level${level}`, |
|||
theme, |
|||
{ |
|||
'top-active-menu': isAppendActiveCls, |
|||
}, |
|||
]; |
|||
return levelCls; |
|||
}); |
|||
return { |
|||
prefixCls, |
|||
getLevelClass, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,53 @@ |
|||
<template> |
|||
<BasicMenuItem v-if="!menuHasChildren(item)" v-bind="$props" /> |
|||
<SubMenu v-else :class="[`${prefixCls}__level${level}`, theme]"> |
|||
<template #title> |
|||
<MenuContent v-bind="$props" :item="item" /> |
|||
</template> |
|||
<!-- <template #expandIcon="{ key }"> |
|||
<ExpandIcon :key="key" /> |
|||
</template> --> |
|||
|
|||
<template v-for="childrenItem in item.children || []" :key="childrenItem.path"> |
|||
<BasicSubMenuItem v-bind="$props" :item="childrenItem" :level="level + 1" /> |
|||
</template> |
|||
</SubMenu> |
|||
</template> |
|||
<script lang="ts"> |
|||
import type { Menu as MenuType } from '/@/router/types'; |
|||
|
|||
import { defineComponent } from 'vue'; |
|||
import { Menu } from 'ant-design-vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { itemProps } from '../props'; |
|||
import BasicMenuItem from './BasicMenuItem.vue'; |
|||
import MenuContent from '../MenuContent'; |
|||
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; |
|||
|
|||
export default defineComponent({ |
|||
name: 'BasicSubMenuItem', |
|||
|
|||
components: { |
|||
BasicMenuItem, |
|||
SubMenu: Menu.SubMenu, |
|||
MenuItem: Menu.Item, |
|||
MenuContent, |
|||
// ExpandIcon: createAsyncComponent(() => import('./ExpandIcon.vue')), |
|||
}, |
|||
props: itemProps, |
|||
setup() { |
|||
const { prefixCls } = useDesign('basic-menu-item'); |
|||
function menuHasChildren(menuTreeItem: MenuType): boolean { |
|||
return ( |
|||
Reflect.has(menuTreeItem, 'children') && |
|||
!!menuTreeItem.children && |
|||
menuTreeItem.children.length > 0 |
|||
); |
|||
} |
|||
return { |
|||
prefixCls, |
|||
menuHasChildren, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -0,0 +1,43 @@ |
|||
<template> |
|||
<BasicArrow :expand="getIsOpen" bottom inset :class="getWrapperClass" /> |
|||
</template> |
|||
<script lang="ts"> |
|||
import { defineComponent, PropType, computed } from 'vue'; |
|||
import { useDesign } from '/@/hooks/web/useDesign'; |
|||
import { BasicArrow } from '/@/components/Basic'; |
|||
|
|||
import { propTypes } from '/@/utils/propTypes'; |
|||
export default defineComponent({ |
|||
name: 'BasicMenuItem', |
|||
components: { BasicArrow }, |
|||
props: { |
|||
key: propTypes.string, |
|||
openKeys: { |
|||
type: Array as PropType<string[]>, |
|||
default: [], |
|||
}, |
|||
collapsed: propTypes.bool, |
|||
}, |
|||
setup(props) { |
|||
const { prefixCls } = useDesign('basic-menu'); |
|||
|
|||
const getIsOpen = computed(() => { |
|||
return props.openKeys.includes(props.key); |
|||
}); |
|||
|
|||
const getWrapperClass = computed(() => { |
|||
return [ |
|||
`${prefixCls}__expand-icon`, |
|||
{ |
|||
[`${prefixCls}__expand-icon--collapsed`]: props.collapsed, |
|||
}, |
|||
]; |
|||
}); |
|||
return { |
|||
prefixCls, |
|||
getIsOpen, |
|||
getWrapperClass, |
|||
}; |
|||
}, |
|||
}); |
|||
</script> |
|||
@ -1,12 +0,0 @@ |
|||
import type { Menu as MenuType } from '/@/router/types'; |
|||
|
|||
/** |
|||
* @description: Whether the menu has child nodes |
|||
*/ |
|||
export function menuHasChildren(menuTreeItem: MenuType): boolean { |
|||
return ( |
|||
Reflect.has(menuTreeItem, 'children') && |
|||
!!menuTreeItem.children && |
|||
menuTreeItem.children.length > 0 |
|||
); |
|||
} |
|||
Loading…
Reference in new issue