zhuj123456 1 ماه پیش
والد
کامیت
b544e5663a

+ 1 - 1
gw-ui/.env.development

@@ -11,4 +11,4 @@ VITE_APP_BASE_TITLE = '/gw'
 VITE_SERVICE_BASE_TITLE = '/gw-api'
 
 # 后端接口基础路径
-VITE_SERVER_URL = 'http://localhost:8080'
+VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'

+ 1 - 1
gw-ui/.env.production

@@ -11,7 +11,7 @@ VITE_APP_BASE_TITLE = '/gw'
 VITE_SERVICE_BASE_TITLE = '/gw-api'
 
 # 后端接口基础路径
-VITE_SERVER_URL = 'http://localhost:8080'
+VITE_SERVER_URL = 'http://39.98.38.2:18055/gw-api'
 
 # 是否在打包时开启压缩,支持 gzip 和 brotli
 VITE_BUILD_COMPRESS = gzip

+ 214 - 212
gw-ui/index.html

@@ -1,215 +1,217 @@
-<!DOCTYPE html>
+<!doctype html>
 <html>
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="renderer" content="webkit" />
+    <meta
+      name="viewport"
+      content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"
+    />
+    <link rel="icon" href="/favicon.ico" />
+    <title>%VITE_APP_TITLE%</title>
+    <!--[if lt IE 11
+      ]><script>
+        window.location.href = "/html/ie.html";
+      </script><!
+    [endif]-->
+    <style>
+      html,
+      body,
+      #app {
+        height: 100%;
+        margin: 0px;
+        padding: 0px;
+      }
 
-<head>
-  <meta charset="utf-8">
-  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-  <meta name="renderer" content="webkit">
-  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-  <link rel="icon" href="/favicon.ico">
-  <title>%VITE_APP_TITLE%</title>
-  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
-  <style>
-    html,
-    body,
-    #app {
-      height: 100%;
-      margin: 0px;
-      padding: 0px;
-    }
-
-    .chromeframe {
-      margin: 0.2em 0;
-      background: #ccc;
-      color: #000;
-      padding: 0.2em 0;
-    }
-
-    #loader-wrapper {
-      position: fixed;
-      top: 0;
-      left: 0;
-      width: 100%;
-      height: 100%;
-      z-index: 999999;
-    }
-
-    #loader {
-      display: block;
-      position: relative;
-      left: 50%;
-      top: 50%;
-      width: 150px;
-      height: 150px;
-      margin: -75px 0 0 -75px;
-      border-radius: 50%;
-      border: 3px solid transparent;
-      border-top-color: #FFF;
-      -webkit-animation: spin 2s linear infinite;
-      -ms-animation: spin 2s linear infinite;
-      -moz-animation: spin 2s linear infinite;
-      -o-animation: spin 2s linear infinite;
-      animation: spin 2s linear infinite;
-      z-index: 1001;
-    }
-
-    #loader:before {
-      content: "";
-      position: absolute;
-      top: 5px;
-      left: 5px;
-      right: 5px;
-      bottom: 5px;
-      border-radius: 50%;
-      border: 3px solid transparent;
-      border-top-color: #FFF;
-      -webkit-animation: spin 3s linear infinite;
-      -moz-animation: spin 3s linear infinite;
-      -o-animation: spin 3s linear infinite;
-      -ms-animation: spin 3s linear infinite;
-      animation: spin 3s linear infinite;
-    }
-
-    #loader:after {
-      content: "";
-      position: absolute;
-      top: 15px;
-      left: 15px;
-      right: 15px;
-      bottom: 15px;
-      border-radius: 50%;
-      border: 3px solid transparent;
-      border-top-color: #FFF;
-      -moz-animation: spin 1.5s linear infinite;
-      -o-animation: spin 1.5s linear infinite;
-      -ms-animation: spin 1.5s linear infinite;
-      -webkit-animation: spin 1.5s linear infinite;
-      animation: spin 1.5s linear infinite;
-    }
-
-
-    @-webkit-keyframes spin {
-      0% {
-        -webkit-transform: rotate(0deg);
-        -ms-transform: rotate(0deg);
-        transform: rotate(0deg);
-      }
-
-      100% {
-        -webkit-transform: rotate(360deg);
-        -ms-transform: rotate(360deg);
-        transform: rotate(360deg);
-      }
-    }
-
-    @keyframes spin {
-      0% {
-        -webkit-transform: rotate(0deg);
-        -ms-transform: rotate(0deg);
-        transform: rotate(0deg);
-      }
-
-      100% {
-        -webkit-transform: rotate(360deg);
-        -ms-transform: rotate(360deg);
-        transform: rotate(360deg);
-      }
-    }
-
-
-    #loader-wrapper .loader-section {
-      position: fixed;
-      top: 0;
-      width: 51%;
-      height: 100%;
-      background: #7171C6;
-      z-index: 1000;
-      -webkit-transform: translateX(0);
-      -ms-transform: translateX(0);
-      transform: translateX(0);
-    }
-
-    #loader-wrapper .loader-section.section-left {
-      left: 0;
-    }
-
-    #loader-wrapper .loader-section.section-right {
-      right: 0;
-    }
-
-
-    .loaded #loader-wrapper .loader-section.section-left {
-      -webkit-transform: translateX(-100%);
-      -ms-transform: translateX(-100%);
-      transform: translateX(-100%);
-      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
-      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
-    }
-
-    .loaded #loader-wrapper .loader-section.section-right {
-      -webkit-transform: translateX(100%);
-      -ms-transform: translateX(100%);
-      transform: translateX(100%);
-      -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
-      transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
-    }
-
-    .loaded #loader {
-      opacity: 0;
-      -webkit-transition: all 0.3s ease-out;
-      transition: all 0.3s ease-out;
-    }
-
-    .loaded #loader-wrapper {
-      visibility: hidden;
-      -webkit-transform: translateY(-100%);
-      -ms-transform: translateY(-100%);
-      transform: translateY(-100%);
-      -webkit-transition: all 0.3s 1s ease-out;
-      transition: all 0.3s 1s ease-out;
-    }
-
-    .no-js #loader-wrapper {
-      display: none;
-    }
-
-    .no-js h1 {
-      color: #222222;
-    }
-
-    #loader-wrapper .load_title {
-      font-family: 'Open Sans';
-      color: #FFF;
-      font-size: 19px;
-      width: 100%;
-      text-align: center;
-      z-index: 9999999999999;
-      position: absolute;
-      top: 60%;
-      opacity: 1;
-      line-height: 30px;
-    }
-
-    #loader-wrapper .load_title span {
-      font-weight: normal;
-      font-style: italic;
-      font-size: 13px;
-      color: #FFF;
-      opacity: 0.5;
-    }
-  </style>
-</head>
-
-<body>
-  <div id="app">
-    <div id="loader-wrapper">
-      <div id="loader"></div>
-      <div class="loader-section section-left"></div>
-      <div class="loader-section section-right"></div>
-      <div class="load_title">正在加载系统资源,请耐心等待</div>
-    </div>
-  </div>
-  <script type="module" src="/src/main.js"></script>
-</body>
+      .chromeframe {
+        margin: 0.2em 0;
+        background: #ccc;
+        color: #000;
+        padding: 0.2em 0;
+      }
+
+      #loader-wrapper {
+        position: fixed;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 100%;
+        z-index: 999999;
+      }
+
+      #loader {
+        display: block;
+        position: relative;
+        left: 50%;
+        top: 50%;
+        width: 150px;
+        height: 150px;
+        margin: -75px 0 0 -75px;
+        border-radius: 50%;
+        border: 3px solid transparent;
+        border-top-color: #fff;
+        -webkit-animation: spin 2s linear infinite;
+        -ms-animation: spin 2s linear infinite;
+        -moz-animation: spin 2s linear infinite;
+        -o-animation: spin 2s linear infinite;
+        animation: spin 2s linear infinite;
+        z-index: 1001;
+      }
+
+      #loader:before {
+        content: "";
+        position: absolute;
+        top: 5px;
+        left: 5px;
+        right: 5px;
+        bottom: 5px;
+        border-radius: 50%;
+        border: 3px solid transparent;
+        border-top-color: #fff;
+        -webkit-animation: spin 3s linear infinite;
+        -moz-animation: spin 3s linear infinite;
+        -o-animation: spin 3s linear infinite;
+        -ms-animation: spin 3s linear infinite;
+        animation: spin 3s linear infinite;
+      }
+
+      #loader:after {
+        content: "";
+        position: absolute;
+        top: 15px;
+        left: 15px;
+        right: 15px;
+        bottom: 15px;
+        border-radius: 50%;
+        border: 3px solid transparent;
+        border-top-color: #fff;
+        -moz-animation: spin 1.5s linear infinite;
+        -o-animation: spin 1.5s linear infinite;
+        -ms-animation: spin 1.5s linear infinite;
+        -webkit-animation: spin 1.5s linear infinite;
+        animation: spin 1.5s linear infinite;
+      }
+
+      @-webkit-keyframes spin {
+        0% {
+          -webkit-transform: rotate(0deg);
+          -ms-transform: rotate(0deg);
+          transform: rotate(0deg);
+        }
+
+        100% {
+          -webkit-transform: rotate(360deg);
+          -ms-transform: rotate(360deg);
+          transform: rotate(360deg);
+        }
+      }
+
+      @keyframes spin {
+        0% {
+          -webkit-transform: rotate(0deg);
+          -ms-transform: rotate(0deg);
+          transform: rotate(0deg);
+        }
+
+        100% {
+          -webkit-transform: rotate(360deg);
+          -ms-transform: rotate(360deg);
+          transform: rotate(360deg);
+        }
+      }
+
+      #loader-wrapper .loader-section {
+        position: fixed;
+        top: 0;
+        width: 51%;
+        height: 100%;
+        background: #7171c6;
+        z-index: 1000;
+        -webkit-transform: translateX(0);
+        -ms-transform: translateX(0);
+        transform: translateX(0);
+      }
+
+      #loader-wrapper .loader-section.section-left {
+        left: 0;
+      }
 
