Browse Source

feat(Layout): Add cutMenu layout

陈凯龙 3 years ago
parent
commit
ff4dd3afbf

+ 1 - 1
src/App.vue

@@ -34,7 +34,7 @@ initDark()
 
 html,
 body {
-  padding: 0;
+  padding: 0 !important;
   margin: 0;
   overflow: hidden;
   .size;

+ 24 - 19
src/components/Logo/src/Logo.vue

@@ -6,7 +6,7 @@ const appStore = useAppStore()
 
 const show = ref(true)
 
-const title = computed(() => appStore.getLogoTitle)
+const title = computed(() => appStore.getTitle)
 
 const layout = computed(() => appStore.getLayout)
 
@@ -19,15 +19,30 @@ onMounted(() => {
 watch(
   () => collapse.value,
   (collapse: boolean) => {
-    if (layout.value !== 'classic') {
+    if (unref(layout) === 'topLeft' || unref(layout) === 'cutMenu') {
+      show.value = true
+      return
+    }
+    if (!collapse) {
+      setTimeout(() => {
+        show.value = !collapse
+      }, 400)
+    } else {
+      show.value = !collapse
+    }
+  }
+)
+
+watch(
+  () => layout.value,
+  (layout) => {
+    if (layout === 'top' || layout === 'cutMenu') {
       show.value = true
     } else {
-      if (!collapse) {
-        setTimeout(() => {
-          show.value = !collapse
-        }, 400)
+      if (unref(collapse)) {
+        show.value = false
       } else {
-        show.value = !collapse
+        show.value = true
       }
     }
   }
@@ -55,7 +70,8 @@ watch(
         'ml-10px text-16px font-700',
         {
           'text-[var(--logo-title-text-color)]': layout === 'classic',
-          'text-[var(--top-header-text-color)]': layout === 'topLeft'
+          'text-[var(--top-header-text-color)]':
+            layout === 'topLeft' || layout === 'top' || layout === 'cutMenu'
         }
       ]"
     >
@@ -66,15 +82,4 @@ watch(
 
 <style lang="less" scoped>
 @prefix-cls: ~'@{namespace}-logo';
-
-.@{prefix-cls} {
-  &:after {
-    position: absolute;
-    right: 0;
-    bottom: 0;
-    width: 100%;
-    border-bottom: 1px solid var(--logo-border-color);
-    content: '';
-  }
-}
 </style>

+ 80 - 36
src/components/Menu/src/Menu.vue

@@ -1,5 +1,5 @@
 <script lang="tsx">
-import { computed, defineComponent, unref } from 'vue'
+import { computed, defineComponent, unref, PropType } from 'vue'
 import { ElMenu, ElScrollbar } from 'element-plus'
 import { useAppStore } from '@/store/modules/app'
 import { usePermissionStore } from '@/store/modules/permission'
@@ -10,25 +10,35 @@ import { isUrl } from '@/utils/is'
 
 export default defineComponent({
   name: 'Menu',
-  setup() {
+  props: {
+    menuSelect: {
+      type: Function as PropType<(index: string) => void>,
+      default: undefined
+    }
+  },
+  setup(props) {
     const appStore = useAppStore()
 
+    const layout = computed(() => appStore.getLayout)
+
     const { push, currentRoute } = useRouter()
 
     const permissionStore = usePermissionStore()
 
     const menuMode = computed((): 'vertical' | 'horizontal' => {
       // 竖
-      const vertical: LayoutType[] = ['classic', 'topLeft']
+      const vertical: LayoutType[] = ['classic', 'topLeft', 'cutMenu']
 
-      if (vertical.includes(appStore.getLayout)) {
+      if (vertical.includes(unref(layout))) {
         return 'vertical'
       } else {
         return 'horizontal'
       }
     })
 
-    const routers = computed(() => permissionStore.getRouters)
+    const routers = computed(() =>
+      unref(layout) === 'cutMenu' ? permissionStore.getMenuTabRouters : permissionStore.getRouters
+    )
 
     const collapse = computed(() => appStore.getCollapse)
 
@@ -42,6 +52,10 @@ export default defineComponent({
     })
 
     const menuSelect = (index: string) => {
+      if (props.menuSelect) {
+        props.menuSelect(index)
+      }
+      // 自定义事件
       if (isUrl(index)) {
         window.open(index)
       } else {
@@ -52,19 +66,21 @@ export default defineComponent({
     return () => (
       <div
         class={[
-          'v-menu',
-          'h-[100%] overflow-hidden z-100 flex-col',
-          appStore.getCollapse
-            ? 'w-[var(--left-menu-min-width)]'
-            : 'w-[var(--left-menu-max-width)]',
-          'bg-[var(--left-menu-bg-color)]'
+          `v-menu v-menu__${unref(menuMode)}`,
+          'h-[100%] overflow-hidden z-100 flex-col bg-[var(--left-menu-bg-color)]',
+          {
+            'w-[var(--left-menu-min-width)]': unref(collapse) && unref(layout) !== 'cutMenu',
+            'w-[var(--left-menu-max-width)]': !unref(collapse) && unref(layout) !== 'cutMenu'
+          }
         ]}
       >
         <ElScrollbar>
           <ElMenu
             defaultActive={unref(activeMenu)}
             mode={unref(menuMode)}
-            collapse={unref(collapse)}
+            collapse={
+              unref(layout) === 'top' || unref(layout) === 'cutMenu' ? false : unref(collapse)
+            }
             backgroundColor="var(--left-menu-bg-color)"
             textColor="var(--left-menu-text-color)"
             activeTextColor="var(--left-menu-text-active-color)"
@@ -180,6 +196,35 @@ export default defineComponent({
       display: none;
     }
   }
+
+  // 水平菜单
+  &__horizontal {
+    height: calc(~'var( - -top-tool-height)') !important;
+
+    :deep(.el-menu--horizontal) {
+      height: calc(~'var( - -top-tool-height)');
+      border-bottom: none;
+      // 重新设置底部高亮颜色
+      & > .el-sub-menu.is-active {
+        .el-sub-menu__title {
+          border-bottom-color: var(--el-color-primary) !important;
+        }
+      }
+
+      .el-menu-item.is-active {
+        position: relative;
+
+        &:after {
+          display: none !important;
+        }
+      }
+
+      .v-menu__title {
+        max-height: calc(~'var( - -top-tool-height)') !important;
+        line-height: calc(~'var( - -top-tool-height)');
+      }
+    }
+  }
 }
 </style>
 
@@ -196,36 +241,35 @@ export default defineComponent({
   content: '';
 }
 
-.@{prefix-cls} {
-  &--vertical {
-    // 设置选中时子标题的颜色
-    .is-active {
-      & > .el-sub-menu__title {
-        color: var(--left-menu-text-active-color) !important;
-      }
+.@{prefix-cls}--vertical,
+.@{prefix-cls}--horizontal {
+  // 设置选中时子标题的颜色
+  .is-active {
+    & > .el-sub-menu__title {
+      color: var(--left-menu-text-active-color) !important;
     }
+  }
 
-    // 设置子菜单悬停的高亮和背景色
-    .el-sub-menu__title,
-    .el-menu-item {
-      &:hover {
-        color: var(--left-menu-text-active-color) !important;
-        background-color: var(--left-menu-bg-color) !important;
-      }
+  // 设置子菜单悬停的高亮和背景色
+  .el-sub-menu__title,
+  .el-menu-item {
+    &:hover {
+      color: var(--left-menu-text-active-color) !important;
+      background-color: var(--left-menu-bg-color) !important;
     }
+  }
 
-    // 设置选中时的高亮背景
-    .el-menu-item.is-active {
-      position: relative;
-      background-color: var(--left-menu-bg-active-color) !important;
+  // 设置选中时的高亮背景
+  .el-menu-item.is-active {
+    position: relative;
+    background-color: var(--left-menu-bg-active-color) !important;
 
-      &:hover {
-        background-color: var(--left-menu-bg-active-color) !important;
-      }
+    &:hover {
+      background-color: var(--left-menu-bg-active-color) !important;
+    }
 
-      &:after {
-        .is-active--after;
-      }
+    &:after {
+      .is-active--after;
     }
   }
 }

+ 43 - 18
src/components/Setting/src/Setting.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { ElDrawer, ElDivider } from 'element-plus'
-import { ref, unref } from 'vue'
+import { ref, unref, computed, watch } from 'vue'
 import { useI18n } from '@/hooks/web/useI18n'
 import { ThemeSwitch } from '@/components/ThemeSwitch'
 import { colorIsDark, lighten, hexToRGB } from '@/utils/color'
@@ -15,6 +15,8 @@ const appStore = useAppStore()
 
 const { t } = useI18n()
 
+const layout = computed(() => appStore.getLayout)
+
 const drawer = ref(false)
 
 // 主题色相关
@@ -34,14 +36,20 @@ const setHeaderTheme = (color: string) => {
   const isDarkColor = colorIsDark(color)
   const textColor = isDarkColor ? '#fff' : 'inherit'
   const textHoverColor = isDarkColor ? lighten(color!, 6) : '#f6f6f6'
+  const topToolBorderColor = isDarkColor ? color : '#eee'
   setCssVar('--top-header-bg-color', color)
   setCssVar('--top-header-text-color', textColor)
   setCssVar('--top-header-hover-color', textHoverColor)
+  setCssVar('--top-tool-border-color', topToolBorderColor)
   appStore.setTheme({
     topHeaderBgColor: color,
     topHeaderTextColor: textColor,
-    topHeaderHoverColor: textHoverColor
+    topHeaderHoverColor: textHoverColor,
+    topToolBorderColor
   })
+  if (unref(layout) === 'top') {
+    setMenuTheme(color)
+  }
 }
 
 // 菜单主题相关
@@ -72,11 +80,26 @@ const setMenuTheme = (color: string) => {
     // logo字体颜色
     logoTitleTextColor: isDarkColor ? '#fff' : 'inherit',
     // logo边框颜色
-    logoBorderColor: isDarkColor ? 'inherit' : '#eee'
+    logoBorderColor: isDarkColor ? color : '#eee'
   }
   appStore.setTheme(theme)
   appStore.setCssVarTheme()
 }
+
+// 监听layout变化,重置一些主题色
+watch(
+  () => layout.value,
+  (n, o) => {
+    if (o === 'top') {
+      menuTheme.value = '#fff'
+      setMenuTheme('#fff')
+    }
+    if ((o === 'classic' || o === 'topLeft') && n === 'top') {
+      menuTheme.value = headerTheme.value
+      setMenuTheme(unref(menuTheme))
+    }
+  }
+)
 </script>
 
 <template>
@@ -136,21 +159,23 @@ const setMenuTheme = (color: string) => {
       />
 
       <!-- 菜单主题 -->
-      <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
-      <ColorRadioPicker
-        v-model="menuTheme"
-        :schema="[
-          '#fff',
-          '#001529',
-          '#212121',
-          '#273352',
-          '#191b24',
-          '#383f45',
-          '#001628',
-          '#344058'
-        ]"
-        @change="setMenuTheme"
-      />
+      <template v-if="layout !== 'top'">
+        <ElDivider>{{ t('setting.menuTheme') }}</ElDivider>
+        <ColorRadioPicker
+          v-model="menuTheme"
+          :schema="[
+            '#fff',
+            '#001529',
+            '#212121',
+            '#273352',
+            '#191b24',
+            '#383f45',
+            '#001628',
+            '#344058'
+          ]"
+          @change="setMenuTheme"
+        />
+      </template>
     </div>
 
     <!-- 界面显示 -->

+ 12 - 13
src/components/Setting/src/components/InterfaceDisplay.vue

@@ -2,7 +2,7 @@
 import { ElSwitch } from 'element-plus'
 import { useI18n } from '@/hooks/web/useI18n'
 import { useAppStore } from '@/store/modules/app'
-import { ref } from 'vue'
+import { computed, ref, watch } from 'vue'
 
 const appStore = useAppStore()
 
@@ -22,13 +22,6 @@ const breadcrumbIconChange = (show: boolean) => {
   appStore.setBreadcrumbIcon(show)
 }
 
-// 折叠菜单
-const collapse = ref(appStore.getCollapse)
-
-const collapseChange = (show: boolean) => {
-  appStore.setCollapse(show)
-}
-
 // 折叠图标
 const hamburger = ref(appStore.getHamburger)
 
@@ -84,6 +77,17 @@ const greyMode = ref(appStore.getGreyMode)
 const greyModeChange = (show: boolean) => {
   appStore.setGreyMode(show)
 }
+
+const layout = computed(() => appStore.getLayout)
+
+watch(
+  () => layout.value,
+  (n) => {
+    if (n === 'top') {
+      appStore.setCollapse(false)
+    }
+  }
+)
 </script>
 
 <template>
@@ -98,11 +102,6 @@ const greyModeChange = (show: boolean) => {
       <ElSwitch v-model="breadcrumbIcon" @change="breadcrumbIconChange" />
     </div>
 
-    <div class="flex justify-between items-center">
-      <span class="text-14px">{{ t('setting.collapseMenu') }}</span>
-      <ElSwitch v-model="collapse" @change="collapseChange" />
-    </div>
-
     <div class="flex justify-between items-center">
       <span class="text-14px">{{ t('setting.hamburgerIcon') }}</span>
       <ElSwitch v-model="hamburger" @change="hamburgerChange" />

+ 67 - 3
src/components/Setting/src/components/LayoutRadioPicker.vue

@@ -5,14 +5,13 @@ import { computed } from 'vue'
 const appStore = useAppStore()
 
 const layout = computed(() => appStore.getLayout)
-console.log(layout.value)
 </script>
 
 <template>
   <div class="v-layout-radio-picker flex flex-wrap space-x-14px">
     <div
       :class="[
-        'v-layout-radio-picker__classic relative w-56px h-48px cursor-pointer bg-gray-100',
+        'v-layout-radio-picker__classic relative w-56px h-48px cursor-pointer bg-gray-300',
         {
           'is-acitve': layout === 'classic'
         }
@@ -21,13 +20,33 @@ console.log(layout.value)
     ></div>
     <div
       :class="[
-        'v-layout-radio-picker__top-left relative w-56px h-48px cursor-pointer bg-gray-100',
+        'v-layout-radio-picker__top-left relative w-56px h-48px cursor-pointer bg-gray-300',
         {
           'is-acitve': layout === 'topLeft'
         }
       ]"
       @click="appStore.setLayout('topLeft')"
     ></div>
+    <div
+      :class="[
+        'v-layout-radio-picker__top relative w-56px h-48px cursor-pointer bg-gray-300',
+        {
+          'is-acitve': layout === 'top'
+        }
+      ]"
+      @click="appStore.setLayout('top')"
+    ></div>
+    <div
+      :class="[
+        'v-layout-radio-picker__cut-menu relative w-56px h-48px cursor-pointer bg-gray-300',
+        {
+          'is-acitve': layout === 'cutMenu'
+        }
+      ]"
+      @click="appStore.setLayout('cutMenu')"
+    >
+      <div class="absolute h-full w-[33%] top-0 left-[10%] bg-gray-200"></div>
+    </div>
   </div>
 </template>
 
@@ -91,6 +110,51 @@ console.log(layout.value)
     }
   }
 
+  &__top {
+    border: 2px solid #e5e7eb;
+    border-radius: 4px;
+
+    &:before {
+      position: absolute;
+      top: 0;
+      left: 0;
+      z-index: 1;
+      width: 100%;
+      height: 33%;
+      background-color: #273352;
+      border-radius: 4px 4px 0 0;
+      content: '';
+    }
+  }
+
+  &__cut-menu {
+    border: 2px solid #e5e7eb;
+    border-radius: 4px;
+
+    &:before {
+      position: absolute;
+      top: 0;
+      left: 0;
+      z-index: 1;
+      width: 100%;
+      height: 33%;
+      background-color: #273352;
+      border-radius: 4px 4px 0 0;
+      content: '';
+    }
+
+    &:after {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 10%;
+      height: 100%;
+      background-color: #fff;
+      border-radius: 4px 0 0 4px;
+      content: '';
+    }
+  }
+
   .is-acitve {
     border-color: var(--el-color-primary);
   }

+ 3 - 0
src/components/TabMenu/index.ts

@@ -0,0 +1,3 @@
+import TabMenu from './src/TabMenu.vue'
+
+export { TabMenu }

+ 211 - 0
src/components/TabMenu/src/TabMenu.vue

@@ -0,0 +1,211 @@
+<script lang="tsx">
+import { usePermissionStore } from '@/store/modules/permission'
+import { useAppStore } from '@/store/modules/app'
+import { computed, unref, defineComponent, watch, ref } from 'vue'
+import { useI18n } from '@/hooks/web/useI18n'
+import { ElScrollbar } from 'element-plus'
+import { Icon } from '@/components/Icon'
+import { Menu } from '@/components/Menu'
+import { useRouter } from 'vue-router'
+import { pathResolve } from '@/utils/routerHelper'
+import { cloneDeep } from 'lodash-es'
+import { filterMenusPath, initTabMap, tabPathMap } from './helper'
+
+export default defineComponent({
+  name: 'TabMenu',
+  setup() {
+    const { push, currentRoute } = useRouter()
+
+    const { t } = useI18n()
+
+    const appStore = useAppStore()
+
+    const collapse = computed(() => appStore.getCollapse)
+
+    const permissionStore = usePermissionStore()
+
+    const routers = computed(() => permissionStore.getRouters)
+
+    const tabRouters = computed(() => unref(routers).filter((v) => !v?.meta?.hidden))
+
+    const setCollapse = () => {
+      appStore.setCollapse(!unref(collapse))
+    }
+
+    watch(
+      () => routers.value,
+      (routers: AppRouteRecordRaw[]) => {
+        initTabMap(routers)
+        filterMenusPath(routers, routers)
+        console.log(tabPathMap)
+      },
+      {
+        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(false)
+
+    // tab高亮
+    const tabActive = ref('')
+
+    // tab点击事件
+    const tabClick = (item: AppRouteRecordRaw) => {
+      tabActive.value = item.children ? item.path : item.path.split('/')[0]
+      if (item.children) {
+        showMenu.value = !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 isActice = (currentPath: string) => {
+      const { path } = unref(currentRoute)
+      if (tabPathMap[currentPath].includes(path)) {
+        return true
+      }
+      return false
+    }
+
+    const mouseleave = () => {
+      if (!unref(showMenu)) return
+      showMenu.value = false
+    }
+
+    return () => (
+      <div
+        class={[
+          'v-tab-menu relative bg-[var(--left-menu-bg-color)] top-1px',
+          {
+            '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)-1px)]">
+          <div>
+            {() => {
+              return unref(tabRouters).map((v) => {
+                const item = (
+                  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={[
+                      'v-tab-menu-item text-center text-12px relative py-12px cursor-pointer',
+                      {
+                        'is-active': isActice(v.path)
+                      }
+                    ]}
+                    onClick={() => {
+                      tabClick(item)
+                    }}
+                  >
+                    <div>
+                      <Icon icon={item?.meta?.icon}></Icon>
+                    </div>
+                    {!unref(showTitle) ? undefined : (
+                      <p class="break-words mt-5px px-2px">{t(item.meta?.title)}</p>
+                    )}
+                  </div>
+                )
+              })
+            }}
+          </div>
+        </ElScrollbar>
+        <div
+          class="v-tab-menu-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 border-left-1 border-solid border-[var(--left-menu-bg-light-color)]',
+            {
+              '!left-[var(--tab-menu-min-width)]': unref(collapse),
+              '!left-[var(--tab-menu-max-width)]': !unref(collapse),
+              '!w-[calc(var(--left-menu-max-width)+1px)]': unref(showMenu),
+              '!w-0': !unref(showMenu)
+            }
+          ]}
+          style="transition: width var(--transition-time-02), left var(--transition-time-02);"
+        ></Menu>
+      </div>
+    )
+  }
+})
+</script>
+
+<style lang="less" scoped>
+@prefix-cls: ~'@{namespace}-tab-menu';
+
+.@{prefix-cls} {
+  transition: all var(--transition-time-02);
+
+  &:after {
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 1px;
+    height: 100%;
+    border-left: 1px solid var(--left-menu-border-color);
+    content: '';
+  }
+
+  &-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);
+    border-top: 1px solid var(--left-menu-border-color);
+  }
+
+  .is-active {
+    color: var(--left-menu-text-active-color);
+    background-color: var(--left-menu-bg-active-color);
+  }
+}
+</style>

+ 52 - 0
src/components/TabMenu/src/helper.ts

@@ -0,0 +1,52 @@
+import { getAllParentPath } from '@/components/Menu/src/helper'
+import type { RouteMeta } from 'vue-router'
+import { isUrl } from '@/utils/is'
+import { cloneDeep } from 'lodash-es'
+import { reactive } from 'vue'
+
+export type TabMapTypes = {
+  [key: string]: string[]
+}
+
+export const tabPathMap = reactive<TabMapTypes>({})
+
+export const initTabMap = (routes: AppRouteRecordRaw[]) => {
+  for (const v of routes) {
+    const meta = (v.meta ?? {}) as RouteMeta
+    if (!meta?.hidden) {
+      tabPathMap[v.path] = []
+    }
+  }
+}
+
+export const filterMenusPath = (
+  routes: AppRouteRecordRaw[],
+  allRoutes: AppRouteRecordRaw[]
+): AppRouteRecordRaw[] => {
+  const res: AppRouteRecordRaw[] = []
+  for (const v of routes) {
+    let data: Nullable<AppRouteRecordRaw> = null
+    const meta = (v.meta ?? {}) as RouteMeta
+    if (!meta.hidden) {
+      const allParentPaht = getAllParentPath<AppRouteRecordRaw>(allRoutes, v.path)
+
+      const fullPath = isUrl(v.path) ? v.path : allParentPaht.join('/')
+
+      data = cloneDeep(v)
+      data.path = fullPath
+      if (v.children && data) {
+        data.children = filterMenusPath(v.children, allRoutes)
+      }
+
+      if (data) {
+        res.push(data)
+      }
+
+      if (allParentPaht.length && Reflect.has(tabPathMap, allParentPaht[0])) {
+        tabPathMap[allParentPaht[0]].push(fullPath)
+      }
+    }
+  }
+
+  return res
+}

+ 14 - 6
src/components/TagsView/src/TagsView.vue

@@ -132,8 +132,8 @@ watch(
       <Icon icon="ep:d-arrow-left" color="#333" />
     </span>
     <div class="overflow-hidden flex-1">
-      <ElScrollbar>
-        <div class="flex h-[var(--tags-view-height)]">
+      <ElScrollbar class="h-full">
+        <div class="flex h-full">
           <ContextMenu
             :schema="[
               {
@@ -202,7 +202,10 @@ watch(
             ]"
           >
             <router-link :to="{ ...item }" custom v-slot="{ navigate }">
-              <div @click="navigate" class="h-full flex justify-center items-center">
+              <div
+                @click="navigate"
+                class="h-full flex justify-center items-center whitespace-nowrap"
+              >
                 {{ t(item?.meta?.title as string) }}
                 <Icon
                   class="v-tags-view__item--close"
@@ -291,6 +294,10 @@ watch(
 @prefix-cls: ~'@{namespace}-tags-view';
 
 .@{prefix-cls} {
+  :deep(.el-scrollbar__view) {
+    height: 100%;
+  }
+
   &__tool {
     position: relative;
 
@@ -302,11 +309,12 @@ watch(
 
     &:after {
       position: absolute;
-      top: 0;
+      top: 1px;
       left: 0;
       width: 100%;
-      height: 100%;
-      border: 1px solid var(--top-tool-border-color);
+      height: calc(~'100% - 1px');
+      border-right: 1px solid var(--tags-view-border-color);
+      border-left: 1px solid var(--tags-view-border-color);
       content: '';
     }
   }

+ 7 - 5
src/config/app.ts

@@ -2,7 +2,7 @@ import { useCache } from '@/hooks/web/useCache'
 
 const { wsCache } = useCache()
 
-export type LayoutType = 'classic' | 'topLeft' | 'leftTop' | 'top' | 'test'
+export type LayoutType = 'classic' | 'topLeft' | 'top' | 'cutMenu'
 
 export interface AppState {
   breadcrumb: boolean
@@ -15,11 +15,11 @@ export interface AppState {
   tagsView: boolean
   logo: boolean
   fixedHeader: boolean
+  fixedMenu: boolean
   greyMode: boolean
 
   layout: LayoutType
   title: string
-  logoTitle: string
   userInfo: string
   isDark: boolean
   currentSize: ElememtPlusSzie
@@ -39,11 +39,11 @@ export const appModules: AppState = {
   tagsView: true, // 标签页
   logo: true, // logo
   fixedHeader: true, // 固定toolheader
+  fixedMenu: false, // 固定切割菜单
   greyMode: false, // 是否开始灰色模式,用于特殊悼念日
 
   layout: wsCache.get('layout') || 'classic', // layout布局
-  title: 'butterfly-admin', // 标题
-  logoTitle: 'ButterflyAdmin', // logo标题
+  title: 'ButterflyAdmin', // 标题
   userInfo: 'userInfo', // 登录信息存储字段-建议每个项目换一个字段,避免与其他项目冲突
   isDark: wsCache.get('isDark') || false, // 是否是暗黑模式
   currentSize: wsCache.get('default') || 'default', // 组件尺寸
@@ -75,6 +75,8 @@ export const appModules: AppState = {
     // 头部字体颜色
     topHeaderTextColor: 'inherit',
     // 头部悬停颜色
-    topHeaderHoverColor: '#f6f6f6'
+    topHeaderHoverColor: '#f6f6f6',
+    // 头部边框颜色
+    topToolBorderColor: '#eee'
   }
 }

+ 8 - 2
src/layout/Layout.vue

@@ -1,7 +1,7 @@
 <script lang="tsx">
 import { computed, defineComponent, unref } from 'vue'
 import { useAppStore } from '@/store/modules/app'
-import { Backtop } from '@/components/Backtop'
+// import { Backtop } from '@/components/Backtop'
 import { Setting } from '@/components/Setting'
 import { useRenderLayout } from './components/useRenderLayout'
 
@@ -27,6 +27,12 @@ const renderLayout = () => {
     case 'topLeft':
       const { renderTopLeft } = useRenderLayout()
       return renderTopLeft()
+    case 'top':
+      const { renderTop } = useRenderLayout()
+      return renderTop()
+    case 'cutMenu':
+      const { renderCutMenu } = useRenderLayout()
+      return renderCutMenu()
     default:
       break
   }
@@ -46,7 +52,7 @@ export default defineComponent({
 
         {renderLayout()}
 
-        <Backtop></Backtop>
+        {/*<Backtop></Backtop>*/}
 
         <Setting></Setting>
       </section>

+ 11 - 6
src/layout/components/ToolHeader.vue

@@ -22,6 +22,9 @@ const screenfull = computed(() => appStore.getScreenfull)
 // 尺寸图标
 const size = computed(() => appStore.getSize)
 
+// 布局
+const layout = computed(() => appStore.getLayout)
+
 // 多语言图标
 const locale = computed(() => appStore.getLocale)
 
@@ -35,12 +38,14 @@ export default defineComponent({
           'h-[var(--top-tool-height)] relative px-[var(--top-tool-p-x)] flex items-center justify-between'
         ]}
       >
-        <div class="h-full flex items-center">
-          {hamburger.value ? (
-            <Collapse class="hover-tigger" color="var(--top-header-text-color)"></Collapse>
-          ) : undefined}
-          {breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
-        </div>
+        {layout.value !== 'top' ? (
+          <div class="h-full flex items-center">
+            {hamburger.value && layout.value !== 'cutMenu' ? (
+              <Collapse class="hover-tigger" color="var(--top-header-text-color)"></Collapse>
+            ) : undefined}
+            {breadcrumb.value ? <Breadcrumb class="<md:hidden"></Breadcrumb> : undefined}
+          </div>
+        ) : undefined}
         <div class="h-full flex items-center">
           {screenfull.value ? (
             <Screenfull class="hover-tigger" color="var(--top-header-text-color)"></Screenfull>

+ 106 - 8
src/layout/components/useRenderLayout.tsx

@@ -1,6 +1,7 @@
 import { computed } from 'vue'
 import { useAppStore } from '@/store/modules/app'
 import { Menu } from '@/components/Menu'
+import { TabMenu } from '@/components/TabMenu'
 import { TagsView } from '@/components/TagsView'
 import { Logo } from '@/components/Logo'
 import AppView from './AppView.vue'
@@ -32,7 +33,7 @@ export const useRenderLayout = () => {
           {logo.value ? (
             <Logo
               class={[
-                'bg-[var(--left-menu-bg-color)]',
+                'bg-[var(--left-menu-bg-color)] border-bottom-1 border-solid border-[var(--logo-border-color)]',
                 {
                   '!pl-0': mobile.value && collapse.value,
                   'w-[var(--left-menu-min-width)]': appStore.getCollapse,
@@ -80,9 +81,11 @@ export const useRenderLayout = () => {
               ]}
               style="transition: all var(--transition-time-02);"
             >
-              <ToolHeader class="border-bottom bg-[var(--top-header-bg-color)]"></ToolHeader>
+              <ToolHeader class="border-bottom-1 border-solid border-[var(--top-tool-border-color)] bg-[var(--top-header-bg-color)]"></ToolHeader>
 
-              {tagsView.value ? <TagsView class="border-bottom"></TagsView> : undefined}
+              {tagsView.value ? (
+                <TagsView class="border-bottom-1 border-solid border-[var(--tags-view-border-color)]"></TagsView>
+              ) : undefined}
             </div>
 
             <AppView></AppView>
@@ -95,12 +98,12 @@ export const useRenderLayout = () => {
   const renderTopLeft = () => {
     return (
       <>
-        <div class="flex items-center bg-[var(--top-header-bg-color)]">
-          <Logo class="hover-tigger !pr-15px"></Logo>
+        <div class="flex items-center bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)]">
+          {logo.value ? <Logo class="hover-tigger !pr-15px"></Logo> : undefined}
 
           <ToolHeader class="flex-1"></ToolHeader>
         </div>
-        <div class="absolute top-[var(--logo-height)] left-0 w-full h-[calc(100%-var(--logo-height))] flex">
+        <div class="absolute top-[var(--logo-height)+1px] left-0 w-full h-[calc(100%-1px-var(--logo-height))] flex">
           <Menu class="!h-full"></Menu>
           <div
             class={[
@@ -127,7 +130,7 @@ export const useRenderLayout = () => {
               {tagsView.value ? (
                 <TagsView
                   class={[
-                    'border-bottom border-top',
+                    'border-bottom-1 border-solid border-[var(--tags-view-border-color)]',
                     {
                       '!fixed top-0 left-0 z-10': fixedHeader.value,
                       'w-[calc(100%-var(--left-menu-min-width))] left-[var(--left-menu-min-width)] mt-[var(--logo-height)]':
@@ -148,8 +151,103 @@ export const useRenderLayout = () => {
     )
   }
 
+  const renderTop = () => {
+    return (
+      <>
+        <div class="flex items-center justify-between bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)]">
+          {logo.value ? <Logo class="hover-tigger"></Logo> : undefined}
+          <Menu class="flex-1 px-10px h-[var(--top-tool-height)]"></Menu>
+          <ToolHeader></ToolHeader>
+        </div>
+        <div class="v-app-right h-full w-full">
+          <ElScrollbar
+            class={[
+              'v-content',
+              {
+                'mt-[var(--tags-view-height)]': fixedHeader.value
+              }
+            ]}
+          >
+            {tagsView.value ? (
+              <TagsView
+                class={[
+                  'border-bottom-1 border-solid border-[var(--tags-view-border-color)]',
+                  {
+                    '!fixed w-full top-[var(--top-tool-height)] left-0': fixedHeader.value
+                  }
+                ]}
+                style="transition: all var(--transition-time-02);"
+              ></TagsView>
+            ) : undefined}
+
+            <AppView></AppView>
+          </ElScrollbar>
+        </div>
+      </>
+    )
+  }
+
+  const renderCutMenu = () => {
+    return (
+      <>
+        <div class="flex items-center bg-[var(--top-header-bg-color)] border-bottom-1 border-solid border-[var(--top-tool-border-color)]">
+          {logo.value ? <Logo class="hover-tigger !pr-15px"></Logo> : undefined}
+
+          <ToolHeader class="flex-1"></ToolHeader>
+        </div>
+        <div class="absolute top-[var(--logo-height)] left-0 w-full h-[calc(100%-var(--logo-height))] flex">
+          <TabMenu></TabMenu>
+          {/* <Menu class="!h-full"></Menu> */}
+          <div
+            class={[
+              'v-app-right',
+              'h-[100%]',
+              {
+                'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)]':
+                  collapse.value,
+                'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)]':
+                  !collapse.value
+              }
+            ]}
+            style="transition: all var(--transition-time-02);"
+          >
+            <ElScrollbar
+              class={[
+                'v-content',
+                {
+                  '!h-[calc(100%-var(--tags-view-height))] mt-[calc(var(--tags-view-height))]':
+                    fixedHeader.value && tagsView.value
+                }
+              ]}
+            >
+              {tagsView.value ? (
+                <TagsView
+                  class={[
+                    'border-bottom-1 border-solid border-[var(--tags-view-border-color)]',
+                    {
+                      '!fixed top-0 left-0 z-10': fixedHeader.value,
+                      'w-[calc(100%-var(--tab-menu-min-width))] left-[var(--tab-menu-min-width)] mt-[var(--logo-height)]':
+                        collapse.value && fixedHeader.value,
+                      'w-[calc(100%-var(--tab-menu-max-width))] left-[var(--tab-menu-max-width)] mt-[var(--logo-height)]':
+                        !collapse.value && fixedHeader.value
+                    }
+                  ]}
+                  style="transition: all var(--transition-time-02);"
+                ></TagsView>
+              ) : undefined}
+
+              <AppView></AppView>
+            </ElScrollbar>
+          </div>
+        </div>
+      </>
+    )
+  }
+
   return {
     renderClassic,
-    renderTopLeft
+    renderTopLeft,
+    renderTop,
+    renderCutMenu
   }
 }

+ 2 - 1
src/locales/en.ts

@@ -37,7 +37,8 @@ export default {
     logo: 'Logo',
     greyMode: 'Grey mode',
     fixedHeader: 'Fixed header',
-    headerTheme: 'Header theme'
+    headerTheme: 'Header theme',
+    cutMenu: 'Cut Menu'
   },
   size: {
     default: 'Default',

+ 2 - 1
src/locales/zh-CN.ts

@@ -37,7 +37,8 @@ export default {
     logo: '标志',
     greyMode: '灰色模式',
     fixedHeader: '固定头部',
-    headerTheme: '头部主题'
+    headerTheme: '头部主题',
+    cutMenu: '切割菜单'
   },
   size: {
     default: '默认',

+ 6 - 6
src/store/modules/app.ts

@@ -42,6 +42,9 @@ export const useAppStore = defineStore({
     getFixedHeader(): boolean {
       return this.fixedHeader
     },
+    getFixedMenu(): boolean {
+      return this.fixedMenu
+    },
     getGreyMode(): boolean {
       return this.greyMode
     },
@@ -52,9 +55,6 @@ export const useAppStore = defineStore({
     getTitle(): string {
       return this.title
     },
-    getLogoTitle(): string {
-      return this.logoTitle
-    },
     getUserInfo(): string {
       return this.userInfo
     },
@@ -105,6 +105,9 @@ export const useAppStore = defineStore({
     setFixedHeader(fixedHeader: boolean) {
       this.fixedHeader = fixedHeader
     },
+    setFixedMenu(fixedMenu: boolean) {
+      this.fixedMenu = fixedMenu
+    },
     setGreyMode(greyMode: boolean) {
       this.greyMode = greyMode
     },
@@ -120,9 +123,6 @@ export const useAppStore = defineStore({
     setTitle(title: string) {
       this.title = title
     },
-    setLogoTitle(logoTitle: string) {
-      this.logoTitle = logoTitle
-    },
     setIsDark(isDark: boolean) {
       this.isDark = isDark
       if (this.isDark) {

+ 1 - 9
src/store/modules/permission.ts

@@ -15,7 +15,6 @@ export interface PermissionState {
   routers: AppRouteRecordRaw[]
   addRouters: AppRouteRecordRaw[]
   isAddRouters: boolean
-  activeTab: string
   menuTabRouters: AppRouteRecordRaw[]
 }
 
@@ -25,8 +24,7 @@ export const usePermissionStore = defineStore({
     routers: [],
     addRouters: [],
     isAddRouters: false,
-    menuTabRouters: [],
-    activeTab: ''
+    menuTabRouters: []
   }),
   getters: {
     getRouters(): AppRouteRecordRaw[] {
@@ -38,9 +36,6 @@ export const usePermissionStore = defineStore({
     getIsAddRouters(): boolean {
       return this.isAddRouters
     },
-    getActiveTab(): string {
-      return this.activeTab
-    },
     getMenuTabRouters(): AppRouteRecordRaw[] {
       return this.menuTabRouters
     }
@@ -84,9 +79,6 @@ export const usePermissionStore = defineStore({
     },
     setMenuTabRouters(routers: AppRouteRecordRaw[]): void {
       this.menuTabRouters = routers
-    },
-    setAcitveTab(activeTab: string): void {
-      this.activeTab = activeTab
     }
   }
 })

+ 8 - 0
src/styles/common.less

@@ -7,6 +7,10 @@
 }
 
 .border-bottom {
+  border-bottom: 1px solid var(--top-tool-border-color);
+}
+
+.border-bottom--after {
   @apply relative;
   &:after {
     content: '';
@@ -16,6 +20,10 @@
 }
 
 .border-top {
+  border-top: 1px solid var(--top-tool-border-color);
+}
+
+.border-top--before {
   @apply relative;
   &:before {
     content: '';

+ 1 - 1
src/styles/index.less

@@ -1,2 +1,2 @@
 @import './var.css';
-@import './common.less';
+// @import './common.less';

+ 13 - 1
src/styles/var.css

@@ -2,7 +2,7 @@
   --dark-bg-color: #293146;
 
   /* left menu start */
-  --left-menu-border-color: 'inherit';
+  --left-menu-border-color: '#eee';
 
   --left-menu-max-width: 200px;
 
@@ -43,8 +43,20 @@
   --top-tool-border-color: #eee;
 
   --tags-view-height: 35px;
+
+  --tags-view-border-color: #eee;
   /* header start */
 
+  /* tab menu start */
+  --tab-menu-max-width: 80px;
+
+  --tab-menu-min-width: 30px;
+
+  --tab-menu-collapse-height: 36px;
+
+  --tab-menu-border-color: #eee;
+  /* tab menu end */
+
   --app-content-padding: 20px;
 
   --transition-time-02: 0.2s;

+ 1 - 1
src/utils/tree.ts

@@ -147,7 +147,7 @@ export const forEach = <T = any>(
   const list: any[] = [...tree]
   const { children } = config
   for (let i = 0; i < list.length; i++) {
-    //func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿
+    // func 返回true就终止遍历,避免大量节点场景下无意义循环,引起浏览器卡顿
     if (func(list[i])) {
       return
     }

+ 38 - 34
windi.config.ts

@@ -1,9 +1,9 @@
 import { defineConfig } from 'windicss/helpers'
-// import plugin from 'windicss/plugin'
+import plugin from 'windicss/plugin'
 
-// function range(size, startAt = 1) {
-//   return Array.from(Array(size).keys()).map((i) => i + startAt)
-// }
+function range(size, startAt = 1) {
+  return Array.from(Array(size).keys()).map((i) => i + startAt)
+}
 
 export default defineConfig({
   extract: {
@@ -34,34 +34,38 @@ export default defineConfig({
     //   // ...range(50).map((i) => `mb-${i}px`),
     //   // ...range(50).map((i) => `ml-${i}px`)
     // }
-  }
-  // plugins: [
-  //   plugin(({ addComponents }) => {
-  //     addComponents({
-  //       '.hover-tigger': {
-  //         display: 'flex',
-  //         height: '100%',
-  //         padding: '1px 10px 0',
-  //         cursor: 'pointer',
-  //         alignItems: 'center',
-  //         transition: 'background var(--transition-time-02)',
-  //         '&:hover': {
-  //           backgroundColor: '#f6f6f6'
-  //         }
-  //       },
-  //       '.border-bottom': {
-  //         position: 'relative',
-  //         '&:after': {
-  //           position: 'absolute',
-  //           bottom: '0',
-  //           left: '0',
-  //           width: '100%',
-  //           height: '1px',
-  //           borderTop: '1px solid var(--top-tool-border-color)',
-  //           content: ''
-  //         }
-  //       }
-  //     })
-  //   })
-  // ]
+  },
+  plugins: [
+    plugin(({ addComponents }) => {
+      const obj = {}
+      range(50).map((i) => {
+        obj[`.border-top-${i}`] = {
+          borderTopWidth: `${i}px`
+        }
+        obj[`.border-left-${i}`] = {
+          borderLeftWidth: `${i}px`
+        }
+        obj[`.border-right-${i}`] = {
+          borderRightWidth: `${i}px`
+        }
+        obj[`.border-bottom-${i}`] = {
+          borderBottomWidth: `${i}px`
+        }
+      })
+      addComponents({
+        '.hover-tigger': {
+          display: 'flex',
+          height: '100%',
+          padding: '1px 10px 0',
+          cursor: 'pointer',
+          alignItems: 'center',
+          transition: 'background var(--transition-time-02)',
+          '&:hover': {
+            backgroundColor: 'var(--top-header-hover-color)'
+          }
+        },
+        ...obj
+      })
+    })
+  ]
 })