提交 cbcd9098 作者: vben

wip(menu): perf menu

上级 f81c4019
...@@ -66,8 +66,9 @@ ...@@ -66,8 +66,9 @@
.@{prefix-cls} { .@{prefix-cls} {
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: 12px; padding-left: 7px;
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
&.collapsed-show-title { &.collapsed-show-title {
padding-left: 20px; padding-left: 20px;
......
...@@ -2,6 +2,8 @@ import { withInstall } from '../util'; ...@@ -2,6 +2,8 @@ import { withInstall } from '../util';
import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent'; import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
export const BasicMenu = createAsyncComponent(() => import('./src/BasicMenu'), { loading: false }); export const BasicMenu = createAsyncComponent(() => import('./src/BasicMenu.vue'), {
loading: false,
});
withInstall(BasicMenu); withInstall(BasicMenu);
import './index.less'; <template>
<slot name="header" v-if="!getIsHorizontal" />
import type { MenuState } from './types'; <ScrollContainer :class="`${prefixCls}-wrapper`" :style="getWrapperStyle">
import type { Menu as MenuType } from '/@/router/types'; <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 { import {
computed, computed,
defineComponent, defineComponent,
unref, unref,
reactive, reactive,
watch, watch,
toRefs, toRefs,
ComputedRef,
ref, ref,
CSSProperties, CSSProperties,
} from 'vue'; } from 'vue';
import { Menu } from 'ant-design-vue'; import { Menu } from 'ant-design-vue';
import MenuContent from './MenuContent'; import BasicSubMenuItem from './components/BasicSubMenuItem.vue';
// import { ScrollContainer } from '/@/components/Container'; import { ScrollContainer } from '/@/components/Container';
// import { BasicArrow } from '/@/components/Basic';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { appStore } from '/@/store/modules/app';
import { ThemeEnum } from '/@/enums/appEnum';
import { appStore } from '/@/store/modules/app'; import { useOpenKeys } from './useOpenKeys';
import { useRouter } from 'vue-router';
import { useOpenKeys } from './useOpenKeys'; import { isFunction } from '/@/utils/is';
import { useRouter } from 'vue-router'; import { getCurrentParentPath } from '/@/router/menus';
import { isFunction } from '/@/utils/is'; import { basicProps } from './props';
import { getSlot } from '/@/utils/helper/tsxHelper'; import { useMenuSetting } from '/@/hooks/setting/useMenuSetting';
import { menuHasChildren } from './helper'; import { REDIRECT_NAME } from '/@/router/constant';
import { getCurrentParentPath } from '/@/router/menus'; import { tabStore } from '/@/store/modules/tab';
import { useDesign } from '/@/hooks/web/useDesign';
// import { createAsyncComponent } from '/@/utils/factory/createAsyncComponent';
import { basicProps } from './props'; export default defineComponent({
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', name: 'BasicMenu',
components: {
Menu,
ScrollContainer,
BasicSubMenuItem,
// BasicSubMenuItem: createAsyncComponent(() => import('./components/BasicSubMenuItem.vue')),
},
props: basicProps, props: basicProps,
emits: ['menuClick'], emits: ['menuClick'],
setup(props, { slots, emit }) { setup(props, { emit }) {
const currentParentPath = ref(''); const currentParentPath = ref('');
const isClickGo = ref(false); const isClickGo = ref(false);
const menuState = reactive<MenuState>({ const menuState = reactive<MenuState>({
defaultSelectedKeys: [], defaultSelectedKeys: [],
mode: props.mode,
theme: computed(() => props.theme) as ComputedRef<ThemeEnum>,
openKeys: [], openKeys: [],
selectedKeys: [], selectedKeys: [],
collapsedOpenKeys: [], collapsedOpenKeys: [],
}); });
const { prefixCls } = useDesign('basic-menu'); const { prefixCls } = useDesign('basic-menu');
const { items, mode, accordion } = toRefs(props); const { items, mode, accordion } = toRefs(props);
const { getCollapsed, getIsHorizontal, getTopMenuAlign, getSplit } = useMenuSetting(); const { getCollapsed, getIsHorizontal, getTopMenuAlign, getSplit } = useMenuSetting();
...@@ -70,8 +98,7 @@ export default defineComponent({ ...@@ -70,8 +98,7 @@ export default defineComponent({
); );
const getMenuClass = computed(() => { const getMenuClass = computed(() => {
const { type } = props; const { type, mode } = props;
const { mode } = menuState;
return [ return [
prefixCls, prefixCls,
`justify-${unref(getTopMenuAlign)}`, `justify-${unref(getTopMenuAlign)}`,
...@@ -100,11 +127,9 @@ export default defineComponent({ ...@@ -100,11 +127,9 @@ export default defineComponent({
const getWrapperStyle = computed( const getWrapperStyle = computed(
(): CSSProperties => { (): CSSProperties => {
const isHorizontal = unref(getIsHorizontal) || getSplit.value;
return { return {
height: isHorizontal ? '100%' : `calc(100% - ${props.showLogo ? '48px' : '0px'})`, height: `calc(100% - ${props.showLogo ? '48px' : '0px'})`,
overflowY: isHorizontal ? 'hidden' : 'auto', overflowY: 'hidden',
}; };
} }
); );
...@@ -157,111 +182,28 @@ export default defineComponent({ ...@@ -157,111 +182,28 @@ export default defineComponent({
return; return;
} }
const path = unref(currentRoute).path; const path = unref(currentRoute).path;
if (menuState.mode !== MenuModeEnum.HORIZONTAL) { if (props.mode !== MenuModeEnum.HORIZONTAL) {
setOpenKeys(path); setOpenKeys(path);
} }
menuState.selectedKeys = [path]; menuState.selectedKeys = [path];
} }
// function renderExpandIcon({ key }: { key: string }) { return {
// const isOpen = getOpenKeys.value.includes(key); prefixCls,
// const collapsed = unref(getCollapsed); getIsHorizontal,
// return ( getWrapperStyle,
// <BasicArrow handleMenuClick,
// expand={isOpen} getInlineCollapseOptions,
// bottom getMenuClass,
// inset handleOpenChange,
// class={[ getOpenKeys,
// `${prefixCls}__expand-icon`, currentParentPath,
// { showTitle,
// [`${prefixCls}__expand-icon--collapsed`]: collapsed, ...toRefs(menuState),
// },
// ]}
// />
// );
// }
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>
</>
);
}; };
}, },
}); });
</script>
<style lang="less">
@import './index.less';
</style>
<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>
<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>
<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>
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
);
}
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
.active-style() { .active-style() {
color: @white; color: @white;
// background: @primary-color !important;
background: linear-gradient( background: linear-gradient(
118deg, 118deg,
rgba(@primary-color, 0.8), rgba(@primary-color, 0.8),
...@@ -27,6 +28,7 @@ ...@@ -27,6 +28,7 @@
// right: 16px; // right: 16px;
// width: 10px; // width: 10px;
// transform-origin: none; // transform-origin: none;
// opacity: 0.45;
// span[role='img'] { // span[role='img'] {
// margin-right: 0; // margin-right: 0;
...@@ -52,9 +54,9 @@ ...@@ -52,9 +54,9 @@
> .ant-menu-item-group-list > .ant-menu-item-group-list
> .ant-menu-submenu > .ant-menu-submenu
> .ant-menu-submenu-title, > .ant-menu-submenu-title,
&.ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title { &.ant-menu-inline-collapsed .ant-menu-submenu-title {
padding-right: 20px !important; padding-right: 16px !important;
padding-left: 20px !important; padding-left: 16px !important;
} }
} }
...@@ -87,32 +89,33 @@ ...@@ -87,32 +89,33 @@
} }
} }
// .ant-menu-item { .ant-menu-item {
// transition: unset; transition: unset;
// } }
// scrollbar -s tart // scrollbar -s tart
&-wrapper { // &-wrapper {
/* 滚动槽 */ /* 滚动槽 */
&::-webkit-scrollbar { // &::-webkit-scrollbar {
width: 5px; // width: 5px;
height: 5px; // height: 5px;
} // }
&::-webkit-scrollbar-track { // &::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0); // background: rgba(0, 0, 0, 0);
} // }
&::-webkit-scrollbar-thumb { // &::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2); // background: rgba(255, 255, 255, 0.2);
border-radius: 3px; // border-radius: 3px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1); // box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1);
} // }
::-webkit-scrollbar-thumb:hover { // ::-webkit-scrollbar-thumb:hover {
background: @border-color-dark; // background: @border-color-dark;
} // }
} // }
// scrollbar end // scrollbar end
...@@ -225,14 +228,6 @@ ...@@ -225,14 +228,6 @@
} }
} }
&:not(.@{basic-menu-prefix-cls}__sidebar-hor).ant-menu-inline-collapsed {
.@{basic-menu-prefix-cls}-item__level1 {
> div {
align-items: center;
}
}
}
&.ant-menu-dark:not(.@{basic-menu-prefix-cls}__sidebar-hor):not(.@{basic-menu-prefix-cls}__second) { &.ant-menu-dark:not(.@{basic-menu-prefix-cls}__sidebar-hor):not(.@{basic-menu-prefix-cls}__second) {
// Reset menu item row height // Reset menu item row height
.ant-menu-item:not(.@{basic-menu-prefix-cls}-item__level1), .ant-menu-item:not(.@{basic-menu-prefix-cls}-item__level1),
...@@ -255,10 +250,6 @@ ...@@ -255,10 +250,6 @@
background: @sider-dark-bg-color; background: @sider-dark-bg-color;
.active-menu-style(); .active-menu-style();
// .menu-item-icon.app-iconify {
// display: inline-block !important;
// }
.ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1, .ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1,
.ant-menu-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 { .ant-menu-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 {
color: @white; color: @white;
...@@ -304,10 +295,6 @@ ...@@ -304,10 +295,6 @@
overflow: hidden; overflow: hidden;
border-right: none; border-right: none;
// .menu-item-icon.app-iconify {
// display: inline-block !important;
// }
.ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1, .ant-menu-item.ant-menu-item-selected.@{basic-menu-prefix-cls}-menu-item__level1,
.ant-menu-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 { .ant-menu-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 {
color: @primary-color; color: @primary-color;
...@@ -332,6 +319,7 @@ ...@@ -332,6 +319,7 @@
align-items: center; align-items: center;
} }
} }
.@{basic-menu-prefix-cls}__tag { .@{basic-menu-prefix-cls}__tag {
position: absolute; position: absolute;
top: calc(50% - 8px); top: calc(50% - 8px);
...@@ -368,6 +356,20 @@ ...@@ -368,6 +356,20 @@
background: @warning-color; background: @warning-color;
} }
} }
.ant-menu-submenu,
.ant-menu-submenu-inline {
transition: unset;
}
// .ant-menu-submenu-arrow {
// transition: all 0.15s ease 0s;
// }
.ant-menu-inline.ant-menu-sub {
box-shadow: unset !important;
transition: unset;
}
} }
.ant-menu-dark { .ant-menu-dark {
...@@ -375,7 +377,6 @@ ...@@ -375,7 +377,6 @@
> ul { > ul {
background: @sider-dark-bg-color; background: @sider-dark-bg-color;
} }
.active-menu-style(); .active-menu-style();
} }
} }
......
...@@ -3,57 +3,47 @@ import type { PropType } from 'vue'; ...@@ -3,57 +3,47 @@ import type { PropType } from 'vue';
import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum';
import { ThemeEnum } from '/@/enums/appEnum'; import { ThemeEnum } from '/@/enums/appEnum';
import { propTypes } from '/@/utils/propTypes';
export const basicProps = { export const basicProps = {
items: { items: {
type: Array as PropType<Menu[]>, type: Array as PropType<Menu[]>,
default: () => [], default: () => [],
}, },
appendClass: { appendClass: propTypes.bool,
type: Boolean as PropType<boolean>,
default: false,
},
collapsedShowTitle: { collapsedShowTitle: propTypes.bool,
type: Boolean as PropType<boolean>,
default: false,
},
// 最好是4 倍数 // 最好是4 倍数
inlineIndent: { inlineIndent: propTypes.number.def(20),
type: Number as PropType<number>,
default: 20,
},
// 菜单组件的mode属性 // 菜单组件的mode属性
mode: { mode: {
type: String as PropType<MenuModeEnum>, type: String as PropType<MenuModeEnum>,
default: MenuModeEnum.INLINE, default: MenuModeEnum.INLINE,
}, },
showLogo: { showLogo: propTypes.bool,
type: Boolean as PropType<boolean>,
default: false,
},
type: { type: {
type: String as PropType<MenuTypeEnum>, type: String as PropType<MenuTypeEnum>,
default: MenuTypeEnum.MIX, default: MenuTypeEnum.MIX,
}, },
theme: { theme: propTypes.string.def(ThemeEnum.DARK),
type: String as PropType<string>, inlineCollapsed: propTypes.bool,
default: ThemeEnum.DARK,
},
inlineCollapsed: {
type: Boolean as PropType<boolean>,
default: false,
},
isHorizontal: { isHorizontal: propTypes.bool,
type: Boolean as PropType<boolean>, accordion: propTypes.bool.def(true),
default: false,
},
accordion: {
type: Boolean as PropType<boolean>,
default: true,
},
beforeClickFn: { beforeClickFn: {
type: Function as PropType<(key: string) => Promise<boolean>>, type: Function as PropType<(key: string) => Promise<boolean>>,
}, },
}; };
export const itemProps = {
item: {
type: Object as PropType<Menu>,
default: {},
},
level: propTypes.number,
theme: propTypes.oneOf(['dark', 'light']),
appendClass: propTypes.bool,
parentPath: propTypes.string,
showTitle: propTypes.bool,
isHorizontal: propTypes.bool,
};
import { ComputedRef } from 'vue'; // import { ComputedRef } from 'vue';
import { ThemeEnum } from '/@/enums/appEnum'; // import { ThemeEnum } from '/@/enums/appEnum';
import { MenuModeEnum } from '/@/enums/menuEnum'; // import { MenuModeEnum } from '/@/enums/menuEnum';
export interface MenuState { export interface MenuState {
// 默认选中的列表 // 默认选中的列表
defaultSelectedKeys: string[]; defaultSelectedKeys: string[];
// 模式 // 模式
mode: MenuModeEnum; // mode: MenuModeEnum;
// 主题 // // 主题
theme: ComputedRef<ThemeEnum> | ThemeEnum; // theme: ComputedRef<ThemeEnum> | ThemeEnum;
// 缩进 // 缩进
inlineIndent?: number; inlineIndent?: number;
......
export const SIDE_BAR_MINI_WIDTH = 58; export const SIDE_BAR_MINI_WIDTH = 48;
export const SIDE_BAR_SHOW_TIT_MINI_WIDTH = 80; export const SIDE_BAR_SHOW_TIT_MINI_WIDTH = 80;
export enum ContentEnum { export enum ContentEnum {
......
...@@ -34,9 +34,9 @@ ...@@ -34,9 +34,9 @@
border: 1px solid darken(@border-color-light, 6%); border: 1px solid darken(@border-color-light, 6%);
transition: none; transition: none;
&:not(.ant-tabs-tab-active)::before { &:not(.ant-tabs-tab-active)::after {
position: absolute; position: absolute;
top: -1px; bottom: -1px;
left: 50%; left: 50%;
width: 100%; width: 100%;
height: 2px; height: 2px;
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
opacity: 1; opacity: 1;
} }
&:not(.ant-tabs-tab-active)::before { &:not(.ant-tabs-tab-active)::after {
opacity: 1; opacity: 1;
transform: translate(-50%, 0) scaleX(1); transform: translate(-50%, 0) scaleX(1);
transition: all 0.3s ease-in-out; transition: all 0.3s ease-in-out;
......
...@@ -42,6 +42,7 @@ export function transformMenuModule(menuModule: MenuModule): Menu { ...@@ -42,6 +42,7 @@ export function transformMenuModule(menuModule: MenuModule): Menu {
forEach(menuList, (m) => { forEach(menuList, (m) => {
!isUrl(m.path) && joinParentPath(menuList, m); !isUrl(m.path) && joinParentPath(menuList, m);
}); });
return menuList[0]; return menuList[0];
} }
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论