VVT789
2025-04-18 4a13279fe13ad1cf6736670f6d1c5c61c8cba59b
Merge branch 'develop1.0' into wt_from_develop1.0
已添加15个文件
已重命名1个文件
已删除1个文件
已修改39个文件
48540 ■■■■■ 文件已修改
sql/postgre.sql 3642 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/postgre20250416.sql 41159 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-system/src/main/resources/mapper/dataitem/DataItemMapper.xml 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/.env.development 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/icons/svg/ai.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/icons/svg/bell.svg 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/2.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/3.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/5.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/6.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/7.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/alarm.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/login-background.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/login-background1.jpg 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/images/robot.png 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/styles/index.scss 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/styles/menu-fix.scss 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/styles/page.scss 188 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/styles/ruoyi.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/styles/sidebar.scss 106 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/assets/styles/variables.module.scss 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/Avatar/index.vue 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/BaseCard/BaseCard.vue 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/BaseCard/index.vue 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/Breadcrumb/index.vue 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/Hamburger/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/HeaderSearch/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/LeftTree/index.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/TagsView/index.vue 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/components/TopNav/index.vue 369 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/AppMain.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/Navbar.vue 187 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/Settings/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/Sidebar/SidebarItem.vue 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/Sidebar/index.vue 550 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/TagsView/ScrollPane.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/components/TagsView/index.vue 128 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/layout/index.vue 288 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/permission.js 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/router/index.js 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/store/modules/app.js 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/store/modules/permission.js 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/store/modules/settings.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/store/modules/tagsView.js 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/businessconfiguration/gatewaystatus/gatewayStatus.vue 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/dataMonitoring/historyDataTrend/index.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/deepanalysis/deepAnalysis.vue 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/energyanalysis/equipment/equipment.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/index.vue 510 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/login.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/realtimemonitor/gatewaystatus/index.vue 383 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/realtimemonitor/realtimemonitor/realtimemonitor.vue 20 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/register.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/system/name/name.vue 83 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/src/views/system/user/profile/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
zhitan-vue/vite.config.js 9 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/postgre.sql
ÎļþÒÑɾ³ý
sql/postgre20250416.sql
¶Ô±ÈÐÂÎļþ
ÎļþÌ«´ó
zhitan-system/src/main/resources/mapper/dataitem/DataItemMapper.xml
@@ -161,7 +161,7 @@
        WHEN di.time_type = 'DAY' THEN
        TO_CHAR( di.create_time, 'DD' )
        WHEN di.time_type = 'HOUR' THEN
        TO_CHAR( di.data_time, 'HH' )
        TO_CHAR( di.data_time, 'HH24' )
        ELSE TO_CHAR( di.data_time, 'YYMMDD' )
        END AS dataTimeStr,
            di.value,
zhitan-vue/.env.development
@@ -5,6 +5,5 @@
VITE_APP_ENV = 'development'
# ç³»ç»Ÿ/开发环境
# test
VITE_APP_BASE_API = 'http://127.0.0.1:8080'
VITE_APP_BASE_API = '/dev-api'
zhitan-vue/src/assets/icons/svg/ai.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 18a8 8 0 1 1 0-16 8 8 0 0 1 0 16z"></path><path d="M12 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm-4 6a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z"></path></svg>
zhitan-vue/src/assets/icons/svg/bell.svg
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path><path d="M13.73 21a2 2 0 0 1-3.46 0"></path></svg>
zhitan-vue/src/assets/images/2.png
zhitan-vue/src/assets/images/3.png
zhitan-vue/src/assets/images/5.png
zhitan-vue/src/assets/images/6.png
zhitan-vue/src/assets/images/7.png
zhitan-vue/src/assets/images/alarm.png
zhitan-vue/src/assets/images/login-background.png
zhitan-vue/src/assets/images/login-background1.jpg

