From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/layout/components/TabMenu/src/TabMenu.vue |  240 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 240 insertions(+), 0 deletions(-)

diff --git a/src/layout/components/TabMenu/src/TabMenu.vue b/src/layout/components/TabMenu/src/TabMenu.vue
new file mode 100644
index 0000000..efad6a6
--- /dev/null
+++ b/src/layout/components/TabMenu/src/TabMenu.vue
@@ -0,0 +1,240 @@
+<script lang="tsx">
+import { usePermissionStore } from '@/store/modules/permission'
+import { useAppStore } from '@/store/modules/app'
+
+import { ElScrollbar } from 'element-plus'
+import { Icon } from '@/components/Icon'
+import { Menu } from '@/layout/components/Menu'
+import { pathResolve } from '@/utils/routerHelper'
+import { cloneDeep } from 'lodash-es'
+import { filterMenusPath, initTabMap, tabPathMap } from './helper'
+import { useDesign } from '@/hooks/web/useDesign'
+import { isUrl } from '@/utils/is'
+
+const { getPrefixCls, variables } = useDesign()
+
+const prefixCls = getPrefixCls('tab-menu')
+
+export default defineComponent({
+  name: 'TabMenu',
+  setup() {
+    const { push, currentRoute } = useRouter()
+
+    const { t } = useI18n()
+
+    const appStore = useAppStore()
+
+    const collapse = computed(() => appStore.getCollapse)
+
+    const fixedMenu = computed(() => appStore.getFixedMenu)
+
+    const permissionStore = usePermissionStore()
+
+    const routers = computed(() => permissionStore.getRouters)
+
+    const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
+
+    const setCollapse = () => {
+      appStore.setCollapse(!unref(collapse))
+    }
+
+    onMounted(() => {
+      if (unref(fixedMenu)) {
+        const path = `/${unref(currentRoute).path.split('/')[1]}`
+        const children = unref(tabRouters).find(
+          (v) =>
+            (v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)) &&
+            v.path === path
+        )?.children
+
+        tabActive.value = path
+        if (children) {
+          permissionStore.setMenuTabRouters(
+            cloneDeep(children).map((v) => {
+              v.path = pathResolve(unref(tabActive), v.path)
+              return v
+            })
+          )
+        }
+      }
+    })
+
+    watch(
+      () => routers.value,
+      (routers: AppRouteRecordRaw[]) => {
+        initTabMap(routers)
+        filterMenusPath(routers, routers)
+      },
+      {
+        immediate: true,
+        deep: true
+      }
+    )
+
+    const showTitle = ref(true)
+
+    watch(
+      () => collapse.value,
+      (collapse: boolean) => {
+        if (!collapse) {
+          setTimeout(() => {
+            showTitle.value = !collapse
+          }, 200)
+        } else {
+          showTitle.value = !collapse
+        }
+      }
+    )
+
+    // 鏄惁鏄剧ず鑿滃崟
+    const showMenu = ref(unref(fixedMenu) ? true : false)
+
+    // tab楂樹寒
+    const tabActive = ref('')
+
+    // tab鐐瑰嚮浜嬩欢
+    const tabClick = (item: AppRouteRecordRaw) => {
+      if (isUrl(item.path)) {
+        window.open(item.path)
+        return
+      }
+      const newPath = item.children ? item.path : item.path.split('/')[0]
+      const oldPath = unref(tabActive)
+      tabActive.value = item.children ? item.path : item.path.split('/')[0]
+      if (item.children) {
+        if (newPath === oldPath || !unref(showMenu)) {
+          showMenu.value = unref(fixedMenu) ? true : !unref(showMenu)
+        }
+        if (unref(showMenu)) {
+          permissionStore.setMenuTabRouters(
+            cloneDeep(item.children).map((v) => {
+              v.path = pathResolve(unref(tabActive), v.path)
+              return v
+            })
+          )
+        }
+      } else {
+        push(item.path)
+        permissionStore.setMenuTabRouters([])
+        showMenu.value = false
+      }
+    }
+
+    // 璁剧疆楂樹寒
+    const isActive = (currentPath: string) => {
+      const { path } = unref(currentRoute)
+      if (tabPathMap[currentPath].includes(path)) {
+        return true
+      }
+      return false
+    }
+
+    const mouseleave = () => {
+      if (!unref(showMenu) || unref(fixedMenu)) return
+      showMenu.value = false
+    }
+
+    return () => (
+      <div
+        id={`${variables.namespace}-menu`}
+        class={[
+          prefixCls,
+          'relative bg-[var(--left-menu-bg-color)] layout-border__right',
+          {
+            'w-[var(--tab-menu-max-width)]': !unref(collapse),
+            'w-[var(--tab-menu-min-width)]': unref(collapse)
+          }
+        ]}
+        onMouseleave={mouseleave}
+      >
+        <ElScrollbar class="!h-[calc(100%-var(--tab-menu-collapse-height))]">
+          <div>
+            {() => {
+              return unref(tabRouters).map((v) => {
+                const item = (
+                  v.meta?.alwaysShow || (v?.children?.length && v?.children?.length > 1)
+                    ? v
+                    : {
+                        ...(v?.children && v?.children[0]),
+                        path: pathResolve(v.path, (v?.children && v?.children[0])?.path as string)
+                      }
+                ) as AppRouteRecordRaw
+                return (
+                  <div
+                    class={[
+                      `${prefixCls}__item`,
+                      'text-center text-12px relative py-12px cursor-pointer',
+                      {
+                        'is-active': isActive(v.path)
+                      }
+                    ]}
+                    onClick={() => {
+                      tabClick(item)
+                    }}
+                  >
+                    <div>
+                      <Icon icon={item?.meta?.icon}></Icon>
+                    </div>
+                    {!unref(showTitle) ? undefined : (
+                      <p class="mt-5px break-words px-2px">{t(item.meta?.title)}</p>
+                    )}
+                  </div>
+                )
+              })
+            }}
+          </div>
+        </ElScrollbar>
+        <div
+          class={[
+            `${prefixCls}--collapse`,
+            'text-center h-[var(--tab-menu-collapse-height)] leading-[var(--tab-menu-collapse-height)] cursor-pointer'
+          ]}
+          onClick={setCollapse}
+        >
+          <Icon icon={unref(collapse) ? 'ep:d-arrow-right' : 'ep:d-arrow-left'}></Icon>
+        </div>
+        <Menu
+          class={[
+            '!absolute top-0 z-11',
+            {
+              '!left-[var(--tab-menu-min-width)]': unref(collapse),
+              '!left-[var(--tab-menu-max-width)]': !unref(collapse),
+              '!w-[var(--left-menu-max-width)]': unref(showMenu) || unref(fixedMenu),
+              '!w-0': !unref(showMenu) && !unref(fixedMenu)
+            }
+          ]}
+          style="transition: width var(--transition-time-02), left var(--transition-time-02);"
+        ></Menu>
+      </div>
+    )
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+$prefix-cls: #{$namespace}-tab-menu;
+
+.#{$prefix-cls} {
+  transition: all var(--transition-time-02);
+
+  &__item {
+    color: var(--left-menu-text-color);
+    transition: all var(--transition-time-02);
+
+    &:hover {
+      color: var(--left-menu-text-active-color);
+      // background-color: var(--left-menu-bg-active-color);
+    }
+  }
+
+  &--collapse {
+    color: var(--left-menu-text-color);
+    background-color: var(--left-menu-bg-light-color);
+  }
+
+  .is-active {
+    color: var(--left-menu-text-active-color);
+    background-color: var(--left-menu-bg-active-color);
+  }
+}
+</style>

--
Gitblit v1.8.0