Quellcode durchsuchen

perf(layout): 优化导航栏逻辑与样式

wangcong vor 1 Monat
Ursprung
Commit
54f7bba9df
4 geänderte Dateien mit 314 neuen und 60 gelöschten Zeilen
  1. 1 0
      package.json
  2. 23 0
      pnpm-lock.yaml
  3. 289 60
      src/layout/HvacAside.vue
  4. 1 0
      src/main.ts

+ 1 - 0
package.json

@@ -31,6 +31,7 @@
     "lodash-es": "^4.17.21",
     "pinia": "^2.3.0",
     "qs": "^6.14.0",
+    "simplebar-vue": "^2.4.0",
     "vue": "^3.5.13",
     "vue-echarts": "^7.0.3",
     "vue-i18n": "^11.0.1",

+ 23 - 0
pnpm-lock.yaml

@@ -40,6 +40,9 @@ importers:
       qs:
         specifier: ^6.14.0
         version: 6.14.0
+      simplebar-vue:
+        specifier: ^2.4.0
+        version: 2.4.0(vue@3.5.13(typescript@5.6.3))
       vue:
         specifier: ^3.5.13
         version: 3.5.13(typescript@5.6.3)
@@ -4727,6 +4730,14 @@ packages:
     resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
     engines: {node: '>=14'}
 
+  simplebar-core@1.3.0:
+    resolution: {integrity: sha512-LpWl3w0caz0bl322E68qsrRPpIn+rWBGAaEJ0lUJA7Xpr2sw92AkIhg6VWj988IefLXYh50ILatfAnbNoCFrlA==}
+
+  simplebar-vue@2.4.0:
+    resolution: {integrity: sha512-XUFGqoTCjzTKRWLHmS0/gy03GF7Id9FZhczrAqC3tbFO5OZ9vRCdzMZ7F2MuCI5+fp6Plpvug9GUgyBDJLTc5A==}
+    peerDependencies:
+      vue: '>=2.5.17'
+
   sirv@3.0.0:
     resolution: {integrity: sha512-BPwJGUeDaDCHihkORDchNyyTvWFhcusy1XMmhEVTQTwGeybFbp8YEmB+njbPnth1FibULBSBVwCQni25XlCUDg==}
     engines: {node: '>=18'}
@@ -10666,6 +10677,18 @@ snapshots:
 
   signal-exit@4.1.0: {}
 
+  simplebar-core@1.3.0:
+    dependencies:
+      lodash: 4.17.21
+
+  simplebar-vue@2.4.0(vue@3.5.13(typescript@5.6.3)):
+    dependencies:
+      simplebar-core: 1.3.0
+      vue: 3.5.13(typescript@5.6.3)
+      vue-demi: 0.13.11(vue@3.5.13(typescript@5.6.3))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+
   sirv@3.0.0:
     dependencies:
       '@polka/url': 1.0.0-next.28

+ 289 - 60
src/layout/HvacAside.vue

@@ -1,12 +1,13 @@
 <script setup lang="ts">
-import { onMounted, ref } from 'vue';
+import { computed, onMounted, ref } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
+import Simplebar from 'simplebar-vue';
 
-import { routes } from '@/router';
+import SvgIcon from '@/components/SvgIcon.vue';
+import { dataCenterRoutes, opsCenterRoutes } from '@/router';
+import { t } from '@/i18n';
 import { translateNavigation } from '@/utils';
 
-import AsideItem from './AsideItem.vue';
-
 import type { MenuInfo } from 'ant-design-vue/es/menu/src/interface';
 
 const router = useRouter();
@@ -14,6 +15,72 @@ const route = useRoute();
 const selectedKeys = ref<string[]>([route.path]);
 const openKeys = ref<string[]>([]);
 