zhitan-vue/src/assets/images/robot.png
zhitan-vue/src/assets/styles/index.scss
@@ -9,10 +9,12 @@
body {
  height: 100%;
  margin: 0;
  padding: 0;
  -moz-osx-font-smoothing: grayscale;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
  overflow-x: hidden; /* é˜²æ­¢æ°´å¹³æ»šåŠ¨æ¡ */
}
label {
@@ -22,10 +24,17 @@
html {
  height: 100%;
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  overflow-x: hidden; /* é˜²æ­¢æ°´å¹³æ»šåŠ¨æ¡ */
}
#app {
  height: 100%;
  width: 100%;
  margin: 0;
  padding: 0;
  overflow-x: hidden; /* é˜²æ­¢æ°´å¹³æ»šåŠ¨æ¡ */
}
*,
@@ -206,3 +215,40 @@
    color: #999 !important;
  }
}
/* æŠ˜å èœå•样式调整,确保在所有情况下菜单项居中 */
.el-menu--collapse {
  width: 54px !important;
  .el-sub-menu, .el-menu-item {
    width: 36px !important;
    min-width: 36px !important;
    margin-left: 9px !important; /* å…³é”®ï¼šè®¾ç½®å›ºå®šçš„左边距9px使菜单项居中 */
    margin-right: 9px !important;
  }
  .el-menu-item, .el-sub-menu__title {
    display: flex !important;
    justify-content: center !important;
    align-items: center !important;
    padding: 0 !important;
    .svg-icon, .el-icon {
      margin: 0 !important; /* é‡è¦ï¼šç§»é™¤å›¾æ ‡çš„æ‰€æœ‰è¾¹è· */
    }
    .el-sub-menu__icon-arrow {
      display: none !important;
    }
    > span {
      display: none !important;
    }
  }
  /* é€‰ä¸­çŠ¶æ€æ ·å¼ */
  .el-menu-item.is-active, .el-sub-menu.is-active > .el-sub-menu__title {
    background-color: #3883FA !important;
    color: #fff !important;
  }
}
zhitan-vue/src/assets/styles/menu-fix.scss
zhitan-vue/src/assets/styles/page.scss
@@ -4,10 +4,35 @@
      display: flex;
      .page-container-left {
        width: 280px;
        width: 220px;
        min-height: calc(100vh - 148px);
        border-right: 1px solid #1a235d;
        background: #1F1C49;
        background: #08234F;
        .el-tree {
          &.el-tree--highlight-current,
          &:not(.el-tree--highlight-current) {
            .el-tree-node {
              .el-tree-node__content {
                .el-tree-node__label,
                span {
                  font-size: 14px !important;
                }
              }
              .el-tree-node__expand-icon {
                font-size: 14px !important;
              }
            }
          }
        }
        .tree-container, .tree {
          .el-tree-node__label,
          .el-tree-node__content span {
            font-size: 14px !important;
          }
        }
        .tree {
          height: calc(100vh - 170px);
@@ -26,10 +51,64 @@
    }
    .form-card {
      background: #1a235d;
      border-radius: 0px 0px 0px 0px;
      border: 1px solid #000000;
      padding: 18px 0 0 15px;
      background: #08234F;
      border-radius: 0px;
      height: 52px;
      line-height: 52px;
      margin: 0 16px;
      display: flex;
      align-items: center;
      .el-form.el-form--inline {
        margin-left: 10px;
        display: flex;
        align-items: center;
        flex-wrap: nowrap;
        width: 100%;
        .el-form-item {
          margin-right: 15px;
          margin-bottom: 0;
          height: 32px;
          display: flex;
          align-items: center;
          &.is-required {
            .el-form-item__label::before {
              vertical-align: middle;
            }
          }
          .el-form-item__label {
            height: 32px;
            line-height: 32px;
            color: #ffffff;
          }
          .el-form-item__content {
            display: flex;
            align-items: center;
            height: 32px;
            line-height: 32px;
            .el-input, .el-select, .el-date-editor {
              height: 32px;
              line-height: 32px;
            }
            .el-input__wrapper {
              height: 32px;
            }
            .el-button {
              height: 32px;
              line-height: 32px;
              padding-top: 0;
              padding-bottom: 0;
            }
          }
        }
      }
    }
    .table-bg-style {
@@ -84,15 +163,39 @@
      display: flex;
      .page-container-left {
        width: 280px;
        width: 220px;
        min-height: calc(100vh - 148px);
        border-right: 1px solid #fff;
        border-right: 1px solid #e8e8e8;
        background: #f1f4fa;
        // border-right: 1px solid #1a235d;
        .el-tree {
          &.el-tree--highlight-current,
          &:not(.el-tree--highlight-current) {
            .el-tree-node {
              .el-tree-node__content {
                .el-tree-node__label,
                span {
                  font-size: 14px !important;
                }
              }
              .el-tree-node__expand-icon {
                font-size: 14px !important;
              }
            }
          }
        }
        .tree-container, .tree {
          .el-tree-node__label,
          .el-tree-node__content span {
            font-size: 14px !important;
          }
        }
        .tree {
          height: calc(100vh - 170px) !important;
          max-height: calc(100vh - 170px) !important;
          height: calc(100vh - 170px);
          max-height: calc(100vh - 170px);
          overflow-y: auto;
        }
      }
@@ -108,10 +211,56 @@
    .form-card {
      background: #fff;
      // background: #F7F8FA;
      border-radius: 0px 0px 0px 0px;
      // border: 1px solid #000000;
      padding: 18px 0 0 15px;
      border-radius: 0px;
      height: 52px;
      line-height: 52px;
      margin: 0 16px;
      display: flex;
      align-items: center;
      .el-form.el-form--inline {
        margin-left: 10px;
        display: flex;
        align-items: center;
        flex-wrap: nowrap;
        width: 100%;
        .el-form-item {
          margin-right: 15px;
          margin-bottom: 0;
          height: 32px;
          display: flex;
          align-items: center;
          .el-form-item__label {
            height: 32px;
            line-height: 32px;
          }
          .el-form-item__content {
            display: flex;
            align-items: center;
            height: 32px;
            line-height: 32px;
            .el-input, .el-select, .el-date-editor {
              height: 32px;
              line-height: 32px;
            }
            .el-input__wrapper {
              height: 32px;
            }
            .el-button {
              height: 32px;
              line-height: 32px;
              padding-top: 0;
              padding-bottom: 0;
            }
          }
        }
      }
    }
    
    .table-bg-style {
@@ -159,6 +308,15 @@
}
// åˆ é™¤ä¹‹å‰æ·»åŠ çš„å…¨å±€æ ·å¼è¦†ç›–ï¼Œé¿å…å½±å“å…¨å±€
// ä¿ç•™è°ƒè¯•建议
/*
您可以在浏览器中使用以下内联样式进行调试:
document.querySelectorAll('.page-container-left .el-tree-node__label').forEach(el => {
  el.style.cssText = 'font-size: 14px !important';
});
*/
.padding {
  padding: 15px;
}
zhitan-vue/src/assets/styles/ruoyi.scss
@@ -394,7 +394,7 @@
  // æ ‘
  .el-tree {
    background: transparent;
    font-size: 16px;
    font-size: 14px;
    color: #ffffff;
    .el-tree-node__content {
zhitan-vue/src/assets/styles/sidebar.scss
@@ -1,7 +1,7 @@
.themeDark {
  #app {
    .el-menu-item.is-active {
      background: #3271eb !important;
     // background: #3271eb !important;
      // border-radius: 30px 30px 30px 30px !important;
      color: #fff;
    }
@@ -13,6 +13,7 @@
    .main-container {
      background-color: #110f2e !important;
      height: 100%;
      transition: margin-left 0.28s;
      margin-left: $base-sidebar-width;
@@ -28,13 +29,14 @@
      transition: width 0.28s;
      width: $base-sidebar-width !important;
      background-color: $base-menu-background;
      height: 100%;
      height: calc(100% - 60px) !important;
      position: fixed;
      top: 0;
      top: 60px;
      bottom: 0;
      left: 0;
      z-index: 1001;
      z-index: 999;
      overflow: hidden;
      background-color: #002866 !important;
      -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
      box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
      font-family: OPPOSans, OPPOSans;
@@ -50,10 +52,11 @@
      .scrollbar-wrapper {
        overflow-x: hidden !important;
        height: calc(100% - 290px) !important;
      }
      .el-scrollbar__bar.is-vertical {
        right: 0px;
        right: 0 !important;
      }
      .el-scrollbar {
@@ -92,7 +95,7 @@
      .menu-title {
        overflow: hidden !important;
        font-weight: 400 !important;
        font-size: 16px !important;
        font-size: 14px !important;
      }
      // @media (min-width: 1440px) {
@@ -191,22 +194,32 @@
      }
      .el-menu--collapse {
        .el-sub-menu {
          & > .el-sub-menu__title {
            & > span {
              height: 0;
              width: 0;
              overflow: hidden;
              visibility: hidden;
              display: inline-block;
        width: 54px !important;
        /* æ¸…除后代选择器中干扰居中的样式 */
        .el-sub-menu, .el-menu-item {
          margin: 0 !important;
          padding: 0 !important;
          width: 100% !important;
          /* è®©å›¾æ ‡å’Œèœå•项居中 */
          .el-menu-item, .el-sub-menu__title {
            width: 36px !important;
            min-width: 36px !important;
            height: 38px !important;
            line-height: 38px !important;
            margin: 4px 9px !important; /* ç²¾ç¡®è®¡ç®—居中边距 */
            padding: 0 !important;
            display: flex !important;
            justify-content: center !important;
            align-items: center !important;
            border-radius: 4px !important;
            }
            & > i {
              height: 0;
              width: 0;
              overflow: hidden;
              visibility: hidden;
              display: inline-block;
            }
          /* ç¡®ä¿å›¾æ ‡å±…中 */
          .svg-icon, .el-icon {
            margin: 0 !important;
            padding: 0 !important;
          }
        }
      }
@@ -284,7 +297,7 @@
.themeLight {
  #app {
    .el-menu-item.is-active {
      background: #e0eafc !important;
      // border-radius: 30px 30px 30px 30px !important;
    }
@@ -294,6 +307,7 @@
    }
    .main-container {
      background-color: #f7f8fa;
      height: 100%;
      transition: margin-left 0.28s;
      margin-left: $base-sidebar-width;
@@ -309,12 +323,12 @@
      transition: width 0.28s;
      width: $base-sidebar-width !important;
      background-color: $base-menu-background;
      height: 100%;
      height: calc(100% - 60px) !important;
      position: fixed;
      top: 0;
      top: 60px;
      bottom: 0;
      left: 0;
      z-index: 1001;
      z-index: 999;
      overflow: hidden;
      -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, 0.35);
      box-shadow: none;
@@ -377,7 +391,7 @@
      .menu-title {
        overflow: hidden !important;
        font-weight: 400 !important;
        font-size: 16px !important;
        font-size: 14px !important;
      }
      // @media (min-width: 1440px) {
@@ -478,22 +492,32 @@
      }
      .el-menu--collapse {
        .el-sub-menu {
          & > .el-sub-menu__title {
            & > span {
              height: 0;
              width: 0;
              overflow: hidden;
              visibility: hidden;
              display: inline-block;
        width: 54px !important;
        /* æ¸…除后代选择器中干扰居中的样式 */
        .el-sub-menu, .el-menu-item {
          margin: 0 !important;
          padding: 0 !important;
          width: 100% !important;
          /* è®©å›¾æ ‡å’Œèœå•项居中 */
          .el-menu-item, .el-sub-menu__title {
            width: 36px !important;
            min-width: 36px !important;
            height: 38px !important;
            line-height: 38px !important;
            margin: 4px 9px !important; /* ç²¾ç¡®è®¡ç®—居中边距 */
            padding: 0 !important;
            display: flex !important;
            justify-content: center !important;
            align-items: center !important;
            border-radius: 4px !important;
            }
            & > i {
              height: 0;
              width: 0;
              overflow: hidden;
              visibility: hidden;
              display: inline-block;
            }
          /* ç¡®ä¿å›¾æ ‡å±…中 */
          .svg-icon, .el-icon {
            margin: 0 !important;
            padding: 0 !important;
          }
        }
      }
zhitan-vue/src/assets/styles/variables.module.scss
@@ -42,7 +42,7 @@
$--color-danger: #F56C6C;
$--color-info: #909399;
$base-sidebar-width: 260px;
$base-sidebar-width: 220px;
// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
zhitan-vue/src/components/Avatar/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,100 @@
<template>
  <div class="avatar-container">
    <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
      <div class="avatar-wrapper">
        <img :src="userStore.avatar" class="user-avatar" />
        <el-icon><caret-bottom /></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="toggleTheme">
            <span>风格切换</span>
          </el-dropdown-item>
          <el-dropdown-item divided command="logout">
            <span>退出登录</span>
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
</template>
<script setup>
import { ElMessageBox } from "element-plus"
import useUserStore from "@/store/modules/user"
import useSettingsStore from "@/store/modules/settings"
const userStore = useUserStore()
const settingsStore = useSettingsStore()
function handleCommand(command) {
  switch (command) {
    case "toggleTheme":
      const newTheme = settingsStore.sideTheme === "theme-dark" ? "theme-light" : "theme-dark";
      settingsStore.changeSetting({ key: "sideTheme", value: newTheme });
      document.querySelector("body").className = newTheme === "theme-dark" ? "themeDark" : "themeLight";
      break;
    case "logout":
      ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
        confirmButtonText: "确定",
        cancelButtonText: "取消",
        type: "warning",
      })
        .then(() => {
          userStore.logOut().then(() => {
            location.href = "/index";
          });
        })
        .catch(() => {});
      break;
  }
}
</script>
<style lang="scss" scoped>
.avatar-container {
  margin-right: 20px;
  .avatar-wrapper {
    margin-top: 5px;
    position: relative;
    display: flex;
    align-items: center;
    .user-avatar {
      cursor: pointer;
      width: 40px;
      height: 40px;
      border-radius: 10px;
    }
    .el-icon {
      cursor: pointer;
      margin-left: 8px;
      font-size: 12px;
      color: #ffffff;
    }
  }
}
.right-menu-item {
  display: inline-block;
  padding: 0 8px;
  height: 100%;
  font-size: 18px;
  color: #ffffff;
  vertical-align: text-bottom;
  &.hover-effect {
    cursor: pointer;
    transition: background 0.3s;
    &:hover {
      background: rgba(0, 0, 0, 0.1);
    }
  }
}
</style>
zhitan-vue/src/components/BaseCard/BaseCard.vue
@@ -67,6 +67,20 @@
      font-family: YouSheBiaoTiHei;
      font-size: 1.25vw; //24px;
      color: #fff;
      position: relative;
      padding-left: 12px;
      &::before {
        content: '';
        position: absolute;
        left: 0;
        top: 50%;
        transform: translateY(-50%);
        width: 5px;
        height: 18px;
        background-color: #3883FA;
        border-radius: 2px;
      }
    }
  }
zhitan-vue/src/components/BaseCard/index.vue
@@ -28,11 +28,26 @@
      border-radius: 7px 7px 0 0;
      padding: 20px;
      background-color: #22408c;
      .name {
        font-family: OPPOSans, OPPOSans;
        font-weight: bold;
        font-size: 18px;
        color: #fff;
        position: relative;
        padding-left: 12px;
        &::before {
          content: '';
          position: absolute;
          left: 0;
          top: 50%;
          transform: translateY(-50%);
          width: 5px;
          height: 18px;
          background-color: #3883FA;
          border-radius: 2px;
        }
      }
    }
  }
@@ -46,6 +61,7 @@
    border: 1px solid #ebebeb;
    padding-bottom: 10px;
    background-color: #fff;
    .mycard-title {
      display: flex;
      justify-content: flex-start;
@@ -54,11 +70,26 @@
      border-radius: 7px 7px 0 0;
      padding: 20px;
      background-color: #e7eefd;
      .name {
        font-family: OPPOSans, OPPOSans;
        font-weight: bold;
        font-size: 18px;
        color: #2d2e31;
        position: relative;
        padding-left: 12px;
        &::before {
          content: '';
          position: absolute;
          left: 0;
          top: 50%;
          transform: translateY(-50%);
          width: 5px;
          height: 18px;
          background-color: #3883FA;
          border-radius: 2px;
        }
      }
    }
  }
zhitan-vue/src/components/Breadcrumb/index.vue
@@ -18,20 +18,30 @@
  // only show routes with meta.title
  let matched = route.matched.filter(item => item.meta && item.meta.title);
  const first = matched[0]
  // åˆ¤æ–­æ˜¯å¦ä¸ºé¦–页
  if (!isDashboard(first)) {
    matched = [{ path: '/index', meta: { title: '首页' } }].concat(matched)
  // æ·»åŠ è°ƒè¯•æ—¥å¿—
  console.log('Current route path:', route.path);
  console.log('Route matched:', route.matched);
  console.log('Filtered matched routes:', matched);
  // å¦‚果是首页看板路由,确保它被添加到面包屑中
  if (route.path === '/index' || route.path === '/index/index') {
    matched = [{ path: '/index', meta: { title: '首页看板' } }].concat(matched)
    console.log('Added index route to matched:', matched);
  }
  levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
  console.log('Final breadcrumb items:', levelList.value);
}
function isDashboard(route) {
  const name = route && route.name
  if (!name) {
    return false
  }
  return name.trim() === 'Index'
  return name.trim().toLowerCase() === 'index'
}
function handleLink(item) {
  const { redirect, path } = item
  if (redirect) {
zhitan-vue/src/components/Hamburger/index.vue
@@ -12,7 +12,7 @@
    </svg> -->
    
    <img src="/src/assets/images/nav-btn.png" width="26" v-if="settingsStore.sideTheme == 'theme-dark'">
    <img src="/src/assets/images/nav-btn2.png" width="26" v-else>
    <img src="/src/assets/images/nav-btn.png" width="26" v-else>
  </div>
</template>
zhitan-vue/src/components/HeaderSearch/index.vue
@@ -52,7 +52,7 @@
    window.open(path.substr(pindex, path.length), "_blank");
  } else {
    if (query) {
      router.push({ path: path, query: JSON.parse(query) });
      router.push({ path: path, query: query });
    } else {
      router.push(path)
    }
zhitan-vue/src/components/LeftTree/index.vue
@@ -53,14 +53,26 @@
getTree();
/** æŸ¥è¯¢éƒ¨é—¨ä¸‹æ‹‰æ ‘结构 */
function getTree() {
  // ç¡®ä¿query.value有初始值
  query.value = query.value || {};
  // ä¼˜å…ˆä½¿ç”¨props中传入的ParentModelCode
  if (props.ParentModelCode) {
    query.value = { modelCode: props.ParentModelCode };
    query.value.modelCode = props.ParentModelCode;
  } else if (useRoute().query.modelCode) {
    // å…¶æ¬¡ä½¿ç”¨è·¯ç”±ä¸­çš„modelCode
    query.value.modelCode = useRoute().query.modelCode;
  } else {
    query.value = { ...useRoute().query };
    // æœ€åŽä½¿ç”¨é»˜è®¤å€¼
    query.value.modelCode = 'JCZBK_CODE';
  }
  console.log('LeftTree getTree modelCode:', query.value.modelCode);
  treeList(query.value).then((response) => {
    nodeOptions.value = response.data;
    if (response.data.length > 0) {
      defaultExpandedKeys.value = []; // æ¸…空已有的key,避免重复
      response.data.map((item) => {
        defaultExpandedKeys.value.push(item.id);
      });
zhitan-vue/src/components/TagsView/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
function isTags(route) {
  return !route.hidden && route.name &&
    route.name !== 'login' &&
    route.name !== '404' &&
    route.name !== '401' &&
    route.name !== 'index' &&
    route.path !== '/index' &&
    route.path !== '/';
}
function addTags() {
  const { name } = route;
  if (name) {
    // æ£€æŸ¥æ˜¯å¦ä¸ºé¦–页
    if (name === 'index' || route.path === '/index' || route.path === '/') {
      return;
    }
    store.dispatch('tagsView/addView', route);
  }
  return false;
}
// åˆå§‹åŒ–标签,确保首页不被添加为固定标签
function initTags() {
  const affixTags = filterAffixTags(routes);
  for (const tag of affixTags) {
    // æŽ’除首页
    if (tag.path === '/index' || tag.path === '/' || tag.name === 'Index') {
      continue;
    }
    // æ·»åŠ å›ºå®šæ ‡ç­¾
    if (tag.name) {
      store.dispatch('tagsView/addVisitedView', tag);
    }
  }
}
zhitan-vue/src/components/TopNav/index.vue
@@ -1,35 +1,30 @@
<template>
  <div class="top-nav-container">
    <div class="scroll-arrow left-arrow" @click="scrollLeft" v-show="canScrollLeft">
      <el-icon><arrow-left /></el-icon>
    </div>
    <div class="menu-container" ref="menuContainer">
  <el-menu
    :default-active="activeMenu"
    mode="horizontal"
    @select="handleSelect"
    :ellipsis="false"
        class="top-menu"
        :class="{ 'theme-dark': theme === 'dark' }"
  >
    <template v-for="(item, index) in topMenus">
      <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
        <svg-icon
        v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
        :icon-class="item.meta.icon"/>
        <template v-for="(item, index) in topMenus" :key="index">
          <el-menu-item :style="{'--theme': theme}" :index="item.path">
        {{ item.meta.title }}
      </el-menu-item>
    </template>
    <!-- é¡¶éƒ¨èœå•超出数量折叠 -->
    <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
      <template #title>更多菜单</template>
      <template v-for="(item, index) in topMenus">
        <el-menu-item
          :index="item.path"
          :key="index"
          v-if="index >= visibleNumber">
        <svg-icon
          v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
          :icon-class="item.meta.icon"/>
        {{ item.meta.title }}
        </el-menu-item>
      </template>
    </el-sub-menu>
  </el-menu>
    </div>
    <div class="scroll-arrow right-arrow" @click="scrollRight" v-show="canScrollRight">
      <el-icon><arrow-right /></el-icon>
    </div>
  </div>
</template>
<script setup>
@@ -38,9 +33,13 @@
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import { ArrowLeft, ArrowRight } from '@element-plus/icons-vue'
// é¡¶éƒ¨æ åˆå§‹æ•°
const visibleNumber = ref(null);
// æ»šåŠ¨ç›¸å…³
const menuContainer = ref(null);
const canScrollLeft = ref(false);
const canScrollRight = ref(false);
// å½“前激活菜单的 index
const currentIndex = ref(null);
// éšè—ä¾§è¾¹æ è·¯ç”±
@@ -104,6 +103,10 @@
    if (!route.meta.link) {
        appStore.toggleSideBarHide(false);
    }
  } else if (path === '/index' || path === '/') {
    // é¦–页时隐藏侧边栏
    activePath = path;
    appStore.toggleSideBarHide(true);
  } else if(!route.children) {
    activePath = path;
    appStore.toggleSideBarHide(true);
@@ -112,36 +115,147 @@
  return activePath;
})
function setVisibleNumber() {
  const width = document.body.getBoundingClientRect().width / 3;
  visibleNumber.value = parseInt(width / 85);
function updateScrollButtons() {
  if (!menuContainer.value) return;
  const container = menuContainer.value;
  canScrollLeft.value = container.scrollLeft > 10;
  canScrollRight.value = container.scrollLeft < (container.scrollWidth - container.clientWidth - 10);
}
function scrollLeft() {
  if (!menuContainer.value) return;
  menuContainer.value.scrollBy({ left: -200, behavior: 'smooth' });
  setTimeout(updateScrollButtons, 300);
}
function scrollRight() {
  if (!menuContainer.value) return;
  menuContainer.value.scrollBy({ left: 200, behavior: 'smooth' });
  setTimeout(updateScrollButtons, 300);
}
/**
 * æŸ¥æ‰¾æœ€æ·±å±‚的子菜单(叶子节点)
 * é€’归查找第一个没有children的子菜单
 */
function findDeepestLeafMenu(route) {
  if (!route) return null;
  // å¦‚果没有子菜单或子菜单为空,则返回当前路由
  if (!route.children || route.children.length === 0) {
    return route;
  }
  // æ‰¾åˆ°ç¬¬ä¸€ä¸ªéžéšè—çš„子菜单
  const firstVisibleChild = route.children.find(child => !child.hidden);
  if (!firstVisibleChild) {
    return route; // å¦‚果所有子菜单都是隐藏的,返回当前路由
  }
  // é€’归查找这个子菜单的最深层子菜单
  return findDeepestLeafMenu(firstVisibleChild);
}
function handleSelect(key, keyPath) {
  currentIndex.value = key;
  const route = routers.value.find(item => item.path === key);
  if (isHttp(key)) {
    // http(s):// è·¯å¾„新窗口打开
    window.open(key, "_blank");
  } else if (!route || !route.children) {
    // æ²¡æœ‰å­è·¯ç”±è·¯å¾„内部打开
    return;
  }
  // æ£€æŸ¥æ˜¯å¦æœ‰å­è·¯ç”±
  if (route && route.children && route.children.length > 0) {
    // æœ‰å­è·¯ç”±ï¼Œæ˜¾ç¤ºä¾§è¾¹æ 
    activeRoutes(key);
    // æŒ‰ç…§æ­£ç¡®çš„路径构建层级,这里是特殊处理
    let targetPath = key; // ä»Žå½“前点击的菜单路径开始
    let targetQuery = null;
    let currentNode = route;
    let pathSegments = [];
    // å½“前路径是第一段
    pathSegments.push(currentNode.path);
    // é€å±‚添加子路径
    while (currentNode.children && currentNode.children.length > 0) {
      const firstChild = currentNode.children.find(child => !child.hidden);
      if (!firstChild) break;
      // è·³è¿‡ParentView类型的中间节点,直接使用其子节点的path
      if (firstChild.component === 'ParentView' || firstChild.component.name === 'ParentView') {
        currentNode = firstChild;
        pathSegments.push(firstChild.path);
        continue;
      }
      // æ™®é€šèŠ‚ç‚¹å¤„ç†
      currentNode = firstChild;
      // å¦‚果路径不是以/开头,则添加到路径片段中
      if (!firstChild.path.startsWith('/')) {
        pathSegments.push(firstChild.path);
      } else {
        // å¦‚果是绝对路径,则替换之前所有路径
        pathSegments = [firstChild.path];
      }
      targetQuery = firstChild.query;
      // å¦‚果到达叶子节点(没有子节点),则结束查找
      if (!firstChild.children || firstChild.children.length === 0) {
        break;
      }
    }
    // æž„建最终路径
    if (pathSegments.length > 0) {
      // å¦‚果第一段不是以/开头,添加/
      if (!pathSegments[0].startsWith('/')) {
        pathSegments[0] = '/' + pathSegments[0];
      }
      // ç»„合路径 - æŠŠæ•°ç»„中所有路径拼接起来,如果某段包含完整路径(以/开头)则从该段重新开始
      targetPath = pathSegments.reduce((fullPath, segment, index) => {
        if (segment.startsWith('/')) {
          return segment;
        } else if (index === 0) {
          return segment;
        } else {
          return `${fullPath}/${segment}`;
        }
      });
    }
    // å¯¼èˆªåˆ°ç›®æ ‡è·¯ç”±
    if (targetQuery) {
      router.push({ path: targetPath, query: targetQuery });
    } else {
      router.push({ path: targetPath });
    }
  } else {
    // æ²¡æœ‰å­è·¯ç”±ï¼Œéšè—ä¾§è¾¹æ 
    const routeMenu = childrenMenus.value.find(item => item.path === key);
    if (routeMenu && routeMenu.query) {
      let query = JSON.parse(routeMenu.query);
      router.push({ path: key, query: query });
      // query å·²ç»åœ¨ permission.js ä¸­è¢«å¤„理为对象,无需再次解析
      router.push({ path: key, query: routeMenu.query });
    } else {
      router.push({ path: key });
    }
    appStore.toggleSideBarHide(true);
  } else {
    // æ˜¾ç¤ºå·¦ä¾§è”动菜单
    activeRoutes(key);
    appStore.toggleSideBarHide(false);
  }
}
function activeRoutes(key) {
  let routes = [];
  // æŸ¥æ‰¾åŒ¹é…çš„路由
  if (childrenMenus.value && childrenMenus.value.length > 0) {
    childrenMenus.value.map((item) => {
      if (key == item.parentPath || (key == "index" && "" == item.path)) {
@@ -149,66 +263,183 @@
      }
    });
  }
  if(routes.length > 0) {
    // æœ‰å­è·¯ç”±ï¼Œåˆ™æ˜¾ç¤ºä¾§è¾¹æ 
    permissionStore.setSidebarRouters(routes);
    appStore.toggleSideBarHide(false);
  } else {
    // æ²¡æœ‰å­è·¯ç”±ï¼Œéšè—ä¾§è¾¹æ 
    appStore.toggleSideBarHide(true);
  }
  return routes;
}
onMounted(() => {
  window.addEventListener('resize', setVisibleNumber)
})
onBeforeUnmount(() => {
  window.removeEventListener('resize', setVisibleNumber)
  // æ ¹æ®å½“前路由决定是否显示侧边栏,而不是直接隐藏
  const currentPath = route.path;
  if (currentPath === '/index' || currentPath === '/') {
    // å¦‚果当前是首页,自动跳转到第一个动态路由
    if (topMenus.value.length > 0) {
      const firstRoute = topMenus.value[0];
      handleSelect(firstRoute.path);
    }
  } else {
    // æ£€æŸ¥å½“前路由是否需要显示侧边栏
    const routeConfig = routers.value.find(item => currentPath.startsWith(item.path) && item.path !== '/');
    if (routeConfig && routeConfig.children && routeConfig.children.length > 0) {
      // æœ‰å­èœå•,显示侧边栏
      activeRoutes(routeConfig.path);
      appStore.toggleSideBarHide(false);
    } else {
      // æ— å­èœå•,可以隐藏侧边栏
      appStore.toggleSideBarHide(true);
    }
  }
  // ç›‘听滚动状态
  if (menuContainer.value) {
    menuContainer.value.addEventListener('scroll', updateScrollButtons);
    nextTick(() => {
      updateScrollButtons();
    });
  }
  window.addEventListener('resize', () => {
    updateScrollButtons();
  });
})
onMounted(() => {
  setVisibleNumber()
onBeforeUnmount(() => {
  if (menuContainer.value) {
    menuContainer.value.removeEventListener('scroll', updateScrollButtons);
  }
  window.removeEventListener('resize', updateScrollButtons);
})
</script>
<style lang="scss">
.topmenu-container.el-menu--horizontal > .el-menu-item {
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #999093 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
.top-nav-container {
  display: flex;
  align-items: center;
  flex: 1;
  position: relative;
  height: 60px;
  overflow: hidden;
  padding: 0 40px; /* Increase padding for arrows */
  .scroll-arrow {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
    background: rgba(255, 255, 255, 0.2);
    border-radius: 50%;
    cursor: pointer;
    color: #ffffff;
    z-index: 20;
    opacity: 0;
    transition: opacity 0.3s;
    position: absolute;
    box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);
    &:hover {
      background: rgba(255, 255, 255, 0.3);
}
.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
  border-bottom: 2px solid #{'var(--theme)'} !important;
  color: #303133;
    &.left-arrow {
      left: 8px;
}
/* sub-menu item */
.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
  float: left;
  height: 50px !important;
  line-height: 50px !important;
  color: #999093 !important;
  padding: 0 5px !important;
  margin: 0 10px !important;
    &.right-arrow {
      right: 8px;
    }
}
/* èƒŒæ™¯è‰²éšè— */
.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
  background-color: #ffffff !important;
  &:hover {
    .scroll-arrow {
      opacity: 1;
    }
  }
  .menu-container {
    width: 100%;
    height: 100%;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none; /* Firefox */
    -ms-overflow-style: none; /* IE and Edge */
    &::-webkit-scrollbar {
      display: none; /* Chrome, Safari, Opera */
    }
  }
}
.top-menu {
  height: 60px;
  border-bottom: none !important;
  white-space: nowrap;
  background: transparent !important;
  &.theme-dark {
    background: #002866 !important;
  }
}
.el-menu--horizontal {
  border-bottom: none !important;
  > .el-menu-item {
    display: inline-block;
    float: none;
    height: 60px !important;
    line-height: 60px !important;
    color: #ffffff !important;
    padding: 0 20px !important;
    margin: 0 !important;
    border-bottom: none !important;
    position: relative;
    font-size: 16px;
    &.is-active {
      background-color: transparent !important;
      color: #ffffff !important;
      font-weight: bold;
      &::after {
        content: '';
        position: absolute;
        bottom: 10px;
        left: 50%;
        transform: translateX(-50%);
        width: calc(100% - 40px);
        height: 2px;
        background-color: #ffffff;
      }
    }
    &:hover {
      background-color: rgba(255, 255, 255, 0.1) !important;
      color: #ffffff !important;
    }
  }
}
/* å›¾æ ‡å³é—´è· */
.topmenu-container .svg-icon {
  margin-right: 4px;
.svg-icon {
  margin-right: 8px;
  color: #ffffff;
}
/* topmenu more arrow */
.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
  position: static;
  vertical-align: middle;
  margin-left: 8px;
  margin-top: 0px;
/* é¦–页按钮样式 */
.el-menu-item:first-child {
  margin-left: 0 !important;
  font-weight: bold;
  .svg-icon {
    font-size: 18px;
  }
}
</style>
zhitan-vue/src/layout/components/AppMain.vue
@@ -27,7 +27,9 @@
    position: relative;
    overflow: hidden;
    background: #110f2e;
    padding: 14px 0 0 14px;
    padding: 14px;
    box-sizing: border-box;
    padding-top: 8px;
  }
  .fixed-header + .app-main {
@@ -37,12 +39,11 @@
  .hasTagsView {
    .app-main {
      /* 84 = navbar + tags-view + padding = 70 + 56 + 14 */
      min-height: calc(100vh - 125px);
      // padding: 20px;
      min-height: calc(100vh - 110px);
    }
    .fixed-header + .app-main {
      padding-top: 130px;
      padding-top: 108px; /* 60px(navbar) + 34px(tagsview) + 14px(内边距) */
    }
  }
}
@@ -55,7 +56,8 @@
    position: relative;
    overflow: hidden;
    background: #f7f8fa;
    padding: 14px 0 0 14px;
    padding: 14px;
    box-sizing: border-box;
  }
  .fixed-header + .app-main {
@@ -65,15 +67,19 @@
  .hasTagsView {
    .app-main {
      /* 84 = navbar + tags-view + padding = 70 + 56 + 14 */
      min-height: calc(100vh - 125px);
      // padding: 20px;
      min-height: calc(100vh - 110px);
    }
    .fixed-header + .app-main {
      padding-top: 130px;
      padding-top: 108px; /* 60px(navbar) + 34px(tagsview) + 14px(内边距) */
    }
  }
}
/* ç”¨æˆ·ä¸ªäººèµ„料页特殊高度处理 */
.user-profile-container {
  min-height: calc(100vh - 50px) !important;
}
</style>
<style lang="scss">
zhitan-vue/src/layout/components/Navbar.vue
@@ -12,108 +12,53 @@
    </div>
    <div class="right-menu">
      <!-- <template v-if="appStore.device !== 'mobile'">
        <header-search id="header-search" class="right-menu-item" />
        <screenfull id="screenfull" class="right-menu-item hover-effect" />
        <el-tooltip content="布局大小" effect="dark" placement="bottom">
          <size-select id="size-select" class="right-menu-item hover-effect" />
        </el-tooltip>
      </template> -->
      <!-- <el-button @click="toggleTheme">切换</el-button> -->
      <div class="avatar-container">
        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
          <div class="avatar-wrapper">
            <img :src="userStore.avatar" class="user-avatar" />
            <el-icon><caret-bottom /></el-icon>
      <!-- æŠ¥è­¦æŒ‰é’® -->
      <div class="right-menu-item hover-effect nav-btn-item">
        <el-tooltip content="报警" effect="dark" placement="bottom">
          <div class="nav-btn" @click="handleAlarm">
            <img src="@/assets/images/alarm.png" alt="报警" />
            <span>报警</span>
          </div>
          <template #dropdown>
            <el-dropdown-menu>
              <router-link to="/user/profile">
                <el-dropdown-item>个人中心</el-dropdown-item>
              </router-link>
              <el-dropdown-item command="toggleTheme">
                <span>风格切换</span>
              </el-dropdown-item>
              <!--
              <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
                <span>布局设置</span>
              </el-dropdown-item> -->
              <el-dropdown-item divided command="logout">
                <span>退出登录</span>
              </el-dropdown-item>
            </el-dropdown-menu>
          </template>
        </el-dropdown>
        </el-tooltip>
      </div>
      <!-- å¤§æ¨¡åž‹æŒ‰é’® -->
      <div class="right-menu-item hover-effect nav-btn-item">
        <el-tooltip content="大模型" effect="dark" placement="bottom">
          <div class="nav-btn" @click="handleRobot">
            <img src="@/assets/images/robot.png" alt="大模型" />
            <span>大模型</span>
          </div>
        </el-tooltip>
      </div>
    </div>
  </div>
</template>
<script setup>
import { ElMessageBox } from "element-plus"
import Breadcrumb from "@/components/Breadcrumb"
import TopNav from "@/components/TopNav"
import Hamburger from "@/components/Hamburger"
import Screenfull from "@/components/Screenfull"
import SizeSelect from "@/components/SizeSelect"
import HeaderSearch from "@/components/HeaderSearch"
import useAppStore from "@/store/modules/app"
import useUserStore from "@/store/modules/user"
import useSettingsStore from "@/store/modules/settings"
import { useRouter } from "vue-router"
const appStore = useAppStore()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
function toggleTheme() {
  if (settingsStore.sideTheme == "theme-dark") {
    settingsStore.sideTheme = "theme-light"
    document.querySelector("body").className = "themeLight"
  } else {
    settingsStore.sideTheme = "theme-dark"
    document.querySelector("body").className = "themeDark"
  }
}
const router = useRouter()
function toggleSideBar() {
  appStore.toggleSideBar()
}
function handleCommand(command) {
  switch (command) {
    case "toggleTheme":
      toggleTheme()
      break
    case "setLayout":
      setLayout()
      break
    case "logout":
      logout()
      break
    default:
      break
  }
function handleAlarm() {
  // è·³è½¬åˆ°æŠ¥è­¦ç®¡ç†é¡µé¢
  router.push('/alarmmanage/measuremen?modelCode=BJGL')
}
function logout() {
  ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      userStore.logOut().then(() => {
        location.href = "/index"
      })
    })
    .catch(() => {})
}
const emits = defineEmits(["setLayout"])
function setLayout() {
  emits("setLayout")
function handleRobot() {
  // è·³è½¬åˆ°æ™ºèƒ½åŠ©æ‰‹é¡µé¢
  window.open('https://deepseek.tan-zhonghe.com/chat', '_blank')
}
</script>
@@ -188,26 +133,35 @@
        }
      }
      .avatar-container {
        margin-right: 40px;
      .nav-btn-item {
        display: flex;
        align-items: center;
        margin-right: 20px;
        height: 70px;
        .avatar-wrapper {
          margin-top: 5px;
          position: relative;
          .user-avatar {
        .nav-btn {
          display: flex;
          align-items: center;
          justify-content: center;
            cursor: pointer;
            width: 40px;
            height: 40px;
            border-radius: 10px;
          color: #fff;
          background-color: rgba(255, 255, 255, 0.1);
          border-radius: 4px;
          padding: 8px 16px;
          &:hover {
            background-color: rgba(255, 255, 255, 0.2);
          }
          i {
            cursor: pointer;
            position: absolute;
            right: -20px;
            top: 25px;
            font-size: 12px;
          img {
            width: 20px;
            height: 20px;
            margin-right: 6px;
          }
          span {
            font-size: 14px;
            font-weight: 500;
          }
        }
      }
@@ -284,26 +238,35 @@
        }
      }
      .avatar-container {
        margin-right: 40px;
      .nav-btn-item {
        display: flex;
        align-items: center;
        margin-right: 20px;
        height: 70px;
        .avatar-wrapper {
          margin-top: 5px;
          position: relative;
          .user-avatar {
        .nav-btn {
          display: flex;
          align-items: center;
          justify-content: center;
            cursor: pointer;
            width: 40px;
            height: 40px;
            border-radius: 10px;
          color: #333;
          background-color: rgba(0, 0, 0, 0.05);
          border-radius: 4px;
          padding: 8px 16px;
          &:hover {
            background-color: rgba(0, 0, 0, 0.1);
          }
          i {
            cursor: pointer;
            position: absolute;
            right: -20px;
            top: 25px;
            font-size: 12px;
          img {
            width: 20px;
            height: 20px;
            margin-right: 6px;
          }
          span {
            font-size: 14px;
            font-weight: 500;
          }
        }
      }
zhitan-vue/src/layout/components/Settings/index.vue
@@ -134,6 +134,8 @@
function handleTheme(val) {
  settingsStore.sideTheme = val
  sideTheme.value = val
  // Update body class to match the theme
  document.querySelector("body").className = val === 'theme-dark' ? "themeDark" : "themeLight"
}
function saveSetting() {
  proxy.$modal.loading("正在保存到本地,请稍候...")
zhitan-vue/src/layout/components/Sidebar/SidebarItem.vue
@@ -9,7 +9,7 @@
      </app-link>
    </template>
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported @click="handleSubMenuClick">
      <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>
@@ -31,6 +31,9 @@
import { isExternal } from '@/utils/validate'
import AppLink from './Link'
import { getNormalPath } from '@/utils/ruoyi'
import { useRouter } from 'vue-router'
const router = useRouter();
const props = defineProps({
  // route object
@@ -50,6 +53,204 @@
const onlyOneChild = ref({});
/**
 * æŸ¥æ‰¾æœ€æ·±å±‚的子菜单(叶子节点)
 * é€’归查找第一个没有children的子菜单
 */
function findDeepestLeafMenu(route) {
  if (!route) return null;
  // å¦‚果没有子菜单或子菜单为空,则返回当前路由
  if (!route.children || route.children.length === 0) {
    return route;
  }
  // æ‰¾åˆ°ç¬¬ä¸€ä¸ªéžéšè—çš„子菜单
  const firstVisibleChild = route.children.find(child => !child.hidden);
  if (!firstVisibleChild) {
    return route; // å¦‚果所有子菜单都是隐藏的,返回当前路由
  }
  // é€’归查找这个子菜单的最深层子菜单
  return findDeepestLeafMenu(firstVisibleChild);
}
// å¤„理子菜单点击
function handleSubMenuClick(e) {
  // é˜»æ­¢äº‹ä»¶å†’泡
  e.stopPropagation();
  // å¦‚果点击的是子菜单标题,则自动导航到最深层的子菜单
  if (e.target.closest('.el-sub-menu__title')) {
    // æŒ‰ç…§æ­£ç¡®çš„路径构建层级
    let currentNode = props.item;
    console.log('当前点击的菜单项:', JSON.stringify(currentNode, null, 2));
    console.log('basePath:', props.basePath);
    // èŽ·å–ç¬¬ä¸€ä¸ªå¯è§å­èœå•ï¼Œå¦‚æžœæ²¡æœ‰å¯è§å­èœå•ï¼Œä¸è¿›è¡Œè·³è½¬
    if (!currentNode.children || currentNode.children.length === 0) {
      return;
    }
    const firstVisibleChild = currentNode.children.find(child => !child.hidden);
    if (!firstVisibleChild) {
      return;
    }
    console.log('第一个可见子菜单:', JSON.stringify(firstVisibleChild, null, 2));
    // æ—¥å¿—管理等三级菜单特殊处理
    // æ£€æŸ¥æ˜¯å¦æœ‰é¢„先写入的完整路径,如果有则直接使用
    if (firstVisibleChild.fullPath) {
      console.log('使用预先设置的完整路径:', firstVisibleChild.fullPath);
      router.push({ path: firstVisibleChild.fullPath });
      return;
    }
    // åˆ¤æ–­æ˜¯å¦æ˜¯ç³»ç»Ÿ/日志管理类型的三级菜单(例如,/system/log/operlog)
    // è¿™ç§æƒ…况下,直接跳转到第一个子菜单的完整路径
    if (firstVisibleChild.component === 'ParentView' ||
        (typeof firstVisibleChild.component === 'object' &&
         firstVisibleChild.component.name === 'ParentView')) {
      console.log('检测到ParentView组件,处理三级菜单');
      // æ˜¯æœ‰ä¸‰çº§èœå•的情况
      if (firstVisibleChild.children && firstVisibleChild.children.length > 0) {
        const grandChild = firstVisibleChild.children.find(child => !child.hidden);
        if (grandChild) {
          console.log('找到第三级菜单:', JSON.stringify(grandChild, null, 2));
          // åˆ¤æ–­æ˜¯å¦åº”该使用parentPath
          if (firstVisibleChild.parentPath && grandChild.path.startsWith('/')) {
            console.log('使用parentPath属性:', firstVisibleChild.parentPath);
            // å¦‚果子菜单是绝对路径,但有parentPath,则应该使用parentPath作为基础
            let fullPath = firstVisibleChild.parentPath;
            if (!fullPath.startsWith('/')) {
              fullPath = '/' + fullPath;
            }
            // ç¬¬äºŒçº§è·¯å¾„基于根路径
            if (firstVisibleChild.path.startsWith('/')) {
              // ç¬¬äºŒçº§å·²ç»æ˜¯ç»å¯¹è·¯å¾„,截取最后部分
              const pathParts = firstVisibleChild.path.split('/');
              const lastPart = pathParts[pathParts.length - 1];
              fullPath = fullPath + '/' + lastPart;
            } else {
              fullPath = buildFullPath(fullPath, firstVisibleChild.path);
            }
            console.log('二级路径:', fullPath);
            // ç¬¬ä¸‰çº§è·¯å¾„基于二级路径
            if (grandChild.path.startsWith('/')) {
              // ç¬¬ä¸‰çº§æ˜¯ç»å¯¹è·¯å¾„,截取最后部分
              const pathParts = grandChild.path.split('/');
              const lastPart = pathParts[pathParts.length - 1];
              fullPath = fullPath + '/' + lastPart;
            } else {
              fullPath = buildFullPath(fullPath, grandChild.path);
            }
            console.log('三级路径 (最终):', fullPath);
            // å¯¼èˆªåˆ°ç¬¬ä¸‰çº§èœå•
            if (grandChild.query) {
              router.push({ path: fullPath, query: grandChild.query });
            } else {
              router.push({ path: fullPath });
            }
            return;
          }
          // å¸¸è§„路径构建
          let fullPath;
          // ç¬¬ä¸€çº§è·¯å¾„必须是完整的(例如/system)
          if (currentNode.path.startsWith('/')) {
            fullPath = currentNode.path;
          } else {
            fullPath = '/' + currentNode.path;
          }
          console.log('一级路径:', fullPath);
          // ç¬¬äºŒçº§è·¯å¾„必须基于第一级路径(例如/system/log)
          fullPath = buildFullPath(fullPath, firstVisibleChild.path);
          console.log('二级路径:', fullPath);
          // ç¬¬ä¸‰çº§è·¯å¾„必须基于二级路径(例如/system/log/operlog)
          fullPath = buildFullPath(fullPath, grandChild.path);
          console.log('三级路径 (最终):', fullPath);
          // å¯¼èˆªåˆ°ç¬¬ä¸‰çº§èœå•
          if (grandChild.query) {
            console.log('跳转到:', fullPath, '带参数:', grandChild.query);
            router.push({ path: fullPath, query: grandChild.query });
          } else {
            console.log('跳转到:', fullPath);
            router.push({ path: fullPath });
          }
          return;
        }
      }
    }
    console.log('处理标准二级菜单');
    // æ£€æŸ¥æ˜¯å¦éœ€è¦ä½¿ç”¨parentPath
    if (firstVisibleChild.parentPath && firstVisibleChild.path.startsWith('/')) {
      console.log('使用parentPath属性:', firstVisibleChild.parentPath);
      // å¦‚果子菜单是绝对路径,但有parentPath,则应该使用parentPath作为基础
      let fullPath = firstVisibleChild.parentPath;
      if (!fullPath.startsWith('/')) {
        fullPath = '/' + fullPath;
      }
      // æž„建完整路径
      if (firstVisibleChild.path.startsWith('/')) {
        // æˆªå–子路径的最后部分
        const pathParts = firstVisibleChild.path.split('/');
        const lastPart = pathParts[pathParts.length - 1];
        fullPath = fullPath + '/' + lastPart;
      } else {
        fullPath = buildFullPath(fullPath, firstVisibleChild.path);
      }
      console.log('构建的最终路径:', fullPath);
      // å¯¼èˆªåˆ°ç›®æ ‡è·¯ç”±
      if (firstVisibleChild.query) {
        router.push({ path: fullPath, query: firstVisibleChild.query });
      } else {
        router.push({ path: fullPath });
      }
      return;
    }
    // æ ‡å‡†çš„二级菜单处理
    // æž„建正确的路径
    let fullPath;
    // å¤„理第一级路径(例如/system)- å¿…须是完整的路径
    if (currentNode.path.startsWith('/')) {
      fullPath = currentNode.path;
    } else {
      fullPath = '/' + currentNode.path;
    }
    console.log('一级路径:', fullPath);
    // å¤„理第二级路径(例如/system/user)- å¿…须基于第一级路径
    fullPath = buildFullPath(fullPath, firstVisibleChild.path);
    console.log('二级路径 (最终):', fullPath);
    // å¯¼èˆªåˆ°ç›®æ ‡è·¯ç”±
    if (firstVisibleChild.query) {
      console.log('跳转到:', fullPath, '带参数:', firstVisibleChild.query);
      router.push({ path: fullPath, query: firstVisibleChild.query });
    } else {
      console.log('跳转到:', fullPath);
      router.push({ path: fullPath });
    }
  }
}
function hasOneShowingChild(children = [], parent) {
  if (!children) {
    children = [];
@@ -65,8 +266,13 @@
  })
  // When there is only one child router, the child router is displayed by default
  if (showingChildren.length === 1) {
  if (showingChildren.length === 1 && !showingChildren[0].children) {
    return true
  }
  // If the single child also has children, don't treat it as a single showing child
  if (showingChildren.length === 1 && showingChildren[0].children && showingChildren[0].children.length > 0) {
    return false
  }
  // Show parent if there are no child router to display
@@ -86,12 +292,34 @@
    return props.basePath
  }
  if (routeQuery) {
    let query = JSON.parse(routeQuery);
    let query = routeQuery;
    // å¦‚æžœ routeQuery æ˜¯å­—符串,则尝试解析它
    if (typeof routeQuery === 'string') {
      try {
        query = JSON.parse(routeQuery);
      } catch (error) {
        console.error('Error parsing query string:', routeQuery, error);
      }
    }
    return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
  }
  return getNormalPath(props.basePath + '/' + routePath)
}
// æ­£ç¡®æž„建路径
function buildFullPath(base, segment) {
  // å¦‚æžœsegment是绝对路径,直接返回
  if (segment.startsWith('/')) {
    return segment;
  }
  // ç¡®ä¿base有正确的开头斜杠
  const normalizedBase = base.startsWith('/') ? base : '/' + base;
  // æ‹¼æŽ¥è·¯å¾„,避免双斜杠
  return normalizedBase.endsWith('/') ? normalizedBase + segment : normalizedBase + '/' + segment;
}
function hasTitle(title){
  if (title.length > 5) {
    return title;
zhitan-vue/src/layout/components/Sidebar/index.vue
@@ -2,44 +2,124 @@
  <div
    :class="{ 'has-logo': showLogo }"
    :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }"
    class="sidebar-container-wrapper"
  >
    <logo v-if="showLogo" :collapse="isCollapse" />
    <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper">
    <el-scrollbar :class="sideTheme" wrap-class="scrollbar-wrapper" view-class="scrollbar-view">
      <!-- å§‹ç»ˆæ˜¾ç¤ºèœå•项,不再根据路径判断 -->
      <el-menu
        :default-active="activeMenu"
        :collapse="isCollapse"
        :background-color="sideTheme === 'theme-dark' ? '#232D70' : '#fff'"
        :background-color="'transparent'"
        :text-color="sideTheme === 'theme-dark' ? '#fff' : '#000'"
        :unique-opened="true"
        :active-text-color="theme"
        :collapse-transition="false"
        mode="vertical"
        class="custom-menu"
      >
        <!-- å½“前是首页看板子路由时,渲染专用路由 -->
        <template v-if="isIndexSubRoute">
          <sidebar-item
            v-for="(route, index) in indexPageRouters"
            :key="route.path + index"
            :item="route"
            :base-path="route.path"
          />
        </template>
        <template v-else>
        <sidebar-item
          v-for="(route, index) in sidebarRouters"
          :key="route.path + index"
          :item="route"
          :base-path="route.path"
        />
        </template>
      </el-menu>
    </el-scrollbar>
    <!-- åº•部用户区域 -->
    <div class="sidebar-footer" :class="{ 'collapsed': isCollapse, 'theme-light': sideTheme === 'theme-light' }">
      <div class="user-avatar-container">
        <img :src="userStore.avatar" class="user-avatar" />
      </div>
      <!-- å±•开状态下显示完整内容 -->
      <div class="user-info" v-if="!isCollapse">
        <div class="username">{{ userStore.name || 'admin' }}</div>
        <div class="action-buttons">
          <div class="action-button" :class="{'theme-light': sideTheme === 'theme-light'}" @click="toUserProfile">
            <el-icon><User /></el-icon>
            <span>个人中心</span>
          </div>
          <div class="action-button" :class="{'theme-light': sideTheme === 'theme-light'}" @click="toggleTheme">
            <el-icon><Brush /></el-icon>
            <span>切换主题</span>
          </div>
          <div class="action-button" :class="{'theme-light': sideTheme === 'theme-light'}" @click="handleLogout">
            <el-icon><SwitchButton /></el-icon>
            <span>退出登录</span>
          </div>
        </div>
      </div>
      <!-- æŠ˜å çŠ¶æ€ä¸‹åªæ˜¾ç¤ºå›¾æ ‡æŒ‰é’® -->
      <div class="collapsed-actions" v-if="isCollapse">
        <div class="action-icon" :class="{'theme-light': sideTheme === 'theme-light'}" @click="toUserProfile" title="个人中心">
          <el-icon><User /></el-icon>
        </div>
        <div class="action-icon" :class="{'theme-light': sideTheme === 'theme-light'}" @click="toggleTheme" title="切换主题">
          <el-icon><Brush /></el-icon>
        </div>
        <div class="action-icon" :class="{'theme-light': sideTheme === 'theme-light'}" @click="handleLogout" title="退出登录">
          <el-icon><SwitchButton /></el-icon>
        </div>
      </div>
    </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"
import useUserStore from "@/store/modules/user"
import { User, Brush, SwitchButton } from '@element-plus/icons-vue'
import { ElMessageBox } from 'element-plus'
import { useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const permissionStore = usePermissionStore()
const userStore = useUserStore()
const sidebarRouters = computed(() => permissionStore.sidebarRouters)
// åˆ¤æ–­å½“前是否为首页子路由(/index/index)
const isIndexSubRoute = computed(() => {
  return route.path === '/index/index'
})
// åˆ¤æ–­å½“前是否为主首页路由(/index或/)
const isMainIndexRoute = computed(() => {
  return route.path === '/index' || route.path === '/'
})
// é¦–页专用路由,首页看板相关菜单
const indexPageRouters = computed(() => {
  // æŸ¥æ‰¾name为Index的路由
  const indexRoute = sidebarRouters.value.find(route => route.name === 'Index')
  return indexRoute ? [indexRoute] : []
})
const showLogo = computed(() => settingsStore.sidebarLogo)
const sideTheme = computed(() => settingsStore.sideTheme)
const theme = computed(() => settingsStore.theme)
@@ -53,5 +133,465 @@
  }
  return path
})
function toUserProfile() {
  router.push('/user/profile')
}
function toggleTheme() {
  if (settingsStore.sideTheme == "theme-dark") {
    settingsStore.sideTheme = "theme-light"
    document.querySelector("body").className = "themeLight"
  } else {
    settingsStore.sideTheme = "theme-dark"
    document.querySelector("body").className = "themeDark"
  }
}
function handleLogout() {
  ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
    confirmButtonText: "确定",
    cancelButtonText: "取消",
    type: "warning",
  })
    .then(() => {
      userStore.logOut().then(() => {
        location.href = "/index"
      })
    })
    .catch(() => {})
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
.sidebar-container-wrapper {
  position: relative;
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
:deep(.scrollbar-wrapper) {
  height: calc(100% - 220px) !important;
  overflow-x: hidden !important;
}
:deep(.scrollbar-view) {
  height: 100%;
  padding-bottom: 20px;
}
:deep(.el-scrollbar__bar.is-vertical) {
  right: 0;
  width: 6px;
}
:deep(.el-scrollbar__thumb) {
  background-color: rgba(144, 147, 153, 0.3);
  &:hover {
    background-color: rgba(144, 147, 153, 0.5);
  }
}
.custom-menu {
  width: 100%;
  padding: 6px 0;
  height: auto !important; // æ”¹ä¸ºè‡ªé€‚应高度,避免固定高度导致内容溢出
  transition: all 0.3s ease;
  // Override Element Plus default menu styles
  .el-menu-item {
    height: 38px !important;
    line-height: 38px !important;
    border-radius: 4px;
    margin: 4px 10px;
    width: calc(100% - 20px);
    transition: all 0.2s ease;
    &.is-active {
      background-color: #3883FA !important;
      color: #fff !important;
      font-weight: bold;
      position: relative;
      box-shadow: 0 2px 8px rgba(56, 131, 250, 0.5);
      // å·¦ä¾§æŒ‡ç¤ºæ¡
    }
    &:hover {
      background-color: rgba(56, 131, 250, 0.1) !important;
    }
  }
  .el-sub-menu {
    .el-sub-menu__title {
      height: 38px !important;
      line-height: 38px !important;
      border-radius: 4px;
      margin: 4px 10px;
      width: calc(100% - 20px);
      transition: all 0.2s ease;
      &:hover {
        background-color: rgba(56, 131, 250, 0.1) !important;
      }
    }
    &.is-active {
      > .el-sub-menu__title {
        color: #3883FA !important;
        font-weight: bold;
      }
    }
    .el-menu-item {
      padding-left: 45px !important;
      min-width: auto !important;
      &.is-active {
        padding-left: 45px !important;
      }
    }
    // For nested submenus
    .el-menu {
      .el-menu-item,
      .el-sub-menu__title {
        height: 38px !important;
        line-height: 38px !important;
      }
      // Add styling for deeply nested submenus (level 3+)
      .el-sub-menu {
        .el-menu-item {
          padding-left: 65px !important;
          &.is-active {
            padding-left: 65px !important;
          }
        }
        // Level 4
        .el-menu {
          .el-menu-item {
            padding-left: 85px !important;
            &.is-active {
              padding-left: 85px !important;
            }
          }
        }
      }
    }
  }
}
// é¦–页空白菜单区域样式
.home-empty-menu {
  height: auto;
  min-height: 100px;
}
// åº•部用户区域样式
.sidebar-footer {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  padding: 16px;
  display: flex;
  flex-direction: column;
  align-items: center;
  &.collapsed {
    padding: 10px;
    .user-avatar-container {
      margin-bottom: 10px;
    }
  }
  &.theme-light {
    background-color: rgba(255, 255, 255, 0.6);
    border-top: 1px solid rgba(0, 0, 0, 0.1);
    .user-avatar-container {
      border-color: rgba(0, 0, 0, 0.1);
    }
    .user-info {
      .username {
        color: #333;
      }
    }
  }
  .user-avatar-container {
    margin-bottom: 10px;
    border: 2px dashed rgba(255, 255, 255, 0.3);
    border-radius: 4px;
    width: 54px;
    height: 54px;
    display: flex;
    align-items: center;
    justify-content: center;
    .user-avatar {
      width: 38px;
      height: 38px;
      border-radius: 4px;
    }
  }
  .user-info {
    width: 100%;
    text-align: center;
    .username {
      color: #fff;
      font-size: 16px;
      font-weight: 500;
      margin-bottom: 16px;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    .action-buttons {
      .action-button {
        display: flex;
        align-items: center;
        justify-content: center;
        background: rgba(56, 131, 250, 0.11);
        border-radius: 9px;
        border: 1px solid rgba(255, 255, 255, 0.3);
        color: #fff;
        padding: 10px;
        margin-bottom: 10px;
        border-radius: 4px;
        cursor: pointer;
        transition: background-color 0.3s;
        &:hover {
          background: rgba(56, 131, 250, 0.2);
        }
        .el-icon {
          margin-right: 8px;
          font-size: 16px;
        }
        span {
          font-size: 14px;
        }
        &.theme-light {
          background-color: rgba(56, 131, 250, 1);
          color: #fff;
          border: 1px solid rgba(56, 131, 250, 0.8);
          &:hover {
            background-color: rgba(56, 131, 250, 0.9);
          }
          .el-icon {
            color: #fff;
          }
        }
      }
    }
  }
  .collapsed-actions {
    display: flex;
    flex-direction: column;
    align-items: center;
    width: 100%;
    .action-icon {
      width: 40px;
      height: 40px;
      margin-bottom: 8px;
      background: rgba(56, 131, 250, 0.11);
      border: 1px solid rgba(255, 255, 255, 0.3);
      border-radius: 4px;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      &:hover {
        background: rgba(56, 131, 250, 0.2);
      }
      .el-icon {
        font-size: 20px;
        color: #fff;
      }
      &.theme-light {
        background: rgba(56, 131, 250, 1);
        border: 1px solid rgba(56, 131, 250, 0.8);
        &:hover {
          background: rgba(56, 131, 250, 0.9);
        }
        .el-icon {
          color: #fff;
        }
      }
    }
  }
}
.theme-light {
  :deep(.custom-menu) {
    // Override Element Plus menu styles for light theme
    .el-menu-item {
      &.is-active {
        background-color: #3883FA !important;
        color: #fff !important;
      }
      &:hover {
        background-color: rgba(56, 131, 250, 0.1) !important;
      }
    }
    .el-sub-menu {
      .el-sub-menu__title {
        &:hover {
          background-color: rgba(56, 131, 250, 0.1) !important;
        }
      }
    }
  }
}
// æ·»åŠ æ·±è‰²æ¨¡å¼ä¸“ç”¨æ ·å¼
.theme-dark {
  :deep(.custom-menu) {
    // Override Element Plus menu styles for dark theme
    .el-menu-item {
      &.is-active {
        background-color: #4e77f8 !important;
        color: #ffffff !important;
        font-weight: bold;
        box-shadow: 0 2px 10px rgba(78, 119, 248, 0.6);
        position: relative;
        // å·¦ä¾§æŒ‡ç¤ºæ¡
      }
      &:hover {
        background-color: rgba(78, 119, 248, 0.2) !important;
      }
    }
    .el-sub-menu {
      &.is-active {
        > .el-sub-menu__title {
          color: #4e77f8 !important;
          font-weight: bold;
        }
      }
      .el-sub-menu__title {
        &:hover {
          background-color: rgba(78, 119, 248, 0.2) !important;
        }
      }
      // åµŒå¥—子菜单样式
      .el-menu {
        .el-menu-item {
          &.is-active {
            background-color: #4e77f8 !important;
            color: #ffffff !important;
          }
        }
        .el-sub-menu {
          &.is-active {
            > .el-sub-menu__title {
              color: #4e77f8 !important;
            }
          }
        }
      }
    }
  }
}
// Add global style to override Element Plus defaults
:global(.el-menu--vertical .el-menu-item),
:global(.el-menu--vertical .el-sub-menu__title) {
  height: 38px !important;
  line-height: 38px !important;
}
// Add styles for collapsed menu items
:deep(.custom-menu.el-menu--collapse) {
  width: 54px !important;
  .el-menu-item, .el-sub-menu__title {
    width: 36px !important;
    min-width: 36px !important;
    margin: 4px 9px !important; /* 9px是为了确保居中:(54px宽 - 36px菜单项) / 2 = 9px */
    padding: 0 !important;
    display: flex;
    justify-content: center;
    align-items: center;
    border-radius: 4px;
    &.is-active {
      background-color: #3883FA !important;
      color: #fff !important;
      box-shadow: 0 2px 6px rgba(56, 131, 250, 0.4);
      transform: scale(0.95);
      transition: all 0.2s ease;
    }
    .el-icon, .svg-icon {
      margin: 0 !important;
      font-size: 18px !important;
      svg {
        width: 1.2em;
        height: 1.2em;
      }
    }
    // ç¡®ä¿æŠ˜å æ—¶å­èœå•的标题也居中对齐
    .el-sub-menu__icon-arrow {
      display: none;
    }
  }
  // ç¡®ä¿æŠ˜å æ—¶å¼¹å‡ºçš„子菜单有正确样式
  .el-tooltip__trigger:focus:not(.focusing) {
    outline: none;
  }
}
// æ·±è‰²æ¨¡å¼ä¸‹æŠ˜å èœå•的样式
.theme-dark {
  :deep(.custom-menu.el-menu--collapse) {
    .el-menu-item, .el-sub-menu__title {
      &.is-active {
        background-color: #4e77f8 !important;
        color: #ffffff !important;
        box-shadow: 0 2px 8px rgba(78, 119, 248, 0.6);
      }
      &:hover {
        background-color: rgba(78, 119, 248, 0.2) !important;
      }
    }
  }
}
</style>
zhitan-vue/src/layout/components/TagsView/ScrollPane.vue
@@ -99,7 +99,7 @@
    bottom: 0px;
  }
  :deep(.el-scrollbar__wrap) {
    height: 39px;
    height: 34px !important;
  }
}
</style>
zhitan-vue/src/layout/components/TagsView/index.vue
@@ -1,5 +1,5 @@
<template>
  <div id="tags-view-container" class="tags-view-container">
  <div id="tags-view-container" class="tags-view-container" :class="{'theme-dark': sideTheme === 'theme-dark', 'theme-light': sideTheme === 'theme-light'}">
    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
      <router-link
        v-for="tag in visitedViews"
@@ -52,6 +52,7 @@
const visitedViews = computed(() => useTagsViewStore().visitedViews)
const routes = computed(() => usePermissionStore().routes)
const theme = computed(() => useSettingsStore().theme)
const sideTheme = computed(() => useSettingsStore().sideTheme)
watch(route, () => {
  addTags()
@@ -232,104 +233,60 @@
</script>
<style lang="scss" scoped>
.themeDark {
  .tags-view-container {
    height: 52px;
    width: 100%;
    background: #1a235d;
    // border-bottom: 1px solid #d8dce5;
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
    .tags-view-wrapper {
      .tags-view-item {
        display: inline-block;
        position: relative;
        cursor: pointer;
        height: 32px;
        line-height: 30px;
        border: 1px solid #5278f5;
        color: #fff;
        // background: #3271eb;
        padding: 0 12px;
        font-size: 14px;
        margin-left: 6px;
  height: 34px;
  width: calc(100% - 42px);
        margin-top: 10px;
        border-radius: 4px;
        font-family: OPPOSans, OPPOSans;
        &:first-of-type {
          margin-left: 16px;
        }
        &:last-of-type {
          margin-right: 15px;
        }
        &.active {
          background-color: #4e77f8 !important;
  margin-left: 14px;
  box-sizing: border-box;
  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
  &.theme-dark {
    background: #0A3465;
    .tags-view-item {
          color: #fff;
          border-color: #4e77f8 !important;
          &::before {
            content: "";
            background: #fff;
            display: inline-block;
            width: 8px;
            height: 8px;
            border-radius: 50%;
            position: relative;
            margin-right: 5px;
          }
        }
      }
    }
    .contextmenu {
      margin: 0;
      background: #fff;
      z-index: 3000;
      position: absolute;
      list-style-type: none;
      padding: 5px 0;
      border-radius: 4px;
      font-size: 12px;
      font-weight: 400;
      color: #333;
      box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
      li {
        margin: 0;
        padding: 7px 16px;
        cursor: pointer;
        &:hover {
          background: #eee;
        }
      }
      border: 1px solid #0c4685;
      background: rgba(10, 52, 101, .48);
      border-radius: 5px;
      &.active {
        background-color: var(--el-color-primary) !important;
        color: #fff !important;
    }
  }
}
.themeLight {
  .tags-view-container {
    height: 52px;
    width: 100%;
  &.theme-light {
    background: #fff;
    // border-bottom: 1px solid #d8dce5;
    box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
    .tags-view-item {
      color: #495060;
      background: #fff;
      border: 1px solid #d8dce5;
      &.active {
        background-color: var(--el-color-primary) !important;
        color: #fff !important;
      }
    }
  }
    .tags-view-wrapper {
      .tags-view-item {
        display: inline-block;
        position: relative;
        cursor: pointer;
        height: 32px;
        line-height: 30px;
        border: 1px solid #d8dce5;
        color: #495060;
        background: #fff;
        padding: 0 12px;
        font-size: 14px;
        margin-left: 6px;
        margin-top: 10px;
        border-radius: 4px;
        font-family: OPPOSans, OPPOSans;
      height: 26px;
      line-height: 26px;
      border-radius: 3px;
      padding: 0 10px;
      font-size: 12px;
      margin-left: 5px;
      margin-top: 4px;
        &:first-of-type {
          margin-left: 16px;
        margin-left: 5px;
        }
        &:last-of-type {
          margin-right: 15px;
        margin-right: 5px;
        }
        &.active {
          background-color: #42b983;
@@ -343,7 +300,7 @@
            height: 8px;
            border-radius: 50%;
            position: relative;
            margin-right: 5px;
          margin-right: 2px;
          }
        }
      }
@@ -366,7 +323,6 @@
        cursor: pointer;
        &:hover {
          background: #eee;
        }
      }
    }
  }
@@ -401,6 +357,6 @@
}
.scroll-container .el-scrollbar__wrap {
  height: 50px !important;
  height: 34px !important;
}
</style>
zhitan-vue/src/layout/index.vue
@@ -1,34 +1,99 @@
<template>
  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
    <div class="navbar-container">
      <div class="navbar">
        <div class="left">
          <div class="sidebar-logo-container" :class="{ collapse: !sidebar.opened }">
            <div class="logo" v-if="systemInfo && systemInfo.leftLogo">
              <img v-if="sideTheme === 'theme-dark'" :src="systemInfo.leftLogo" class="sidebar-logo" />
              <img v-else :src="systemInfo.leftLogo" class="sidebar-logo" />
            </div>
            <div class="name" v-if="sidebar.opened" :style="{ color: sideTheme === 'theme-dark' ? '#fff' : '#333' }">
              {{ title }}
            </div>
          </div>
          <hamburger
            id="hamburger-container"
            :is-active="appStore.sidebar.opened"
            class="hamburger-container"
            @toggleClick="toggleSideBar"
          />
        </div>
        <top-nav />
        <div class="right">
          <!-- æŠ¥è­¦å’Œå¤§æ¨¡åž‹æŒ‰é’® -->
          <div class="right-menu">
            <!-- æŠ¥è­¦æŒ‰é’® -->
            <div class="right-menu-item alarm-btn" @click="goToAlarm">
              <el-badge :value="alarmCount" :max="99" class="alarm-badge">
                <svg-icon icon-class="bell" class="right-menu-icon" />
              </el-badge>
              <span class="right-menu-text">报警</span>
            </div>
            <!-- å¤§æ¨¡åž‹æŒ‰é’® -->
            <div class="right-menu-item ai-btn" @click="openAIModel">
              <svg-icon icon-class="ai" class="right-menu-icon" />
              <span class="right-menu-text">智能助手</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="content-container">
    <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" v-if="!sidebar.hide"/>
        <tags-view v-if="needTagsView" v-show="!sidebar.hide"/>
      </div>
      <app-main />
      <settings ref="settingRef" />
    </div>
  </div>
  </div>
</template>
<script setup>
import { ref, computed, watchEffect, onMounted } from 'vue'
import { useWindowSize } from '@vueuse/core'
import { useRoute, useRouter } from 'vue-router'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import { AppMain, Settings, TagsView } from './components'
import TopNav from '@/components/TopNav'
import Hamburger from '@/components/Hamburger'
import defaultSettings from '@/settings'
import Cookies from "js-cookie"
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
const route = useRoute()
const router = useRouter()
const appStore = useAppStore()
const settingsStore = useSettingsStore()
const theme = computed(() => settingsStore.theme);
const sideTheme = computed(() => settingsStore.sideTheme);
const sidebar = computed(() => useAppStore().sidebar);
const device = computed(() => useAppStore().device);
const sidebar = computed(() => appStore.sidebar);
const device = computed(() => appStore.device);
const needTagsView = computed(() => settingsStore.tagsView);
const fixedHeader = computed(() => settingsStore.fixedHeader);
const systemInfo = JSON.parse(Cookies.get("SystemInfo") || '{"systemName":"智汕能源管理系统","leftLogo":""}')
const title = systemInfo.systemName || import.meta.env.VITE_APP_TITLE
// æŠ¥è­¦æ•°é‡ï¼Œå¯ä»¥ä»ŽæŽ¥å£èŽ·å–
const alarmCount = ref(5)
// è·³è½¬åˆ°æŠ¥è­¦é¡µé¢
function goToAlarm() {
  router.push('/alarmmanage/measuremen?modelCode=BJGL')
}
// æ‰“å¼€AI大模型对话框
function openAIModel() {
  // è·³è½¬åˆ°æŒ‡å®šçš„URL
  window.open('https://deepseek.tan-zhonghe.com/chat', '_blank')
}
const classObj = computed(() => ({
  hideSidebar: !sidebar.value.opened,
@@ -38,22 +103,51 @@
}))
const { width, height } = useWindowSize();
const WIDTH = 992; // refer to Bootstrap's responsive design
const WIDTH = 992;
watchEffect(() => {
  if (device.value === 'mobile' && sidebar.value.opened) {
    useAppStore().closeSideBar({ withoutAnimation: false })
    appStore.closeSideBar({ withoutAnimation: false })
  }
  if (width.value - 1 < WIDTH) {
    useAppStore().toggleDevice('mobile')
    useAppStore().closeSideBar({ withoutAnimation: true })
    appStore.toggleDevice('mobile')
    appStore.closeSideBar({ withoutAnimation: true })
  } else {
    useAppStore().toggleDevice('desktop')
    appStore.toggleDevice('desktop')
  }
})
// ç›‘听路由变化,处理首页的侧边栏显示
watchEffect(() => {
  // æ£€æŸ¥æ˜¯å¦æ˜¯é¦–页路由,但排除/index/index子路由
  if ((route.path === '/index' || route.path === '/') && route.path !== '/index/index') {
    // é¦–页路由,确保侧边栏不隐藏,但状态是折叠的
    appStore.toggleSideBarHide(false) // æ”¹ä¸ºä¸éšè—ä¾§è¾¹æ 
  } else if (route.meta && route.meta.showSidebar === false) {
    // å¦‚果路由明确指定隐藏侧边栏
    appStore.toggleSideBarHide(true)
  } else {
    // å…¶ä»–路由,确保侧边栏可见
    appStore.toggleSideBarHide(false)
  }
})
// ç»„件挂载时,确保首页侧边栏状态正确
onMounted(() => {
  // å¦‚果当前是首页子页面,只确保侧边栏不被隐藏,但保持折叠/展开状态不变
  if (route.path === '/index/index') {
    // åªè®¾ç½®ä¸éšè—ä¾§è¾¹æ ï¼Œä½†ä¸æ”¹å˜å…¶å±•å¼€/折叠状态
    appStore.toggleSideBarHide(false)
    // ä¸å†å¼ºåˆ¶è®¾ç½®opened为true,保持用户之前的设置
  }
})
function handleClickOutside() {
  useAppStore().closeSideBar({ withoutAnimation: false })
  appStore.closeSideBar({ withoutAnimation: false })
}
function toggleSideBar() {
  appStore.toggleSideBar()
}
const settingRef = ref(null);
@@ -69,8 +163,11 @@
.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;
  height: 100vh;
  width: 100vw;
  display: flex;
  flex-direction: column;
  overflow-x: hidden;
  &.mobile.openSidebar {
    position: fixed;
@@ -88,13 +185,138 @@
  z-index: 999;
}
.fixed-header {
.navbar-container {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  height: 60px;
  z-index: 1000;
  width: 100%;
}
.navbar {
  display: flex;
  justify-content: space-between;
  align-items: center;
  background-color: var(--current-color);
  height: 60px;
  width: 100%;
  padding: 0;
  .left {
    display: flex;
    align-items: center;
    padding-left: 16px;
    .sidebar-logo-container {
      display: flex;
      align-items: center;
      height: 60px;
      padding: 0 15px;
      min-width: 220px;
      .logo {
        width: 40px;
        height: 40px;
        margin-right: 10px;
        flex-shrink: 0;
        .sidebar-logo {
          width: 100%;
          height: 100%;
        }
      }
      .name {
        font-family: OPPOSans, OPPOSans;
        font-weight: bold;
        font-size: 20px;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }
      &.collapse {
        min-width: 70px;
        .name {
          display: none;
        }
      }
    }
  }
  .right {
    display: flex;
    align-items: center;
    padding-right: 20px;
    .right-menu {
      display: flex;
      align-items: center;
      .right-menu-item {
        display: flex;
        align-items: center;
        margin-left: 20px;
        cursor: pointer;
        color: #fff;
        font-size: 14px;
        transition: all 0.3s;
        &:hover {
          opacity: 0.8;
        }
        .right-menu-icon {
          font-size: 18px;
          margin-right: 5px;
        }
        .right-menu-text {
          margin-left: 5px;
        }
      }
      .alarm-badge {
        :deep(.el-badge__content) {
          background-color: #f56c6c;
        }
      }
    }
  }
}
.content-container {
  display: flex;
  position: relative;
  margin-top: 60px;
  height: calc(100vh - 60px);
  width: 100%;
  overflow: hidden;
}
.sidebar-container {
  position: relative;
  height: 100%;
  z-index: 900;
  flex-shrink: 0;
  width: $base-sidebar-width;
}
.fixed-header {
  position: fixed;
  top: 60px;
  right: 0;
  z-index: 9;
  width: calc(100% - #{$base-sidebar-width});
  transition: width 0.28s;
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
  padding: 0;
}
.hideSidebar .fixed-header {
@@ -108,4 +330,44 @@
.mobile .fixed-header {
  width: 100%;
}
.main-container {
  flex: 1;
  position: relative;
  height: 100%;
  overflow-y: auto;
  overflow-x: hidden;
  transition: margin-left 0.28s;
  box-sizing: border-box;
}
.hideSidebar .main-container {
  margin-left: 0;
}
.sidebarHide .main-container {
  margin-left: 0;
}
.mobile .main-container {
  margin-left: 0;
}
.hideSidebar .sidebar-container {
  width: 54px;
}
.sidebarHide .sidebar-container {
  display: none;
}
.themeDark {
  .navbar {
    background: #002866 !important;
  }
  .sidebar-container {
    background-color: #002866 !important;
  }
}
</style>
zhitan-vue/src/permission.js
@@ -8,10 +8,90 @@
import useUserStore from '@/store/modules/user'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
import useTagsViewStore from '@/store/modules/tagsView'
NProgress.configure({ showSpinner: false });
const whiteList = ['/login', '/register', '/energy']
/**
 * æŸ¥æ‰¾æœ€æ·±å±‚的子菜单并构建完整路径
 */
function findDeepestPath(route) {
  if (!route) return { path: null, query: null };
  // é¦–先添加当前节点的路径
  let currentNode = route;
  let pathSegments = [];
  if (currentNode.path) {
    pathSegments.push(currentNode.path);
  }
  // é€å±‚添加子路径
  while (currentNode.children && currentNode.children.length > 0) {
    const firstChild = currentNode.children.find(child => !child.hidden);
    if (!firstChild) break;
    // è·³è¿‡ParentView类型的中间节点
    if (firstChild.component === 'ParentView' ||
        (typeof firstChild.component === 'object' &&
         firstChild.component.name === 'ParentView')) {
      currentNode = firstChild;
      // å¦‚果路径不是以/开头,则添加到路径片段中
      if (!firstChild.path.startsWith('/')) {
        pathSegments.push(firstChild.path);
      } else {
        // å¦‚果是绝对路径,则替换之前所有路径
        pathSegments = [firstChild.path];
      }
      continue;
    }
    // æ™®é€šèŠ‚ç‚¹å¤„ç†
    currentNode = firstChild;
    // å¦‚果路径不是以/开头,则添加到路径片段中
    if (!firstChild.path.startsWith('/')) {
      pathSegments.push(firstChild.path);
    } else {
      // å¦‚果是绝对路径,则替换之前所有路径
      pathSegments = [firstChild.path];
    }
    // å¦‚果到达叶子节点,则结束查找
    if (!firstChild.children || firstChild.children.length === 0) {
      break;
    }
  }
  // æž„建最终路径
  let targetPath = '';
  if (pathSegments.length > 0) {
    // å¦‚果第一段不是以/开头,添加/
    if (!pathSegments[0].startsWith('/')) {
      pathSegments[0] = '/' + pathSegments[0];
    }
    // ç»„合路径
    targetPath = pathSegments.reduce((fullPath, segment, index) => {
      if (segment.startsWith('/')) {
        return segment;
      } else if (index === 0) {
        return segment;
      } else {
        // ç¡®ä¿è·¯å¾„之间不会出现重复的斜杠
        const base = fullPath.endsWith('/') ? fullPath.slice(0, -1) : fullPath;
        const part = segment.startsWith('/') ? segment : '/' + segment;
        return `${base}${part}`;
      }
    });
  }
  return {
    path: targetPath,
    query: currentNode.query
  };
}
router.beforeEach((to, from, next) => {
  NProgress.start()
@@ -36,6 +116,40 @@
                router.addRoute(route) // åŠ¨æ€æ·»åŠ å¯è®¿é—®è·¯ç”±è¡¨
              }
            })
            // å¦‚果是首页,自动重定向到第一个菜单
            if (to.path === '/' || to.path === '/index') {
              const permissionStore = usePermissionStore()
              const topMenus = permissionStore.topbarRouters.filter(menu => !menu.hidden)
              if (topMenus.length > 0) {
                // è·³è½¬åˆ°ç¬¬ä¸€ä¸ªèœå•
                const firstMenu = topMenus[0]
                // æŸ¥æ‰¾æœ€æ·±å±‚的子菜单并构建路径
                const { path, query } = findDeepestPath(firstMenu);
                if (path) {
                  // æœ‰æœ€æ·±å±‚子菜单,跳转到该菜单
                  if (query) {
                    next({ path, query, replace: true });
                  } else {
                    next({ path, replace: true });
                  }
                  return;
                } else if (firstMenu.children && firstMenu.children.length > 0) {
                  // ä½¿ç”¨åŽŸæœ‰é€»è¾‘
                  const firstChild = firstMenu.children[0]
                  const childPath = firstMenu.path.endsWith('/') ? firstMenu.path + firstChild.path : `${firstMenu.path}/${firstChild.path}`
                  next({ path: childPath, replace: true })
                  return
                } else {
                  // æ²¡æœ‰å­èœå•,直接跳转
                  next({ path: firstMenu.path, replace: true })
                  return
                }
              }
            }
            next({ ...to, replace: true }) // hack方法 ç¡®ä¿addRoutes已完成
          })
        }).catch(err => {
@@ -45,6 +159,58 @@
          })
        })
      } else {
        // å¦‚果是首页,自动重定向到第一个菜单
        if (to.path === '/' || to.path === '/index') {
          const permissionStore = usePermissionStore()
          const topMenus = permissionStore.topbarRouters.filter(menu => !menu.hidden)
          if (topMenus.length > 0) {
            // è·³è½¬åˆ°ç¬¬ä¸€ä¸ªèœå•
            const firstMenu = topMenus[0]
            // æŸ¥æ‰¾æœ€æ·±å±‚的子菜单并构建路径
            const { path, query } = findDeepestPath(firstMenu);
            if (path) {
              // æœ‰æœ€æ·±å±‚子菜单,跳转到该菜单
              if (query) {
                next({ path, query, replace: true });
              } else {
                next({ path, replace: true });
              }
              return;
            } else if (firstMenu.children && firstMenu.children.length > 0) {
              // ä½¿ç”¨åŽŸæœ‰é€»è¾‘
              const firstChild = firstMenu.children[0]
              const childPath = firstMenu.path.endsWith('/') ? firstMenu.path + firstChild.path : `${firstMenu.path}/${firstChild.path}`
              next({ path: childPath, replace: true })
              return
            } else {
              // æ²¡æœ‰å­èœå•,直接跳转
              next({ path: firstMenu.path, replace: true })
              return
            }
          }
        }
        // è‡ªåŠ¨å¤„ç†å¸¦æœ‰é‡å®šå‘çš„è·¯ç”±
        if (to.matched.length > 0 && to.matched[0].path === to.path) {
          const currentRouteConfig = router.getRoutes().find(r => r.path === to.path);
          if (currentRouteConfig && currentRouteConfig.children && currentRouteConfig.children.length > 0) {
            // æœ‰å­è·¯ç”±ï¼Œè‡ªåŠ¨å¯¼èˆªåˆ°æœ€æ·±å±‚å­èœå•
            const { path, query } = findDeepestPath(currentRouteConfig);
            if (path && path !== to.path) {
              if (query) {
                next({ path, query, replace: true });
              } else {
                next({ path, replace: true });
              }
              return;
            }
          }
        }
        next()
      }
    }
@@ -62,4 +228,12 @@
router.afterEach(() => {
  NProgress.done()
  // ç§»é™¤æ‰€æœ‰å¯èƒ½çš„首页标签
  const tagsViewStore = useTagsViewStore();
  if (tagsViewStore && tagsViewStore.visitedViews) {
    tagsViewStore.visitedViews = tagsViewStore.visitedViews.filter(
      tag => tag.path !== '/index' && tag.path !== '/' && tag.name !== 'Index'
    );
  }
})
zhitan-vue/src/router/index.js
@@ -1,6 +1,7 @@
import { createWebHistory, createRouter } from 'vue-router'
/* Layout */
import Layout from '@/layout'
import useAppStore from '@/store/modules/app'
/**
 * Note: è·¯ç”±é…ç½®é¡¹
@@ -66,7 +67,7 @@
        path: '/index',
        component: () => import('@/views/index'),
        name: 'Index',
        meta: { title: '首页', icon: 'dashboard', affix: true }
        meta: { title: '首页', icon: 'dashboard', affix: true, showSidebar: true, breadcrumb: false }
      }
    ]
  },
zhitan-vue/src/store/modules/app.js
@@ -7,7 +7,7 @@
      sidebar: {
        opened: sessionStorage.getItem('sidebarStatus') ? !!+sessionStorage.getItem('sidebarStatus') : true,
        withoutAnimation: false,
        hide: false
        hide: sessionStorage.getItem('sidebarHide') ? JSON.parse(sessionStorage.getItem('sidebarHide')) : false
      },
      device: 'desktop',
      size: sessionStorage.getItem('size') || 'default'
@@ -39,6 +39,20 @@
      },
      toggleSideBarHide(status) {
        this.sidebar.hide = status
        sessionStorage.setItem('sidebarHide', status)
      },
      openMenu() {
        this.sidebar.hide = false
        this.sidebar.opened = true
        sessionStorage.setItem('sidebarHide', 'false')
        sessionStorage.setItem('sidebarStatus', 1)
      },
      showCollapsedSidebar() {
        this.sidebar.hide = false
        this.sidebar.opened = false
        this.sidebar.withoutAnimation = false
        sessionStorage.setItem('sidebarHide', 'false')
        sessionStorage.setItem('sidebarStatus', 0)
      }
    }
  })
zhitan-vue/src/store/modules/permission.js
@@ -56,8 +56,27 @@
  })
// éåŽ†åŽå°ä¼ æ¥çš„è·¯ç”±å­—ç¬¦ä¸²ï¼Œè½¬æ¢ä¸ºç»„ä»¶å¯¹è±¡
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false, parentRoute = null) {
  return asyncRouterMap.filter(route => {
    // ä¸å†è¿‡æ»¤æŽ‰é¦–页看板相关路由
    /*
    // è¿‡æ»¤æŽ‰é¦–页看板相关路由
    if (route.name === 'Index' && route.meta && route.meta.title === '首页看板') {
      return false;
    }
    // å¦‚果是首页看板的子菜单,也过滤掉
    if (route.path === '/index' || route.path === 'index' ||
        (route.meta && route.meta.title === '首页看板')) {
      return false;
    }
    */
    // è®¾ç½®çˆ¶è·¯ç”±å¼•用
    if (parentRoute) {
      route.parent = parentRoute;
    }
    if (type && route.children) {
      route.children = filterChildren(route.children)
    }
@@ -73,8 +92,17 @@
        route.component = loadView(route.component)
      }
    }
    // å¤„理 query å‚数,将字符串转换为对象
    if (route.query && typeof route.query === 'string') {
      try {
        route.query = JSON.parse(route.query);
      } catch (error) {
        console.error('Error parsing query string:', route.query, error);
      }
    }
    if (route.children != null && route.children && route.children.length) {
      route.children = filterAsyncRouter(route.children, route, type)
      // å°†å½“前路由作为父路由传递给子路由
      route.children = filterAsyncRouter(route.children, route, type, route)
    } else {
      delete route['children']
      delete route['redirect']
@@ -89,7 +117,21 @@
    if (el.children && el.children.length) {
      if (el.component === 'ParentView' && !lastRouter) {
        el.children.forEach(c => {
          c.path = el.path + '/' + c.path
          // è®¾ç½®çˆ¶è·¯ç”±å¼•用
          c.parent = el;
          // ç¡®ä¿è·¯å¾„格式正确拼接
          if (el.path) {
            if (c.path.startsWith('/')) {
              // ç»å¯¹è·¯å¾„保持不变
              // ä½†ä¹Ÿè®¾ç½®åŽŸå§‹çˆ¶è·¯å¾„ç”¨äºŽèœå•å¯¼èˆª
              c.parentPath = el.path;
            } else {
              // ç›¸å¯¹è·¯å¾„需要拼接
              c.path = el.path.endsWith('/') ? el.path + c.path : el.path + '/' + c.path;
            }
          }
          if (c.children && c.children.length) {
            children = children.concat(filterChildren(c.children, c))
            return
@@ -100,7 +142,21 @@
      }
    }
    if (lastRouter) {
      el.path = lastRouter.path + '/' + el.path
      // è®¾ç½®çˆ¶è·¯ç”±å¼•用
      el.parent = lastRouter;
      // ç¡®ä¿è·¯å¾„格式正确拼接
      if (lastRouter.path) {
        if (el.path.startsWith('/')) {
          // ç»å¯¹è·¯å¾„保持不变
          // ä½†ä¹Ÿè®¾ç½®åŽŸå§‹çˆ¶è·¯å¾„ç”¨äºŽèœå•å¯¼èˆª
          el.parentPath = lastRouter.path;
        } else {
          // ç›¸å¯¹è·¯å¾„需要拼接
          el.path = lastRouter.path.endsWith('/') ? lastRouter.path + el.path : lastRouter.path + '/' + el.path;
        }
      }
      if (el.children && el.children.length) {
        children = children.concat(filterChildren(el.children, el))
        return
zhitan-vue/src/store/modules/settings.js
@@ -23,7 +23,7 @@
      // ä¿®æ”¹å¸ƒå±€è®¾ç½®
      changeSetting(data) {
        const { key, value } = data
        if (this.hasOwnProperty(key)) {
        if (key in this.$state) {
          this[key] = value
        }
      },
zhitan-vue/src/store/modules/tagsView.js
@@ -20,6 +20,11 @@
        )
      },
      addVisitedView(view) {
        // è¿‡æ»¤é¦–页标签
        if (view.path === '/index' || view.path === '/' || view.name === 'Index') {
          return;
        }
        if (this.visitedViews.some(v => v.path === view.path)) return
        this.visitedViews.push(
          Object.assign({}, view, {
@@ -110,7 +115,8 @@
      },
      delAllVisitedViews(view) {
        return new Promise(resolve => {
          const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
          // è¿‡æ»¤æŽ‰é¦–页标签,只保留其他固定标签
          const affixTags = this.visitedViews.filter(tag => tag.meta.affix && tag.path !== '/index' && tag.path !== '/' && tag.name !== 'Index')
          this.visitedViews = affixTags
          this.iframeViews = []
          resolve([...this.visitedViews])
zhitan-vue/src/views/businessconfiguration/gatewaystatus/gatewayStatus.vue
@@ -1,5 +1,11 @@
<template>
  <div class="page">
    <div class="page-title">
      <div class="title-bar">
        <span class="title-text">网关状态监测</span>
      </div>
    </div>
    <div class="table-box">
      <div class="border">
        <div class="table" v-for="(item, index) in dataList" :key="index">
@@ -460,14 +466,62 @@
])
</script>
<style lang="scss" scoped>
<style scoped lang="scss">
@import "@/assets/styles/page.scss";
.page {
  background: #08234F;
  min-height: calc(100vh - 145px)
}
.page-title {
  position: relative;
  .title-bar {
    position: relative;
    padding: 14px 0;
    padding-left: 16px;
    .title-text {
      font-size: 18px;
      font-weight: 600;
      position: relative;
      padding-left: 22px;
      color: #fff;
      &::before {
        content: '';
        position: absolute;
        left: 0;
        top: 50%;
        transform: translateY(-50%);
        width: 5px;
        height: 18px;
        background-color: #3883FA;
        border-radius: 2px;
      }
    }
  }
  &::after {
    content: '';
    position: absolute;
    bottom: -4px;
    opacity: 0.12;
    left: 0;
    width: 100%;
    height: 1px;
    background-color: #E6E6E6;
  }
}
.themeDark {
  .border {
    border: 1px solid #fff;
    color: #fff;
    .table {
      margin-top: 10px;
      display: flex;
      align-items: center;
      justify-content: flex-start;
@@ -544,6 +598,10 @@
        }
      }
    }
  }
  .table-box {
    margin: 20px 25px;
  }
}
@@ -630,5 +688,15 @@
      }
    }
  }
  .table-box {
    margin: 10px 25px;
  }
}
.page {
  .table-box {
    // ... ä¿æŒçŽ°æœ‰æ ·å¼
  }
}
</style>
zhitan-vue/src/views/dataMonitoring/historyDataTrend/index.vue
@@ -210,6 +210,10 @@
<style scoped lang="scss">
@import "@/assets/styles/page.scss";
.page {
  background-color: #08234F;
}
.themeDark {
  .card-list {
    width: 100%;
zhitan-vue/src/views/deepanalysis/deepAnalysis.vue
@@ -18,15 +18,23 @@
          </el-select>
        </el-form-item>
        <el-form-item label="时间选择" prop="dataTime">
          <el-date-picker v-if="form.timeType == 'YEAR'" v-model="form.dataTime" type="year" />
          <el-date-picker v-if="form.timeType == 'YEAR'" v-model="form.dataTime" type="year" :clearable="false" />
          <el-date-picker
            v-else-if="form.timeType == 'MONTH'"
            v-model="form.dataTime"
            type="month"
            format="YYYY-MM"
            value-format="YYYY-MM"
            :clearable="false"
          />
          <el-date-picker v-else v-model="form.dataTime" type="date" format="YYYY-MM-DD" value-format="YYYY-MM-DD" />
          <el-date-picker
            v-else
            v-model="form.dataTime"
            :clearable="false"
            type="date"
            format="YYYY-MM-DD"
            value-format="YYYY-MM-DD"
          />
        </el-form-item>
        <el-form-item>
          <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -141,7 +149,8 @@
  getFlowCharts({
    energyType: form.value.energyType,
    nodeId: "",
    queryTime: form.value.dataTime,
    queryTime: proxy.dayjs(new Date(form.value.dataTime)).format("YYYY-MM-DD"),
    dataTime: proxy.dayjs(new Date(form.value.dataTime)).format("YYYY-MM-DD"),
    timeType: form.value.timeType,
    modelCode: proxy.$route.query.modelCode,
  }).then((res) => {
@@ -298,6 +307,7 @@
    display: flex;
    justify-content: space-between;
    padding-top: 12px;
    padding: 12px 14px 0;
    .card-list-item {
      width: 24%;
@@ -365,6 +375,7 @@
    display: flex;
    justify-content: space-between;
    padding-top: 12px;
    padding: 12px 14px 0;
    .card-list-item {
      width: 24%;
zhitan-vue/src/views/energyanalysis/equipment/equipment.vue
@@ -115,9 +115,10 @@
</template>
<script setup name="equipment">
import { listRegion, listDepartment } from "@/api/energyAnalysis/energyAnalysis"
import { listDepartment } from "@/api/energyAnalysis/energyAnalysis"
import { listEnergyTypeList } from "@/api/modelConfiguration/energyType"
import * as echarts from "echarts"
import request from "@/utils/request"
const { proxy } = getCurrentInstance()
const { period } = proxy.useDict("period")
import { useRoute } from "vue-router"
@@ -183,12 +184,16 @@
  // }
  const myChart1 = echarts.init(document.getElementById("Chart1"))
  // const myChart2 = echarts.init(document.getElementById("Chart2"));
  listRegion(
    proxy.addDateRange({
  // ä¿®æ”¹ä¸ºç›´æŽ¥è°ƒç”¨consumptionanalysis/getByArea接口
  request({
    url: "/consumptionanalysis/getByArea",
    method: "get",
    params: proxy.addDateRange({
      ...queryParams.value,
      ...query.value,
    })
  ).then((res) => {
  }).then((res) => {
    if (!!res.code && res.code == 200) {
      loading.value = false
      let xdata = []
@@ -551,7 +556,7 @@
// èƒ½è€—对比分析-设备能耗分析-导出
function handleExport() {
  proxy.download(
    "consumptionanalysis/energyExport",
    "consumptionanalysis/getByArea/export",
    {
      ...queryParams.value,
      ...query.value,
zhitan-vue/src/views/index.vue
@@ -1,22 +1,27 @@
<template>
  <div class="page" style="padding-left: 8px; padding-top: 8px">
    <CardHeader :showBtn="true" :active="'0'" :period="period" @handleClick="handleTimeType">
      <span>
      <span class="card-header-title">
        å…¨åŽ‚èƒ½è€—ç»Ÿè®¡
        <el-button @click="dialogVisible = true" v-if="list.length > 1"> æŸ¥çœ‹æ›´å¤š </el-button>
        <el-button @click="dialogVisible = true" v-if="list.length > 1" type="primary" size="small" class="header-more-btn"> æŸ¥çœ‹æ›´å¤š </el-button>
      </span>
    </CardHeader>
    <template v-for="(row, rowIndex) in list" :key="rowIndex" v-loading="loading02">
      <div class="card-list" v-if="settingsStore.sideTheme == 'theme-dark' && rowIndex == 0">
        <template v-for="(item, index) in row" :key="index">
          <div
            class="card-list-item"
          <div class="card-list-item">
            <div class="item-left">
              <div class="top-icon"
            :style="{
              backgroundImage: 'url(' + bgList[index].bg + ')',
                  backgroundImage: 'url(' + bgList[index].icon + ')',
                  backgroundColor: bgList[index].iconBg,
                  width: '73px',
                  height: '73px',
                  backgroundSize: '40px'
            }"
          >
            <div class="item-top">
              <div class="top-icon" :style="{ backgroundImage: 'url(' + bgList[index].icon + ')' }" />
              />
            </div>
            <div class="item-right">
              <div class="top-right">
                <div class="right-name">
                  {{ item.energyName }}
@@ -24,28 +29,27 @@
                </div>
                <div class="right-value">
                  <span> {{ item.count }}</span>
                  <!-- <span class="unit">{{ item.energyUnit }}</span> -->
                </div>
              </div>
            </div>
            <div class="item-bottom">
              <div class="bottom-left">
                <span>
                  åŒæ¯”: {{ Math.abs(item.tongbi) }}
                  <el-icon :color="item.tongbi > 0 ? 'green' : item.tongbi < 0 ? 'red' : ''">
                    åŒæ¯”: {{ Math.abs(item.tongbi).toFixed(1) }}
                    <el-icon :color="item.tongbi > 0 ? '#4CAF50' : item.tongbi < 0 ? '#F44336' : ''">
                    <Top v-if="item.tongbi > 0" />
                    <Bottom v-if="item.tongbi < 0" />
                  </el-icon>
                </span>
              </div>
              <div class="bottom-right">
                <span
                  >环比: {{ Math.abs(item.huanbi) }}
                  <el-icon :color="item.huanbi > 0 ? 'green' : item.huanbi < 0 ? 'red' : ''">
                  <span>
                    çŽ¯æ¯”: {{ Math.abs(item.huanbi).toFixed(1) }}
                    <el-icon :color="item.huanbi > 0 ? '#4CAF50' : item.huanbi < 0 ? '#F44336' : ''">
                    <Top v-if="item.huanbi > 0" />
                    <Bottom v-if="item.huanbi < 0" />
                  </el-icon>
                </span>
                </div>
              </div>
            </div>
          </div>
@@ -54,36 +58,46 @@
      <div class="card-list" v-if="settingsStore.sideTheme != 'theme-dark' && rowIndex == 0">
        <template v-for="(item, index) in row" :key="index" v-show="rowIndex == 0">
          <div class="card-list-item">
            <div class="item-top">
              <div class="top-icon" :style="{ backgroundImage: 'url(' + bgList[index].icon2 + ')' }" />
            <div class="item-left">
              <div class="top-icon"
                :style="{
                  backgroundImage: 'url(' + bgList[index].icon2 + ')',
                  backgroundColor: bgList[index].iconBg,
                  width: '73px',
                  height: '73px',
                  backgroundSize: '40px'
                }"
              />
            </div>
            <div class="item-right">
              <div class="top-right">
                <div class="right-name">
                  {{ item.energyName }}
                  <span v-if="item.energyUnit" class="unit">({{ item.energyUnit }})</span>
                </div>
                <div class="right-value">
                  <span>{{ item.count }}</span>
                  <span class="unit">{{ item.energyUnit }}</span>
                </div>
              </div>
            </div>
            <div class="item-bottom">
              <div class="bottom-left">
                <span>
                  åŒæ¯”: {{ Math.abs(item.tongbi) }}
                  <el-icon :color="item.tongbi > 0 ? 'green' : item.tongbi < 0 ? 'red' : ''">
                    åŒæ¯”: {{ Math.abs(item.tongbi).toFixed(1) }}
                    <el-icon :color="item.tongbi > 0 ? '#4CAF50' : item.tongbi < 0 ? '#F44336' : ''">
                    <Top v-if="item.tongbi > 0" />
                    <Bottom v-if="item.tongbi < 0" />
                  </el-icon>
                </span>
              </div>
              <div class="bottom-right">
                <span
                  >环比: {{ Math.abs(item.huanbi) }}
                  <el-icon :color="item.huanbi > 0 ? 'green' : item.huanbi < 0 ? 'red' : ''">
                  <span>
                    çŽ¯æ¯”: {{ Math.abs(item.huanbi).toFixed(1) }}
                    <el-icon :color="item.huanbi > 0 ? '#4CAF50' : item.huanbi < 0 ? '#F44336' : ''">
                    <Top v-if="item.huanbi > 0" />
                    <Bottom v-if="item.huanbi < 0" />
                  </el-icon>
                </span>
                </div>
              </div>
            </div>
          </div>
@@ -133,66 +147,71 @@
        </el-col>
      </el-row>
    </div>
    <el-dialog v-model="dialogVisible" title="查看全厂能耗统计" width="80%" v-if="dialogVisible">
    <el-dialog v-model="dialogVisible" title="查看全厂能耗统计" width="90%" v-if="dialogVisible">
      <template v-for="(row, rowIndex) in list" :key="rowIndex">
        <div class="card-list" v-if="settingsStore.sideTheme == 'theme-dark'">
          <template v-for="(item, index) in row" :key="index">
            <div
              class="card-list-item"
              :style="{
                backgroundImage: 'url(' + bgList[index].bg + ')',
              }"
            >
              <div class="item-top">
                <div
                  class="top-icon"
            <div class="card-list-item">
              <div class="item-left">
                <div class="top-icon"
                  :style="{
                    backgroundImage: 'url(' + bgList[index].icon + ')',
                    backgroundColor: bgList[index].iconBg,
                    width: '73px',
                    height: '73px',
                    backgroundSize: '40px'
                  }"
                />
              </div>
              <div class="item-right">
                <div class="top-right">
                  <div class="right-name">
                    {{ item.energyName }}
                    <span v-if="item.energyUnit" class="unit">({{ item.energyUnit }})</span>
                  </div>
                  <div class="right-value">
                    <span> {{ item.count }}</span>
                    <span class="unit">{{ item.energyUnit }}</span>
                  </div>
                </div>
              </div>
              <div class="item-bottom">
                <div class="bottom-left">
                  <span>
                    åŒæ¯”: {{ Math.abs(item.tongbi) }}
                    <el-icon :color="item.tongbi > 0 ? 'green' : item.tongbi < 0 ? 'red' : ''">
                      åŒæ¯”: {{ Math.abs(item.tongbi).toFixed(1) }}
                      <el-icon :color="item.tongbi > 0 ? '#4CAF50' : item.tongbi < 0 ? '#F44336' : ''">
                      <Top v-if="item.tongbi > 0" />
                      <Bottom v-if="item.tongbi < 0" />
                    </el-icon>
                  </span>
                </div>
                <div class="bottom-right">
                  <span
                    >环比: {{ Math.abs(item.huanbi) }}
                    <el-icon :color="item.huanbi > 0 ? 'green' : item.huanbi < 0 ? 'red' : ''">
                    <span>
                      çŽ¯æ¯”: {{ Math.abs(item.huanbi).toFixed(1) }}
                      <el-icon :color="item.huanbi > 0 ? '#4CAF50' : item.huanbi < 0 ? '#F44336' : ''">
                      <Top v-if="item.huanbi > 0" />
                      <Bottom v-if="item.huanbi < 0" />
                    </el-icon>
                  </span>
                  </div>
                </div>
              </div>
            </div>
          </template>
        </div>
        <div class="card-list" v-if="settingsStore.sideTheme != 'theme-dark'">
          <template v-for="(item, index) in row" :key="index" v-show="rowIndex == 0">
          <template v-for="(item, index) in row" :key="index">
            <div class="card-list-item">
              <div class="item-top">
                <div
                  class="top-icon"
              <div class="item-left">
                <div class="top-icon"
                  :style="{
                    backgroundImage: 'url(' + bgList[index].icon2 + ')',
                    backgroundColor: bgList[index].iconBg,
                    width: '73px',
                    height: '73px',
                    backgroundSize: '40px'
                  }"
                />
              </div>
              <div class="item-right">
                <div class="top-right">
                  <div class="right-name">
                    {{ item.energyName }}
@@ -202,21 +221,20 @@
                    <span class="unit">{{ item.energyUnit }}</span>
                  </div>
                </div>
              </div>
              <div class="item-bottom">
                <div class="bottom-left">
                  <span>
                    åŒæ¯”: {{ Math.abs(item.tongbi) }}
                    <el-icon :color="item.tongbi > 0 ? 'green' : item.tongbi < 0 ? 'red' : ''">
                      åŒæ¯”: {{ Math.abs(item.tongbi).toFixed(1) }}
                      <el-icon :color="item.tongbi > 0 ? '#4CAF50' : item.tongbi < 0 ? '#F44336' : ''">
                      <Top v-if="item.tongbi > 0" />
                      <Bottom v-if="item.tongbi < 0" />
                    </el-icon>
                  </span>
                </div>
                <div class="bottom-right">
                  <span
                    >环比: {{ Math.abs(item.huanbi) }}
                    <el-icon :color="item.huanbi > 0 ? 'green' : item.huanbi < 0 ? 'red' : ''">
                    <span>
                      çŽ¯æ¯”: {{ Math.abs(item.huanbi).toFixed(1) }}
                      <el-icon :color="item.huanbi > 0 ? '#4CAF50' : item.huanbi < 0 ? '#F44336' : ''">
                      <Top v-if="item.huanbi > 0" />
                      <Bottom v-if="item.huanbi < 0" />
                    </el-icon>
@@ -224,7 +242,7 @@
                </div>
              </div>
            </div>
            <div class="line"></div>
            </div>
          </template>
        </div>
      </template>
@@ -258,42 +276,47 @@
import index_card_3 from "@/assets/images/home/index-card-3.png"
import index_card_4 from "@/assets/images/home/index-card-4.png"
import index_card_5 from "@/assets/images/home/index-card-5.png"
import card_icon_1 from "@/assets/images/home/card-icon-1.png"
import card_icon_2 from "@/assets/images/home/card-icon-2.png"
import card_icon_3 from "@/assets/images/home/card-icon-3.png"
import card_icon_4 from "@/assets/images/home/card-icon-4.png"
import card_icon_5 from "@/assets/images/home/card-icon-5.png"
import card_icon2_1 from "@/assets/images/home/card-icon2-1.png"
import card_icon2_2 from "@/assets/images/home/card-icon2-2.png"
import card_icon2_3 from "@/assets/images/home/card-icon2-3.png"
import card_icon2_4 from "@/assets/images/home/card-icon2-4.png"
import card_icon2_5 from "@/assets/images/home/card-icon2-5.png"
import card_icon_1 from "@/assets/images/2.png"
import card_icon_2 from "@/assets/images/3.png"
import card_icon_3 from "@/assets/images/5.png"
import card_icon_4 from "@/assets/images/6.png"
import card_icon_5 from "@/assets/images/7.png"
import card_icon2_1 from "@/assets/images/2.png"
import card_icon2_2 from "@/assets/images/3.png"
import card_icon2_3 from "@/assets/images/5.png"
import card_icon2_4 from "@/assets/images/6.png"
import card_icon2_5 from "@/assets/images/7.png"
import { fa } from "element-plus/es/locales.mjs"
const bgList = ref([
  {
    bg: index_card_1,
    icon: card_icon_1,
    icon2: card_icon2_1,
    iconBg: "#3F7EE8"
  },
  {
    bg: index_card_2,
    icon: card_icon_2,
    icon2: card_icon2_2,
    iconBg: "#FFA024"
  },
  {
    bg: index_card_3,
    icon: card_icon_3,
    icon2: card_icon2_3,
    iconBg: "#FFCC00"
  },
  {
    bg: index_card_4,
    icon: card_icon_4,
    icon2: card_icon2_4,
    iconBg: "#3CC8D9"
  },
  {
    bg: index_card_5,
    icon: card_icon_5,
    icon2: card_icon2_5,
    iconBg: "#8833FF"
  },
])
const list = ref([[{}, {}, {}, {}, {}]])
@@ -857,10 +880,24 @@
}
</script>
<style scoped lang="scss">
.card-header-title {
  font-size: 18px;
  font-weight: bold;
  display: flex;
  align-items: center;
  .header-more-btn {
    margin-left: 12px;
    padding: 4px 10px;
    font-size: 12px;
    height: 28px;
  }
}
.themeDark {
  .page {
    padding: 20px;
    background: #120f2e;
    background: #05234A;
    .card-title {
      width: 132px;
@@ -873,54 +910,82 @@
    .card-list {
      margin-top: 14px;
      display: flex;
      // justify-content: space-between;
      width: 100%;
      flex-wrap: wrap;
      flex-wrap: nowrap;
      justify-content: space-between;
      gap: 15px;
      .card-list-item {
        width: 19%;
        margin-right: 1%;
        height: 157px;
        background-size: 100% 100%;
        box-sizing: border-box;
        padding: 25px 18px 12px 16px;
        color: #fff;
        .item-top {
          display: flex;
          .top-icon {
            width: 50px;
            height: 50px;
            background-size: 100% 100%;
      &:after {
        content: "";
        flex: 0 0 0;
          }
      .card-list-item {
        width: 0;
        flex: 1 1 320px;
        max-width: 320px;
        height: 135px;
        background: rgba(242, 246, 250, 0.1);
        box-sizing: border-box;
        padding: 16px;
        color: #fff;
        border-radius: 9px;
        box-shadow: none;
        border: none;
        display: flex;
        flex-direction: row;
        align-items: center;
        &:hover {
          background: rgba(242, 246, 250, 0.15);
        }
        .item-left {
          margin-right: 20px;
          .top-icon {
            width: 73px;
            height: 73px;
            background-size: 40px;
            background-repeat: no-repeat;
            background-position: center;
            border-radius: 50%;
          }
        }
        .item-right {
          flex: 1;
          display: flex;
          flex-direction: column;
          .top-right {
            margin-left: 12px;
            display: flex;
            flex-direction: column;
            .right-name {
              font-weight: bold;
              font-size: 16px;
              font-family: OPPOSans-Bold;
              font-weight: 400;
              font-size: 14px;
              font-family: OPPOSans-Regular;
              color: rgba(255, 255, 255, 0.65);
              letter-spacing: 0.5px;
              margin-bottom: 4px;
              .unit {
                margin-left: 2px;
                font-size: 16px;
                color: rgba(255, 255, 255, 0.65);
                margin-left: 4px;
                font-size: 12px;
                font-weight: normal;
              }
            }
            .right-value {
              font-weight: 800;
              font-size: 25px;
              margin-top: 10px;
              font-weight: 500;
              font-size: 26px;
              margin-top: 6px;
              font-family: OPPOSans-Medium;
              .unit {
                margin-left: 5px;
                font-size: 16px;
                font-weight: normal;
              }
            }
              color: #fff;
              line-height: 1.2;
              letter-spacing: 0.5px;
          }
        }
@@ -929,33 +994,52 @@
          justify-content: space-between;
          margin-top: 18px;
          font-family: OPPOSans, OPPOSans;
          font-weight: bold;
          font-size: 14px;
            font-weight: normal;
            font-size: 12px;
            color: rgba(255, 255, 255, 0.5);
            line-height: 1.2;
            .bottom-left, .bottom-right {
              display: flex;
              align-items: center;
              letter-spacing: 0.3px;
              :deep(.el-icon) {
                margin-left: 6px;
                font-size: 12px;
              }
            }
          }
        }
      }
    }
    .page-main {
      margin-top: 23px;
    }
  }
      margin-top: 20px;
  .chart {
    width: 100%;
    height: 292px;
    margin-top: 10px;
      .el-card {
        background-color: #0E2E5E;
        border: none;
        border-radius: 6px;
        :deep(.el-card__body) {
          padding: 12px;
        }
      }
  }
  .top-header {
    margin-top: 15px;
    height: 23px;
      margin-top: 12px;
      height: 32px;
    font-family: OPPOSans, OPPOSans;
    font-weight: 500;
    font-size: 14px;
    color: rgba(196, 213, 255, 0.6);
    border-bottom: 1px solid rgba(196, 213, 255, 0.6);
      color: rgba(255, 255, 255, 0.8);
      border-bottom: 1px solid rgba(255, 255, 255, 0.2);
    display: flex;
    justify-content: space-between;
      align-items: center;
      padding: 0 10px;
    .header-left {
      display: flex;
@@ -965,72 +1049,128 @@
        margin-right: 7px;
      }
    }
    }
  }
  .chart {
    width: 100%;
    height: 292px;
    margin-top: 10px;
  }
  :deep(.el-button--primary) {
    background-color: #1976D2;
    border-color: #1976D2;
  }
  :deep(.el-card__header) {
    padding: 10px 15px;
    border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  }
  :deep(.el-tabs--card > .el-tabs__header .el-tabs__item) {
    border-color: rgba(255, 255, 255, 0.1);
    background-color: transparent;
    color: #fff;
    &.is-active {
      background-color: #1976D2;
      border-color: #1976D2;
    }
  }
  :deep(.el-tabs--card > .el-tabs__header) {
    border-color: rgba(255, 255, 255, 0.1);
  }
}
.themeLight {
  .page {
    padding: 20px;
    background: #f7f8fa;
    background: #fff;
    .card-title {
      width: 132px;
      height: 29px;
      font-weight: bold;
      font-size: 22px;
      color: #ffffff;
      color: #333;
    }
    .card-list {
      width: 100%;
      margin-top: 14px;
      display: flex;
      // justify-content: space-between;
      align-items: center;
      background-image: url("@/assets/images/home/index-card-bg2.png");
      background-size: 100% 100%;
      flex-wrap: wrap;
      border-radius: 20px;
      width: 100%;
      flex-wrap: nowrap;
      justify-content: space-between;
      gap: 15px;
      &:after {
        content: "";
        flex: 0 0 0;
      }
      .card-list-item {
        width: 19%;
        margin-right: 0.5%;
        height: 157px;
        background-size: 100% 100%;
        width: 0;
        flex: 1 1 320px;
        max-width: 320px;
        height: 135px;
        background: #fff;
        box-sizing: border-box;
        padding: 25px 18px 12px 16px;
        color: #fff;
        .item-top {
        padding: 16px;
        color: #333;
        border-radius: 9px;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
          display: flex;
        flex-direction: row;
        align-items: center;
        .item-left {
          margin-right: 20px;
          .top-icon {
            width: 69px;
            height: 69px;
            background-size: 100% 100%;
            width: 73px;
            height: 73px;
            background-size: 40px;
            background-repeat: no-repeat;
            background-position: center;
            border-radius: 50%;
          }
        }
        .item-right {
          flex: 1;
          display: flex;
          flex-direction: column;
          .top-right {
            margin-left: 16px;
            display: flex;
            flex-direction: column;
            .right-name {
              font-weight: bold;
              font-size: 16px;
              font-family: OPPOSans-Bold;
            }
            .right-value {
              font-weight: 800;
              font-size: 30px;
              margin-top: 10px;
              font-family: OPPOSans-Medium;
              font-weight: 400;
              font-size: 14px;
              font-family: OPPOSans-Regular;
              color: rgba(0, 0, 0, 0.65);
              letter-spacing: 0.5px;
              margin-bottom: 4px;
              .unit {
                margin-left: 5px;
                font-size: 16px;
                color: rgba(0, 0, 0, 0.65);
                margin-left: 4px;
                font-size: 12px;
                font-weight: normal;
              }
            }
            .right-value {
              font-weight: 500;
              font-size: 26px;
              margin-top: 6px;
              font-family: OPPOSans-Medium;
              color: #333;
              line-height: 1.2;
              letter-spacing: 0.5px;
          }
        }
@@ -1039,40 +1179,53 @@
          justify-content: space-between;
          margin-top: 18px;
          font-family: OPPOSans, OPPOSans;
          font-weight: bold;
          font-size: 14px;
        }
      }
            font-weight: normal;
            font-size: 12px;
            color: rgba(0, 0, 0, 0.5);
            line-height: 1.2;
      .line {
        width: 1px;
        height: 64px;
        background-image: url("@/assets/images/home/line@2x.png");
        background-size: 100% 100%;
            .bottom-left, .bottom-right {
              display: flex;
              align-items: center;
              letter-spacing: 0.3px;
              :deep(.el-icon) {
                margin-left: 6px;
                font-size: 12px;
              }
            }
          }
        }
      }
    }
    .page-main {
      margin-top: 23px;
    }
  }
      margin-top: 20px;
  .chart {
    width: 100%;
    height: 292px;
    margin-top: 10px;
      .el-card {
        background-color: #fff;
        border: none;
        border-radius: 6px;
        box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.05);
        :deep(.el-card__body) {
          padding: 12px;
        }
      }
  }
  .top-header {
    margin-top: 15px;
    height: 23px;
      margin-top: 12px;
      height: 32px;
    font-family: OPPOSans, OPPOSans;
    font-weight: 500;
    font-size: 14px;
    color: rgba(29, 29, 29, 0.6);
    border-bottom: 1px solid rgba(196, 213, 255, 0.6);
      color: rgba(0, 0, 0, 0.6);
      border-bottom: 1px solid rgba(0, 0, 0, 0.1);
    display: flex;
    justify-content: space-between;
      align-items: center;
      padding: 0 10px;
    .header-left {
      display: flex;
@@ -1084,4 +1237,35 @@
    }
  }
}
  .chart {
    width: 100%;
    height: 292px;
    margin-top: 10px;
  }
  :deep(.el-button--primary) {
    background-color: #1976D2;
    border-color: #1976D2;
  }
  :deep(.el-card__header) {
    padding: 10px 15px;
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
  }
  :deep(.el-tabs--card > .el-tabs__header .el-tabs__item) {
    border-color: rgba(0, 0, 0, 0.1);
    &.is-active {
      background-color: #1976D2;
      border-color: #1976D2;
      color: #fff;
    }
  }
  :deep(.el-tabs--card > .el-tabs__header) {
    border-color: rgba(0, 0, 0, 0.1);
  }
}
</style>
zhitan-vue/src/views/login.vue
@@ -201,14 +201,15 @@
.login {
  display: flex;
  align-items: center;
  height: 100%;
  background-image: url("@/assets/images/login-background.jpg");
  height: 100vh;
  background-image: url("@/assets/images/login-background.png");
  background-repeat: no-repeat;
  background-size: cover;
  flex-direction: column;
  position: relative;
  min-width: 700px;
  min-height: 700px;
  background-color:#001146
}
.middle-view {
zhitan-vue/src/views/realtimemonitor/gatewaystatus/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,383 @@
<template>
  <div class="page">
    <!-- æ·»åŠ æ ‡é¢˜æ  -->
    <div class="page-title">
      <div class="title-bar">
        <span class="title-text">网关状态监测</span>
      </div>
    </div>
    <div class="table-box">
      <div class="border">
        <div class="table" v-for="(item, index) in dataList" :key="index">
          <div class="num">
            <div class="li name">{{ item.name }}</div>
            <div class="firstLi">
              <div :class="item.list.length < 16 ? 'li hasRightLine' : 'li'" v-for="(i, inde) in item.list" :key="inde">
                <div class="title_num" v-if="i.title_num">{{ i.title_num }}</div>
                <div class="dot" v-if="i.dot && i.dot.length > 0">
                  <div class="dot_li" v-for="(j, ind) in 3" :key="ind"></div>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>
<script setup>
let dataList = ref([
  {
    name: "网关",
    list: [
      {
        id: 1,
        title_num: "数据库",
        dot: [],
      },
      {
        id: 2,
        title_num: "200311",
        dot: [],
      },
      {
        id: 3,
        title_num: "200311",
        dot: [],
      },
      {
        id: 4,
        title_num: "200311",
        dot: [],
      },
      {
        id: 5,
        title_num: "200311",
        dot: [],
      },
      {
        id: 6,
        title_num: "200311",
        dot: [],
      },
      {
        id: 7,
        title_num: "200311",
        dot: [],
      },
      {
        id: 8,
        title_num: "200311",
        dot: [],
      },
      {
        id: 9,
        title_num: "200311",
        dot: [],
      },
      {
        id: 10,
        title_num: "200311",
        dot: [],
      },
      {
        id: 11,
        title_num: "200311",
        dot: [],
      },
      {
        id: 12,
        title_num: "200311",
        dot: [],
      },
      {
        id: 13,
        title_num: "200311",
        dot: [],
      },
      {
        id: 14,
        title_num: "200461",
        dot: [],
      },
      {
        id: 15,
        title_num: "200475",
        dot: [],
      },
    ],
  },
  {
    name: "计算器具",
    list: [
      {
        id: 1,
        title_num: "",
        dot: [
          {
            id: "001",
            dot_li: "",
          },
          {
            id: "002",
            dot_li: "",
          },
          {
            id: "003",
            dot_li: "",
          },
        ],
      },
      {
        id: 2,
        title_num: "",
        dot: [
          {
            id: "001",
            dot_li: "",
          },
          {
            id: "002",
            dot_li: "",
          },
        ],
      },
      {
        id: 3,
        title_num: "",
        dot: [
          {
            id: "001",
            dot_li: "",
          },
          {
            id: "002",
            dot_li: "",
          },
          {
            id: "003",
            dot_li: "",
          },
        ],
      },
    ],
  },
  {
    name: "网关",
    list: [
      {
        id: 1,
        title_num: "数据库",
        dot: [],
      },
      {
        id: 2,
        title_num: "200311",
        dot: [],
      },
      {
        id: 3,
        title_num: "200311",
        dot: [],
      },
      {
        id: 4,
        title_num: "200311",
        dot: [],
      },
    ],
  }
])
</script>
<style scoped lang="scss">
@import "@/assets/styles/page.scss";
.page {
  background: #08234F;
}
// æ ‡é¢˜æ ·å¼
.page-title {
  margin: 0 16px 16px;
  position: relative;
  .title-bar {
    position: relative;
    padding: 14px 0;
    padding-left: 16px;
    .title-text {
      font-size: 18px;
      font-weight: 600;
      position: relative;
      padding-left: 12px;
      color: #fff;
      &::before {
        content: '';
        position: absolute;
        left: 0;
        top: 50%;
        transform: translateY(-50%);
        width: 5px;
        height: 18px;
        background-color: #3883FA;
        border-radius: 2px;
      }
    }
  }
  &::after {
    content: '';
    position: absolute;
    bottom: -10px;
    left: 0;
    width: 100%;
    height: 1px;
    background-color: #E6E6E6;
  }
}
.themeDark {
  .table-box {
    margin: 10px 25px;
    .border {
      border: 1px solid #22408c;
      border-radius: 8px;
      background: #1a235d;
      padding: 10px 0;
      .table {
        margin: 15px;
        // border: 1px solid #22408c;
        .num {
          height: 56px;
          display: flex;
        }
        .li {
          height: 56px;
          padding: 0 10px;
          min-width: 60px;
          display: flex;
          flex-direction: column;
          justify-content: space-evenly;
          align-items: center;
          border-top: 1px solid #22408c;
          border-left: 1px solid #22408c;
          border-bottom: 1px solid #22408c;
          position: relative;
        }
        .hasRightLine {
          border-right: 1px solid #22408c;
        }
        .name {
          width: 80px;
          background: #1a235d;
          color: rgba(255, 255, 255, 0.8);
          text-align: center;
          font-family: OPPOSans, OPPOSans;
          font-weight: 500;
          font-size: 16px;
        }
        .title_num {
          color: #eeeeee;
          font-family: OPPOSans, OPPOSans;
          font-weight: 500;
          font-size: 16px;
        }
        .dot {
          display: flex;
          flex-direction: column;
          // justify-content: space-evenly;
          height: 30px;
          width: 12px;
          align-items: center;
        }
        .dot_li {
          height: 8px;
          width: 8px;
          margin: 2px 0;
          background: red;
          border-radius: 50%;
        }
        .firstLi {
          display: flex;
          flex: 1;
        }
      }
    }
  }
}
.themeLight {
  .table-box {
    margin: 10px 25px;
    .border {
      border: 1px solid #ebebeb;
      border-radius: 8px;
      background: #fff;
      padding: 10px 0;
      .table {
        margin: 15px;
        // border: 1px solid #22408c;
        .num {
          height: 56px;
          display: flex;
        }
        .li {
          height: 56px;
          padding: 0 10px;
          min-width: 60px;
          display: flex;
          flex-direction: column;
          justify-content: space-evenly;
          align-items: center;
          border-top: 1px solid #ebebeb;
          border-left: 1px solid #ebebeb;
          border-bottom: 1px solid #ebebeb;
          position: relative;
        }
        .hasRightLine {
          border-right: 1px solid #ebebeb;
        }
        .name {
          width: 80px;
          background: #fff;
          color: #0d0d0d;
          text-align: center;
          font-family: OPPOSans, OPPOSans;
          font-weight: 500;
          font-size: 16px;
        }
        .title_num {
          color: #0d0d0d;
          font-family: OPPOSans, OPPOSans;
          font-weight: 500;
          font-size: 16px;
        }
        .dot {
          display: flex;
          flex-direction: column;
          // justify-content: space-evenly;
          height: 30px;
          width: 12px;
          align-items: center;
        }
        .dot_li {
          height: 8px;
          width: 8px;
          margin: 2px 0;
          background: red;
          border-radius: 50%;
        }
        .firstLi {
          display: flex;
          flex: 1;
        }
      }
    }
  }
}
</style>
zhitan-vue/src/views/realtimemonitor/realtimemonitor/realtimemonitor.vue
@@ -2,7 +2,7 @@
  <div class="page">
    <div class="page-container">
      <div class="page-container-left">
        <LeftTree ref="leftTreeRef" @handleNodeClick="handleNodeClick" />
        <LeftTree ref="leftTreeRef" @handleNodeClick="handleNodeClick" ParentModelCode="YSCJMX" />
      </div>
      <div class="page-container-right">
        <div class="form-card">
@@ -207,8 +207,8 @@
      text-align: left;
      font-weight: bold;
      font-family: OPPOSans, OPPOSans;
      font-weight: 500;
      font-size: 16px;
      font-size: 14px;
      font-style: normal;
      text-transform: none;
    }
@@ -233,8 +233,8 @@
      .title {
        color: rgba(255, 255, 255, 0.8);
        font-family: OPPOSans, OPPOSans;
        font-weight: 500;
        font-size: 16px;
        font-size: 14px;
        line-height: 19px;
        text-align: left;
        font-style: normal;
@@ -242,7 +242,7 @@
      }
      .num {
        font-size: 24px;
        font-size: 26px;
        color: #36d3ff;
        font-family: OPPOSans, OPPOSans;
        font-weight: 800;
@@ -292,7 +292,7 @@
      text-align: center;
      margin: 5px 8px;
      border-radius: 8px;
      padding: 7px 10px;
      padding: 2px 6px;
      font-family: OPPOSans, OPPOSans;
      font-weight: 500;
      font-size: 16px;
@@ -349,7 +349,7 @@
      }
      .num {
        font-size: 24px;
        font-size: 26px;
        color: #3271eb;
        font-family: OPPOSans, OPPOSans;
        font-weight: 800;
@@ -399,7 +399,7 @@
      text-align: center;
      margin: 5px 8px;
      border-radius: 8px;
      padding: 7px 3px;
      padding: 2px 6px;
      font-family: OPPOSans, OPPOSans;
      font-weight: 500;
      font-size: 16px;
@@ -431,7 +431,7 @@
  text-align: center;
  margin: 2px 6px;
  border-radius: 8px;
  padding: 5px 10px;
  padding: 2px 6px;
  font-family: OPPOSans, OPPOSans;
  font-weight: 500;
  font-size: 14px;
zhitan-vue/src/views/register.vue
@@ -160,7 +160,7 @@
  justify-content: center;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background.jpg");
  background-image: url("../assets/images/login-background.png");
  background-size: cover;
}
.title {
zhitan-vue/src/views/system/name/name.vue
@@ -1,30 +1,30 @@
<template>
  <div class="app-container page">
    <div class="form-card">
      <el-form :model="form" label-width="80px">
        <el-row class="mb20 mt20">
          <el-col :offset="1" :span="18">
  <div class="app-container page name-settings-page">
    <div class="name-settings-card">
      <el-form :model="form" label-width="120px">
        <el-row class="form-row">
          <el-col :span="18">
            <el-form-item label="系统名称" prop="systemName">
              <el-input v-model="form.systemName" placeholder="请输入系统名称" maxlength="64" show-word-limit />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row class="mb20 mt20">
          <el-col :offset="1" :span="18">
        <el-row class="form-row">
          <el-col :span="18">
            <el-form-item label="登录logo" prop="homeLogo">
              <ImageUpload v-model="form.homeLogo" :fileType="['png']" :limit="1" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row class="mb20 mt20">
          <el-col :offset="1" :span="18">
        <el-row class="form-row">
          <el-col :span="18">
            <el-form-item label="后台logo" prop="leftLogo">
              <ImageUpload v-model="form.leftLogo" :fileType="['png']" :limit="1" />
            </el-form-item>
          </el-col>
        </el-row>
        <el-row class="mb20 mt20">
          <el-col :offset="1" :span="18">
        <el-row class="form-row">
          <el-col :span="18">
            <el-form-item label="copyRight" prop="copyRight">
              <el-input
                v-model="form.copyRight"
@@ -37,9 +37,11 @@
            </el-form-item>
          </el-col>
        </el-row>
        <el-row class="mb20 mt20" style="padding-bottom: 20px">
          <el-col :offset="2" :span="18">
        <el-row class="form-row form-footer">
          <el-col :span="18">
            <el-form-item>
            <el-button type="primary" @click="handleSave">保存</el-button>
            </el-form-item>
          </el-col>
        </el-row>
      </el-form>
@@ -68,6 +70,11 @@
}
getSystemName()
function handleSave() {
  if (!form.value.leftLogo || !form.value.homeLogo) {
    proxy.$modal.msgError("请上传logo")
    return
  }
  if (!form.value.leftLogo.includes(baseUrl)) {
    form.value.leftLogo = baseUrl + form.value.leftLogo
  }
@@ -87,7 +94,51 @@
<style lang="scss" scoped>
@import "@/assets/styles/page.scss";
:deep .avatar-uploader .el-upload {
.name-settings-page {
  height: 100%;
  min-height: calc(100vh - 60px);
  display: flex;
  flex-direction: column;
}
.name-settings-card {
  border-radius: 4px;
  margin: 16px;
  padding: 24px;
  flex: 1;
  height: calc(100vh - 100px);
  overflow-y: auto;
  .el-form {
    width: 100%;
    .form-row {
      margin-bottom: 24px;
      &.form-footer {
        margin-top: 40px;
      }
    }
    .el-form-item {
      margin-bottom: 0;
      .el-form-item__label {
        color: var(--el-text-color-primary, #fff);
      }
      .el-form-item__content {
        .el-button {
          padding: 10px 24px;
        }
      }
    }
  }
}
:deep(.avatar-uploader .el-upload) {
  border: 1px dashed var(--el-border-color);
  border-radius: 6px;
  cursor: pointer;
@@ -96,11 +147,11 @@
  transition: var(--el-transition-duration-fast);
}
:deep .avatar-uploader .el-upload:hover {
:deep(.avatar-uploader .el-upload:hover) {
  border-color: var(--el-color-primary);
}
:deep .el-icon.avatar-uploader-icon {
:deep(.el-icon.avatar-uploader-icon) {
  font-size: 28px;
  color: #8c939d;
  width: 178px;
zhitan-vue/src/views/system/user/profile/index.vue
@@ -1,5 +1,5 @@
<template>
  <div class="app-container">
  <div class="app-container user-profile-container">
    <el-row :gutter="20">
      <el-col :span="6" :xs="24">
        <el-card class="box-card">
zhitan-vue/vite.config.js
@@ -33,9 +33,14 @@
      proxy: {
        // https://cn.vitejs.dev/config/#server-proxy
        "/dev-api": {
          target: "http://localhost",
          target: "https://demo-ems.zhitancloud.com",
          changeOrigin: true,
          rewrite: (p) => p.replace(/^\/dev-api/, ""),
          rewrite: (p) => p.replace(/^\/dev-api/, "/prod-api"),
        },
        "/prod-api": {
          target: "https://demo-ems.zhitancloud.com",
          changeOrigin: true,
          secure: true,
        },
      },
    },