瀏覽代碼

状态监控左侧菜单:加按路口名过滤功能,命中分支自动展开

    - StatusMonitoring 总览 / 路口 两个 tab 顶部加输入框,data 加
      menuQueries.{overview,crossing} + menuSearchComposing;computed
      filteredOverviewMenu / filteredCrossingMenu;methods 加 pruneMenu
    - pruneMenu 递归 walk 菜单树:叶子按 label.includes(q) 命中,命中节点
      的整条祖先链保留,中间节点附 forceExpand:true;空 query 时直接返回
      原数组引用避免无意义 re-render;composing 期间不过滤防抖动
    - MenuItem 加 isExpanded computed(node.forceExpand || isOpen),
      v-show 与箭头 is-open 改读 isExpanded;不修改 isOpen,退出搜索后
      用户原本展开/折叠状态自动恢复
    - 选中行为零接线改动:搜索结果点击仍走原 handleMenuClick /
      handleFolderClick,与从树点击业务流完全等价
画安 1 月之前
父節點
當前提交
5c734dc0ca
共有 2 個文件被更改,包括 119 次插入7 次删除
  1. 11 5
      src/components/ui/MenuItem.vue
  2. 108 2
      src/views/StatusMonitoring.vue

+ 11 - 5
src/components/ui/MenuItem.vue

@@ -25,16 +25,16 @@
         </span>
       </span>
       
-      <span 
-        v-if="hasChildren" 
-        class="arrow-icon" 
-        :class="{ 'is-open': isOpen }"
+      <span
+        v-if="hasChildren"
+        class="arrow-icon"
+        :class="{ 'is-open': isExpanded }"
       >
         <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m18 15-6-6-6 6"/></svg>
       </span>
     </div>
 
-    <div class="menu-children" v-show="isOpen" v-if="hasChildren">
+    <div class="menu-children" v-show="isExpanded" v-if="hasChildren">
       <MenuItem
         v-for="child in node.children"
         :key="child.id"
