sub-menu.vue 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <script lang="ts" setup>
  2. import type { HoverCardContentProps } from '@vben-core/shadcn-ui';
  3. import type {
  4. MenuItemRegistered,
  5. MenuProvider,
  6. SubMenuProps,
  7. } from '../interface';
  8. import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
  9. import { useNamespace } from '@vben-core/hooks';
  10. import { VbenHoverCard } from '@vben-core/shadcn-ui';
  11. import {
  12. createSubMenuContext,
  13. useMenu,
  14. useMenuContext,
  15. useMenuStyle,
  16. useSubMenuContext,
  17. } from '../hooks';
  18. import CollapseTransition from './collapse-transition.vue';
  19. import SubMenuContent from './sub-menu-content.vue';
  20. interface Props extends SubMenuProps {
  21. isSubMenuMore?: boolean;
  22. }
  23. defineOptions({ name: 'SubMenu' });
  24. const props = withDefaults(defineProps<Props>(), {
  25. disabled: false,
  26. isSubMenuMore: false,
  27. });
  28. const { parentMenu, parentPaths } = useMenu();
  29. const { b, is } = useNamespace('sub-menu');
  30. const nsMenu = useNamespace('menu');
  31. const rootMenu = useMenuContext();
  32. const subMenu = useSubMenuContext();
  33. const subMenuStyle = useMenuStyle(subMenu);
  34. const mouseInChild = ref(false);
  35. const items = ref<MenuProvider['items']>({});
  36. const subMenus = ref<MenuProvider['subMenus']>({});
  37. const timer = ref<ReturnType<typeof setTimeout> | null>(null);
  38. createSubMenuContext({
  39. addSubMenu,
  40. handleMouseleave,
  41. level: (subMenu?.level ?? 0) + 1,
  42. mouseInChild,
  43. removeSubMenu,
  44. });
  45. const opened = computed(() => {
  46. return rootMenu?.openedMenus.includes(props.path);
  47. });
  48. const isTopLevelMenuSubmenu = computed(
  49. () => parentMenu.value?.type.name === 'Menu',
  50. );
  51. const mode = computed(() => rootMenu?.props.mode ?? 'vertical');
  52. const rounded = computed(() => rootMenu?.props.rounded);
  53. const currentLevel = computed(() => subMenu?.level ?? 0);
  54. const isFirstLevel = computed(() => {
  55. return currentLevel.value === 1;
  56. });
  57. const contentProps = computed((): HoverCardContentProps => {
  58. const side =
  59. mode.value === 'horizontal' && isFirstLevel.value ? 'bottom' : 'right';
  60. return {
  61. side,
  62. sideOffset: isFirstLevel.value ? 5 : 10,
  63. };
  64. });
  65. const active = computed(() => {
  66. let isActive = false;
  67. Object.values(items.value).forEach((item) => {
  68. if (item.active) {
  69. isActive = true;
  70. }
  71. });
  72. Object.values(subMenus.value).forEach((subItem) => {
  73. if (subItem.active) {
  74. isActive = true;
  75. }
  76. });
  77. return isActive;
  78. });
  79. function addSubMenu(subMenu: MenuItemRegistered) {
  80. subMenus.value[subMenu.path] = subMenu;
  81. }
  82. function removeSubMenu(subMenu: MenuItemRegistered) {
  83. Reflect.deleteProperty(subMenus.value, subMenu.path);
  84. }
  85. /**
  86. * 点击submenu展开/关闭
  87. */
  88. function handleClick() {
  89. const mode = rootMenu?.props.mode;
  90. if (
  91. // 当前菜单禁用时,不展开
  92. props.disabled ||
  93. (rootMenu?.props.collapse && mode === 'vertical') ||
  94. // 水平模式下不展开
  95. mode === 'horizontal'
  96. ) {
  97. return;
  98. }
  99. rootMenu?.handleSubMenuClick({
  100. active: active.value,
  101. parentPaths: parentPaths.value,
  102. path: props.path,
  103. });
  104. }
  105. function handleMouseenter(event: FocusEvent | MouseEvent, showTimeout = 300) {
  106. if (event.type === 'focus') {
  107. return;
  108. }
  109. if (
  110. (!rootMenu?.props.collapse && rootMenu?.props.mode === 'vertical') ||
  111. props.disabled
  112. ) {
  113. if (subMenu) {
  114. subMenu.mouseInChild.value = true;
  115. }
  116. return;
  117. }
  118. if (subMenu) {
  119. subMenu.mouseInChild.value = true;
  120. }
  121. timer.value && window.clearTimeout(timer.value);
  122. timer.value = setTimeout(() => {
  123. rootMenu?.openMenu(props.path, parentPaths.value);
  124. }, showTimeout);
  125. parentMenu.value?.vnode.el?.dispatchEvent(new MouseEvent('mouseenter'));
  126. }
  127. function handleMouseleave(deepDispatch = false) {
  128. if (
  129. !rootMenu?.props.collapse &&
  130. rootMenu?.props.mode === 'vertical' &&
  131. subMenu
  132. ) {
  133. subMenu.mouseInChild.value = false;
  134. return;
  135. }
  136. timer.value && window.clearTimeout(timer.value);
  137. if (subMenu) {
  138. subMenu.mouseInChild.value = false;
  139. }
  140. timer.value = setTimeout(() => {
  141. !mouseInChild.value && rootMenu?.closeMenu(props.path, parentPaths.value);
  142. }, 300);
  143. if (deepDispatch) {
  144. subMenu?.handleMouseleave?.(true);
  145. }
  146. }
  147. const item = reactive({
  148. active,
  149. parentPaths,
  150. path: props.path,
  151. });
  152. onMounted(() => {
  153. subMenu?.addSubMenu?.(item);
  154. rootMenu?.addSubMenu?.(item);
  155. });
  156. onBeforeUnmount(() => {
  157. subMenu?.removeSubMenu?.(item);
  158. rootMenu?.removeSubMenu?.(item);
  159. });
  160. </script>
  161. <template>
  162. <li
  163. :class="[
  164. b(),
  165. is('opened', opened),
  166. is('active', active),
  167. is('disabled', disabled),
  168. ]"
  169. @focus="handleMouseenter"
  170. @mouseenter="handleMouseenter"
  171. @mouseleave="() => handleMouseleave()"
  172. >
  173. <template v-if="rootMenu.isMenuPopup">
  174. <VbenHoverCard
  175. :content-class="[
  176. nsMenu.e('popup-container'),
  177. is(rootMenu.theme, true),
  178. opened ? '' : 'hidden',
  179. ]"
  180. :content-props="contentProps"
  181. :open="true"
  182. :open-delay="0"
  183. >
  184. <template #trigger>
  185. <SubMenuContent
  186. :class="is('active', active)"
  187. :icon="icon"
  188. :is-menu-more="isSubMenuMore"
  189. :is-top-level-menu-submenu="isTopLevelMenuSubmenu"
  190. :level="currentLevel"
  191. :path="path"
  192. @click.stop="handleClick"
  193. >
  194. <template #title>
  195. <slot name="title"></slot>
  196. </template>
  197. </SubMenuContent>
  198. </template>
  199. <div
  200. :class="[nsMenu.is(mode, true), nsMenu.e('popup')]"
  201. @focus="(e) => handleMouseenter(e, 100)"
  202. @mouseenter="(e) => handleMouseenter(e, 100)"
  203. @mouseleave="() => handleMouseleave(true)"
  204. >
  205. <ul
  206. :class="[nsMenu.b(), is('rounded', rounded)]"
  207. :style="subMenuStyle"
  208. >
  209. <slot></slot>
  210. </ul>
  211. </div>
  212. </VbenHoverCard>
  213. </template>
  214. <template v-else>
  215. <SubMenuContent
  216. :class="is('active', active)"
  217. :icon="icon"
  218. :is-menu-more="isSubMenuMore"
  219. :is-top-level-menu-submenu="isTopLevelMenuSubmenu"
  220. :level="currentLevel"
  221. :path="path"
  222. @click.stop="handleClick"
  223. >
  224. <slot name="content"></slot>
  225. <template #title>
  226. <slot name="title"></slot>
  227. </template>
  228. </SubMenuContent>
  229. <CollapseTransition>
  230. <ul
  231. v-show="opened"
  232. :class="[nsMenu.b(), is('rounded', rounded)]"
  233. :style="subMenuStyle"
  234. >
  235. <slot></slot>
  236. </ul>
  237. </CollapseTransition>
  238. </template>
  239. </li>
  240. </template>