Procházet zdrojové kódy

feat: optimize the visibility calculation of the horizontal menu and adjust the display logic of more buttons

nian před 1 měsícem
rodič
revize
f79fb9185d
1 změnil soubory, kde provedl 47 přidání a 32 odebrání
  1. 47 32
      src/layout/header/HorizontalMenu.vue

+ 47 - 32
src/layout/header/HorizontalMenu.vue

@@ -2,7 +2,7 @@
 import { useElementSize, watchThrottled } from '@vueuse/core'
 import { isFunction, isEmpty } from 'lodash-es'
 import { NDropdown } from 'naive-ui'
-import { h, computed, ref, watch, nextTick, onBeforeUnmount } from 'vue'
+import { h, computed, ref, watch, nextTick, onBeforeUnmount, reactive } from 'vue'
 
 import { useInjection } from '@/composables'
 import { headerLayoutInjectionKey } from '@/injection'
@@ -14,13 +14,14 @@ import type { ComponentPublicInstance } from 'vue'
 
 type Key = string | number | undefined
 
-const MENU_SIZE = {
-  MORE_BUTTON: 44,
+const MENU = {
+  MORE_BUTTON_WIDTH: 44,
   ITEM_COLUMN_GAP: 4,
   BOUNDARY_OFFSET: 1,
 }
 
 let rafId: number | null = null
+
 let initialized = false
 
 const userStore = useUserStore()
@@ -35,16 +36,35 @@ const navigationWrapperRef = ref<HTMLElement | null>(null)
 
 const menuActiveKey = ref('')
 
-const menuRightBoundMap = new Map<Key, number>()
+const menuRightBoundMap = reactive(new Map<Key, number>())
 
-const partiallyVisibleMenuKeys = ref<Set<Key>>(new Set())
+const threshold = ref(Number.POSITIVE_INFINITY)
 
 const moreDropdownOptions = computed<DropdownProps['options']>(() => {
-  return (userStore.menuList as NonNullable<MenuProps['options']>).filter((item) =>
-    partiallyVisibleMenuKeys.value.has(item.key),
-  )
+  return (userStore.menuList as NonNullable<MenuProps['options']>).filter((item: any) => {
+    if (item?.type) return false
+    const menuRightBound = menuRightBoundMap.get(item.key) ?? 0
+    return menuRightBound + MENU.ITEM_COLUMN_GAP > threshold.value
+  })
+})
+
+const hasActiveChild = computed(() => {
+  if (!Array.isArray(moreDropdownOptions.value)) return false
+  const stack = [...moreDropdownOptions.value]
+  while (stack.length) {
+    const node = stack.pop()
+    if (node?.key === menuActiveKey.value) return true
+    if (Array.isArray(node?.children)) {
+      stack.push(...node.children)
+    }
+  }
+  return false
 })
 
+const shouldShowMore = computed(
+  () => Array.isArray(moreDropdownOptions.value) && !isEmpty(moreDropdownOptions.value),
+)
+
 const renderIcon: DropdownProps['renderIcon'] = (option) => {
   return isFunction(option.icon)
     ? h(option.icon, {
@@ -67,16 +87,18 @@ function forwardRef(key: Key, ref: Element | ComponentPublicInstance | null) {
 
 function updateMenuVisibility(containerWidth: number) {
   if (containerWidth <= 0) return
-  for (const [key, rightBound] of menuRightBoundMap.entries()) {
-    if (
-      rightBound + MENU_SIZE.ITEM_COLUMN_GAP >
-      containerWidth - MENU_SIZE.MORE_BUTTON + MENU_SIZE.BOUNDARY_OFFSET
-    ) {
-      partiallyVisibleMenuKeys.value.add(key)
-    } else {
-      partiallyVisibleMenuKeys.value.delete(key)
+  const widthWithoutMore = containerWidth + MENU.BOUNDARY_OFFSET
+  const widthWithMore = containerWidth - MENU.MORE_BUTTON_WIDTH + MENU.BOUNDARY_OFFSET
+
+  let shouldComputeMoreWidth = false
+  for (const rightBound of menuRightBoundMap.values()) {
+    if (rightBound + MENU.ITEM_COLUMN_GAP > widthWithoutMore) {
+      shouldComputeMoreWidth = true
+      break
     }
   }
+
+  threshold.value = shouldComputeMoreWidth ? widthWithMore : widthWithoutMore
 }
 
 function scheduleUpdateMenuVisibility() {
@@ -88,16 +110,9 @@ function scheduleUpdateMenuVisibility() {
   })
 }
 
-function hasActiveChild(children: any[]): boolean {
-  const stack = [...children]
-  while (stack.length) {
-    const node = stack.pop()
-    if (node?.key === menuActiveKey.value) return true
-    if (Array.isArray(node?.children)) {
-      stack.push(...node.children)
-    }
-  }
-  return false
+function isMenuVisibleByKey(key: Key) {
+  const menuRightBound = menuRightBoundMap.get(key) ?? 0
+  return menuRightBound + MENU.ITEM_COLUMN_GAP <= threshold.value
 }
 
 watch(
@@ -130,7 +145,7 @@ onBeforeUnmount(() => {
     ref="navigationWrapperRef"
     class="relative flex items-center overflow-hidden"
     :style="{
-      columnGap: `${MENU_SIZE.ITEM_COLUMN_GAP}px`,
+      columnGap: `${MENU.ITEM_COLUMN_GAP}px`,
     }"
   >
     <template
@@ -140,14 +155,14 @@ onBeforeUnmount(() => {
       <div
         v-if="!type"
         :ref="(ref) => forwardRef(key, ref)"
-        v-show="!partiallyVisibleMenuKeys.has(key)"
+        v-show="isMenuVisibleByKey(key)"
         class="shrink-0 rounded-naive transition-[background-color,color]"
         :class="[
           {
             'relative flex items-center px-2.5 py-2': isEmpty(children),
           },
           disabled ? 'cursor-not-allowed opacity-50' : 'cursor-pointer',
-          menuActiveKey === key || (Array.isArray(children) && hasActiveChild(children))
+          menuActiveKey === key || hasActiveChild
             ? 'bg-primary/15 text-primary'
             : 'hover:bg-neutral-150 dark:hover:bg-neutral-800',
         ]"
@@ -193,15 +208,15 @@ onBeforeUnmount(() => {
     <NDropdown
       :options="moreDropdownOptions"
       :value="menuActiveKey"
-      :disabled="isEmpty(moreDropdownOptions)"
+      :disabled="!shouldShowMore"
       :render-icon="renderIcon"
       size="large"
     >
       <div
-        v-show="!isEmpty(partiallyVisibleMenuKeys)"
+        v-show="shouldShowMore"
         class="flex shrink-0 cursor-pointer items-center rounded-naive px-3 py-2 leading-4 font-medium transition-[background-color,color]"
         :class="[
-          Array.isArray(moreDropdownOptions) && hasActiveChild(moreDropdownOptions)
+          hasActiveChild
             ? 'bg-primary/15 text-primary'
             : 'hover:bg-neutral-150 dark:hover:bg-neutral-800',
         ]"