From c5d7868e9e5fb8013edb088a70e75fc83d575690 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期六, 07 三月 2026 10:36:05 +0800
Subject: [PATCH] #

---
 src/main/webapp/views/locMap/locMap.html                    | 1836 ++++++++++++++++++++++++++++++++++++++-----------------
 src/main/webapp/components/MapCanvas.js                     |   14 
 src/main/java/com/zy/asrs/controller/ConsoleController.java |  117 +++
 3 files changed, 1,388 insertions(+), 579 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/ConsoleController.java b/src/main/java/com/zy/asrs/controller/ConsoleController.java
index 51b7777..54a1ef5 100644
--- a/src/main/java/com/zy/asrs/controller/ConsoleController.java
+++ b/src/main/java/com/zy/asrs/controller/ConsoleController.java
@@ -350,21 +350,126 @@
      */
     @GetMapping("/map/{lev}/auth")
     public R getLocMap(@PathVariable Integer lev) {
-        Object object = redisUtil.get(RedisKeyType.LOC_MAP_BASE.key);
-        List<List<HashMap<String, Object>>> mapNodeList = null;
-        if (object != null) {
-            mapNodeList = (List<List<HashMap<String, Object>>>) object;
+        List<List<HashMap<String, Object>>> mapNodeList = getLocMapBaseSnapshot();
+        if (mapNodeList == null || mapNodeList.isEmpty()) {
+            return R.error("璇峰厛鍒濆鍖栧湴鍥�");
         }
         List<LocMast> locMastList = locMastService.selectLocByLev(lev);
+        boolean needRefreshBase = false;
         for (LocMast locMast : locMastList) {
-            String[] locType = locMast.getLocType().split("-");
-            HashMap<String, Object> mapNode = mapNodeList.get(Integer.parseInt(locType[0])).get(Integer.parseInt(locType[1]));
+            Integer[] pos = parseLocTypePos(locMast.getLocType());
+            if (pos == null || !isValidMapNodeIndex(mapNodeList, pos[0], pos[1])) {
+                needRefreshBase = true;
+                break;
+            }
+        }
+
+        if (needRefreshBase) {
+            refreshLocMapBaseCache();
+            mapNodeList = getLocMapBaseSnapshot();
+        }
+
+        for (LocMast locMast : locMastList) {
+            Integer[] pos = parseLocTypePos(locMast.getLocType());
+            if (pos == null || !isValidMapNodeIndex(mapNodeList, pos[0], pos[1])) {
+                log.warn("locMap skip invalid locType, locNo={}, locType={}", locMast.getLocNo(), locMast.getLocType());
+                continue;
+            }
+            HashMap<String, Object> mapNode = mapNodeList.get(pos[0]).get(pos[1]);
             mapNode.put("locSts", locMast.getLocSts());
             mapNode.put("locNo", locMast.getLocNo());
         }
         return R.ok().add(mapNodeList);
     }
 
+    private List<List<HashMap<String, Object>>> getLocMapBaseSnapshot() {
+        Object object = redisUtil.get(RedisKeyType.LOC_MAP_BASE.key);
+        if (!(object instanceof List)) {
+            return buildLocMapBase();
+        }
+        return cloneMapNodeList((List<List<HashMap<String, Object>>>) object);
+    }
+
+    private void refreshLocMapBaseCache() {
+        List<List<HashMap<String, Object>>> base = buildLocMapBase();
+        if (base != null && !base.isEmpty()) {
+            redisUtil.set(RedisKeyType.LOC_MAP_BASE.key, base);
+        }
+    }
+
+    private List<List<HashMap<String, Object>>> buildLocMapBase() {
+        BasMap basMap = basMapService.selectOne(new EntityWrapper<BasMap>().eq("lev", 1));
+        if (Cools.isEmpty(basMap) || Cools.isEmpty(basMap.getData())) {
+            return null;
+        }
+
+        List<List<JSONObject>> dataList = JSON.parseObject(basMap.getData(), List.class);
+        List<List<HashMap<String, Object>>> mapNodeList = new ArrayList<>();
+        for (int i = 0; i < dataList.size(); i++) {
+            List<JSONObject> row = dataList.get(i);
+            List<HashMap<String, Object>> mapNodeRow = new ArrayList<>();
+            for (int j = 0; j < row.size(); j++) {
+                JSONObject map = row.get(j);
+                HashMap<String, Object> mapNode = new HashMap<>();
+                mapNode.put("id", i + "-" + j);
+
+                String nodeType = map.getString("type");
+                mapNode.put("type", nodeType);
+                if ("shelf".equals(nodeType)) {
+                    mapNode.put("value", MapNodeType.NORMAL_PATH.id);
+                } else if ("devp".equals(nodeType)) {
+                    mapNode.put("value", MapNodeType.DISABLE.id);
+                } else if ("crn".equals(nodeType) || "dualCrn".equals(nodeType) || "rgv".equals(nodeType)) {
+                    mapNode.put("value", MapNodeType.MAIN_PATH.id);
+                } else {
+                    mapNode.put("value", MapNodeType.DISABLE.id);
+                }
+
+                mapNodeRow.add(mapNode);
+            }
+            mapNodeList.add(mapNodeRow);
+        }
+        return mapNodeList;
+    }
+
+    private List<List<HashMap<String, Object>>> cloneMapNodeList(List<List<HashMap<String, Object>>> source) {
+        List<List<HashMap<String, Object>>> copy = new ArrayList<>();
+        for (List<HashMap<String, Object>> row : source) {
+            List<HashMap<String, Object>> rowCopy = new ArrayList<>();
+            if (row != null) {
+                for (HashMap<String, Object> item : row) {
+                    rowCopy.add(item == null ? new HashMap<>() : new HashMap<>(item));
+                }
+            }
+            copy.add(rowCopy);
+        }
+        return copy;
+    }
+
+    private Integer[] parseLocTypePos(String locType) {
+        if (Cools.isEmpty(locType)) {
+            return null;
+        }
+        String[] parts = locType.split("-");
+        if (parts.length < 2) {
+            return null;
+        }
+        try {
+            return new Integer[]{Integer.parseInt(parts[0]), Integer.parseInt(parts[1])};
+        } catch (NumberFormatException e) {
+            log.warn("parse locType fail, locType={}", locType, e);
+            return null;
+        }
+    }
+
+    private boolean isValidMapNodeIndex(List<List<HashMap<String, Object>>> mapNodeList, Integer rowIdx, Integer colIdx) {
+        if (mapNodeList == null || rowIdx == null || colIdx == null || rowIdx < 0 || colIdx < 0 || rowIdx >= mapNodeList.size()) {
+            return false;
+        }
+        List<HashMap<String, Object>> row = mapNodeList.get(rowIdx);
+        return row != null && colIdx < row.size();
+    }
+
     @RequestMapping(value = "/map/locList")
     public R mapLocList() {
         Object object = redisUtil.get(RedisKeyType.LOC_MAST_MAP_LIST.key);
diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index be2b7d8..b5ced58 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -26,6 +26,7 @@
         <div v-show="showMapToolPanel" :style="mapToolBarStyle()">
           <div :style="mapToolRowStyle()">
             <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '闅愯棌绔欑偣鏂瑰悜' : '鏄剧ず绔欑偣鏂瑰悜' }}</button>
+            <button type="button" @click="resetMapView" :style="mapToolButtonStyle(false)">閲嶇疆瑙嗗浘</button>
             <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">鏃嬭浆</button>
             <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
           </div>
@@ -154,7 +155,7 @@
     this.loadStationColorConfig();
     this.loadLocList();
     this.connectWs();
-    
+
     setTimeout(() => {
       this.getMap(this.currentLev);
     }, 1000);
@@ -2408,6 +2409,11 @@
       this.applyMapTransform(true);
       this.saveMapTransformConfig();
     },
+    resetMapView() {
+      this.fitStageToContent();
+      this.scheduleAdjustLabels();
+      this.scheduleShelfChunkCulling();
+    },
     toggleStationDirection() {
       this.showStationDirection = !this.showStationDirection;
       this.applyStationDirectionVisibility();
@@ -2640,12 +2646,6 @@
     }
   }
 });
-
-
-
-
-
-
 
 
 
diff --git a/src/main/webapp/views/locMap/locMap.html b/src/main/webapp/views/locMap/locMap.html
index 36fdee8..fdd8960 100644
--- a/src/main/webapp/views/locMap/locMap.html
+++ b/src/main/webapp/views/locMap/locMap.html
@@ -3,486 +3,1218 @@
 <head>
   <meta charset="UTF-8">
   <title>搴撲綅鍦板浘</title>
-  <link rel="stylesheet" href="../../static/css/animate.min.css">
   <link rel="stylesheet" href="../../static/vue/element/element.css">
-  <link rel="stylesheet" href="../../static/css/console_vue.css">
-  <link rel="stylesheet" href="../../static/css/toggle-switch.css">
   <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
-  <script type="text/javascript" src="../../static/layui/layui.js"></script>
-  <script type="text/javascript" src="../../static/js/handlebars/handlebars-v4.5.3.js"></script>
   <script type="text/javascript" src="../../static/js/common.js"></script>
   <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
   <script type="text/javascript" src="../../static/vue/element/element.js"></script>
   <script src="../../static/js/gsap.min.js"></script>
   <script src="../../static/js/pixi-legacy.min.js"></script>
   <style>
