From 686fe55892de7bf8d206cddbead77a5fbdb0e091 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期日, 08 三月 2026 19:59:29 +0800
Subject: [PATCH] #

---
 src/main/webapp/views/index.html |  247 +++++++++++++++++++++++++++++++++++++++----------
 1 files changed, 196 insertions(+), 51 deletions(-)

diff --git a/src/main/webapp/views/index.html b/src/main/webapp/views/index.html
index 8cff576..37d2110 100644
--- a/src/main/webapp/views/index.html
+++ b/src/main/webapp/views/index.html
@@ -228,6 +228,44 @@
       color: rgba(255, 255, 255, 0.58);
     }
 
+    .aside-loading {
+      padding: 4px 12px 12px;
+      box-sizing: border-box;
+    }
+
+    .aside-skeleton-item {
+      position: relative;
+      height: 46px;
+      margin: 0 10px 6px;
+      overflow: hidden;
+      border-radius: 10px;
+      background: rgba(255, 255, 255, 0.08);
+    }
+
+    .aside-skeleton-item::after {
+      content: "";
+      position: absolute;
+      inset: 0;
+      transform: translateX(-100%);
+      background: linear-gradient(90deg,
+      rgba(255, 255, 255, 0) 0%,
+      rgba(255, 255, 255, 0.16) 48%,
+      rgba(255, 255, 255, 0) 100%);
+      animation: asideSkeletonShimmer 1.25s ease-in-out infinite;
+    }
+
+    .aside-skeleton-item:nth-child(3n) {
+      width: calc(100% - 28px);
+    }
+
+    .aside-skeleton-item:nth-child(4n + 2) {
+      width: calc(100% - 44px);
+    }
+
+    .aside-skeleton-item:nth-child(5n) {
+      width: calc(100% - 36px);
+    }
+
     .aside-footer {
       padding: 14px 16px 16px;
       border-top: 1px solid rgba(255, 255, 255, 0.08);
@@ -450,6 +488,12 @@
       }
     }
 
+    @keyframes asideSkeletonShimmer {
+      100% {
+        transform: translateX(100%);
+      }
+    }
+
     .ai-drawer-layer {
       box-shadow: -8px 0 24px rgba(0, 0, 0, 0.15) !important;
       border-radius: 8px 0 0 8px !important;
@@ -500,42 +544,52 @@
         </el-input>
       </div>
 
-      <el-scrollbar class="aside-scroll" v-loading="menuLoading">
-        <el-menu
-            ref="sideMenu"
-            class="side-menu"
-            :default-active="activeMenuKey"
-            :collapse="isCollapse"
-            :collapse-transition="false"
-            :default-openeds="defaultOpeneds"
-            unique-opened
-            background-color="transparent"
-            text-color="#c6d1df"
-            active-text-color="#ffffff">
-          <el-submenu
-              v-for="group in filteredMenus"
-              :key="'group-' + group.menuId"
-              :index="'group-' + group.menuId">
-            <template slot="title">
-              <i :class="resolveMenuIcon(group.menuCode)"></i>
-              <span>{{ group.menu }}</span>
-            </template>
-            <el-menu-item
-                v-for="item in group.subMenu"
-                :key="item.tabKey"
-                :index="item.tabKey"
-                @click="handleMenuSelect(group, item)">
-              {{ item.name }}
-            </el-menu-item>
-          </el-submenu>
-        </el-menu>
-
-        <div class="aside-empty" v-if="!menuLoading && filteredMenus.length === 0">
-          <el-empty
-              :image-size="80"
-              :description="menuKeyword ? '娌℃湁鍖归厤鑿滃崟' : '褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟'">
-          </el-empty>
+      <el-scrollbar class="aside-scroll">
+        <div v-if="menuLoading" class="aside-loading" aria-hidden="true">
+          <div
+              v-for="n in 8"
+              :key="'menu-skeleton-' + n"
+              class="aside-skeleton-item">
+          </div>
         </div>
+
+        <template v-else>
+          <el-menu
+              ref="sideMenu"
+              class="side-menu"
+              :default-active="activeMenuKey"
+              :collapse="isCollapse"
+              :collapse-transition="false"
+              :default-openeds="defaultOpeneds"
+              unique-opened
+              background-color="transparent"
+              text-color="#c6d1df"
+              active-text-color="#ffffff">
+            <el-submenu
+                v-for="group in filteredMenus"
+                :key="'group-' + group.menuId"
+                :index="'group-' + group.menuId">
+              <template slot="title">
+                <i :class="resolveMenuIcon(group.menuCode)"></i>
+                <span>{{ group.menu }}</span>
+              </template>
+              <el-menu-item
+                  v-for="item in group.subMenu"
+                  :key="item.tabKey"
+                  :index="item.tabKey"
+                  @click="handleMenuSelect(group, item)">
+                {{ item.name }}
+              </el-menu-item>
+            </el-submenu>
+          </el-menu>
+
+          <div class="aside-empty" v-if="filteredMenus.length === 0">
+            <el-empty
+                :image-size="80"
+                :description="menuKeyword ? '娌℃湁鍖归厤鑿滃崟' : '褰撳墠璐﹀彿娌℃湁鍙敤鑿滃崟'">
+            </el-empty>
+          </div>
+        </template>
       </el-scrollbar>
 
       <div class="aside-footer" v-show="!isCollapse">
@@ -568,8 +622,12 @@
         </div>
 
         <div class="header-right">
-          <el-tag v-if="licenseVisible" size="mini" type="warning" effect="dark">
-            璁稿彲璇� {{ licenseDays }} 澶╂父鏁�
+          <el-tag
+              v-if="licenseDisplayVisible"
+              size="mini"
+              :type="licenseTagType"
+              effect="dark">
+            涓存椂璁稿彲璇佹湁鏁堟湡锛歿{ licenseDays }}澶�
           </el-tag>
           <el-tag
               v-if="fakeVisible"