-</html>
+      #loader-wrapper .loader-section.section-right {
+        right: 0;
+      }
+
+      .loaded #loader-wrapper .loader-section.section-left {
+        -webkit-transform: translateX(-100%);
+        -ms-transform: translateX(-100%);
+        transform: translateX(-100%);
+        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      }
+
+      .loaded #loader-wrapper .loader-section.section-right {
+        -webkit-transform: translateX(100%);
+        -ms-transform: translateX(100%);
+        transform: translateX(100%);
+        -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+        transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      }
+
+      .loaded #loader {
+        opacity: 0;
+        -webkit-transition: all 0.3s ease-out;
+        transition: all 0.3s ease-out;
+      }
+
+      .loaded #loader-wrapper {
+        visibility: hidden;
+        -webkit-transform: translateY(-100%);
+        -ms-transform: translateY(-100%);
+        transform: translateY(-100%);
+        -webkit-transition: all 0.3s 1s ease-out;
+        transition: all 0.3s 1s ease-out;
+      }
+
+      .no-js #loader-wrapper {
+        display: none;
+      }
+
+      .no-js h1 {
+        color: #222222;
+      }
+
+      #loader-wrapper .load_title {
+        font-family: "Open Sans";
+        color: #fff;
+        font-size: 19px;
+        width: 100%;
+        text-align: center;
+        z-index: 9999999999999;
+        position: absolute;
+        top: 60%;
+        opacity: 1;
+        line-height: 30px;
+      }
+
+      #loader-wrapper .load_title span {
+        font-weight: normal;
+        font-style: italic;
+        font-size: 13px;
+        color: #fff;
+        opacity: 0.5;
+      }
+    </style>
+  </head>
+
+  <body>
+    <div id="app">
+      <div id="loader-wrapper">
+        <div id="loader"></div>
+        <div class="loader-section section-left"></div>
+        <div class="loader-section section-right"></div>
+        <div class="load_title">正在加载系统资源,请耐心等待</div>
+      </div>
+    </div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 2 - 0
gw-ui/src/assets/styles/index.scss