-    *{
+    html, body, #app {
+      width: 100%;
+      height: 100%;
       margin: 0;
-      padding: 0;
+      overflow: hidden;
+    }
+
+    body {
+      background: linear-gradient(180deg, #eef4f8 0%, #e7edf4 100%);
+      font-family: "Helvetica Neue", Arial, sans-serif;
+    }
+
+    * {
+      box-sizing: border-box;
+    }
+
+    .locmap-shell {
+      position: relative;
+      width: 100%;
+      height: 100%;
+      overflow: hidden;
+    }
+
+    .locmap-canvas {
+      position: absolute;
+      inset: 0;
+    }
+
+    .locmap-title-card {
+      position: absolute;
+      top: 18px;
+      left: 18px;
+      z-index: 20;
+      padding: 14px 16px;
+      min-width: 220px;
+      border-radius: 18px;
+      border: 1px solid rgba(255, 255, 255, 0.42);
+      background: rgba(248, 251, 253, 0.92);
+      box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
+      backdrop-filter: blur(6px);
+      pointer-events: none;
+    }
+
+    .locmap-title {
+      color: #243447;
+      font-size: 18px;
+      font-weight: 700;
+      line-height: 1.2;
+    }
+
+    .locmap-title-desc {
+      margin-top: 6px;
+      color: #6b7b8d;
+      font-size: 12px;
+      line-height: 1.5;
+    }
+
+    .locmap-tools {
+      position: absolute;
+      top: 18px;
+      right: 28px;
+      z-index: 30;
+      display: flex;
+      flex-direction: column;
+      align-items: flex-end;
+      gap: 8px;
+    }
+
+    .locmap-fps {
+      padding: 4px 10px;
+      border-radius: 999px;
+      background: rgba(255, 255, 255, 0.7);
+      border: 1px solid rgba(160, 180, 205, 0.28);
+      color: #48617c;
+      font-size: 12px;
+      line-height: 18px;
+      letter-spacing: 0.04em;
+      box-shadow: 0 6px 16px rgba(37, 64, 97, 0.06);
+      user-select: none;
+    }
+
+    .locmap-tool-toggle,
+    .locmap-tool-btn,
+    .locmap-floor-btn,
+    .locmap-detail-close {
+      appearance: none;
+      cursor: pointer;
+      transition: all .18s ease;
+      outline: none;
+    }
+
+    .locmap-tool-toggle {
+      height: 30px;
+      padding: 0 12px;
+      border-radius: 999px;
+      border: 1px solid rgba(160, 180, 205, 0.3);
+      background: rgba(255, 255, 255, 0.82);
+      color: #46617b;
+      font-size: 12px;
+      line-height: 30px;
+      box-shadow: 0 6px 16px rgba(37, 64, 97, 0.06);
+    }
+
+    .locmap-tool-toggle.is-active {
+      border-color: rgba(96, 132, 170, 0.36);
+      background: rgba(235, 243, 251, 0.96);
+    }
+
+    .locmap-tool-panel {
+      display: flex;
+      flex-direction: column;
+      gap: 8px;
+      min-width: 168px;
+      padding: 8px;
+      border-radius: 14px;
+      background: rgba(255, 255, 255, 0.72);
+      border: 1px solid rgba(160, 180, 205, 0.3);
+      box-shadow: 0 8px 20px rgba(37, 64, 97, 0.08);
+      backdrop-filter: blur(4px);
+    }
+
+    .locmap-tool-row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 8px;
+      justify-content: flex-end;
+    }
+
+    .locmap-tool-btn,
+    .locmap-floor-btn {
+      min-width: 64px;
+      height: 30px;
+      padding: 0 12px;
+      border-radius: 10px;
+      border: 1px solid rgba(160, 180, 205, 0.3);
+      background: rgba(255, 255, 255, 0.88);
+      color: #4d647d;
+      font-size: 12px;
+      line-height: 30px;
+      white-space: nowrap;
+    }
+
+    .locmap-tool-btn:hover,
+    .locmap-floor-btn:hover,
+    .locmap-detail-close:hover,
+    .locmap-tool-toggle:hover {
+      transform: translateY(-1px);
+    }
+
+    .locmap-tool-btn.is-active,
+    .locmap-floor-btn.is-active {
+      border-color: rgba(255, 136, 93, 0.38);
+      background: rgba(255, 119, 77, 0.16);
+      color: #d85a31;
+    }
+
+    .locmap-tool-section {
+      display: flex;
+      flex-direction: column;
+      gap: 4px;
+      padding-top: 6px;
+      border-top: 1px solid rgba(160, 180, 205, 0.22);
+    }
+
+    .locmap-tool-label {
+      color: #6a7f95;
+      font-size: 10px;
+      line-height: 14px;
+      text-align: right;
+    }
+
+    .locmap-detail-panel {
+      position: absolute;
+      top: 92px;
+      right: 18px;
+      bottom: 18px;
+      z-index: 25;
+      width: min(max(320px, 25vw), calc(100vw - 92px));
+      border-radius: 20px;
+      border: 1px solid rgba(255, 255, 255, 0.42);
+      background: rgba(248, 251, 253, 0.94);
+      box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
+      overflow: hidden;
+      display: flex;
+      flex-direction: column;
+      backdrop-filter: blur(6px);
+    }
+
+    .locmap-detail-header {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 12px;
+      padding: 16px 16px 12px;
+      border-bottom: 1px solid rgba(226, 232, 240, 0.72);
+      background: rgba(255, 255, 255, 0.24);
+    }
+
+    .locmap-detail-title-wrap {
+      min-width: 0;
+    }
+
+    .locmap-detail-type {
+      display: inline-flex;
+      align-items: center;
+      height: 22px;
+      padding: 0 10px;
+      border-radius: 999px;
+      background: rgba(103, 149, 193, 0.12);
+      color: #4b6782;
+      font-size: 11px;
+      font-weight: 700;
+    }
+
+    .locmap-detail-title {
+      margin-top: 8px;
+      color: #243447;
+      font-size: 18px;
+      font-weight: 700;
+      line-height: 1.2;
+    }
+
+    .locmap-detail-subtitle {
+      margin-top: 6px;
+      color: #6b7b8d;
+      font-size: 12px;
+      line-height: 1.4;
+    }
+
+    .locmap-detail-close {
+      flex-shrink: 0;
+      width: 32px;
+      height: 32px;
+      border-radius: 10px;
+      border: 1px solid rgba(160, 180, 205, 0.28);
+      background: rgba(255, 255, 255, 0.84);
+      color: #4d647d;
+      font-size: 18px;
+      line-height: 30px;
+      text-align: center;
+    }
+
+    .locmap-detail-body {
+      flex: 1;
+      min-height: 0;
+      padding: 16px;
+      overflow: auto;
+    }
+
+    .locmap-kv-grid {
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 12px;
+    }
+
+    .locmap-kv-card {
+      padding: 14px;
+      border-radius: 16px;
+      border: 1px solid rgba(224, 232, 239, 0.92);
+      background: rgba(255, 255, 255, 0.62);
+      box-shadow: 0 8px 18px rgba(148, 163, 184, 0.06);
+    }
+
+    .locmap-kv-label {
+      color: #7d8fa2;
+      font-size: 12px;
+      line-height: 1.4;
+    }
+
+    .locmap-kv-value {
+      margin-top: 8px;
+      color: #334155;
+      font-size: 18px;
+      font-weight: 600;
+      line-height: 1.35;
+      word-break: break-all;
+    }
+
+    .locmap-empty {
+      position: absolute;
+      right: 18px;
+      bottom: 18px;
+      z-index: 20;
+      width: min(max(260px, 20vw), calc(100vw - 92px));
+      padding: 18px;
+      border-radius: 18px;
+      border: 1px solid rgba(255, 255, 255, 0.4);
+      background: rgba(248, 251, 253, 0.92);
+      box-shadow: 0 10px 24px rgba(88, 110, 136, 0.08);
+      color: #6b7b8d;
+      font-size: 12px;
+      line-height: 1.7;
+      backdrop-filter: blur(6px);
+    }
+
+    @media (max-width: 960px) {
+      .locmap-title-card {
+        right: 18px;
+        min-width: 0;
+      }
+
+      .locmap-detail-panel {
+        top: auto;
+        height: min(54vh, 460px);
+        width: calc(100vw - 36px);
+      }
+
+      .locmap-empty {
+        width: calc(100vw - 36px);
+      }
+
+      .locmap-kv-grid {
+        grid-template-columns: 1fr;
+      }
     }
   </style>
 </head>
 <body>
 <div id="app">
-  <div id="pixiView">
-
-  </div>
-
-  <!--杈撳嚭鎿嶄綔鍜孎PS-->
-  <div style="position: absolute;top: 20px;right: 50px;">
-    <div>FPS:{{mapFps}}</div>
-    <el-button @click="drawer = true">鎿嶄綔</el-button>
-  </div>
-
-  <el-drawer
-          title="鎿嶄綔鍖哄煙"
-          :visible.sync="drawer"
-          :with-header="true"
-          :modal="false"
-  >
-    <div class="floorBtnBox" v-for="(lev,idx) in floorList">
-      <el-button :style="{background:currentLev === lev ? '#7DCDFF':''}" @click="changeFloor(lev)">{{lev}}F</el-button>
-    </div>
-<!--    <div>-->
-<!--      <el-button @click="testMove()">娴嬭瘯绉诲姩杞�</el-button>-->
-<!--    </div>-->
-<!--    <div style="margin-top: 10px;">-->
-<!--      <el-button @click="resetMap()">閲嶇疆鍦板浘</el-button>-->
-<!--    </div>-->
-  </el-drawer>
-
-  <el-drawer
-          title="搴撲綅璇︽儏"
-          :visible.sync="drawerLocNo"
-          :with-header="true"
-          :modal="false"
-  >
-    <div v-if="drawerLocNoData!=null">
-      <div style="margin: 10px;">
-<!--        <div style="margin-top: 5px;">鎺掞細{{drawerLocNoData.row}}</div>-->
-<!--        <div style="margin-top: 5px;">鍒楋細{{drawerLocNoData.bay}}</div>-->
-<!--        <div style="margin-top: 5px;">灞傦細{{drawerLocNoData.lev}}</div>-->
-        <div style="margin-top: 5px;">搴撲綅鍙凤細{{drawerLocNoData.locNo}}</div>
-        <div style="margin-top: 5px;">搴撲綅鐘舵�侊細{{drawerLocNoData.locSts}}</div>
-      </div>
-    </div>
-  </el-drawer>
-
-  <el-drawer
-          title="绔欑偣淇℃伅"
-          :visible.sync="drawerSta"
-          :with-header="true"
-          :modal="false"
-  >
-    <div v-if="drawerStaData!=null">
-      <div style="margin: 10px;">
-        <div style="margin-top: 5px;">绔欑偣锛歿{drawerStaData.siteId}}</div>
-        <div style="margin-top: 5px;">宸ヤ綔鍙凤細{{drawerStaData.workNo}}</div>
-        <div style="margin-top: 5px;">宸ヤ綔鐘舵�侊細{{drawerStaData.wrkSts}}</div>
-        <div style="margin-top: 5px;">宸ヤ綔绫诲瀷锛歿{drawerStaData.ioType}}</div>
-        <div style="margin-top: 5px;">婧愮珯锛歿{drawerStaData.sourceStaNo}}</div>
-        <div style="margin-top: 5px;">鐩爣绔欙細{{drawerStaData.staNo}}</div>
-        <div style="margin-top: 5px;">婧愬簱浣嶏細{{drawerStaData.sourceLocNo}}</div>
-        <div style="margin-top: 5px;">鐩爣搴撲綅锛歿{drawerStaData.locNo}}</div>
-        <div style="margin-top: 5px;">鑷姩锛歿{drawerStaData.autoing}}</div>
-        <div style="margin-top: 5px;">鏈夌墿锛歿{drawerStaData.loading}}</div>
-        <div style="margin-top: 5px;">鑳藉叆锛歿{drawerStaData.canining}}</div>
-        <div style="margin-top: 5px;">鑳藉嚭锛歿{drawerStaData.canouting}}</div>
-        <div style="margin-top: 5px;">鑳藉嚭锛歿{drawerStaData.canouting}}</div>
-      </div>
-    </div>
-  </el-drawer>
-
+  <loc-map-canvas></loc-map-canvas>
 </div>