@@ -602,7 +660,6 @@
             class="page-tabs"
             v-model="activeTab"
             type="card"
-            @tab-click="handleTabClick"
             @tab-remove="removeTab">
           <el-tab-pane
               v-for="tab in tabs"
@@ -691,7 +748,7 @@
     data: function () {
       return {
         isCollapse: false,
-        menuLoading: false,
+        menuLoading: true,
         pageLoading: true,
         loadingText: "姝e湪鍔犺浇椤甸潰...",
         menuKeyword: "",
@@ -708,6 +765,8 @@
         fakeVisible: false,
         fakeRunning: false,
         fakeStatusInterval: null,
+        menuSyncVersion: 0,
+        menuSyncTimer: null,
         userName: localStorage.getItem(USER_STORAGE_KEY) || "绠$悊鍛�",
         aiLayerIndex: null,
         aiTipIndex: null
@@ -789,6 +848,18 @@
       licenseVisible: function () {
         return this.licenseDays !== null && this.licenseDays <= 30;
       },
+      licenseDisplayVisible: function () {
+        return this.licenseVisible && this.licenseDays >= 0;
+      },
+      licenseTagType: function () {
+        if (this.licenseDays !== null && this.licenseDays <= 15) {
+          return "danger";
+        }
+        if (this.licenseDays !== null && this.licenseDays <= 30) {
+          return "warning";
+        }
+        return "info";
+      },
       userShortName: function () {
         return (this.userName || "绠$悊鍛�").substring(0, 1);
       }
@@ -796,7 +867,7 @@
     watch: {
       activeTab: function () {
         var tab = this.getTabByName(this.activeTab);
-        this.activeMenuKey = tab ? (tab.menuKey || this.findMenuKeyByUrl(tab.url)) : "";
+        this.syncMenuStateByUrl(tab ? tab.url : HOME_TAB_CONFIG.url);
         this.pageLoading = !!(tab && !tab.loaded);
         if (this.pageLoading) {
           this.loadingText = "姝e湪鍔犺浇 鈥�" + tab.title + "鈥� ...";
@@ -831,6 +902,10 @@
       if (this.userSyncTimer) {
         clearInterval(this.userSyncTimer);
         this.userSyncTimer = null;
+      }
+      if (this.menuSyncTimer) {
+        clearTimeout(this.menuSyncTimer);
+        this.menuSyncTimer = null;
       }
       if (this.aiTipIndex) {
         layer.close(this.aiTipIndex);
@@ -945,6 +1020,9 @@
       hasTab: function (name) {
         return !!this.getTabByName(name);
       },
+      isHomeTabUrl: function (url) {
+        return this.resolveViewSrc(url || HOME_TAB_CONFIG.url) === this.resolveViewSrc(HOME_TAB_CONFIG.url);
+      },
       getTabByName: function (name) {
         var i;
         for (i = 0; i < this.tabs.length; i++) {
@@ -979,7 +1057,7 @@
         this.loadingText = "姝e湪鍔犺浇 鈥�" + tab.title + "鈥� ...";
         this.pageLoading = !tab.loaded;
         this.activeTab = tab.name;
-        this.activeMenuKey = tab.menuKey || this.findMenuKeyByUrl(tab.url);
+        this.syncMenuStateByUrl(tab.url);
       },
       openHomeTab: function () {
         this.addOrActivateTab(HOME_TAB_CONFIG);
@@ -990,7 +1068,7 @@
       closeAllTabs: function () {
         this.tabs = [this.createTab(HOME_TAB_CONFIG)];
         this.activeTab = HOME_TAB_CONFIG.url;
-        this.activeMenuKey = "";
+        this.syncMenuStateByUrl(HOME_TAB_CONFIG.url);
         this.pageLoading = true;
         this.loadingText = "姝e湪鍔犺浇 鈥滄帶鍒朵腑蹇冣�� ...";
         this.persistTabs();
@@ -1032,9 +1110,6 @@
 
         this.activeTab = nextTabName;
         this.persistTabs();
-      },
-      handleTabClick: function () {
-        this.activeMenuKey = this.findMenuKeyByUrl(this.activeTabUrl);
       },
       handleFrameLoad: function (name) {
         var tab = this.getTabByName(name);
@@ -1083,6 +1158,24 @@
             item = group.subMenu[j];
             if (item.url === normalized) {
               return item.tabKey;
+            }
+          }
+        }
+        return "";
+      },
+      findMenuGroupIndexByUrl: function (url) {
+        var normalized = this.resolveViewSrc(url);
+        var i;
+        var j;
+        var group;
+        var item;
+
+        for (i = 0; i < this.menus.length; i++) {
+          group = this.menus[i];
+          for (j = 0; j < group.subMenu.length; j++) {
+            item = group.subMenu[j];
+            if (item.url === normalized) {
+              return "group-" + group.menuId;
             }
           }
         }
@@ -1172,6 +1265,62 @@
           menu.close(openedMenus[i]);
         }
       },
+      syncMenuStateByUrl: function (url) {
+        var targetUrl = this.resolveViewSrc(url || HOME_TAB_CONFIG.url);
+        var activeMenuKey = "";
+        var groupIndex = "";
+        var that = this;
+        var syncVersion;
+        var applyMenuState;
+
+        if (!this.isHomeTabUrl(targetUrl)) {
+          activeMenuKey = this.findMenuKeyByUrl(targetUrl);
+          if (activeMenuKey) {
+            groupIndex = this.findMenuGroupIndexByUrl(targetUrl);
+          }
+        }
+
+        this.activeMenuKey = activeMenuKey;
+        this.defaultOpeneds = groupIndex ? [groupIndex] : [];
+        this.menuSyncVersion += 1;
+        syncVersion = this.menuSyncVersion;
+        applyMenuState = function () {
+          var menu = that.$refs.sideMenu;
+          var openedMenus;
+          if (syncVersion !== that.menuSyncVersion) {
+            return;
+          }
+          if (!menu) {
+            return;
+          }
+          openedMenus = menu.openedMenus ? menu.openedMenus.slice() : [];
+          if (!groupIndex) {
+            if (openedMenus.length) {
+              that.collapseAllMenus();
+            }
+            return;
+          }
+          if (openedMenus.indexOf(groupIndex) > -1) {
+            return;
+          }
+          if (openedMenus.length) {
+            that.collapseAllMenus();
+          }
+          menu.open(groupIndex);
+        };
+
+        this.$nextTick(function () {
+          applyMenuState();
+          if (that.menuSyncTimer) {
+            clearTimeout(that.menuSyncTimer);
+            that.menuSyncTimer = null;
+          }
+          that.menuSyncTimer = setTimeout(function () {
+            applyMenuState();
+            that.menuSyncTimer = null;
+          }, 160);
+        });
+      },
       loadMenu: function () {
         var that = this;
         this.menuLoading = true;
@@ -1183,11 +1332,7 @@
             that.menuLoading = false;
             if (res.code === 200) {
               that.menus = that.normalizeMenuData(res.data || []);
-              that.defaultOpeneds = [];
-              that.activeMenuKey = that.findMenuKeyByUrl(that.activeTabUrl);
-              that.$nextTick(function () {
-                that.collapseAllMenus();
-              });
+              that.syncMenuStateByUrl(that.activeTabUrl);
             } else if (res.code === 403) {
               top.location.href = baseUrl + "/login";
             } else {
@@ -1432,7 +1577,7 @@
         };
         window.admin.changeTheme = window.admin.changeTheme || function () {};
         window.admin.activeNav = function (url) {
-          that.activeMenuKey = that.findMenuKeyByUrl(that.resolveViewSrc(url));
+          that.syncMenuStateByUrl(that.resolveViewSrc(url));
         };
 
         window.index = window.index || {};

--
Gitblit v1.9.1