@@ -84,6 +84,12 @@ export default {
   computed: {
     hasChildren() {
       return this.node.children && this.node.children.length > 0;
+    },
+    // 搜索过滤期间,prune 后的祖先节点会带 forceExpand:true,
+    // 用 computed 覆盖用户手动折叠状态,但不修改 isOpen,
+    // 退出搜索后用户原本展开/折叠状态即自动恢复。
+    isExpanded() {
+      return this.node.forceExpand === true || this.isOpen;
     }
   },
   methods: {

+ 108 - 2
src/views/StatusMonitoring.vue

@@ -32,11 +32,33 @@
             <div class="left-sidebar-wrap" v-if="currentView !== 'list-mode'">
                 <TechTabs v-model="activeLeftTab" type="underline" @tab-click="handleTabClick">
                     <TechTabPane label="总览" name="overview" class="menu-scroll-view" :loading="menuData.length === 0">
-                        <MenuItem theme="tech" v-for="item in menuData" :key="item.id" :node="item" :level="0"
+                        <div class="menu-search">
+                            <input
+                                v-model="menuQueries.overview"
+                                class="menu-search-input"
+                                placeholder="请输入路口名"
+                                @compositionstart="menuSearchComposing = true"
+                                @compositionend="menuSearchComposing = false"
+                            />
+                            <i v-if="menuQueries.overview" class="menu-search-clear" @click="menuQueries.overview = ''">✕</i>
+                        </div>
+                        <div v-if="menuQueries.overview && !menuSearchComposing && filteredOverviewMenu.length === 0" class="menu-search-empty">无匹配路口</div>
+                        <MenuItem theme="tech" v-for="item in filteredOverviewMenu" :key="item.id" :node="item" :level="0"
                             @node-click="handleMenuClick" @folder-click="handleFolderClick"/>
                     </TechTabPane>
                     <TechTabPane label="路口" name="crossing" class="menu-scroll-view" :loading="menuData.length === 0">
-                        <MenuItem theme="tech" v-for="item in menuData" :key="item.id" :node="item" :level="0"
+                        <div class="menu-search">
+                            <input
+                                v-model="menuQueries.crossing"
+                                class="menu-search-input"
+                                placeholder="请输入路口名"
+                                @compositionstart="menuSearchComposing = true"
+                                @compositionend="menuSearchComposing = false"
+                            />
+                            <i v-if="menuQueries.crossing" class="menu-search-clear" @click="menuQueries.crossing = ''">✕</i>
+                        </div>
+                        <div v-if="menuQueries.crossing && !menuSearchComposing && filteredCrossingMenu.length === 0" class="menu-search-empty">无匹配路口</div>
+                        <MenuItem theme="tech" v-for="item in filteredCrossingMenu" :key="item.id" :node="item" :level="0"
                             @node-click="handleMenuClick" @folder-click="handleFolderClick" />
                     </TechTabPane>
                     <TechTabPane label="干线" name="trunkLine" class="menu-scroll-view" :loading="trunkLineMenuData.length === 0">
@@ -163,8 +185,19 @@ export default {
             // 在线状态 & 设备状态数据
             onlineStatusData: null,
             deviceFaultData: null,
+            // 左侧菜单按路口名搜索:每个 tab 独立 query;composing 期间不触发过滤
+            menuQueries: { overview: '', crossing: '' },
+            menuSearchComposing: false,
         };
     },
+    computed: {
+        filteredOverviewMenu() {
+            return this.pruneMenu(this.menuData, this.menuQueries.overview);
+        },
+        filteredCrossingMenu() {
+            return this.pruneMenu(this.menuData, this.menuQueries.crossing);
+        },
+    },
     watch: {
         // 监听路由参数变化(解决多次从首页点击不同数据跳转过来,页面不刷新的问题)
         '$route.query': {
@@ -202,6 +235,28 @@ export default {
 
     },
     methods: {
+        /**
+         * 按 query 递归 prune 菜单树:仅保留命中叶子.label.includes(q) 的整条祖先链,
+         * 给保留的中间节点打 forceExpand 标记(MenuItem 的 isExpanded computed 会读它)。
+         * composing 期间(中文输入未提交)不过滤,避免抖动。
+         */
+        pruneMenu(nodes, query) {
+            const q = (query || '').trim().toLowerCase();
+            if (!q || this.menuSearchComposing) return nodes;
+            const walk = (list) => {
+                const out = [];
+                for (const n of list) {
+                    if (n.children && n.children.length) {
+                        const kept = walk(n.children);
+                        if (kept.length) out.push({ ...n, children: kept, forceExpand: true });
+                    } else if ((n.label || '').toLowerCase().includes(q)) {
+                        out.push(n);
+                    }
+                }
+                return out;
+            };
+            return walk(nodes);
+        },
         // 处理地图鼠标滑入事件
         handleMapCrossingMouseover(mapData, lnglat, pixel) {
             console.log('父组件接收到了地图路口鼠标滑入事件:', mapData);
@@ -775,6 +830,57 @@ export default {
 }
 </script>
 <style scoped>
+.menu-search {
+    position: relative;
+    padding: 8px 10px 6px;
+    background: #05142e;
+}
+.menu-search-input {
+    width: 100%;
+    height: 32px;
+    padding: 0 28px 0 10px;
+    background: rgba(5, 22, 45, 0.9);
+    border: 1px solid #1e4d8e;
+    border-radius: 4px;
+    color: #fff;
+    font-size: 13px;
+    outline: none;
+    box-sizing: border-box;
+    transition: border-color 0.15s;
+}
+.menu-search-input::placeholder {
+    color: rgba(255, 255, 255, 0.4);
+}
+.menu-search-input:focus {
+    border-color: #3a7fd1;
+}
+.menu-search-clear {
+    position: absolute;
+    right: 18px;
+    top: 50%;
+    transform: translateY(-50%);
+    width: 18px;
+    height: 18px;
+    line-height: 18px;
+    text-align: center;
+    color: rgba(255, 255, 255, 0.5);
+    font-size: 12px;
+    font-style: normal;
+    cursor: pointer;
+    border-radius: 50%;
+    user-select: none;
+}
+.menu-search-clear:hover {
+    color: #fff;
+    background: rgba(255, 255, 255, 0.1);
+}
+.menu-search-empty {
+    padding: 16px;
+    text-align: center;
+    color: rgba(255, 255, 255, 0.4);
+    font-size: 13px;
+}
+
 .mode-switch {
     display: flex;
     flex-direction: row;