-<script>
-  let width = 25;
-  let height = 25;
-  let pixiApp;
-  let pixiStageList = [];
-  let pixiStaMap = new Map();
-  let objectsContainer;
-  let objectsContainer3;
-  let tracksGraphics;
-  let mapRoot;
-  let mapContentSize = { width: 0, height: 0 };
-  let graphics0;
-  let graphicsF;
-  let graphics3;
-  let graphics4;
-  let graphics5;
-  let graphics9;
-  let graphics67;
-  let graphicsLock;
-  let ws;
 
-  var app = new Vue({
-    el: '#app',
-    data: {
-      map: [],
-      currentLev: 1,
-      floorList: [], //褰撳墠椤圭洰妤煎眰
-      drawer: false,
-      drawerLocNo: false,
-      drawerLocNoData: null,
-      drawerLocDetls: [],
-      reloadMap: true,
-      mapFps: 0,
-      currentLevStaList: [],//褰撳墠妤煎眰绔欑偣list
-      drawerSta: false,
-      drawerStaData: null,
-      mapRotation: 0,
-      mapMirrorX: false,
-      mapConfigCodes: {
-        rotate: 'map_canvas_rotation',
-        mirror: 'map_canvas_mirror_x'
+<script>
+  Vue.component('loc-map-canvas', {
+    template: `
+      <div class="locmap-shell" ref="shell">
+        <div ref="pixiView" class="locmap-canvas"></div>
+
+        <div class="locmap-title-card">
+          <div class="locmap-title">搴撲綅鍦板浘</div>
+          <div class="locmap-title-desc">鐐瑰嚮搴撲綅鍚庡湪鍙充晶鏌ョ湅璇︽儏銆�</div>
+        </div>
+
+        <div class="locmap-tools">
+          <div class="locmap-fps">FPS {{ mapFps }}</div>
+          <button type="button"
+                  class="locmap-tool-toggle"
+                  :class="{ 'is-active': showMapToolPanel }"
+                  @click="toggleMapToolPanel">{{ showMapToolPanel ? '鏀惰捣鎿嶄綔' : '鍦板浘鎿嶄綔' }}</button>
+          <div v-show="showMapToolPanel" class="locmap-tool-panel">
+            <div class="locmap-tool-row">
+              <button type="button" class="locmap-tool-btn" @click="fitStageToContent">閲嶇疆瑙嗗浘</button>
+              <button type="button" class="locmap-tool-btn" @click="rotateMap">鏃嬭浆</button>
+              <button type="button"
+                      class="locmap-tool-btn"
+                      :class="{ 'is-active': mapMirrorX }"
+                      @click="toggleMirror">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
+            </div>
+            <div v-if="floorList && floorList.length > 0" class="locmap-tool-section">
+              <div class="locmap-tool-label">妤煎眰</div>
+              <div class="locmap-tool-row">
+                <button v-for="lev in floorList"
+                        :key="'loc-floor-' + lev"
+                        type="button"
+                        class="locmap-floor-btn"
+                        :class="{ 'is-active': currentLev === lev }"
+                        @click="changeFloor(lev)">{{ lev }}F</button>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div v-if="detailPanelOpen" class="locmap-detail-panel" ref="detailPanel">
+          <div class="locmap-detail-header">
+            <div class="locmap-detail-title-wrap">
+              <div class="locmap-detail-type">{{ detailTypeLabel }}</div>
+              <div class="locmap-detail-title">{{ detailTitle }}</div>
+              <div class="locmap-detail-subtitle">{{ detailSubtitle }}</div>
+            </div>
+            <button type="button" class="locmap-detail-close" @click="closeDetailPanel">脳</button>
+          </div>
+          <div class="locmap-detail-body">
+            <div class="locmap-kv-grid">
+              <div v-for="item in detailFields" :key="item.label" class="locmap-kv-card">
+                <div class="locmap-kv-label">{{ item.label }}</div>
+                <div class="locmap-kv-value">{{ formatDetailValue(item.value) }}</div>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div v-else class="locmap-empty">
+          鐐瑰嚮搴撲綅鍙煡鐪嬪簱浣嶇姸鎬侊紝鐐瑰嚮绔欑偣鍙煡鐪嬬珯鐐逛綔涓氳鎯呫�傚湴鍥炬搷浣滃湪鍙充笂瑙掑伐鍏烽潰鏉夸腑銆�
+        </div>
+      </div>
+    `,
+    data() {
+      return {
+        cellWidth: 25,
+        cellHeight: 25,
+        pixiApp: null,
+        pixiStageList: [],
+        pixiStaMap: new Map(),
+        pixiTrackMap: new Map(),
+        pixiLabelList: [],
+        mapRoot: null,
+        shelvesContainer: null,
+        objectsContainer: null,
+        objectsOverlayContainer: null,
+        tracksGraphics: null,
+        mapContentSize: { width: 0, height: 0 },
+        textureMap: {},
+        locChunkList: [],
+        locChunkSize: 1024,
+        locCullPadding: 160,
+        locCullRaf: null,
+        ws: null,
+        wsReconnectTimer: null,
+        wsReconnectAttempts: 0,
+        wsReconnectBaseDelay: 1000,
+        wsReconnectMaxDelay: 15000,
+        containerResizeObserver: null,
+        adjustLabelTimer: null,
+        map: [],
+        floorList: [],
+        currentLev: 1,
+        reloadMap: true,
+        mapFps: 0,
+        showMapToolPanel: false,
+        detailPanelOpen: false,
+        detailType: '',
+        detailPayload: null,
+        highlightedSprite: null,
+        highlightedLocCell: null,
+        highlightedLocGraphic: null,
+        mapRotation: 0,
+        mapMirrorX: false,
+        mapConfigCodes: {
+          rotate: 'map_canvas_rotation',
+          mirror: 'map_canvas_mirror_x'
+        }
+      };
+    },
+    computed: {
+      detailTypeLabel() {
+        return this.detailType === 'site' ? '绔欑偣淇℃伅' : '搴撲綅璇︽儏';
+      },
+      detailTitle() {
+        if (this.detailType === 'site' && this.detailPayload) {
+          return '绔欑偣 ' + this.formatDetailValue(this.detailPayload.siteId);
+        }
+        if (this.detailType === 'loc' && this.detailPayload) {
+          return this.formatDetailValue(this.detailPayload.locNo);
+        }
+        return '璇︽儏';
+      },
+      detailSubtitle() {
+        if (this.detailType === 'site') {
+          return '绔欑偣浣滀笟鐘舵�併�佹潵婧愮洰鏍囧拰浣滀笟鍙傛暟';
+        }
+        return '搴撲綅褰撳墠鐘舵�佸拰鎵�鍦ㄦ帓鍒楀眰淇℃伅';
+      },
+      detailFields() {
+        if (!this.detailPayload) { return []; }
+        if (this.detailType === 'site') {
+          return [
+            { label: '绔欑偣', value: this.detailPayload.siteId },
+            { label: '宸ヤ綔鍙�', value: this.detailPayload.workNo },
+            { label: '宸ヤ綔鐘舵��', value: this.detailPayload.wrkSts },
+            { label: '宸ヤ綔绫诲瀷', value: this.detailPayload.ioType },
+            { label: '婧愮珯', value: this.detailPayload.sourceStaNo },
+            { label: '鐩爣绔�', value: this.detailPayload.staNo },
+            { label: '婧愬簱浣�', value: this.detailPayload.sourceLocNo },
+            { label: '鐩爣搴撲綅', value: this.detailPayload.locNo },
+            { label: '鑷姩', value: this.detailPayload.autoing },
+            { label: '鏈夌墿', value: this.detailPayload.loading },
+            { label: '鑳藉叆', value: this.detailPayload.canining },
+            { label: '鑳藉嚭', value: this.detailPayload.canouting }
+          ];
+        }
+        return [
+          { label: '搴撲綅鍙�', value: this.detailPayload.locNo },
+          { label: '搴撲綅鐘舵��', value: this.detailPayload.locSts },
+          { label: '鎺�', value: this.detailPayload.row },
+          { label: '鍒�', value: this.detailPayload.bay },
+          { label: '灞�', value: this.detailPayload.lev }
+        ];
       }
     },
     mounted() {
-      this.init()
-      this.createMap()
+      this.createMap();
+      this.startContainerResizeObserve();
+      this.loadMapTransformConfig();
+      this.initLev();
+      this.connectWs();
+      setTimeout(() => {
+        this.getMap(this.currentLev);
+      }, 300);
     },
-    watch: {
-      map: {
-        deep: true,
-        handler(val) {
-
-        }
-      },
-      drawerLocNo: {
-        deep: true,
-        handler(val) {
-          if (!val) {
-            var sprite = pixiStageList[this.drawerLocNoData.x][this.drawerLocNoData.y];
-            updateColor(sprite, 0xFFFFFF);//鎭㈠棰滆壊
-          }
-        }
+    beforeDestroy() {
+      if (this.adjustLabelTimer) {
+        clearTimeout(this.adjustLabelTimer);
+        this.adjustLabelTimer = null;
+      }
+      if (this.locCullRaf) {
+        cancelAnimationFrame(this.locCullRaf);
+        this.locCullRaf = null;
+      }
+      if (this.containerResizeObserver) {
+        this.containerResizeObserver.disconnect();
+        this.containerResizeObserver = null;
+      }
+      window.removeEventListener('resize', this.resizeToContainer);
+      if (this.wsReconnectTimer) {
+        clearTimeout(this.wsReconnectTimer);
+        this.wsReconnectTimer = null;
+      }
+      if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
+        try { this.ws.close(); } catch (e) {}
+      }
+      if (window.gsap && this.pixiApp && this.pixiApp.stage) {
+        window.gsap.killTweensOf(this.pixiApp.stage.position);
+      }
+      this.clearLocChunks();
+      if (this.pixiApp) {
+        this.pixiApp.destroy(true, { children: true });
+        this.pixiApp = null;
       }
     },
     methods: {
-      init(){
-        let that = this
-        ws = new WebSocket("ws://" + window.location.host + baseUrl + "/console/websocket");
-        ws.onopen = this.webSocketOnOpen
-        ws.onerror = this.webSocketOnError
-        ws.onmessage = this.webSocketOnMessage
-        ws.onclose = this.webSocketClose
-
-
-        this.loadMapTransformConfig()
-        this.initLev()//鍒濆鍖栨ゼ灞備俊鎭�
-        setTimeout(() => {
-          that.getMap(this.currentLev)
-        }, 1000);
-
+      toggleMapToolPanel() {
+        this.showMapToolPanel = !this.showMapToolPanel;
       },
-      initLev(){
-        let that = this
+      formatDetailValue(value) {
+        return value == null || value === '' ? '-' : value;
+      },
+      initLev() {
         $.ajax({
           url: baseUrl + "/console/map/lev/list",
-          headers: {
-            'token': localStorage.getItem('token')
-          },
-          data: {},
+          headers: { token: localStorage.getItem('token') },
           method: 'get',
-          success: function(res) {
+          success: (res) => {
             if (res.code === 200) {
-              that.floorList = res.data;
+              this.floorList = Array.isArray(res.data) ? res.data : [];
+              if (this.floorList.length > 0 && this.floorList.indexOf(this.currentLev) === -1) {
+                this.currentLev = this.floorList[0];
+              }
             } else if (res.code === 403) {
               parent.location.href = baseUrl + "/login";
             } else {
-              that.$message({
-                message: res.msg,
-                type: 'error'
-              });
+              this.showMessage('error', res.msg || '妤煎眰淇℃伅鍔犺浇澶辫触');
             }
           }
         });
       },
-      //鑾峰彇鍦板浘鏁版嵁
       getMap(lev) {
-        let that = this;
-          $.ajax({
-              url: baseUrl + "/console/map/" + lev + "/auth",
-              headers: {
-                  'token': localStorage.getItem('token')
-              },
-              data: {},
-              method: 'get',
-              success: function(res) {
-                //鑾峰彇鍦板浘鏁版嵁
-                let data = res.data
-                that.createMapData(data)
-              }
-          })
+        $.ajax({
+          url: baseUrl + "/console/map/" + lev + "/auth",
+          headers: { token: localStorage.getItem('token') },
+          method: 'get',
+          success: (res) => {
+            if (res.code === 200) {
+              this.reloadMap = true;
+              this.createMapData(res.data || []);
+            } else if (res.code === 403) {
+              parent.location.href = baseUrl + "/login";
+            } else {
+              this.showMessage('error', res.msg || '鍦板浘鍔犺浇澶辫触');
+            }
+          }
+        });
       },
       changeFloor(lev) {
-        this.currentLev = lev
-        this.loadMapTransformConfig()
-        this.reloadMap = true
-        this.getMap(lev)
+        if (this.currentLev === lev) { return; }
+        this.currentLev = lev;
+        this.reloadMap = true;
+        this.closeDetailPanel();
+        this.getMap(lev);
       },
-      createMap(){
-        //Create a Pixi Application
-        pixiApp = new PIXI.Application({
-          // width: 1500,
-          // height: 800,
-          backgroundColor: 0xF5F7F9FF,
-          resizeTo: window
+      connectWs() {
+        if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
+          return;
+        }
+        this.ws = new WebSocket("ws://" + window.location.host + baseUrl + "/console/websocket");
+        this.ws.onopen = this.webSocketOnOpen;
+        this.ws.onerror = this.webSocketOnError;
+        this.ws.onmessage = this.webSocketOnMessage;
+        this.ws.onclose = this.webSocketClose;
+      },
+      scheduleReconnectWs() {
+        if (this.wsReconnectTimer) { return; }
+        const delay = Math.min(this.wsReconnectMaxDelay, this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts));
+        this.wsReconnectAttempts += 1;
+        this.wsReconnectTimer = setTimeout(() => {
+          this.wsReconnectTimer = null;
+          this.connectWs();
+        }, delay);
+      },
+      webSocketOnOpen() {
+        this.wsReconnectAttempts = 0;
+      },
+      webSocketOnError() {},
+      webSocketOnMessage(event) {
+        let result = null;
+        try {
+          result = JSON.parse(event.data);
+        } catch (e) {
+          return;
+        }
+        if (!result || !result.url) { return; }
+        if (result.url === "/console/map/auth" || result.url === "/console/locMap/auth") {
+          let data = [];
+          try {
+            data = JSON.parse(result.data || '[]');
+          } catch (e) {
+            data = [];
+          }
+          this.setMap(data);
+        }
+      },
+      webSocketClose() {
+        this.scheduleReconnectWs();
+      },
+      setMap(data) {
+        if (!Array.isArray(data) || this.currentLev == null) { return; }
+        this.reloadMap = true;
+        this.createMapData(data);
+      },
+      showMessage(type, message) {
+        if (this.$message) {
+          this.$message({ type: type, message: message });
+        }
+      },
+      createMap() {
+        this.pixiApp = new PIXI.Application({
+          backgroundColor: 0xEEF4F8,
+          resizeTo: this.$refs.shell,
+          antialias: true,
+          autoDensity: true
         });
-        //Add the canvas that Pixi automatically created for you to the HTML document
-        $("#pixiView").append(pixiApp.view)
+        this.$refs.pixiView.appendChild(this.pixiApp.view);
+        this.createBaseTextures();
 
-        // 浠嶨raphics瀵硅薄鍒涘缓涓�涓汗鐞�
-		    graphicsF = pixiApp.renderer.generateTexture(getContainer(1000));
-        graphics0 = pixiApp.renderer.generateTexture(getContainer(0));
-        graphics3 = pixiApp.renderer.generateTexture(getContainer(3));
-        graphics4 = pixiApp.renderer.generateTexture(getContainer(4));
-        graphics5 = pixiApp.renderer.generateTexture(getContainer(5));
-        graphics9 = pixiApp.renderer.generateTexture(getContainer(9));
-        graphics67 = pixiApp.renderer.generateTexture(getContainer(67));
-        graphicsLock = pixiApp.renderer.generateTexture(getContainer(-999));
+        this.mapRoot = new PIXI.Container();
+        this.pixiApp.stage.addChild(this.mapRoot);
 
-        mapRoot = new PIXI.Container();
-        pixiApp.stage.addChild(mapRoot);
-        // 鍒涘缓涓�涓鍣ㄦ潵绠$悊澶ф壒閲忕殑鏄剧ず瀵硅薄
-        objectsContainer = new PIXI.Container();
-        mapRoot.addChild(objectsContainer);
+        this.shelvesContainer = new PIXI.Container();
+        this.mapRoot.addChild(this.shelvesContainer);
 
-        tracksGraphics = new PIXI.Graphics();
-        mapRoot.addChild(tracksGraphics);
+        this.objectsContainer = new PIXI.Container();
+        this.mapRoot.addChild(this.objectsContainer);
 
-        // 鍒涘缓涓�涓鍣ㄦ潵绠$悊澶ф壒閲忕殑鏄剧ず瀵硅薄
-        objectsContainer3 = new PIXI.Container();
-        mapRoot.addChild(objectsContainer3);
+        this.tracksGraphics = new PIXI.Graphics();
+        this.mapRoot.addChild(this.tracksGraphics);
 
-        //*******************鎷栧姩鐢诲竷*******************
-        let stageOriginalPos;
-        let mouseDownPoint;
+        this.objectsOverlayContainer = new PIXI.Container();
+        this.mapRoot.addChild(this.objectsOverlayContainer);
+
+        this.initStageInteractions();
+        this.initFpsTicker();
+      },
+      createBaseTextures() {
+        this.textureMap = {
+          locEmpty: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0x5faeff)),
+          locFull: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xf05d5d)),
+          site: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xf6ca4b)),
+          charge: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xffa66b)),
+          elevator: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0x7dd9ff)),
+          lock: this.pixiApp.renderer.generateTexture(this.buildCellGraphic(0xf83333))
+        };
+      },
+      buildCellGraphic(fillColor) {
+        const graphics = new PIXI.Graphics();
+        graphics.beginFill(fillColor);
+        graphics.lineStyle(1, 0xffffff, 1);
+        graphics.drawRect(0, 0, this.cellWidth, this.cellHeight);
+        graphics.endFill();
+        return graphics;
+      },
+      initStageInteractions() {
+        let stageOriginalPos = null;
+        let mouseDownPoint = null;
         let touchBlank = false;
-        pixiApp.renderer.plugins.interaction.on(
-                'pointerdown',
-                (event) => {
-                  const globalPos = event.data.global;
-                  // 璁板綍涓媠tage鍘熸潵鐨勪綅缃�
-                  stageOriginalPos = [pixiApp.stage.position._x, pixiApp.stage.position._y];
-                  // 璁板綍涓媘ouse down鐨勪綅缃�
-                  mouseDownPoint = [globalPos.x, globalPos.y];
-                  if (!event.target) {
-                    // 鐐瑰埌浜嗙敾甯冪殑绌虹櫧浣嶇疆
-                    touchBlank = true;
-                  }
-                }
-        );
+        let pointerDownMoved = false;
+        const interaction = this.pixiApp.renderer.plugins.interaction;
 
-        pixiApp.renderer.plugins.interaction.on(
-                'pointermove',
-                (event) => {
-                  const globalPos = event.data.global;
+        interaction.on('pointerdown', (event) => {
+          const globalPos = event.data.global;
+          stageOriginalPos = [this.pixiApp.stage.position.x, this.pixiApp.stage.position.y];
+          mouseDownPoint = [globalPos.x, globalPos.y];
+          pointerDownMoved = false;
+          touchBlank = !event.target;
+        });
 
-                  if (touchBlank) {
-                    // 鎷栨嫿鐢诲竷
-                    const dx = globalPos.x - mouseDownPoint[0];
-                    const dy = globalPos.y - mouseDownPoint[1];
-                    pixiApp.stage.position.set(
-                            stageOriginalPos[0] + dx,
-                            stageOriginalPos[1] + dy
-                    );
-                  }
-                }
-        );
+        interaction.on('pointermove', (event) => {
+          const globalPos = event.data.global;
+          if (mouseDownPoint) {
+            const dragDx = globalPos.x - mouseDownPoint[0];
+            const dragDy = globalPos.y - mouseDownPoint[1];
+            if (Math.abs(dragDx) > 4 || Math.abs(dragDy) > 4) {
+              pointerDownMoved = true;
+            }
+          }
+          if (!touchBlank || !stageOriginalPos || !mouseDownPoint) { return; }
+          const dx = globalPos.x - mouseDownPoint[0];
+          const dy = globalPos.y - mouseDownPoint[1];
+          this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy);
+          this.scheduleAdjustLabels();
+          this.scheduleLocChunkCulling();
+        });
 
-        pixiApp.renderer.plugins.interaction.on(
-                'pointerup',
-                (event) => {
-                  touchBlank = false;
-                }
-        );
-        //*******************鎷栧姩鐢诲竷*******************
+        interaction.on('pointerup', (event) => {
+          if (touchBlank && !pointerDownMoved && event && event.data && event.data.global) {
+            this.handleBlankPointerTap(event.data.global);
+          }
+          touchBlank = false;
+          mouseDownPoint = null;
+          stageOriginalPos = null;
+        });
 
-        //*******************缂╂斁鐢诲竷*******************
-        pixiApp.view.addEventListener('wheel', (event) => {
+        interaction.on('pointerupoutside', () => {
+          touchBlank = false;
+          mouseDownPoint = null;
+          stageOriginalPos = null;
+        });
+
+        this.pixiApp.view.addEventListener('wheel', (event) => {
           event.stopPropagation();
           event.preventDefault();
-          const rect = pixiApp.view.getBoundingClientRect();
+          const rect = this.pixiApp.view.getBoundingClientRect();
           const sx = event.clientX - rect.left;
           const sy = event.clientY - rect.top;
-          const oldZoomX = pixiApp.stage.scale.x || 1;
-          const oldZoomY = pixiApp.stage.scale.y || 1;
+          const oldZoomX = this.pixiApp.stage.scale.x || 1;
+          const oldZoomY = this.pixiApp.stage.scale.y || 1;
           const oldZoomAbs = Math.abs(oldZoomX) || 1;
-          const delta = event.deltaY;
-          let newZoomAbs = oldZoomAbs * 0.999 ** delta;
+          let newZoomAbs = oldZoomAbs * Math.pow(0.999, event.deltaY);
+          newZoomAbs = Math.max(0.08, Math.min(newZoomAbs, 8));
           const mirrorX = this.mapMirrorX ? -1 : 1;
           const newZoomX = mirrorX * newZoomAbs;
           const newZoomY = newZoomAbs;
-          const worldX = (sx - pixiApp.stage.position.x) / oldZoomX;
-          const worldY = (sy - pixiApp.stage.position.y) / oldZoomY;
+          const worldX = (sx - this.pixiApp.stage.position.x) / oldZoomX;
+          const worldY = (sy - this.pixiApp.stage.position.y) / oldZoomY;
           const newPosX = sx - worldX * newZoomX;
           const newPosY = sy - worldY * newZoomY;
-          pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
-
-        });
-        //*******************缂╂斁鐢诲竷*******************
-
-        //*******************FPS*******************
-        let g_Time = 0;
-        let fpsLastUpdateTs = 0;
-        let fpsDeltaSumMs = 0;
-        let fpsFrameCount = 0;
-        const fpsUpdateInterval = 200;
-        pixiApp.ticker.add((delta) => {
-          const timeNow = (new Date()).getTime();
-          const timeDiff = timeNow - g_Time;
-          g_Time = timeNow;
-          fpsDeltaSumMs += timeDiff;
-          fpsFrameCount += 1;
-          if (timeNow - fpsLastUpdateTs >= fpsUpdateInterval) {
-            const avgFps = fpsDeltaSumMs > 0 ? (fpsFrameCount * 1000 / fpsDeltaSumMs) : 0;
-            this.mapFps = Math.round(avgFps);
-            fpsDeltaSumMs = 0;
-            fpsFrameCount = 0;
-            fpsLastUpdateTs = timeNow;
+          this.pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
+          this.scheduleAdjustLabels();
+          this.scheduleLocChunkCulling();
+        }, { passive: false });
+      },
+      initFpsTicker() {
+        let lastTs = 0;
+        let deltaSum = 0;
+        let frameCount = 0;
+        const updateInterval = 200;
+        this.pixiApp.ticker.add(() => {
+          const now = Date.now();
+          if (!lastTs) {
+            lastTs = now;
+            return;
+          }
+          deltaSum += now - lastTs;
+          frameCount += 1;
+          lastTs = now;
+          if (deltaSum >= updateInterval) {
+            this.mapFps = deltaSum > 0 ? Math.round(frameCount * 1000 / deltaSum) : 0;
+            deltaSum = 0;
+            frameCount = 0;
           }
         });
-        //*******************FPS*******************
-
       },
       createMapData(map) {
-        if (this.reloadMap) {
-          this.reloadMap = false
-          pixiStageList = [map.length]//鍒濆鍖栧垪琛�
-          pixiStaMap = new Map();//閲嶇疆
-          objectsContainer.removeChildren()
-          if (tracksGraphics) { tracksGraphics.clear(); }
-          map.forEach((item,index) => {
-            pixiStageList[index] = [item.length]
-            for (let idx = 0; idx < item.length; idx++) {
-              let val = item[idx]
-              if (val.value < 0 && (val.value != -999)) {
-                continue;
-              }
+        if (!this.reloadMap) {
+          this.map = map;
+          return;
+        }
+        this.reloadMap = false;
+        this.map = map;
+        this.pixiStageList = [];
+        this.pixiStaMap = new Map();
+        this.pixiLabelList = [];
+        this.restoreHighlightedSprite();
+        this.clearLocHighlight();
+        this.clearLocChunks();
+        this.objectsContainer.removeChildren();
+        this.objectsOverlayContainer.removeChildren();
+        if (this.tracksGraphics) { this.tracksGraphics.clear(); }
 
-              let sprite = getSprite(val.value, idx * width, index * height, val, (e) => {
-                if (val.value == 4) {
-                  //绔欑偣
-                  this.openDrawerSta(val)
-                }else {
-                  //搴撲綅
-                  this.rightEvent(index, idx, e);
-                  updateColor(sprite, 0x9900ff);
-                }
-              });
+        let rows = Array.isArray(map) ? map.length : 0;
+        let maxCols = 0;
+        map.forEach((row, rowIndex) => {
+          if (!Array.isArray(row)) { return; }
+          this.pixiStageList[rowIndex] = [];
+          maxCols = Math.max(maxCols, row.length);
+          row.forEach((cell, colIndex) => {
+            if (cell.value < 0 && cell.value !== -999) { return; }
+            if (parseInt(cell.value, 10) === 0) {
+              this.pixiStageList[rowIndex][colIndex] = null;
+              return;
+            }
+            if (this.isTrackCell(cell)) {
+              cell.trackMask = this.resolveTrackMask(map, rowIndex, colIndex);
+            }
+            const sprite = this.createSprite(cell, rowIndex, colIndex);
+            if (cell.value === -999) {
+              this.objectsOverlayContainer.addChild(sprite);
+            } else {
+              this.objectsContainer.addChild(sprite);
+            }
+            this.pixiStageList[rowIndex][colIndex] = sprite;
+          });
+        });
 
-              if (val.value == 4) {
-                // 鍒涘缓鏂囨湰瀵硅薄
-                const style = new PIXI.TextStyle({
-                  fontFamily: 'Arial',
-                  fontSize: 10,
-                  fill: '#000000',
-                });
-                const text = new PIXI.Text(val.data, style);
-                text.anchor.set(0.5); // 璁剧疆鏂囨湰閿氱偣涓轰腑蹇冪偣
-                text.position.set(sprite.width / 2, sprite.height / 2); // 灏嗘枃鏈綅缃缃负Graphics瀵硅薄鐨勪腑蹇冪偣
-                // 灏嗘枃鏈璞℃坊鍔犲埌Graphics瀵硅薄涓�
-                sprite.addChild(text);
-                sprite.textObj = text;
-                pixiStaMap.set(parseInt(val.data), sprite);//绔欑偣鏁版嵁娣诲姞鍒癿ap涓�
-              }else if (val.value == 67) {
-                // 鍒涘缓鎻愬崌鏈烘枃鏈璞�
-                const style = new PIXI.TextStyle({
-                  fontFamily: 'Arial',
-                  fontSize: 10,
-                  fill: '#000000',
-                });
-                const text = new PIXI.Text(val.data, style);
-                text.anchor.set(0.5); // 璁剧疆鏂囨湰閿氱偣涓轰腑蹇冪偣
-                text.position.set(sprite.width / 2, sprite.height / 2); // 灏嗘枃鏈綅缃缃负Graphics瀵硅薄鐨勪腑蹇冪偣
-                // 灏嗘枃鏈璞℃坊鍔犲埌Graphics瀵硅薄涓�
-                sprite.addChild(text);
-                sprite.textObj = text;
-                pixiStaMap.set(parseInt(val.data), sprite);//绔欑偣鏁版嵁娣诲姞鍒癿ap涓�
-              }
+        this.mapContentSize = {
+          width: maxCols * this.cellWidth,
+          height: rows * this.cellHeight
+        };
+        this.buildLocChunks(map, this.mapContentSize.width, this.mapContentSize.height);
+        this.drawTracks(map);
+        this.applyMapTransform(true);
+      },
+      createSprite(cell, rowIndex, colIndex) {
+        const sprite = this.isTrackCell(cell)
+          ? this.createTrackSprite(this.cellWidth, this.cellHeight, cell.trackMask)
+          : new PIXI.Sprite(this.getTextureForCell(cell));
+        sprite.position.set(colIndex * this.cellWidth, rowIndex * this.cellHeight);
+        sprite.baseTint = 0xFFFFFF;
+        sprite.cellData = cell;
 
-              if (val.value == -999) {
-                pixiShuttleLockPathMap.set(val.locNo, sprite);
-                objectsContainer3.addChild(sprite);
-              }else {
-                objectsContainer.addChild(sprite);
-              }
-              pixiStageList[index][idx] = sprite
+        if (cell.value === 4 || cell.value === 67) {
+          this.attachCellLabel(sprite, cell.data);
+          if (cell.value === 4 && cell.data != null) {
+            this.pixiStaMap.set(parseInt(cell.data, 10), sprite);
+          }
+        }
+
+        if (!this.isTrackCell(cell)) {
+          sprite.interactive = true;
+          sprite.buttonMode = true;
+          sprite.on('pointerdown', () => {
+            if (cell.value === 4) {
+              this.openSiteDetail(cell, sprite);
+            } else {
+              this.openLocDetail(rowIndex, colIndex, sprite);
             }
           });
-
-          const b1 = objectsContainer.getLocalBounds();
-          const minX = b1.x;
-          const minY = b1.y;
-          const maxX = b1.x + b1.width;
-          const maxY = b1.y + b1.height;
-          const contentW = Math.max(0, maxX - minX);
-          const contentH = Math.max(0, maxY - minY);
-          mapContentSize = { width: contentW, height: contentH };
-          this.drawTracks(map);
-          this.applyMapTransform(true);
         }
-        this.map = map;
+        return sprite;
+      },
+      createTrackSprite(width, height, mask) {
+        const trackMask = mask != null ? mask : 10;
+        const key = width + '-' + height + '-' + trackMask;
+        let texture = this.pixiTrackMap.get(key);
+        if (!texture) {
+          texture = this.createTrackTexture(width, height, trackMask);
+          this.pixiTrackMap.set(key, texture);
+        }
+        return new PIXI.Sprite(texture);
+      },
+      createTrackTexture(width, height, mask) {
+        const TRACK_N = 1;
+        const TRACK_E = 2;
+        const TRACK_S = 4;
+        const TRACK_W = 8;
+        const trackMask = mask != null ? mask : (TRACK_E | TRACK_W);
+        const g = new PIXI.Graphics();
+        const size = Math.max(1, Math.min(width, height));
+        const rail = Math.max(2, Math.round(size * 0.12));
+        const gap = Math.max(4, Math.round(size * 0.38));
+        const midX = Math.round(width / 2);
+        const midY = Math.round(height / 2);
+
+        const hasN = (trackMask & TRACK_N) !== 0;
+        const hasE = (trackMask & TRACK_E) !== 0;
+        const hasS = (trackMask & TRACK_S) !== 0;
+        const hasW = (trackMask & TRACK_W) !== 0;
+
+        const hasH = hasW || hasE;
+        const hasV = hasN || hasS;
+        const isCorner = hasH && hasV && !(hasW && hasE) && !(hasN && hasS);
+        const railColor = 0x555555;
+
+        if (hasH && !isCorner) {
+          const hStart = hasW ? 0 : midX;
+          const hEnd = hasE ? width : midX;
+          const hWidth = Math.max(1, hEnd - hStart);
+          g.beginFill(railColor);
+          g.drawRect(hStart, midY - Math.round(rail / 2), hWidth, rail);
+          g.endFill();
+        }
+        if (hasV && !isCorner) {
+          const vStart = hasN ? 0 : midY;
+          const vEnd = hasS ? height : midY;
+          const vHeight = Math.max(1, vEnd - vStart);
+          g.beginFill(railColor);
+          g.drawRect(midX - Math.round(rail / 2), vStart, rail, vHeight);
+          g.endFill();
+        }
+        if (isCorner) {
+          const cornerEast = hasE;
+          const cornerSouth = hasS;
+          const centerX = cornerEast ? (width - 1) : 0;
+          const centerY = cornerSouth ? (height - 1) : 0;
+          const angleStart = (cornerEast && cornerSouth) ? Math.PI : (cornerEast ? Math.PI / 2 : (cornerSouth ? -Math.PI / 2 : 0));
+          const angleEnd = (cornerEast && cornerSouth) ? Math.PI * 1.5 : (cornerEast ? Math.PI : (cornerSouth ? 0 : Math.PI / 2));
+          const radius = Math.min(Math.abs(centerX - midX), Math.abs(centerY - midY));
+          g.lineStyle(rail, railColor, 1);
+          g.arc(centerX, centerY, radius, angleStart, angleEnd);
+          g.lineStyle(0, 0, 0);
+        }
+
+        const rt = PIXI.RenderTexture.create({ width: width, height: height });
+        this.pixiApp.renderer.render(g, rt);
+        return rt;
+      },
+      attachCellLabel(sprite, textValue) {
+        const text = new PIXI.Text(String(textValue == null ? '' : textValue), {
+          fontFamily: 'Arial',
+          fontSize: 10,
+          fill: '#1f2937',
+          align: 'center'
+        });
+        text.anchor.set(0.5);
+        text.position.set(sprite.width / 2, sprite.height / 2);
+        sprite.addChild(text);
+        sprite.textObj = text;
+        this.pixiLabelList.push(sprite);
+      },
+      getTextureForCell(cell) {
+        const value = parseInt(cell.value, 10);
+        if (value === 4) { return this.textureMap.site; }
+        if (value === 5) { return this.textureMap.charge; }
+        if (value === 67) { return this.textureMap.elevator; }
+        if (value === -999) { return this.textureMap.lock; }
+        return this.textureMap.locEmpty;
       },
       isTrackCell(cell) {
         if (!cell) { return false; }
         const type = cell.type ? String(cell.type).toLowerCase() : '';
         if (type === 'track' || type === 'crn' || type === 'dualcrn' || type === 'rgv') { return true; }
         if (cell.trackSiteNo != null) { return true; }
-        const v = parseInt(cell.value, 10);
-        if (v === 3 || v === 9) { return true; }
-        if (cell.value != null) {
-          try {
-            const obj = (typeof cell.value === 'string') ? JSON.parse(cell.value) : cell.value;
-            if (obj && (obj.trackSiteNo != null || (obj.deviceNo != null && (type === 'crn' || type === 'dualcrn' || type === 'rgv')))) {
-              return true;
-            }
-          } catch (e) {}
+        const value = parseInt(cell.value, 10);
+        return value === 3 || value === 9;
+      },
+      resolveTrackMask(map, rowIndex, colIndex) {
+        const TRACK_N = 1;
+        const TRACK_E = 2;
+        const TRACK_S = 4;
+        const TRACK_W = 8;
+        const row = Array.isArray(map) ? map[rowIndex] : null;
+        const cell = row && row[colIndex] ? row[colIndex] : null;
+        if (!this.isTrackCell(cell)) {
+          return TRACK_E | TRACK_W;
         }
-        return false;
+        let mask = 0;
+        const north = rowIndex > 0 && Array.isArray(map[rowIndex - 1]) ? map[rowIndex - 1][colIndex] : null;
+        const east = row && colIndex + 1 < row.length ? row[colIndex + 1] : null;
+        const south = rowIndex + 1 < map.length && Array.isArray(map[rowIndex + 1]) ? map[rowIndex + 1][colIndex] : null;
+        const west = row && colIndex > 0 ? row[colIndex - 1] : null;
+        if (north && this.isTrackCell(north)) { mask |= TRACK_N; }
+        if (east && this.isTrackCell(east)) { mask |= TRACK_E; }
+        if (south && this.isTrackCell(south)) { mask |= TRACK_S; }
+        if (west && this.isTrackCell(west)) { mask |= TRACK_W; }
+        return mask || (TRACK_E | TRACK_W);
       },
       drawTracks(map) {
-        if (!tracksGraphics || !Array.isArray(map)) { return; }
-        tracksGraphics.clear();
-        const railColor = 0x6c727a;
-        const railWidth = Math.max(1, Math.round(Math.min(width, height) * 0.08));
-        tracksGraphics.lineStyle(railWidth, railColor, 1);
-
-        for (let r = 0; r < map.length; r++) {
-          const row = map[r];
+        if (this.tracksGraphics) { this.tracksGraphics.clear(); }
+      },
+      buildLocChunks(map, contentW, contentH) {
+        this.clearLocChunks();
+        if (!this.pixiApp || !this.pixiApp.renderer || !this.shelvesContainer || !Array.isArray(map)) { return; }
+        const chunkSize = Math.max(256, parseInt(this.locChunkSize, 10) || 1024);
+        const chunkMap = new Map();
+        for (let rowIndex = 0; rowIndex < map.length; rowIndex += 1) {
+          const row = map[rowIndex];
           if (!Array.isArray(row)) { continue; }
-          for (let c = 0; c < row.length; c++) {
-            const cell = row[c];
-            if (!this.isTrackCell(cell)) { continue; }
-            const cx = c * width + width / 2;
-            const cy = r * height + height / 2;
-            const up = (r - 1 >= 0 && Array.isArray(map[r - 1])) ? map[r - 1][c] : null;
-            const right = (c + 1 < row.length) ? row[c + 1] : null;
-            const down = (r + 1 < map.length && Array.isArray(map[r + 1])) ? map[r + 1][c] : null;
-            const left = (c - 1 >= 0) ? row[c - 1] : null;
-            const hasN = this.isTrackCell(up);
-            const hasE = this.isTrackCell(right);
-            const hasS = this.isTrackCell(down);
-            const hasW = this.isTrackCell(left);
-            const seg = Math.min(width, height) * 0.5;
-            let drew = false;
-            if (hasN) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx, cy - seg); drew = true; }
-            if (hasE) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx + seg, cy); drew = true; }
-            if (hasS) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx, cy + seg); drew = true; }
-            if (hasW) { tracksGraphics.moveTo(cx, cy); tracksGraphics.lineTo(cx - seg, cy); drew = true; }
-            if (!drew) {
-              tracksGraphics.moveTo(cx - seg * 0.4, cy);
-              tracksGraphics.lineTo(cx + seg * 0.4, cy);
+          for (let colIndex = 0; colIndex < row.length; colIndex += 1) {
+            const cell = row[colIndex];
+            if (!cell || parseInt(cell.value, 10) !== 0) { continue; }
+            const posX = colIndex * this.cellWidth;
+            const posY = rowIndex * this.cellHeight;
+            const chunkX = Math.floor(posX / chunkSize);
+            const chunkY = Math.floor(posY / chunkSize);
+            const key = chunkX + ',' + chunkY;
+            let list = chunkMap.get(key);
+            if (!list) {
+              list = [];
+              chunkMap.set(key, list);
             }
+            list.push({
+              x: posX,
+              y: posY,
+              width: this.cellWidth,
+              height: this.cellHeight,
+              color: cell.locSts === 'F' ? 0xf05d5d : 0x5faeff
+            });
           }
         }
+
+        const chunkList = [];
+        chunkMap.forEach((cells, key) => {
+          const keyParts = key.split(',');
+          const chunkX = parseInt(keyParts[0], 10) || 0;
+          const chunkY = parseInt(keyParts[1], 10) || 0;
+          const chunkLeft = chunkX * chunkSize;
+          const chunkTop = chunkY * chunkSize;
+          const chunkWidth = Math.max(1, Math.min(chunkSize, contentW - chunkLeft));
+          const chunkHeight = Math.max(1, Math.min(chunkSize, contentH - chunkTop));
+          const graphics = new PIXI.Graphics();
+          for (let i = 0; i < cells.length; i += 1) {
+            const cell = cells[i];
+            graphics.beginFill(cell.color);
+            graphics.lineStyle(1, 0xffffff, 1);
+            graphics.drawRect(cell.x - chunkLeft, cell.y - chunkTop, cell.width, cell.height);
+            graphics.endFill();
+          }
+          const texture = this.pixiApp.renderer.generateTexture(
+            graphics,
+            PIXI.SCALE_MODES.LINEAR,
+            1,
+            new PIXI.Rectangle(0, 0, chunkWidth, chunkHeight)
+          );
+          graphics.destroy(true);
+          const sprite = new PIXI.Sprite(texture);
+          sprite.position.set(chunkLeft, chunkTop);
+          sprite._chunkBounds = {
+            x: chunkLeft,
+            y: chunkTop,
+            width: chunkWidth,
+            height: chunkHeight
+          };
+          this.shelvesContainer.addChild(sprite);
+          chunkList.push(sprite);
+        });
+        this.locChunkList = chunkList;
+        this.updateVisibleLocChunks();
+      },
+      clearLocChunks() {
+        if (this.locCullRaf) {
+          cancelAnimationFrame(this.locCullRaf);
+          this.locCullRaf = null;
+        }
+        this.locChunkList = [];
+        if (!this.shelvesContainer) { return; }
+        const children = this.shelvesContainer.removeChildren();
+        children.forEach((child) => {
+          if (child && typeof child.destroy === 'function') {
+            child.destroy({ children: true, texture: true, baseTexture: true });
+          }
+        });
+      },
+      getViewportLocalBounds(padding) {
+        if (!this.mapRoot || !this.pixiApp) { return null; }
+        const viewport = this.getViewportSize();
+        const pad = Math.max(0, Number(padding) || 0);
+        const points = [
+          new PIXI.Point(-pad, -pad),
+          new PIXI.Point(viewport.width + pad, -pad),
+          new PIXI.Point(-pad, viewport.height + pad),
+          new PIXI.Point(viewport.width + pad, viewport.height + pad)
+        ];
+        let minX = Infinity;
+        let minY = Infinity;
+        let maxX = -Infinity;
+        let maxY = -Infinity;
+        points.forEach((point) => {
+          const local = this.mapRoot.toLocal(point);
+          if (local.x < minX) { minX = local.x; }
+          if (local.y < minY) { minY = local.y; }
+          if (local.x > maxX) { maxX = local.x; }
+          if (local.y > maxY) { maxY = local.y; }
+        });
+        if (!isFinite(minX) || !isFinite(minY) || !isFinite(maxX) || !isFinite(maxY)) { return null; }
+        return { minX: minX, minY: minY, maxX: maxX, maxY: maxY };
+      },
+      updateVisibleLocChunks() {
+        if (!this.locChunkList || this.locChunkList.length === 0) { return; }
+        const localBounds = this.getViewportLocalBounds(this.locCullPadding);
+        if (!localBounds) { return; }
+        for (let i = 0; i < this.locChunkList.length; i += 1) {
+          const sprite = this.locChunkList[i];
+          const bounds = sprite && sprite._chunkBounds;
+          if (!bounds) { continue; }
+          const visible = bounds.x < localBounds.maxX &&
+            bounds.x + bounds.width > localBounds.minX &&
+            bounds.y < localBounds.maxY &&
+            bounds.y + bounds.height > localBounds.minY;
+          if (sprite.visible !== visible) {
+            sprite.visible = visible;
+          }
+        }
+      },
+      scheduleLocChunkCulling() {
+        if (this.locCullRaf) { return; }
+        this.locCullRaf = requestAnimationFrame(() => {
+          this.locCullRaf = null;
+          this.updateVisibleLocChunks();
+        });
+      },
+      handleBlankPointerTap(globalPos) {
+        if (!globalPos || !this.mapRoot || !Array.isArray(this.map) || this.map.length === 0) { return; }
+        const local = this.mapRoot.toLocal(new PIXI.Point(globalPos.x, globalPos.y));
+        const rowIndex = Math.floor(local.y / this.cellHeight);
+        const colIndex = Math.floor(local.x / this.cellWidth);
+        if (rowIndex < 0 || colIndex < 0 || rowIndex >= this.map.length) {
+          this.closeDetailPanel();
+          return;
+        }
+        const row = this.map[rowIndex];
+        if (!Array.isArray(row) || colIndex >= row.length) {
+          this.closeDetailPanel();
+          return;
+        }
+        const cell = row[colIndex];
+        if (!cell || parseInt(cell.value, 10) !== 0) {
+          this.closeDetailPanel();
+          return;
+        }
+        this.openLocDetail(rowIndex, colIndex);
+      },
+      parseLocNoMeta(locNo) {
+        if (locNo == null || locNo === '') { return { row: null, bay: null, lev: null }; }
+        const parts = String(locNo).split('-');
+        return {
+          row: parts.length > 0 ? parts[0] : null,
+          bay: parts.length > 1 ? parts[1] : null,
+          lev: parts.length > 2 ? parts[2] : null
+        };
+      },
+      openLocDetail(rowIndex, colIndex, sprite) {
+        const cell = this.map[rowIndex] && this.map[rowIndex][colIndex] ? this.map[rowIndex][colIndex] : null;
+        if (!cell) { return; }
+        const locMeta = this.parseLocNoMeta(cell.locNo);
+        if (sprite) {
+          this.highlightSprite(sprite, 0x915eff);
+        } else {
+          this.restoreHighlightedSprite();
+          this.highlightLocCell(rowIndex, colIndex);
+        }
+        this.detailType = 'loc';
+        this.detailPayload = {
+          locNo: cell.locNo,
+          locSts: cell.locSts,
+          row: cell.row != null ? cell.row : locMeta.row,
+          bay: cell.bay != null ? cell.bay : locMeta.bay,
+          lev: cell.lev != null ? cell.lev : (locMeta.lev != null ? locMeta.lev : this.currentLev)
+        };
+        this.detailPanelOpen = true;
+      },
+      openSiteDetail(cell, sprite) {
+        this.clearLocHighlight();
+        this.highlightSprite(sprite, 0xff7e47);
+        $.ajax({
+          url: baseUrl + "/console/site/detail",
+          headers: { token: localStorage.getItem('token') },
+          data: { siteId: cell.data },
+          method: 'post',
+          success: (res) => {
+            if (res.code === 200) {
+              this.detailType = 'site';
+              this.detailPayload = res.data || {};
+              this.detailPanelOpen = true;
+            } else if (res.code === 403) {
+              parent.location.href = baseUrl + "/login";
+            } else {
+              this.showMessage('error', res.msg || '绔欑偣璇︽儏鍔犺浇澶辫触');
+            }
+          }
+        });
+      },
+      highlightSprite(sprite, tint) {
+        this.restoreHighlightedSprite();
+        if (!sprite) { return; }
+        sprite.tint = tint;
+        this.highlightedSprite = sprite;
+      },
+      restoreHighlightedSprite() {
+        if (this.highlightedSprite) {
+          this.highlightedSprite.tint = this.highlightedSprite.baseTint || 0xFFFFFF;
+          this.highlightedSprite = null;
+        }
+      },
+      highlightLocCell(rowIndex, colIndex) {
+        this.clearLocHighlight();
+        this.highlightedLocCell = { rowIndex: rowIndex, colIndex: colIndex };
+        const graphic = new PIXI.Graphics();
+        graphic.lineStyle(2, 0x915eff, 0.95);
+        graphic.beginFill(0x915eff, 0.14);
+        graphic.drawRect(colIndex * this.cellWidth, rowIndex * this.cellHeight, this.cellWidth, this.cellHeight);
+        graphic.endFill();
+        this.objectsOverlayContainer.addChild(graphic);
+        this.highlightedLocGraphic = graphic;
+      },
+      clearLocHighlight() {
+        this.highlightedLocCell = null;
+        if (this.highlightedLocGraphic) {
+          if (this.highlightedLocGraphic.parent) {
+            this.highlightedLocGraphic.parent.removeChild(this.highlightedLocGraphic);
+          }
+          this.highlightedLocGraphic.destroy(true);
+          this.highlightedLocGraphic = null;
+        }
+      },
+      closeDetailPanel() {
+        this.detailPanelOpen = false;
+        this.detailType = '';
+        this.detailPayload = null;
+        this.restoreHighlightedSprite();
+        this.clearLocHighlight();
       },
       parseRotation(value) {
         const num = parseInt(value, 10);
         if (!isFinite(num)) { return 0; }
-        const rot = ((num % 360) + 360) % 360;
-        return (rot === 90 || rot === 180 || rot === 270) ? rot : 0;
+        const rotation = ((num % 360) + 360) % 360;
+        return rotation === 90 || rotation === 180 || rotation === 270 ? rotation : 0;
       },
       parseMirror(value) {
         if (value === true || value === false) { return value; }
@@ -490,11 +1222,45 @@
         const str = String(value).toLowerCase();
         return str === '1' || str === 'true' || str === 'y';
       },
+      buildMissingMapConfigList(byCode) {
+        const list = [];
+        if (!byCode[this.mapConfigCodes.rotate]) {
+          list.push({
+            name: '鍦板浘鏃嬭浆',
+            code: this.mapConfigCodes.rotate,
+            value: String(this.mapRotation || 0),
+            type: 1,
+            status: 1,
+            selectType: 'map'
+          });
+        }
+        if (!byCode[this.mapConfigCodes.mirror]) {
+          list.push({
+            name: '鍦板浘闀滃儚',
+            code: this.mapConfigCodes.mirror,
+            value: this.mapMirrorX ? '1' : '0',
+            type: 1,
+            status: 1,
+            selectType: 'map'
+          });
+        }
+        return list;
+      },
+      createMapConfigs(list) {
+        if (!Array.isArray(list) || list.length === 0) { return; }
+        list.forEach((cfg) => {
+          $.ajax({
+            url: baseUrl + "/config/add/auth",
+            headers: { token: localStorage.getItem('token') },
+            method: 'POST',
+            data: cfg
+          });
+        });
+      },
       loadMapTransformConfig() {
-        if (!window.$ || typeof baseUrl === 'undefined') { return; }
         $.ajax({
           url: baseUrl + "/config/listAll/auth",
-          headers: { 'token': localStorage.getItem('token') },
+          headers: { token: localStorage.getItem('token') },
           dataType: 'json',
           method: 'GET',
           success: (res) => {
@@ -504,210 +1270,148 @@
             }
             const byCode = {};
             res.data.forEach((item) => {
-              if (item && item.code) { byCode[item.code] = item; }
+              if (item && item.code) {
+                byCode[item.code] = item;
+              }
             });
-            const rotateCfg = byCode[this.mapConfigCodes.rotate];
-            const mirrorCfg = byCode[this.mapConfigCodes.mirror];
-            if (rotateCfg && rotateCfg.value != null) {
-              this.mapRotation = this.parseRotation(rotateCfg.value);
+            if (byCode[this.mapConfigCodes.rotate] && byCode[this.mapConfigCodes.rotate].value != null) {
+              this.mapRotation = this.parseRotation(byCode[this.mapConfigCodes.rotate].value);
             }
-            if (mirrorCfg && mirrorCfg.value != null) {
-              this.mapMirrorX = this.parseMirror(mirrorCfg.value);
+            if (byCode[this.mapConfigCodes.mirror] && byCode[this.mapConfigCodes.mirror].value != null) {
+              this.mapMirrorX = this.parseMirror(byCode[this.mapConfigCodes.mirror].value);
             }
-            if (mapContentSize && mapContentSize.width > 0 && mapContentSize.height > 0) {
+            this.createMapConfigs(this.buildMissingMapConfigList(byCode));
+            if (this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
               this.applyMapTransform(true);
             }
           }
         });
       },
+      saveMapTransformConfig() {
+        $.ajax({
+          url: baseUrl + "/config/updateBatch",
+          headers: { token: localStorage.getItem('token') },
+          data: JSON.stringify([
+            { code: this.mapConfigCodes.rotate, value: String(this.mapRotation || 0) },
+            { code: this.mapConfigCodes.mirror, value: this.mapMirrorX ? '1' : '0' }
+          ]),
+          dataType: 'json',
+          contentType: 'application/json;charset=UTF-8',
+          method: 'POST'
+        });
+      },
+      rotateMap() {
+        this.mapRotation = (this.mapRotation + 90) % 360;
+        this.applyMapTransform(true);
+        this.saveMapTransformConfig();
+      },
+      toggleMirror() {
+        this.mapMirrorX = !this.mapMirrorX;
+        this.applyMapTransform(true);
+        this.saveMapTransformConfig();
+      },
       getViewportSize() {
-        if (!pixiApp || !pixiApp.renderer) { return { width: 0, height: 0 }; }
-        const screen = pixiApp.renderer.screen;
+        if (!this.pixiApp || !this.pixiApp.renderer) { return { width: 0, height: 0 }; }
+        const screen = this.pixiApp.renderer.screen;
         if (screen && screen.width > 0 && screen.height > 0) {
           return { width: screen.width, height: screen.height };
         }
-        const rect = pixiApp.view ? pixiApp.view.getBoundingClientRect() : null;
+        const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null;
         return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
       },
+      getViewportPadding() {
+        return { top: 24, right: 24, bottom: 24, left: 24 };
+      },
       getTransformedContentSize() {
-        const size = mapContentSize || { width: 0, height: 0 };
-        const w = size.width || 0;
-        const h = size.height || 0;
-        const rot = ((this.mapRotation % 360) + 360) % 360;
-        const swap = rot === 90 || rot === 270;
-        return { width: swap ? h : w, height: swap ? w : h };
+        const width = this.mapContentSize.width || 0;
+        const height = this.mapContentSize.height || 0;
+        const rotation = ((this.mapRotation % 360) + 360) % 360;
+        const swap = rotation === 90 || rotation === 270;
+        return { width: swap ? height : width, height: swap ? width : height };
       },
       fitStageToContent() {
-        if (!pixiApp || !mapContentSize) { return; }
+        if (!this.pixiApp || !this.mapContentSize.width || !this.mapContentSize.height) { return; }
         const size = this.getTransformedContentSize();
-        const contentW = size.width || 0;
-        const contentH = size.height || 0;
-        if (contentW <= 0 || contentH <= 0) { return; }
         const viewport = this.getViewportSize();
-        const vw = viewport.width;
-        const vh = viewport.height;
-        let scale = Math.min(vw / contentW, vh / contentH) * 0.95;
+        const padding = this.getViewportPadding();
+        const availableW = Math.max(1, viewport.width - padding.left - padding.right);
+        const availableH = Math.max(1, viewport.height - padding.top - padding.bottom);
+        let scale = Math.min(availableW / size.width, availableH / size.height) * 0.95;
         if (!isFinite(scale) || scale <= 0) { scale = 1; }
-        const baseW = mapContentSize.width || contentW;
-        const baseH = mapContentSize.height || contentH;
         const mirrorX = this.mapMirrorX ? -1 : 1;
         const scaleX = scale * mirrorX;
         const scaleY = scale;
-        const posX = (vw / 2) - (baseW / 2) * scaleX;
-        const posY = (vh / 2) - (baseH / 2) * scaleY;
-        pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
+        const centerX = padding.left + availableW / 2;
+        const centerY = padding.top + availableH / 2;
+        const posX = centerX - (this.mapContentSize.width / 2) * scaleX;
+        const posY = centerY - (this.mapContentSize.height / 2) * scaleY;
+        this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
+        this.scheduleAdjustLabels();
+        this.scheduleLocChunkCulling();
       },
       applyMapTransform(fitToView) {
-        if (!mapRoot || !mapContentSize) { return; }
-        const contentW = mapContentSize.width || 0;
-        const contentH = mapContentSize.height || 0;
-        if (contentW <= 0 || contentH <= 0) { return; }
-        mapRoot.pivot.set(contentW / 2, contentH / 2);
-        mapRoot.position.set(contentW / 2, contentH / 2);
-        mapRoot.rotation = (this.mapRotation % 360) * Math.PI / 180;
-        mapRoot.scale.set(1, 1);
-        if (fitToView) { this.fitStageToContent(); }
-      },
-      rightEvent(x, y, e) {
-        this.drawerLocNo = true
-        this.drawerLocNoData =  {x:x, y: y, z: this.currentLev, locNo: this.map[x][y].locNo,
-            locSts: this.map[x][y].locSts,row:this.map[x][y].row, bay: this.map[x][y].bay, lev: this.currentLev};
-      },
-      webSocketOnOpen(e) {
-        console.log("open");
-      },
-      webSocketOnError(e) {
-        console.log(e);
-      },
-      webSocketOnMessage(e) {
-        const result = JSON.parse(e.data);
-        if (result.url == "/console/map/auth") {
-          this.setMap(JSON.parse(result.data))
-        }else if (result.url == "/console/locMap/auth") {
-          this.setMap(JSON.parse(result.data))
+        if (!this.mapRoot || !this.mapContentSize.width || !this.mapContentSize.height) { return; }
+        const contentW = this.mapContentSize.width;
+        const contentH = this.mapContentSize.height;
+        this.mapRoot.pivot.set(contentW / 2, contentH / 2);
+        this.mapRoot.position.set(contentW / 2, contentH / 2);
+        this.mapRoot.rotation = (this.mapRotation % 360) * Math.PI / 180;
+        this.mapRoot.scale.set(1, 1);
+        if (fitToView) {
+          this.fitStageToContent();
+        } else {
+          this.scheduleAdjustLabels();
+          this.scheduleLocChunkCulling();
         }
       },
-      webSocketClose(e) {
-        console.log("close");
-      },
-      sendWs(message) {
-        if (ws.readyState == WebSocket.OPEN) {
-          ws.send(message)
+      scheduleAdjustLabels() {
+        if (this.adjustLabelTimer) {
+          clearTimeout(this.adjustLabelTimer);
         }
+        this.adjustLabelTimer = setTimeout(() => {
+          this.adjustLabelScale();
+          this.adjustLabelTimer = null;
+        }, 20);
       },
-      openDrawerSta(data) {
-        let that = this
-        this.drawerSta = true;
-        $.ajax({
-          url: baseUrl + "/console/site/detail",
-          headers: {
-            'token': localStorage.getItem('token')
-          },
-          data: {
-            siteId: data.data
-          },
-          method: 'post',
-          success: function(res) {
-            //瑙f瀽鏁版嵁
-            let siteInfo = res.data;
-            if (res.code == 200) {
-              that.drawerStaData = siteInfo;
-            }
-          }
-        })
+      adjustLabelScale() {
+        if (!this.pixiApp || !this.pixiLabelList.length) { return; }
+        const scaleX = this.pixiApp.stage.scale.x || 1;
+        const scaleY = this.pixiApp.stage.scale.y || 1;
+        const scale = Math.max(Math.abs(scaleX), Math.abs(scaleY), 0.0001);
+        const mirrorSign = scaleX < 0 ? -1 : 1;
+        const inverseRotation = -this.mapRoot.rotation;
+        const visible = scale >= 0.25;
+        this.pixiLabelList.forEach((sprite) => {
+          if (!sprite || !sprite.textObj) { return; }
+          const textObj = sprite.textObj;
+          let textScale = 1 / scale;
+          textScale = Math.max(0.9, Math.min(textScale, 3));
+          textObj.scale.set(textScale * mirrorSign, textScale);
+          textObj.rotation = inverseRotation;
+          textObj.visible = visible;
+          textObj.position.set(sprite.width / 2, sprite.height / 2);
+        });
       },
-    }
-  })
-
-  function getContainer(value) {
-    let graphics = new PIXI.Graphics();
-    if (value === 0) {
-      graphics.beginFill(0x55aaff);
-    } else if (value === 3) {//姣嶈建閬�
-      graphics.beginFill(0x00ff7f);
-      graphics.visible = true;
-    } else if (value === 4) {//绔欑偣
-      graphics.beginFill(0xffff00);
-      graphics.visible = true;
-    } else if (value === 5) {//鍏呯數妗�
-      graphics.beginFill(0xffaa7f);
-      graphics.visible = true;
-    } else if (value === 9) {//杞ㄨ抗
-      graphics.beginFill(0xff0000);
-    }else if (value === 67) {//鎻愬崌鏈�
-      graphics.beginFill(0xaaffff);
-    }else if (value === -999) {//璺緞閿佸畾
-      graphics.beginFill(0xf83333);
-    }else if (value === 1000) {//婊″簱浣�
-	  graphics.beginFill(0xf83333);
-	}
-    graphics.lineStyle(1, 0xffffff, 1);
-    graphics.drawRect(0, 0, width, height);
-    graphics.endFill();
-
-    return graphics;
-  }
-
-  function getGraphics(color, width, height, x, y) {
-    let graphics = new PIXI.Graphics();
-    graphics.beginFill(color);
-    graphics.lineStyle(1, 0xffffff, 1);
-    graphics.drawRect(0, 0, width, height);
-    graphics.position.set(x, y);
-    graphics.endFill();
-    return graphics;
-  }
-
-  function getSprite(value, x, y, item, pointerDownEvent) {
-    let sprite;
-    if (value == 0) {
-      if(item.locSts == 'O') {
-        sprite = new PIXI.Sprite(graphics0);
-      }else if(item.locSts == 'F') {
-        sprite = new PIXI.Sprite(graphicsF);
-      }else {
-        sprite = new PIXI.Sprite(graphics0);
+      startContainerResizeObserve() {
+        this.resizeToContainer = () => {
+          if (!this.pixiApp || !this.mapContentSize.width || !this.mapContentSize.height) { return; }
+          this.fitStageToContent();
+        };
+        if (window.ResizeObserver) {
+          this.containerResizeObserver = new ResizeObserver(() => {
+            this.resizeToContainer();
+          });
+          this.containerResizeObserver.observe(this.$refs.shell);
+        } else {
+          window.addEventListener('resize', this.resizeToContainer);
+        }
       }
-    } else if (value == 3) {
-      sprite = new PIXI.Sprite(graphics3);
-    } else if (value == 4) {
-      sprite = new PIXI.Sprite(graphics4);
-    } else if (value == 5) {
-      sprite = new PIXI.Sprite(graphics5);
-    } else if (value == 9) {
-      sprite = new PIXI.Sprite(graphics9);
-    } else if (value == 67) {
-      sprite = new PIXI.Sprite(graphics67);
-    } else if (value == -999) {
-      sprite = new PIXI.Sprite(graphicsLock);
-    } else {
-      sprite = new PIXI.Sprite(graphics0);
     }
-    sprite.position.set(x, y);
-    const type = item && item.type ? String(item.type).toLowerCase() : '';
-    const numVal = parseInt(value, 10);
-    const isTrackCell = numVal === 3 || numVal === 9 || type === 'track' || type === 'crn' || type === 'dualcrn' || type === 'rgv';
-    if (!isTrackCell) {
-      sprite.interactive = true; // 蹇呴』瑕佽缃墠鑳芥帴鏀朵簨浠�
-      sprite.buttonMode = true; // 璁╁厜鏍囧湪hover鏃跺彉涓烘墜鍨嬫寚閽�
-      sprite.on('pointerdown', (e) => {
-        pointerDownEvent(e)
-      })
-    }
+  });
 
-    return sprite;
-  }
-
-  /**
-   * 鏇存柊棰滆壊
-   */
-  function updateColor(sprite, color) {
-    // graphics.clear()
-    // graphics.beginFill(color);
-    // graphics.drawRect(0, 0, width, height);
-    sprite.tint = color;
-  }
-
+  new Vue({
+    el: '#app'
+  });
 </script>
 </body>
 </html>

--
Gitblit v1.9.1