+const menuGroupList = computed(() => {
+  return [
+    {
+      category: t('common.dataCenter'),
+      routes: dataCenterRoutes,
+    },
+    {
+      category: t('common.opsCenter'),
+      routes: opsCenterRoutes,
+    },
+  ];
+});
+
+type DeviceGroup = {
+  id: string;
+  name: string;
+  children?: DeviceGroup[];
+};
+
+const deviceGroupList: DeviceGroup[] = [
+  {
+    id: '1',
+    name: '空调总站房',
+    children: [
+      {
+        id: '1-1',
+        name: '一二期空调群控',
+      },
+      {
+        id: '1-2',
+        name: '三四期空调群控',
+      },
+    ],
+  },
+  {
+    id: '2',
+    name: '空调次站房',
+    children: [
+      {
+        id: '2-1',
+        name: '一二期空调群控',
+      },
+      {
+        id: '2-2',
+        name: '三四期空调群控',
+      },
+      {
+        id: '2-3',
+        name: '五期空调群控',
+      },
+    ],
+  },
+  {
+    id: '3',
+    name: '空调次站房',
+  },
+  {
+    id: '4',
+    name: '空调次站房',
+  },
+  {
+    id: '5',
+    name: '空调次站房',
+  },
+];
+
 onMounted(() => {
   const firstPath = route.path.split('/')[1];
 
@@ -27,53 +94,109 @@ const handleMenuClick = ({ key }: MenuInfo) => {
 };
 
 const collapsed = ref<boolean>(false);
+
+const toggleCollapsed = () => {
+  collapsed.value = !collapsed.value;
+};
 </script>
 
 <template>
-  <ALayoutSider class="aside-container" v-model:collapsed="collapsed" collapsible :trigger="null" :width="246">
+  <ALayoutSider
+    class="aside-container"
+    v-model:collapsed="collapsed"
+    collapsible
+    :collapsed-width="64"
+    :trigger="null"
+    :width="246"
+  >
     <div class="aside-header">
-      <div class="aside-header-logo"></div>
-      <span class="aside-header-title">{{ $t('common.unimatIoT') }}</span>
+      <img class="aside-header-logo" src="@/assets/img/logo.png" />
+      <span v-show="!collapsed" class="aside-header-title">{{ $t('common.unimatIoT') }}</span>
     </div>
-    <AMenu
-      class="aside-menu"
-      v-model:selected-keys="selectedKeys"
-      v-model:open-keys="openKeys"
-      mode="inline"
-      @click="handleMenuClick"
-    >
-      <template v-for="{ path, meta, children } in routes" :key="path">
-        <template v-if="meta && !meta.hideInMenu">
-          <ASubMenu v-if="!meta.hideSubMenu && children" :key="path">
-            <template #title>
-              <AsideItem :title="translateNavigation(meta.title)" :icon="meta.icon" />
+    <Simplebar class="aside-scroll">
+      <AMenu
+        class="aside-menu"
+        v-model:selected-keys="selectedKeys"
+        v-model:open-keys="openKeys"
+        mode="inline"
+        @click="handleMenuClick"
+      >
+        <div class="aside-menu-category">{{ $t('common.aiCtrl') }}</div>
+        <template v-for="item in deviceGroupList" :key="item.id">
+          <ASubMenu v-if="item.children?.length" :key="item.id" :title="item.name">
+            <template #icon>
+              <SvgIcon name="air-conditioning" />
             </template>
-            <AMenuItem v-for="{ path: subPath, meta } in children" :key="`${path}/${subPath}`">
-              <AsideItem :title="translateNavigation(meta?.title)" />
+            <AMenuItem v-for="subItem in item.children" :key="subItem.id" disabled>
+              {{ subItem.name }}
             </AMenuItem>
           </ASubMenu>
-          <AMenuItem v-else :key="`${path}/index`">
-            <AsideItem :title="translateNavigation(meta.title)" :icon="meta.icon" />
-          </AMenuItem>
+          <template v-else>
+            <AMenuItem :key="item.id" :title="item.name" disabled>
+              <template #icon>
+                <SvgIcon name="air-conditioning" />
+              </template>
+              {{ item.name }}
+            </AMenuItem>
+          </template>
         </template>
-      </template>
-      <div class="aside-menu-ai-ctrl">{{ $t('common.aiCtrl') }}</div>
-      <AMenuItem key="ai-1" disabled>
-        <AsideItem title="一级菜单" icon="setting" />
-      </AMenuItem>
-      <AMenuItem key="ai-2" disabled>
-        <AsideItem title="一级菜单" icon="setting" />
-      </AMenuItem>
-      <AMenuItem key="ai-3" disabled>
-        <AsideItem title="一级菜单" icon="setting" />
-      </AMenuItem>
-    </AMenu>
+        <template v-for="(item, index) in menuGroupList" :key="index">
+          <div class="aside-menu-category">{{ item.category }}</div>
+          <template v-for="{ path, meta, children } in item.routes" :key="path">
+            <template v-if="meta && !meta.hideInMenu">
+              <ASubMenu
+                v-if="!meta.hideSubMenu && children"
+                :key="path"
+                :title="translateNavigation(meta.title)"
+                popup-class-name="aside-menu-submenu-popup"
+              >
+                <template #icon>
+                  <SvgIcon v-if="meta.icon" :name="meta.icon" />
+                </template>
+                <AMenuItem v-for="{ path: subPath, meta } in children" :key="`${path}/${subPath}`">
+                  {{ translateNavigation(meta?.title) }}
+                </AMenuItem>
+              </ASubMenu>
+              <AMenuItem v-else :key="`${path}/index`" :title="translateNavigation(meta.title)">
+                <template #icon>
+                  <SvgIcon v-if="meta.icon" :name="meta.icon" />
+                </template>
+                {{ translateNavigation(meta.title) }}
+              </AMenuItem>
+            </template>
+          </template>
+        </template>
+      </AMenu>
+    </Simplebar>
+    <div v-show="collapsed" class="aside-collapsed-icons">
+      <SvgIcon name="information" />
+      <SvgIcon name="setting" />
+      <SvgIcon name="unfold" @click="toggleCollapsed" />
+    </div>
     <div class="aside-footer">
       <div class="aside-footer-avatar"></div>
+      <div v-show="!collapsed">
+        <SvgIcon name="setting" />
+        <SvgIcon name="information" />
+        <SvgIcon name="fold" @click="toggleCollapsed" />
+      </div>
     </div>
   </ALayoutSider>
 </template>
 
+<style lang="scss">
+.aside-menu-submenu-popup.ant-menu-submenu-popup {
+  .ant-menu-item:not(.ant-menu-item-selected):hover {
+    color: var(--antd-color-primary);
+    background-color: initial;
+  }
+
+  .ant-menu-item-selected {
+    background-color: initial;
+  }
+}
+</style>
+
 <style lang="scss" scoped>
 .aside-container {
   --aside-border-radius: 18px;
@@ -92,31 +215,39 @@ const collapsed = ref<boolean>(false);
 
     & > .ant-menu-item,
     .ant-menu-submenu-title {
-      padding-left: 6px !important;
+      padding-left: 12px !important;
     }
 
     .ant-menu-item,
     .ant-menu-submenu-title {
       width: calc(100%);
-      height: 36px;
+      height: 40px;
       padding-inline: 12px;
       margin-block: 0;
       margin-inline: 0;
       overflow: hidden;
-      line-height: 36px;
+      line-height: 40px;
       text-overflow: ellipsis;
+
+      .ant-menu-item-icon {
+        font-size: 16px;
+
+        + span {
+          margin-inline-start: 8px;
+        }
+      }
     }
 
     & > .ant-menu-submenu,
     & > .ant-menu-item {
       & + .ant-menu-submenu,
       & + .ant-menu-item {
-        margin-top: 16px;
+        margin-top: 2px;
       }
     }
 
     .ant-menu-submenu-title + .ant-menu-sub > li:first-child {
-      margin-top: 13px;
+      margin-top: 8px;
     }
 
     .ant-menu-submenu-arrow {
@@ -133,14 +264,14 @@ const collapsed = ref<boolean>(false);
     }
 
     .ant-menu-sub .ant-menu-item {
-      padding-left: 42px !important;
+      padding-left: 34px !important;
 
       &::before {
         position: absolute;
-        left: 15px;
+        left: 16px;
         height: 100%;
         content: '';
-        border: 1px solid var(--antd-color-primary-bg-hover);
+        border: 1px solid var(--antd-color-primary-opacity-15);
       }
 
       &.ant-menu-item-selected::before {
@@ -153,8 +284,7 @@ const collapsed = ref<boolean>(false);
     }
 
     .ant-menu-submenu-selected > .ant-menu-submenu-title {
-      color: var(--antd-color-text-secondary);
-      background-color: var(--antd-color-primary-bg);
+      background-color: var(--antd-color-primary-opacity-15);
     }
 
     .ant-menu-item:not(.ant-menu-item-selected):hover,
@@ -164,12 +294,13 @@ const collapsed = ref<boolean>(false);
     }
 
     & > .ant-menu-item.ant-menu-item-selected {
-      background-color: var(--antd-color-primary-bg);
+      background-color: var(--antd-color-primary-opacity-15);
     }
 
     .ant-menu-title-content {
       display: flex;
       align-items: center;
+      font-weight: 500;
     }
   }
 }
@@ -177,7 +308,7 @@ const collapsed = ref<boolean>(false);
 .aside-header {
   display: flex;
   align-items: center;
-  padding: 24px var(--aside-padding) 32px;
+  padding: 24px var(--aside-padding);
   background-color: var(--antd-color-bg-base);
   border-top-left-radius: var(--aside-border-radius);
   border-top-right-radius: var(--aside-border-radius);
@@ -186,45 +317,67 @@ const collapsed = ref<boolean>(false);
 .aside-header-logo {
   width: 40px;
   height: 40px;
-  margin-right: 12px;
-  background-color: var(--antd-color-primary);
-  border-radius: 16px;
 }
 
 .aside-header-title {
+  margin-left: 12px;
+  overflow: hidden;
   font-size: 16px;
   font-style: normal;
   font-weight: 600;
   line-height: 24px;
   color: var(--antd-color-text);
+  text-overflow: clip;
+  white-space: nowrap;
+}
+
+.aside-scroll {
+  height: calc(100% - 155px);
+  background-color: #fff;
 }
 
 .aside-menu {
   padding: 0 var(--aside-padding);
-  color: var(--antd-color-text-secondary);
+  color: var(--antd-color-text);
 }
 
-.aside-menu-ai-ctrl {
-  width: 48px;
-  height: 22px;
-  margin: 24px 0;
+.aside-menu-category {
+  width: 56px;
+  height: 24px;
+  margin-top: 24px;
+  margin-bottom: 8px;
   font-size: 12px;
   font-weight: 500;
-  line-height: 22px;
+  line-height: 24px;
   color: var(--antd-color-text-secondary);
   text-align: center;
-  background-color: var(--antd-color-primary-bg);
-  border-radius: 8px;
+  background-color: #f5f7fa;
+  border-radius: 4px;
+
+  &:first-child {
+    margin-top: 0;
+  }
 }
 
 .aside-footer {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
   height: 65px;
   padding: var(--aside-padding);
-  padding-right: 23px;
   margin-top: 2px;
   background-color: var(--antd-color-bg-base);
   border-bottom-right-radius: var(--aside-border-radius);
   border-bottom-left-radius: var(--aside-border-radius);
+
+  i {
+    font-size: 16px;
+    cursor: pointer;
+
+    + i {
+      margin-left: 12px;
+    }
+  }
 }
 
 .aside-footer-avatar {
@@ -245,4 +398,80 @@ const collapsed = ref<boolean>(false);
     content: '贾';
   }
 }
+
+.aside-container.ant-layout-sider-collapsed {
+  .aside-menu-category {
+    position: relative;
+    left: -8px;
+  }
+
+  .aside-scroll {
+    height: calc(100% - 304px);
+  }
+
+  :deep(.aside-menu) {
+    .ant-menu-submenu,
+    .ant-menu-submenu-title {
+      border-radius: 4px;
+    }
+
+    .ant-menu-item,
+    .ant-menu-submenu-title {
+      text-overflow: initial;
+    }
+
+    .ant-menu-submenu-title {
+      &::before {
+        position: absolute;
+        right: 6px;
+        bottom: 6px;
+        display: block;
+        width: 4px;
+        height: 4px;
+        content: '';
+        border: 1px solid var(--antd-color-text);
+        border-top-width: 0;
+        border-left-width: 0;
+      }
+
+      &:hover::before {
+        border-color: var(--antd-color-primary);
+      }
+    }
+
+    .ant-menu-submenu-selected {
+      .ant-menu-submenu-title {
+        &::before {
+          border-color: var(--antd-color-primary);
+        }
+      }
+    }
+  }
+}
+
+.aside-collapsed-icons {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding-top: 25px;
+  background-color: #fff;
+
+  i {
+    width: 40px;
+    height: 40px;
+    font-size: 16px;
+    line-height: 40px;
+    text-align: center;
+    cursor: pointer;
+    transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+
+    + i {
+      margin-bottom: 2px;
+    }
+
+    &:hover {
+      color: var(--antd-color-primary);
+    }
+  }
+}
 </style>

+ 1 - 0
src/main.ts

@@ -5,6 +5,7 @@ import App from './App.vue';
 import i18n from './i18n';
 import router from './router';
 
+import 'simplebar-vue/dist/simplebar.min.css';
 import 'ant-design-vue/dist/reset.css';
 import 'virtual:uno.css';
 import './styles/global.scss';