Просмотр исходного кода

feat(layout): 支持菜单激活项自动滚动

- 路由切换后自动定位侧边菜单激活项
- 将激活菜单平滑滚动到可视区域
- 修正首页菜单路径匹配
Zimo 2 месяцев назад
Родитель
Сommit
9c617059ff

+ 28 - 5
src/layout/components/Menu/src/Menu.vue

@@ -7,6 +7,8 @@ import { useRenderMenuItem } from './components/useRenderMenuItem'
 import { isUrl } from '@/utils/is'
 import { useDesign } from '@/hooks/web/useDesign'
 import { LayoutType } from '@/types/layout'
+import { getCenteredScrollTop } from './helper'
+import { useScrollTo } from '@/hooks/event/useScrollTo'
 
 const { getPrefixCls } = useDesign()
 
@@ -58,6 +60,29 @@ export default defineComponent({
       return path
     })
 
+    const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
+
+    const scrollActiveMenuToCenter = async () => {
+      if (unref(menuMode) !== 'vertical') return
+
+      await nextTick()
+      const wrap$ = unref(scrollbarRef)?.wrapRef
+      const activeMenuItem = wrap$?.querySelector<HTMLElement>('.el-menu-item.is-active')
+      if (!wrap$ || !activeMenuItem) return
+
+      const { start } = useScrollTo({
+        el: wrap$,
+        position: 'scrollTop',
+        to: getCenteredScrollTop(wrap$, activeMenuItem),
+        duration: 300
+      })
+      start()
+    }
+
+    onMounted(scrollActiveMenuToCenter)
+
+    watch(() => unref(activeMenu), scrollActiveMenuToCenter, { flush: 'post' })
+
     const menuSelect = (index: string) => {
       if (props.menuSelect) {
         props.menuSelect(index)
@@ -74,7 +99,7 @@ export default defineComponent({
       if (unref(layout) === 'top') {
         return renderMenu()
       } else {
-        return <ElScrollbar>{renderMenu()}</ElScrollbar>
+        return <ElScrollbar ref={scrollbarRef}>{renderMenu()}</ElScrollbar>
       }
     }
 
@@ -95,8 +120,7 @@ export default defineComponent({
               ? `${prefixCls}-popper--vertical`
               : `${prefixCls}-popper--horizontal`
           }
-          onSelect={menuSelect}
-        >
+          onSelect={menuSelect}>
           {{
             default: () => {
               const { renderMenuItem } = useRenderMenuItem(unref(menuMode))
@@ -117,8 +141,7 @@ export default defineComponent({
             'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
             'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
           }
-        ]}
-      >
+        ]}>
         {renderMenuWrap()}
       </div>
     )

+ 10 - 0
src/layout/components/Menu/src/helper.ts

@@ -52,3 +52,13 @@ export const hasOneShowingChild = (
     onlyOneChild: unref(onlyOneChild)
   }
 }
+
+export const getCenteredScrollTop = (container: HTMLElement, target: HTMLElement) => {
+  const containerRect = container.getBoundingClientRect()
+  const targetRect = target.getBoundingClientRect()
+  const targetTop = container.scrollTop + targetRect.top - containerRect.top
+  const centeredScrollTop = targetTop - (container.clientHeight - targetRect.height) / 2
+  const maxScrollTop = Math.max(0, container.scrollHeight - container.clientHeight)
+
+  return Math.min(maxScrollTop, Math.max(0, centeredScrollTop))
+}

+ 28 - 9
src/layout/components/TabMenu/src/TabMenu.vue

@@ -10,6 +10,8 @@ import { cloneDeep } from 'lodash-es'
 import { filterMenusPath, initTabMap, tabPathMap } from './helper'
 import { useDesign } from '@/hooks/web/useDesign'
 import { isUrl } from '@/utils/is'
+import { getCenteredScrollTop } from '@/layout/components/Menu/src/helper'
+import { useScrollTo } from '@/hooks/event/useScrollTo'
 
 const { getPrefixCls, variables } = useDesign()
 
@@ -34,6 +36,25 @@ export default defineComponent({
 
     const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
 
+    const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
+
+    const scrollActiveMenuToCenter = async () => {
+      await nextTick()
+      const wrap$ = unref(scrollbarRef)?.wrapRef
+      const activeMenuItem = wrap$?.querySelector<HTMLElement>(`.${prefixCls}__item.is-active`)
+      if (!wrap$ || !activeMenuItem) return
+
+      const { start } = useScrollTo({
+        el: wrap$,
+        position: 'scrollTop',
+        to: getCenteredScrollTop(wrap$, activeMenuItem),
+        duration: 300
+      })
+      start()
+    }
+
+    onMounted(scrollActiveMenuToCenter)
+
     const setCollapse = () => {
       appStore.setCollapse(!unref(collapse))
     }
@@ -71,6 +92,8 @@ export default defineComponent({
       }
     )
 
+    watch(() => unref(currentRoute).fullPath, scrollActiveMenuToCenter, { flush: 'post' })
+
     const showTitle = ref(true)
 
     watch(
@@ -145,9 +168,8 @@ export default defineComponent({
             'w-[var(--tab-menu-min-width)]': unref(collapse)
           }
         ]}
-        onMouseleave={mouseleave}
-      >
-        <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height))]">
+        onMouseleave={mouseleave}>
+        <ElScrollbar ref={scrollbarRef} class="!h-[calc(100%-var(--tab-menu-collapse-height))]">
           <div>
             {() => {
               return unref(tabRouters).map((v) => {
@@ -170,8 +192,7 @@ export default defineComponent({
                     ]}
                     onClick={() => {
                       tabClick(item)
-                    }}
-                  >
+                    }}>
                     <div>
                       <Icon icon={item?.meta?.icon}></Icon>
                     </div>
@@ -189,8 +210,7 @@ export default defineComponent({
             `${prefixCls}--collapse`,
             'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
           ]}
-          onClick={setCollapse}
-        >
+          onClick={setCollapse}>
           <Icon icon={unref(collapse) ? 'ep:d-arrow-right' : 'ep:d-arrow-left'}></Icon>
         </div>
         <Menu
@@ -203,8 +223,7 @@ export default defineComponent({
               '!w-0': !unref(showMenu) && !unref(fixedMenu)
             }
           ]}
-          style="transition: width var(--transition-time-02), left var(--transition-time-02);"
-        ></Menu>
+          style="transition: width var(--transition-time-02), left var(--transition-time-02);"></Menu>
       </div>
     )
   }

+ 4 - 1
src/layout/components/TabMenu/src/helper.ts

@@ -2,6 +2,7 @@ import { getAllParentPath } from '@/layout/components/Menu/src/helper'
 import type { RouteMeta } from 'vue-router'
 import { isUrl } from '@/utils/is'
 import { cloneDeep } from 'lodash-es'
+import { pathResolve } from '@/utils/routerHelper'
 
 export type TabMapTypes = {
   [key: string]: string[]
@@ -29,7 +30,9 @@ export const filterMenusPath = (
     if (!meta.hidden || meta.canTo) {
       const allParentPath = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
 
-      const fullPath = isUrl(v.path) ? v.path : allParentPath.join('/')
+      const fullPath = isUrl(v.path)
+        ? v.path
+        : allParentPath.reduce((parentPath, path) => pathResolve(parentPath, path), '')
 
       data = cloneDeep(v)
       data.path = fullPath