@@ -123,6 +123,8 @@ aside {
 //main-container全局样式
 .app-container {
   padding: 20px;
+  height: 100%;
+  overflow: hidden;
 }
 
 .components-container {

+ 184 - 189
gw-ui/src/assets/styles/sidebar.scss

@@ -5,7 +5,7 @@
   .main-container {
     min-height: 100%;
     transition: margin-left .28s;
-    margin-left: vars.$base-sidebar-width;
+    // margin-left: vars.$base-sidebar-width;
     position: relative;
   }
 
@@ -13,195 +13,190 @@
     margin-left: 0!important;
   }
 
-  .sidebar-container {
-    transition: width 0.28s;
-    width: vars.$base-sidebar-width !important;
-    height: 100%;
-    position: fixed;
-    font-size: 0px;
-    top: 0;
-    bottom: 0;
-    left: 0;
-    z-index: 1001;
-    overflow: hidden;
-    -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
-    box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
-
-    // reset element-ui css
-    .horizontal-collapse-transition {
-      transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
-    }
-
-    .scrollbar-wrapper {
-      overflow-x: hidden !important;
-    }
-
-    .el-scrollbar__bar.is-vertical {
-      right: 0px;
-    }
-
-    .el-scrollbar {
-      height: 100%;
-    }
-
-    &.has-logo {
-      .el-scrollbar {
-        height: calc(100% - 50px);
-      }
-    }
-
-    .is-horizontal {
-      display: none;
-    }
-
-    a {
-      display: inline-block;
-      width: 100%;
-      overflow: hidden;
-    }
-
-    .svg-icon {
-      margin-right: 10px !important;
-    }
-
-    .el-menu {
-      border: none;
-      height: 100%;
-      width: 100% !important;
-    }
-
-    .el-menu-item, .menu-title {
-      overflow: hidden !important;
-      text-overflow: ellipsis !important;
-      white-space: nowrap !important;
-      height: 44px !important;
-      line-height: 44px !important;
-    }
-
-    .el-menu-item .el-menu-tooltip__trigger {
-      display: inline-block !important;
-    }
-
-    // menu hover
-    .sub-menu-title-noDropdown,
-    .el-sub-menu__title {
-      &:hover {
-        background-color: rgba(0, 0, 0, 0.06);
-      }
-    }
-
-    & .theme-dark .is-active > .el-sub-menu__title {
-      color: vars.$base-menu-color-active !important;
-    }
-
-    & .nest-menu .el-sub-menu>.el-sub-menu__title,
-    & .el-sub-menu .el-menu-item {
-      min-width: vars.$base-sidebar-width !important;
-
-      &:hover {
-        background-color: rgba(0, 0, 0, 0.06);
-      }
-    }
-
-    & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
-    & .theme-dark .el-sub-menu .el-menu-item {
-      background-color: vars.$base-sub-menu-background;
-
-      &:hover {
-        background-color: vars.$base-sub-menu-hover !important;
-      }
-    }
-
-    // theme-dark 深色主题
-    &.theme-dark {
-      box-shadow: 2px 0 8px rgba(0, 0, 0, 0.4);
-      border-right: none;
-
-      .el-menu-item.is-active {
-        position: relative;
-
-        &::before {
-          content: '';
-          position: absolute;
-          inset: 0;
-          background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
-          border-right: 3px solid var(--current-color, #409eff);
-          pointer-events: none;
-          z-index: 1;
-        }
-      }
-
-      .el-sub-menu.is-active > .el-sub-menu__title {
-        color: var(--current-color, #409eff) !important;
-      }
-
-      .el-menu-item:not(.is-active),
-      .submenu-title-noDropdown,
-      .el-sub-menu__title {
-        position: relative;
-
-        &::before {
-          content: '';
-          position: absolute;
-          inset: 0;
-          background-color: transparent;
-          pointer-events: none;
-          z-index: 1;
-          transition: background-color 0.2s;
-        }
-
-        &:hover::before {
-          background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
-        }
-      }
-    }
+  // .sidebar-container {
+  //   transition: width 0.28s;
+  //   width: vars.$base-sidebar-width !important;
+  //   height: 100%;
+  //   z-index: 1001;
+  //   overflow: hidden;
+  //   -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
+  //   box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
+
+  //   // reset element-ui css
+  //   .horizontal-collapse-transition {
+  //     transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+  //   }
+
+  //   .scrollbar-wrapper {
+  //     overflow-x: hidden !important;
+  //   }
+
+  //   .el-scrollbar__bar.is-vertical {
+  //     right: 0px;
+  //   }
+
+  //   .el-scrollbar {
+  //     height: 100%;
+  //   }
+
+  //   &.has-logo {
+  //     .el-scrollbar {
+  //       height: calc(100% - 50px);
+  //     }
+  //   }
+
+  //   .is-horizontal {
+  //     display: none;
+  //   }
+
+  //   a {
+  //     display: inline-block;
+  //     width: 100%;
+  //     overflow: hidden;
+  //   }
+
+  //   .svg-icon {
+  //     margin-right: 10px !important;
+  //   }
+
+  //   .el-menu {
+  //     border: none;
+  //     height: 100%;
+  //     width: 100% !important;
+  //   }
+
+  //   .el-menu-item, .menu-title {
+  //     overflow: hidden !important;
+  //     text-overflow: ellipsis !important;
+  //     white-space: nowrap !important;
+  //     height: 44px !important;
+  //     line-height: 44px !important;
+  //   }
+
+  //   .el-menu-item .el-menu-tooltip__trigger {
+  //     display: inline-block !important;
+  //   }
+
+  //   // menu hover
+  //   .sub-menu-title-noDropdown,
+  //   .el-sub-menu__title {
+  //     &:hover {
+  //       background-color: rgba(0, 0, 0, 0.06);
+  //     }
+  //   }
+
+  //   & .theme-dark .is-active > .el-sub-menu__title {
+  //     color: vars.$base-menu-color-active !important;
+  //   }
+
+  //   & .nest-menu .el-sub-menu>.el-sub-menu__title,
+  //   & .el-sub-menu .el-menu-item {
+  //     min-width: vars.$base-sidebar-width !important;
+
+  //     &:hover {
+  //       background-color: rgba(0, 0, 0, 0.06);
+  //     }
+  //   }
+
+  //   & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
+  //   & .theme-dark .el-sub-menu .el-menu-item {
+  //     background-color: vars.$base-sub-menu-background;
+
+  //     &:hover {
+  //       background-color: vars.$base-sub-menu-hover !important;
+  //     }
+  //   }
+
+  //   // theme-dark 深色主题
+  //   &.theme-dark {
+  //     box-shadow: 2px 0 8px rgba(0, 0, 0, 0.4);
+  //     border-right: none;
+
+  //     .el-menu-item.is-active {
+  //       position: relative;
+
+  //       &::before {
+  //         content: '';
+  //         position: absolute;
+  //         inset: 0;
+  //         background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
+  //         border-right: 3px solid var(--current-color, #409eff);
+  //         pointer-events: none;
+  //         z-index: 1;
+  //       }
+  //     }
+
+  //     .el-sub-menu.is-active > .el-sub-menu__title {
+  //       color: var(--current-color, #409eff) !important;
+  //     }
+
+  //     .el-menu-item:not(.is-active),
+  //     .submenu-title-noDropdown,
+  //     .el-sub-menu__title {
+  //       position: relative;
+
+  //       &::before {
+  //         content: '';
+  //         position: absolute;
+  //         inset: 0;
+  //         background-color: transparent;
+  //         pointer-events: none;
+  //         z-index: 1;
+  //         transition: background-color 0.2s;
+  //       }
+
+  //       &:hover::before {
+  //         background-color: var(--current-color-dark-bg, rgba(64, 158, 255, 0.2));
+  //       }
+  //     }
+  //   }
     
-    // theme-light 浅色主题
-    &.theme-light {
-      border-right: 1px solid #e8eaf0;
-      box-shadow: none;
-
-      .el-menu-item,
-      .el-sub-menu__title {
-        color: rgba(0, 0, 0, 0.65);
-      }
-
-      .el-menu-item.is-active {
-        color: var(--current-color, #409eff) !important;
-        position: relative;
-
-        &::before {
-          content: '';
-          position: absolute;
-          inset: 0;
-          background-color: var(--current-color-light, #ecf5ff);
-          border-right: 3px solid var(--current-color, #409eff);
-          pointer-events: none;
-          z-index: 1;
-        }
-      }
-
-      .el-sub-menu.is-active > .el-sub-menu__title {
-        color: var(--current-color, #409eff) !important;
-      }
-
-      .el-menu-item:not(.is-active):hover,
-      .submenu-title-noDropdown:hover,
-      .el-sub-menu__title:hover {
-        background-color: #f5f7fa;
-        color: rgba(0, 0, 0, 0.85) !important;
-      }
-
-      .nest-menu .el-sub-menu > .el-sub-menu__title,
-      .el-sub-menu .el-menu-item {
-        background-color: #fafafa;
-
-        &:hover {
-          background-color: #f0f5ff;
-        }
-      }
-    }
-  }
+  //   // theme-light 浅色主题
+  //   &.theme-light {
+  //     border-right: 1px solid #e8eaf0;
+  //     box-shadow: none;
+
+  //     .el-menu-item,
+  //     .el-sub-menu__title {
+  //       color: rgba(0, 0, 0, 0.65);
+  //     }
+
+  //     .el-menu-item.is-active {
+  //       color: var(--current-color, #409eff) !important;
+  //       position: relative;
+
+  //       &::before {
+  //         content: '';
+  //         position: absolute;
+  //         inset: 0;
+  //         background-color: var(--current-color-light, #ecf5ff);
+  //         border-right: 3px solid var(--current-color, #409eff);
+  //         pointer-events: none;
+  //         z-index: 1;
+  //       }
+  //     }
+
+  //     .el-sub-menu.is-active > .el-sub-menu__title {
+  //       color: var(--current-color, #409eff) !important;
+  //     }
+
+  //     .el-menu-item:not(.is-active):hover,
+  //     .submenu-title-noDropdown:hover,
+  //     .el-sub-menu__title:hover {
+  //       background-color: #f5f7fa;
+  //       color: rgba(0, 0, 0, 0.85) !important;
+  //     }
+
+  //     .nest-menu .el-sub-menu > .el-sub-menu__title,
+  //     .el-sub-menu .el-menu-item {
+  //       background-color: #fafafa;
+
+  //       &:hover {
+  //         background-color: #f0f5ff;
+  //       }
+  //     }
+  //   }
+  // }
 
   .hideSidebar {
     .sidebar-container {

+ 11 - 0
gw-ui/src/assets/styles/variables.module.scss

@@ -23,6 +23,11 @@ $menuLightActiveText: #409EFF;
 // 基础变量
 $base-sidebar-width: 200px;
 $sideBarWidth: 200px;
+$base-sidebar-collapsed-width: 54px;
+$base-top-navbar-height: 50px;
+$menuBg: #304156;
+$menuText: #bfcbd9;
+$menuActiveText: #409EFF;
 
 // 菜单暗色变量
 $base-menu-color: rgba(255,255,255,.65);
@@ -63,6 +68,12 @@ $--color-info: #909399;
   colorWarning: $--color-warning;
   colorDanger: $--color-danger;
   colorInfo: $--color-info;
+  baseSidebarWidth: $base-sidebar-width;
+  baseSidebarCollapsedWidth: $base-sidebar-collapsed-width;
+  baseTopNavbarHeight: $base-top-navbar-height;
+  menuBg: $menuBg;
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
 }
 
 // CSS变量定义

+ 20 - 16
gw-ui/src/layout/components/AppMain.vue

@@ -3,7 +3,11 @@
     <router-view v-slot="{ Component, route }">
       <transition name="fade-transform" mode="out-in">
         <keep-alive :include="tagsViewStore.cachedViews">
-          <component v-if="!route.meta.link" :is="Component" :key="route.path"/>
+          <component
+            v-if="!route.meta.link"
+            :is="Component"
+            :key="route.path"
+          />
         </keep-alive>
       </transition>
     </router-view>
@@ -13,24 +17,24 @@
 </template>
 
 <script setup>
-import copyright from "./Copyright/index"
-import iframeToggle from "./IframeToggle/index"
-import useTagsViewStore from '@/store/modules/tagsView'
+import copyright from "./Copyright/index";
+import iframeToggle from "./IframeToggle/index";
+import useTagsViewStore from "@/store/modules/tagsView";
 
-const route = useRoute()
-const tagsViewStore = useTagsViewStore()
+const route = useRoute();
+const tagsViewStore = useTagsViewStore();
 
 onMounted(() => {
-  addIframe()
-})
+  addIframe();
+});
 
 watchEffect(() => {
-  addIframe()
-})
+  addIframe();
+});
 
 function addIframe() {
   if (route.meta.link) {
-    useTagsViewStore().addIframeView(route)
+    useTagsViewStore().addIframeView(route);
   }
 }
 </script>
@@ -38,7 +42,7 @@ function addIframe() {
 <style lang="scss" scoped>
 .app-main {
   /* 50= navbar  50  */
-  min-height: calc(100vh - 50px);
+  min-height: calc(100vh - 60px);
   width: 100%;
   position: relative;
   overflow: hidden;
@@ -47,7 +51,7 @@ function addIframe() {
 .fixed-header + .app-main {
   overflow-y: auto;
   scrollbar-gutter: auto;
-  height: calc(100vh - 50px);
+  height: calc(100vh - 60px);
   min-height: 0px;
 }
 
@@ -56,7 +60,7 @@ function addIframe() {
 }
 
 .fixed-header + .app-main {
-  margin-top: 50px;
+  margin-top: 60px;
 }
 
 .hasTagsView {
@@ -92,8 +96,8 @@ function addIframe() {
     .fixed-header + .app-main {
       padding-bottom: max(17px, calc(constant(safe-area-inset-bottom) + 10px));
       padding-bottom: max(17px, calc(env(safe-area-inset-bottom) + 10px));
-      height: calc(100svh - 50px);
-      height: calc(100dvh - 50px);
+      height: calc(100svh - 60px);
+      height: calc(100dvh - 60px);
     }
 
     .hasTagsView .fixed-header + .app-main {

+ 47 - 67
gw-ui/src/layout/components/Sidebar/SidebarItem.vue

@@ -1,100 +1,80 @@
+<!-- layout/components/Sidebar/SidebarItem.vue -->
 <template>
-  <div v-if="!item.hidden">
-    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
-      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
-        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
-          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
-          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
-        </el-menu-item>
-      </app-link>
+  <template v-if="!item.hidden">
+    <template
+      v-if="
+        hasOneShowingChild &&
+        (!onlyOneChild.children || onlyOneChild.noShowingChildren)
+      "
+    >
+      <el-menu-item :index="resolvePath(onlyOneChild.path)">
+        <el-icon>
+          <component
+            :is="onlyOneChild.meta?.icon || (item.meta && item.meta.icon)"
+          />
+        </el-icon>
+        <template #title>
+          <span>{{ onlyOneChild.meta?.title || item.meta?.title }}</span>
+        </template>
+      </el-menu-item>
     </template>
 
-    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
-      <template v-if="item.meta" #title>
-        <svg-icon :icon-class="item.meta && item.meta.icon" />
-        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
+    <el-sub-menu v-else :index="resolvePath(item.path)" teleported>
+      <template #title>
+        <el-icon v-if="item.meta && item.meta.icon">
+          <component :is="item.meta.icon" />
+        </el-icon>
+        <span>{{ item.meta?.title }}</span>
       </template>
 
       <sidebar-item
-        v-for="(child, index) in item.children"
-        :key="child.path + index"
-        :is-nest="true"
+        v-for="child in item.children"
+        :key="child.path"
         :item="child"
         :base-path="resolvePath(child.path)"
-        class="nest-menu"
       />
     </el-sub-menu>
-  </div>
+  </template>
 </template>
 
 <script setup>
-import { isExternal } from '@/utils/validate'
-import AppLink from './Link'
-import { getNormalPath } from '@/utils/goldenwater'
+import { computed, ref } from "vue";
+import { isExternal } from "@/utils/validate";
 
 const props = defineProps({
-  // route object
   item: {
     type: Object,
-    required: true
-  },
-  isNest: {
-    type: Boolean,
-    default: false
+    required: true,
   },
   basePath: {
     type: String,
-    default: ''
-  }
-})
-
-const onlyOneChild = ref({})
+    default: "",
+  },
+});
 
-function hasOneShowingChild(children = [], parent) {
-  if (!children) {
-    children = []
-  }
-  const showingChildren = children.filter(item => {
-    if (item.hidden) {
-      return false
-    }
-    onlyOneChild.value = item
-    return true
-  })
+const onlyOneChild = ref(null);
 
-  // When there is only one child router, the child router is displayed by default
+const hasOneShowingChild = computed(() => {
+  const showingChildren =
+    props.item.children?.filter((child) => !child.hidden) || [];
   if (showingChildren.length === 1) {
-    return true
+    onlyOneChild.value = showingChildren[0];
+    return true;
   }
-
-  // Show parent if there are no child router to display
   if (showingChildren.length === 0) {
-    onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
-    return true
+    onlyOneChild.value = { ...props.item, path: "", noShowingChildren: true };
+    return true;
   }
+  return false;
+});
 
-  return false
-}
-
-function resolvePath(routePath, routeQuery) {
+function resolvePath(routePath) {
   if (isExternal(routePath)) {
-    return routePath
+    return routePath;
   }
   if (isExternal(props.basePath)) {
-    return props.basePath
-  }
-  if (routeQuery) {
-    let query = JSON.parse(routeQuery)
-    return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
-  }
-  return getNormalPath(props.basePath + '/' + routePath)
-}
-
-function hasTitle(title){
-  if (title.length > 5) {
-    return title
-  } else {
-    return ""
+    return props.basePath;
   }
+  return props.basePath + "/" + routePath;
 }
 </script>

+ 171 - 92
gw-ui/src/layout/components/Sidebar/index.vue

@@ -1,104 +1,183 @@
+<!-- layout/components/Sidebar/index.vue -->
 <template>
-  <div :class="['sidebar-theme-wrapper', {'has-logo':showLogo}, sideTheme]" class="sidebar-container">
-    <logo v-if="showLogo" :collapse="isCollapse" />
-    <el-scrollbar wrap-class="scrollbar-wrapper">
-      <el-menu
-        :default-active="activeMenu"
-        :collapse="isCollapse"
-        :background-color="getMenuBackground"
-        :text-color="getMenuTextColor"
-        :unique-opened="true"
-        :active-text-color="theme"
-        :collapse-transition="false"
-        mode="vertical"
-        :class="sideTheme"
+  <div class="sidebar-container" :class="{ collapsed: isCollapse }">
+    <div class="menu-list">
+      <div
+        v-for="item in menuList"
+        :key="item.path"
+        :class="['menu-item', { active: isActive(item.path) }]"
+        @click="handleMenuClick(item)"
       >
-        <sidebar-item
-          v-for="(route, index) in sidebarRouters"
-          :key="route.path + index"
-          :item="route"
-          :base-path="route.path"
+        <!-- 使用原本的 svg 图标 -->
+        <svg-icon
+          v-if="item.meta?.icon"
+          :icon-class="item.meta.icon"
+          class="menu-icon"
         />
-      </el-menu>
-    </el-scrollbar>
+        <span class="menu-text">{{ item.meta?.title }}</span>
+      </div>
+    </div>
+
+    <div class="collapse-btn" @click="toggleCollapse">
+      <span class="btn-text">{{ isCollapse ? "▶" : "◀" }}</span>
+    </div>
   </div>
 </template>
 
-<script setup>
-import Logo from './Logo'
-import SidebarItem from './SidebarItem'
-import variables from '@/assets/styles/variables.module.scss'
-import useAppStore from '@/store/modules/app'
-import useSettingsStore from '@/store/modules/settings'
-import usePermissionStore from '@/store/modules/permission'
-
-const route = useRoute()
-const appStore = useAppStore()
-const settingsStore = useSettingsStore()
-const permissionStore = usePermissionStore()
-
-const sidebarRouters = computed(() => permissionStore.sidebarRouters)
-const showLogo = computed(() => settingsStore.sidebarLogo)
-const sideTheme = computed(() => settingsStore.sideTheme)
-const theme = computed(() => settingsStore.theme)
-const isCollapse = computed(() => !appStore.sidebar.opened)
-
-// 获取菜单背景色
-const getMenuBackground = computed(() => {
-  if (settingsStore.isDark) {
-    return 'var(--sidebar-bg)'
-  }
-  return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg
-})
-
-// 获取菜单文字颜色
-const getMenuTextColor = computed(() => {
-  if (settingsStore.isDark) {
-    return 'var(--sidebar-text)'
-  }
-  return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText
-})
-
-const activeMenu = computed(() => {
-  const { meta, path } = route
-  if (meta.activeMenu) {
-    return meta.activeMenu
-  }
-  return path
-})
-</script>
+<script>
+import { computed } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import useAppStore from "@/store/modules/app";
+import usePermissionStore from "@/store/modules/permission";
 
-<style lang="scss" scoped>
-.sidebar-container {
-  background-color: v-bind(getMenuBackground);
-  
-  .scrollbar-wrapper {
-    background-color: v-bind(getMenuBackground);
-  }
-
-  .el-menu {
-    border: none;
-    height: 100%;
-    width: 100% !important;
-    
-    .el-menu-item, .el-sub-menu__title {
-      &:hover {
-        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
+export default {
+  name: "Sidebar",
+  props: {
+    topMenuKey: {
+      type: String,
+      default: "",
+    },
+  },
+  setup(props) {
+    const route = useRoute();
+    const router = useRouter();
+    const appStore = useAppStore();
+    const permissionStore = usePermissionStore();
+
+    const isCollapse = computed(() => appStore.sidebar.isCollapse);
+
+    const menuList = computed(() => {
+      const routes = permissionStore.sidebarRouters || [];
+      if (!routes.length) return [];
+
+      if (props.topMenuKey) {
+        const current = routes.find((item) => item.path === props.topMenuKey);
+        if (current && current.children) {
+          return current.children.filter((child) => !child.hidden);
+        }
       }
-    }
-
-    .el-menu-item {
-      color: v-bind(getMenuTextColor);
-      
-      &.is-active {
-        color: var(--menu-active-text, #409eff);
-        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
+
+      const firstMenu = routes[0];
+      if (firstMenu && firstMenu.children) {
+        return firstMenu.children.filter((child) => !child.hidden);
+      }
+
+      return [];
+    });
+
+    const isActive = (path) => {
+      return route.path.includes(path);
+    };
+
+    const handleMenuClick = (item) => {
+      let fullPath = item.path;
+      if (!fullPath.startsWith("/") && props.topMenuKey) {
+        fullPath = props.topMenuKey + "/" + fullPath;
       }
-    }
+      router.push(fullPath);
+    };
+
+    const toggleCollapse = () => {
+      appStore.sidebar.isCollapse = !appStore.sidebar.isCollapse;
+    };
+
+    return {
+      menuList,
+      isCollapse,
+      isActive,
+      handleMenuClick,
+      toggleCollapse,
+    };
+  },
+};
+</script>
+
+<style scoped>
+.sidebar-container {
+  width: 210px;
+  background-color: #304156;
+  height: 100%;
+  transition: width 0.2s;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.sidebar-container.collapsed {
+  width: 64px;
+}
+
+.menu-list {
+  flex: 1;
+  overflow-y: auto;
+  padding: 10px 0;
+}
+
+.menu-item {
+  padding: 0 20px;
+  height: 44px;
+  line-height: 44px;
+  color: #bfcbd9;
+  cursor: pointer;
+  white-space: nowrap;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+/* 收起时的样式 */
+.sidebar-container.collapsed .menu-item {
+  justify-content: center;
+  padding: 0;
+}
+
+.menu-item:hover {
+  background-color: #263445;
+  color: #fff;
+}
+
+.menu-item.active {
+  background-color: #409eff;
+  color: #fff;
+}
+
+/* 图标样式 */
+.menu-icon {
+  width: 20px;
+  height: 20px;
+  flex-shrink: 0;
+}
+
+/* 文字样式 */
+.menu-text {
+  font-size: 14px;
+}
+
+/* 收起时隐藏文字 */
+.sidebar-container.collapsed .menu-text {
+  display: none;
+}
+
+/* 收起时图标居中 */
+.sidebar-container.collapsed .menu-icon {
+  margin: 0;
+}
+
+.collapse-btn {
+  height: 44px;
+  line-height: 44px;
+  text-align: center;
+  color: #bfcbd9;
+  cursor: pointer;
+  border-top: 1px solid #1f2d3d;
+}
+
+.collapse-btn:hover {
+  background-color: #263445;
+  color: #fff;
+}
 
-    .el-sub-menu__title {
-      color: v-bind(getMenuTextColor);
-    }
-  }
+.btn-text {
+  font-size: 16px;
 }
 </style>

+ 270 - 0
gw-ui/src/layout/components/TopNavbar/index.vue

@@ -0,0 +1,270 @@
+<!-- layout/components/TopNavbar/index.vue -->
+<template>
+  <div class="top-navbar">
+    <div class="logo-area">
+      <span class="logo-text">若依管理系统</span>
+    </div>
+
+    <div class="menu-area">
+      <!-- 首页菜单 -->
+      <div
+        :class="[
+          'menu-link',
+          { active: activeMenu === '/' || activeMenu === '/index' },
+        ]"
+        @click="handleMenuClick('/index')"
+      >
+        首页
+      </div>
+
+      <!-- 动态菜单 -->
+      <div
+        v-for="item in topMenus"
+        :key="item.path"
+        :class="['menu-link', { active: activeMenu === item.path }]"
+        @click="handleMenuClick(item.path)"
+      >
+        {{ item.meta?.title }}
+      </div>
+    </div>
+
+    <div class="right-menu">
+      <el-dropdown
+        @command="handleCommand"
+        class="avatar-container"
+        trigger="hover"
+      >
+        <div class="avatar-wrapper">
+          <!-- 用户图标 -->
+          <svg-icon icon-class="user" class="user-icon" />
+          <!-- 用户名 - 添加溢出处理 -->
+          <span
+            class="user-nickname"
+            :title="userStore.nickName || userStore.name"
+          >
+            {{ userStore.nickName || userStore.name }}
+          </span>
+          <!-- 下拉箭头 -->
+          <el-icon class="dropdown-icon">
+            <ArrowDown />
+          </el-icon>
+        </div>
+        <template #dropdown>
+          <el-dropdown-menu>
+            <router-link to="/user/profile">
+              <el-dropdown-item>个人中心</el-dropdown-item>
+            </router-link>
+            <el-dropdown-item
+              command="setLayout"
+              v-if="settingsStore.showSettings"
+            >
+              <span>布局设置</span>
+            </el-dropdown-item>
+            <el-dropdown-item command="lockScreen">
+              <span>锁定屏幕</span>
+            </el-dropdown-item>
+            <el-dropdown-item divided command="logout">
+              <span>退出登录</span>
+            </el-dropdown-item>
+          </el-dropdown-menu>
+        </template>
+      </el-dropdown>
+    </div>
+  </div>
+</template>
+
+<script>
+import { computed, ref } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessageBox } from "element-plus";
+import { ArrowDown } from "@element-plus/icons-vue";
+import usePermissionStore from "@/store/modules/permission";
+import useUserStore from "@/store/modules/user";
+import useSettingsStore from "@/store/modules/settings";
+import useLockStore from "@/store/modules/lock";
+
+export default {
+  name: "TopNavbar",
+  components: {
+    ArrowDown,
+  },
+  props: {
+    activeMenu: {
+      type: String,
+      default: "",
+    },
+  },
+  emits: ["menu-click", "setLayout"],
+  setup(props, { emit }) {
+    const route = useRoute();
+    const router = useRouter();
+    const permissionStore = usePermissionStore();
+    const userStore = useUserStore();
+    const settingsStore = useSettingsStore();
+    const lockStore = useLockStore();
+
+    const isFullscreen = ref(false);
+
+    const topMenus = computed(() => {
+      const routes = permissionStore.topbarRouters || [];
+      return routes.filter((item) => item.meta?.title && !item.hidden);
+    });
+
+    const handleMenuClick = (path) => {
+      emit("menu-click", path);
+    };
+
+    const handleCommand = (command) => {
+      switch (command) {
+        case "setLayout":
+          emit("setLayout");
+          break;
+        case "lockScreen":
+          lockScreen();
+          break;
+        case "logout":
+          logout();
+          break;
+        default:
+          break;
+      }
+    };
+
+    const lockScreen = () => {
+      const currentPath = route.fullPath;
+      lockStore.lockScreen(currentPath);
+      router.push("/lock");
+    };
+
+    const logout = () => {
+      ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      })
+        .then(() => {
+          userStore.logOut().then(() => {
+            location.href = "/index";
+          });
+        })
+        .catch(() => {});
+    };
+
+    return {
+      topMenus,
+      userStore,
+      settingsStore,
+      isFullscreen,
+      handleMenuClick,
+      handleCommand,
+    };
+  },
+};
+</script>
+
+<style scoped>
+.top-navbar {
+  height: 60px;
+  background: #217ff4;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 0 30px;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+}
+
+.logo-area {
+  width: 260px;
+  flex-shrink: 0;
+}
+
+.logo-text {
+  color: #fff;
+  font-size: 20px;
+  font-weight: bold;
+}
+
+.menu-area {
+  flex: 1;
+  display: flex;
+  gap: 8px;
+  margin: 0 30px;
+  overflow-x: auto;
+}
+
+.menu-link {
+  padding: 0 24px;
+  height: 60px;
+  line-height: 60px;
+  color: #fff;
+  cursor: pointer;
+  font-size: 18px;
+  font-weight: 500;
+  letter-spacing: 1px;
+  white-space: nowrap;
+  flex-shrink: 0;
+}
+
+.menu-link:hover {
+  background: rgba(255, 255, 255, 0.15);
+}
+
+.menu-link.active {
+  background: rgba(255, 255, 255, 0.25);
+}
+
+.right-menu {
+  display: flex;
+  align-items: center;
+  color: #fff;
+  flex-shrink: 0;
+  height: 100%;
+}
+
+.avatar-container {
+  margin-right: 0;
+  padding-right: 0;
+}
+
+.avatar-wrapper {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  cursor: pointer;
+  padding: 0 12px;
+  height: 60px;
+  max-width: 200px;
+}
+
+.avatar-wrapper:hover {
+  background: rgba(255, 255, 255, 0.15);
+}
+
+.user-icon {
+  width: 18px;
+  height: 18px;
+  color: #fff;
+  flex-shrink: 0;
+}
+
+.user-nickname {
+  font-size: 14px;
+  color: #fff;
+  max-width: 120px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  flex-shrink: 1;
+}
+
+.dropdown-icon {
+  font-size: 14px;
+  color: #fff;
+  transition: transform 0.3s;
+  flex-shrink: 0;
+}
+
+.avatar-container:hover .dropdown-icon {
+  transform: rotate(180deg);
+}
+</style>

+ 134 - 89
gw-ui/src/layout/index.vue

@@ -1,116 +1,161 @@
+<!-- layout/index.vue -->
 <template>
-  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme, '--current-color-light': theme + '1a', '--current-color-dark-bg': theme + '33' }">
-    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
-    <sidebar v-if="!sidebar.hide" class="sidebar-container" />
-    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
-      <div :class="{ 'fixed-header': fixedHeader }">
-        <navbar @setLayout="setLayout" />
-        <tags-view v-if="needTagsView" />
+  <div
+    class="app-wrapper"
+    :style="{
+      '--current-color': theme,
+      '--current-color-light': theme + '1a',
+      '--current-color-dark-bg': theme + '33',
+    }"
+  >
+    <!-- 顶部一级菜单栏 -->
+    <top-navbar
+      v-if="!sidebar.hide"
+      :active-menu="activeTopMenu"
+      @menu-click="handleTopMenuClick"
+      @setLayout="openSettings"
+    />
+
+    <!-- 下方区域:左右结构 -->
+    <div class="main-layout">
+      <!-- 左侧二三级菜单 -->
+      <div class="sidebar-wrapper" :class="{ collapsed: isCollapse }">
+        <sidebar v-if="!sidebar.hide" :top-menu-key="activeTopMenu" />
+      </div>
+
+      <!-- 右侧内容区域 -->
+      <div class="content-wrapper">
+        <app-main />
+        <settings ref="settingRef" />
       </div>
-      <app-main />
-      <settings ref="settingRef" />
     </div>
   </div>
 </template>
 
 <script setup>
-import { useWindowSize } from '@vueuse/core'
-import Sidebar from './components/Sidebar/index.vue'
-import { AppMain, Navbar, Settings, TagsView } from './components'
-import useAppStore from '@/store/modules/app'
-import useSettingsStore from '@/store/modules/settings'
-
-const settingsStore = useSettingsStore()
-const theme = computed(() => settingsStore.theme)
-const sidebar = computed(() => useAppStore().sidebar)
-const device = computed(() => useAppStore().device)
-const needTagsView = computed(() => settingsStore.tagsView)
-const fixedHeader = computed(() => settingsStore.fixedHeader)
-
-const classObj = computed(() => ({
-  hideSidebar: !sidebar.value.opened,
-  openSidebar: sidebar.value.opened,
-  withoutAnimation: sidebar.value.withoutAnimation,
-  mobile: device.value === 'mobile'
-}))
-
-const { width, height } = useWindowSize()
-const WIDTH = 992 // refer to Bootstrap's responsive design
-
-watch(() => device.value, () => {
-  if (device.value === 'mobile' && sidebar.value.opened) {
-    useAppStore().closeSideBar({ withoutAnimation: false })
-  }
-})
-
-watchEffect(() => {
-  if (width.value - 1 < WIDTH) {
-    useAppStore().toggleDevice('mobile')
-    useAppStore().closeSideBar({ withoutAnimation: true })
-  } else {
-    useAppStore().toggleDevice('desktop')
+import { computed, ref, watch, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import TopNavbar from "./components/TopNavbar/index.vue";
+import Sidebar from "./components/Sidebar/index.vue";
+import { AppMain, Settings } from "./components";
+import useAppStore from "@/store/modules/app";
+import useSettingsStore from "@/store/modules/settings";
+import usePermissionStore from "@/store/modules/permission";
+
+const settingsStore = useSettingsStore();
+const permissionStore = usePermissionStore();
+const appStore = useAppStore();
+const route = useRoute();
+const router = useRouter();
+
+// 首页路径
+const HOME_PATH = "/index";
+
+// 计算属性
+const theme = computed(() => settingsStore.theme);
+const sidebar = computed(() => appStore.sidebar);
+const isCollapse = computed(() => appStore.sidebar.isCollapse);
+
+// 当前选中的顶级菜单
+const activeTopMenu = ref("");
+
+// 获取当前选中的顶级菜单
+const getCurrentTopMenu = () => {
+  const currentPath = route.path;
+
+  // 如果是首页,选中首页菜单
+  if (currentPath === HOME_PATH) {
+    return HOME_PATH;
   }
-})
-
-function handleClickOutside() {
-  useAppStore().closeSideBar({ withoutAnimation: false })
-}
 
-const settingRef = ref(null)
-function setLayout() {
-  settingRef.value.openSetting()
-}
-</script>
+  try {
+    const routes = permissionStore.topbarRouters || [];
+    const firstLevel = "/" + currentPath.split("/")[1];
+    const matched = routes.find((r) => r.path === firstLevel);
+    return matched?.path || HOME_PATH;
+  } catch (error) {
+    return HOME_PATH;
+  }
+};
 
-<style lang="scss" scoped>
-@use "@/assets/styles/mixin.scss" as mix;
-@use "@/assets/styles/variables.module.scss" as vars;
+// 处理顶级菜单点击
+function handleTopMenuClick(menuPath) {
+  activeTopMenu.value = menuPath;
 
-.app-wrapper {
-  @include mix.clearfix;
-  position: relative;
-  height: 100%;
-  width: 100%;
+  // 如果点击的是首页,直接跳转
+  if (menuPath === HOME_PATH) {
+    router.push(HOME_PATH);
+    return;
+  }
 
-  &.mobile.openSidebar {
-    position: fixed;
-    top: 0;
+  // 其他菜单:跳转到第一个子路由
+  const routes = permissionStore.topbarRouters || [];
+  const targetRoute = routes.find((r) => r.path === menuPath);
+
+  if (targetRoute && targetRoute.children && targetRoute.children.length > 0) {
+    const firstChild = targetRoute.children.find((child) => !child.hidden);
+    if (firstChild) {
+      let fullPath = firstChild.path;
+      if (!fullPath.startsWith("/")) {
+        fullPath = menuPath + "/" + fullPath;
+      }
+      router.push(fullPath);
+    }
   }
 }
 
-.main-container:has(.fixed-header) {
-  height: 100vh;
-  overflow: hidden;
+// 打开设置面板
+const settingRef = ref(null);
+function openSettings() {
+  if (settingRef.value) {
+    settingRef.value.openSetting();
+  }
 }
 
-.drawer-bg {
-  background: #000;
-  opacity: 0.3;
-  width: 100%;
-  top: 0;
+// 监听路由变化,更新选中的顶级菜单
+watch(
+  () => route.path,
+  () => {
+    activeTopMenu.value = getCurrentTopMenu();
+  },
+  { immediate: true },
+);
+
+onMounted(() => {
+  // 如果当前是根路径,跳转到首页
+  if (route.path === "/") {
+    router.push(HOME_PATH);
+  }
+  activeTopMenu.value = getCurrentTopMenu();
+});
+</script>
+
+<style scoped>
+.app-wrapper {
   height: 100%;
-  position: absolute;
-  z-index: 999;
+  display: flex;
+  flex-direction: column;
 }
 
-.fixed-header {
-  position: fixed;
-  top: 0;
-  right: 0;
-  z-index: 9;
-  width: calc(100% - #{vars.$base-sidebar-width});
-  transition: width 0.28s;
+.main-layout {
+  flex: 1;
+  display: flex;
+  overflow: hidden;
 }
 
-.hideSidebar .fixed-header {
-  width: calc(100% - 54px);
+.sidebar-wrapper {
+  width: 210px;
+  transition: width 0.3s;
+  flex-shrink: 0;
 }
 
-.sidebarHide .fixed-header {
-  width: 100%;
+.sidebar-wrapper.collapsed {
+  width: 64px;
 }
 
-.mobile .fixed-header {
-  width: 100%;
+.content-wrapper {
+  flex: 1;
+  overflow: auto;
+  background: #f0f2f6;
 }
-</style>
+</style>

+ 61 - 44
gw-ui/src/store/modules/app.js

@@ -1,46 +1,63 @@
-import Cookies from 'js-cookie'
-
-const useAppStore = defineStore(
-  'app',
-  {
-    state: () => ({
-      sidebar: {
-        opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
-        withoutAnimation: false,
-        hide: false
-      },
-      device: 'desktop',
-      size: Cookies.get('size') || 'default'
-    }),
-    actions: {
-      toggleSideBar(withoutAnimation) {
-        if (this.sidebar.hide) {
-          return false
-        }
-        this.sidebar.opened = !this.sidebar.opened
-        this.sidebar.withoutAnimation = withoutAnimation
-        if (this.sidebar.opened) {
-          Cookies.set('sidebarStatus', 1)
-        } else {
-          Cookies.set('sidebarStatus', 0)
-        }
-      },
-      closeSideBar({ withoutAnimation }) {
-        Cookies.set('sidebarStatus', 0)
-        this.sidebar.opened = false
-        this.sidebar.withoutAnimation = withoutAnimation
-      },
-      toggleDevice(device) {
-        this.device = device
-      },
-      setSize(size) {
-        this.size = size
-        Cookies.set('size', size)
-      },
-      toggleSideBarHide(status) {
-        this.sidebar.hide = status
+// store/modules/app.js
+import Cookies from "js-cookie";
+import { defineStore } from "pinia";
+
+const useAppStore = defineStore("app", {
+  state: () => ({
+    sidebar: {
+      opened: Cookies.get("sidebarStatus")
+        ? !!+Cookies.get("sidebarStatus")
+        : true,
+      withoutAnimation: false,
+      isCollapse: false,
+      hide: false,
+    },
+    size: Cookies.get("size") || "default",
+  }),
+
+  actions: {
+    // 切换侧边栏的展开/收起
+    toggleSideBar(withoutAnimation) {
+      if (this.sidebar.hide) {
+        return false;
+      }
+      this.sidebar.opened = !this.sidebar.opened;
+      this.sidebar.isCollapse = !this.sidebar.isCollapse;
+      this.sidebar.withoutAnimation = withoutAnimation;
+
+      if (this.sidebar.opened) {
+        Cookies.set("sidebarStatus", 1);
+      } else {
+        Cookies.set("sidebarStatus", 0);
       }
-    }
-  })
+    },
+
+    // 收起侧边栏
+    collapseSideBar({ withoutAnimation }) {
+      if (this.sidebar.hide) return false;
+      this.sidebar.isCollapse = true;
+      this.sidebar.opened = false;
+      this.sidebar.withoutAnimation = withoutAnimation;
+      Cookies.set("sidebarStatus", 0);
+    },
+
+    // 展开侧边栏
+    expandSideBar({ withoutAnimation }) {
+      if (this.sidebar.hide) return false;
+      this.sidebar.isCollapse = false;
+      this.sidebar.opened = true;
+      this.sidebar.withoutAnimation = withoutAnimation;
+      Cookies.set("sidebarStatus", 1);
+    },
+
+    toggleSideBarHide(status) {
+      this.sidebar.hide = status;
+    },
+  },
+
+  getters: {
+    isSidebarCollapse: (state) => state.sidebar.isCollapse,
+  },
+});
 
-export default useAppStore
+export default useAppStore;

+ 1 - 1
gw-ui/src/utils/request.js

@@ -89,7 +89,7 @@ service.interceptors.response.use(res => {
         ElMessageBox.confirm('登录状态已过期,您可以继续留在该页面,或者重新登录', '系统提示', { confirmButtonText: '重新登录', cancelButtonText: '取消', type: 'warning' }).then(() => {
           isRelogin.show = false
           useUserStore().logOut().then(() => {
-            location.href = '/index'
+            location.href = "/gw/index";
           })
       }).catch(() => {
         isRelogin.show = false