| | |
| | | .@{prefix-cls} { |
| | | display: flex; |
| | | align-items: center; |
| | | padding-left: 12px; |
| | | padding-left: 7px; |
| | | cursor: pointer; |
| | | transition: all 0.2s ease; |
| | | |
| | | &.collapsed-show-title { |
| | | padding-left: 20px; |
| | |
| | | |
| | | 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); |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
New file |
| | |
| | | <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> |
| | |
| | | |
| | | .active-style() { |
| | | color: @white; |
| | | // background: @primary-color !important; |
| | | background: linear-gradient( |
| | | 118deg, |
| | | rgba(@primary-color, 0.8), |
| | |
| | | // right: 16px; |
| | | // width: 10px; |
| | | // transform-origin: none; |
| | | // opacity: 0.45; |
| | | |
| | | // span[role='img'] { |
| | | // margin-right: 0; |
| | |
| | | > .ant-menu-item-group-list |
| | | > .ant-menu-submenu |
| | | > .ant-menu-submenu-title, |
| | | &.ant-menu-inline-collapsed > .ant-menu-submenu > .ant-menu-submenu-title { |
| | | padding-right: 20px !important; |
| | | padding-left: 20px !important; |
| | | &.ant-menu-inline-collapsed .ant-menu-submenu-title { |
| | | padding-right: 16px !important; |
| | | padding-left: 16px !important; |
| | | } |
| | | } |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | // .ant-menu-item { |
| | | // transition: unset; |
| | | // } |
| | | .ant-menu-item { |
| | | transition: unset; |
| | | } |
| | | |
| | | // scrollbar -s tart |
| | | &-wrapper { |
| | | /* 滚动槽 */ |
| | | &::-webkit-scrollbar { |
| | | width: 5px; |
| | | height: 5px; |
| | | } |
| | | // &-wrapper { |
| | | |
| | | &::-webkit-scrollbar-track { |
| | | background: rgba(0, 0, 0, 0); |
| | | } |
| | | /* 滚动槽 */ |
| | | // &::-webkit-scrollbar { |
| | | // width: 5px; |
| | | // height: 5px; |
| | | // } |
| | | |
| | | &::-webkit-scrollbar-thumb { |
| | | background: rgba(255, 255, 255, 0.2); |
| | | border-radius: 3px; |
| | | box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1); |
| | | } |
| | | // &::-webkit-scrollbar-track { |
| | | // background: rgba(0, 0, 0, 0); |
| | | // } |
| | | |
| | | ::-webkit-scrollbar-thumb:hover { |
| | | background: @border-color-dark; |
| | | } |
| | | } |
| | | // &::-webkit-scrollbar-thumb { |
| | | // background: rgba(255, 255, 255, 0.2); |
| | | // border-radius: 3px; |
| | | // box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.1); |
| | | // } |
| | | |
| | | // ::-webkit-scrollbar-thumb:hover { |
| | | // background: @border-color-dark; |
| | | // } |
| | | // } |
| | | |
| | | // scrollbar end |
| | | |
| | |
| | | } |
| | | } |
| | | |
| | | &: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) { |
| | | // Reset menu item row height |
| | | .ant-menu-item:not(.@{basic-menu-prefix-cls}-item__level1), |
| | |
| | | overflow: hidden; |
| | | background: @sider-dark-bg-color; |
| | | .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-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 { |
| | |
| | | overflow: hidden; |
| | | 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-submenu-selected.@{basic-menu-prefix-cls}-menu-item__level1 { |
| | | color: @primary-color; |
| | |
| | | align-items: center; |
| | | } |
| | | } |
| | | |
| | | .@{basic-menu-prefix-cls}__tag { |
| | | position: absolute; |
| | | top: calc(50% - 8px); |
| | |
| | | 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 { |
| | |
| | | > ul { |
| | | background: @sider-dark-bg-color; |
| | | } |
| | | |
| | | .active-menu-style(); |
| | | } |
| | | } |
| | |
| | | |
| | | import { MenuModeEnum, MenuTypeEnum } from '/@/enums/menuEnum'; |
| | | import { ThemeEnum } from '/@/enums/appEnum'; |
| | | import { propTypes } from '/@/utils/propTypes'; |
| | | export const basicProps = { |
| | | items: { |
| | | type: Array as PropType<Menu[]>, |
| | | default: () => [], |
| | | }, |
| | | appendClass: { |
| | | type: Boolean as PropType<boolean>, |
| | | default: false, |
| | | }, |
| | | appendClass: propTypes.bool, |
| | | |
| | | collapsedShowTitle: { |
| | | type: Boolean as PropType<boolean>, |
| | | default: false, |
| | | }, |
| | | collapsedShowTitle: propTypes.bool, |
| | | |
| | | // 最好是4 倍数 |
| | | inlineIndent: { |
| | | type: Number as PropType<number>, |
| | | default: 20, |
| | | }, |
| | | inlineIndent: propTypes.number.def(20), |
| | | // 菜单组件的mode属性 |
| | | mode: { |
| | | type: String as PropType<MenuModeEnum>, |
| | | default: MenuModeEnum.INLINE, |
| | | }, |
| | | showLogo: { |
| | | type: Boolean as PropType<boolean>, |
| | | default: false, |
| | | }, |
| | | showLogo: propTypes.bool, |
| | | type: { |
| | | type: String as PropType<MenuTypeEnum>, |
| | | default: MenuTypeEnum.MIX, |
| | | }, |
| | | theme: { |
| | | type: String as PropType<string>, |
| | | default: ThemeEnum.DARK, |
| | | }, |
| | | inlineCollapsed: { |
| | | type: Boolean as PropType<boolean>, |
| | | default: false, |
| | | }, |
| | | theme: propTypes.string.def(ThemeEnum.DARK), |
| | | inlineCollapsed: propTypes.bool, |
| | | |
| | | isHorizontal: { |
| | | type: Boolean as PropType<boolean>, |
| | | default: false, |
| | | }, |
| | | accordion: { |
| | | type: Boolean as PropType<boolean>, |
| | | default: true, |
| | | }, |
| | | isHorizontal: propTypes.bool, |
| | | accordion: propTypes.bool.def(true), |
| | | beforeClickFn: { |
| | | 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 { ThemeEnum } from '/@/enums/appEnum'; |
| | | import { MenuModeEnum } from '/@/enums/menuEnum'; |
| | | // import { ComputedRef } from 'vue'; |
| | | // import { ThemeEnum } from '/@/enums/appEnum'; |
| | | // import { MenuModeEnum } from '/@/enums/menuEnum'; |
| | | export interface MenuState { |
| | | // 默认选中的列表 |
| | | defaultSelectedKeys: string[]; |
| | | |
| | | // 模式 |
| | | mode: MenuModeEnum; |
| | | // mode: MenuModeEnum; |
| | | |
| | | // 主题 |
| | | theme: ComputedRef<ThemeEnum> | ThemeEnum; |
| | | // // 主题 |
| | | // theme: ComputedRef<ThemeEnum> | ThemeEnum; |
| | | |
| | | // 缩进 |
| | | 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 enum ContentEnum { |
| | |
| | | border: 1px solid darken(@border-color-light, 6%); |
| | | transition: none; |
| | | |
| | | &:not(.ant-tabs-tab-active)::before { |
| | | &:not(.ant-tabs-tab-active)::after { |
| | | position: absolute; |
| | | top: -1px; |
| | | bottom: -1px; |
| | | left: 50%; |
| | | width: 100%; |
| | | height: 2px; |
| | |
| | | opacity: 1; |
| | | } |
| | | |
| | | &:not(.ant-tabs-tab-active)::before { |
| | | &:not(.ant-tabs-tab-active)::after { |
| | | opacity: 1; |
| | | transform: translate(-50%, 0) scaleX(1); |
| | | transition: all 0.3s ease-in-out; |
| | |
| | | forEach(menuList, (m) => { |
| | | !isUrl(m.path) && joinParentPath(menuList, m); |
| | | }); |
| | | |
| | | return menuList[0]; |
| | | } |
| | | |