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

---
 src/main/webapp/components/MapCanvas.js |  562 ++++++++++++++++++++++++++++++++++++++++++++++++--------
 1 files changed, 480 insertions(+), 82 deletions(-)

diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index 276827b..05d703b 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -2,7 +2,7 @@
   template: `
     <div style="width: 100%; height: 100%; position: relative;">
       <div ref="pixiView" style="position: absolute; inset: 0;"></div>
-      <div style="position: absolute; top: 12px; left: 14px; z-index: 30; pointer-events: none; max-width: 52%;">
+      <div :style="cycleCapacityPanelStyle()">
         <div style="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;">
           <div v-for="item in cycleCapacity.loopList"
                :key="'loop-' + item.loopNo"
@@ -24,14 +24,31 @@
         <div :style="mapToolFpsStyle()">FPS {{ mapFps }}</div>
         <button type="button" @click="toggleMapToolPanel" :style="mapToolToggleStyle(showMapToolPanel)">{{ showMapToolPanel ? '鏀惰捣鎿嶄綔' : '鍦板浘鎿嶄綔' }}</button>
         <div v-show="showMapToolPanel" :style="mapToolBarStyle()">
-          <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '闅愯棌绔欑偣鏂瑰悜' : '鏄剧ず绔欑偣鏂瑰悜' }}</button>
-          <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">鏃嬭浆</button>
-          <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
+          <div :style="mapToolRowStyle()">
+            <button type="button" @click="toggleStationDirection" :style="mapToolButtonStyle(showStationDirection)">{{ showStationDirection ? '闅愯棌绔欑偣鏂瑰悜' : '鏄剧ず绔欑偣鏂瑰悜' }}</button>
+            <button type="button" @click="rotateMap" :style="mapToolButtonStyle(false)">鏃嬭浆</button>
+            <button type="button" @click="toggleMirror" :style="mapToolButtonStyle(mapMirrorX)">{{ mapMirrorX ? '鍙栨秷闀滃儚' : '闀滃儚' }}</button>
+          </div>
+          <div :style="mapToolRowStyle()">
+            <button type="button" @click="openStationColorConfigPage" :style="mapToolButtonStyle(false)">绔欑偣棰滆壊</button>
+          </div>
+          <div v-if="levList && levList.length > 1" :style="mapToolFloorSectionStyle()">
+            <div :style="mapToolSectionLabelStyle()">妤煎眰</div>
+            <div :style="mapToolFloorListStyle()">
+              <button
+                v-for="floor in levList"
+                :key="'tool-floor-' + floor"
+                type="button"
+                @click="selectFloorFromTool(floor)"
+                :style="mapToolFloorButtonStyle(currentLev == floor)"
+              >{{ floor }}F</button>
+            </div>
+          </div>
         </div>
       </div>
     </div>
   `,
-  props: ['lev', 'crnParam', 'rgvParam', 'devpParam', 'highlightOnParamChange'],
+  props: ['lev', 'levList', 'crnParam', 'rgvParam', 'devpParam', 'stationTaskRange', 'highlightOnParamChange', 'viewportPadding', 'hudPadding'],
   data() {
     return {
       map: [],
@@ -63,6 +80,10 @@
       pixiDevpTextureMap: new Map(),
       pixiCrnColorTextureMap: new Map(),
       pixiRgvColorTextureMap: new Map(),
+      shelfChunkList: [],
+      shelfChunkSize: 2048,
+      shelfCullPadding: 160,
+      shelfCullRaf: null,
       crnList: [],
       dualCrnList: [],
       rgvList: [],
@@ -111,7 +132,18 @@
       hoverLoopNo: null,
       hoverLoopStationIdSet: new Set(),
       loopHighlightColor: 0xfff34d,
-      stationDirectionColor: 0xff5a36
+      stationDirectionColor: 0xff5a36,
+      stationStatusColors: {
+        'site-auto': 0x78ff81,
+        'site-auto-run': 0xfa51f6,
+        'site-auto-id': 0xc4c400,
+        'site-auto-run-id': 0x30bffc,
+        'site-enable-in': 0x18c7b8,
+        'site-unauto': 0xb8b8b8,
+        'machine-pakin': 0x30bffc,
+        'machine-pakout': 0x97b400,
+        'site-run-block': 0xe69138
+      }
     }
   },
     mounted() {
@@ -119,6 +151,7 @@
     this.createMap();
     this.startContainerResizeObserve();
     this.loadMapTransformConfig();
+    this.loadStationColorConfig();
     this.loadLocList();
     this.connectWs();
     
@@ -138,6 +171,8 @@
     if (this.timer) { clearInterval(this.timer); }
 
     if (this.hoverRaf) { cancelAnimationFrame(this.hoverRaf); this.hoverRaf = null; }
+    if (this.shelfCullRaf) { cancelAnimationFrame(this.shelfCullRaf); this.shelfCullRaf = null; }
+    if (window.gsap && this.pixiApp && this.pixiApp.stage) { window.gsap.killTweensOf(this.pixiApp.stage.position); }
     if (this.pixiApp) { this.pixiApp.destroy(true, { children: true }); }
     if (this.containerResizeObserver) { this.containerResizeObserver.disconnect(); this.containerResizeObserver = null; }
     window.removeEventListener('resize', this.resizeToContainer);
@@ -147,6 +182,14 @@
   watch: {
     lev(newLev) {
       if (newLev != null) { this.changeFloor(newLev); }
+    },
+    viewportPadding: {
+      deep: true,
+      handler(newVal, oldVal) {
+        if (this.mapContentSize && this.mapContentSize.width > 0 && this.mapContentSize.height > 0) {
+          this.adjustStageForViewportPadding(oldVal, newVal);
+        }
+      }
     },
     crnParam: {
       deep: true,
@@ -192,17 +235,65 @@
     }
   },
   methods: {
+    cycleCapacityPanelStyle() {
+      const hud = this.hudPadding || {};
+      const left = Math.max(14, Number(hud.left) || 0);
+      const rightReserve = 220;
+      return {
+        position: 'absolute',
+        top: '12px',
+        left: left + 'px',
+        zIndex: 30,
+        pointerEvents: 'none',
+        maxWidth: 'calc(100% - ' + (left + rightReserve) + 'px)'
+      };
+    },
     mapToolBarStyle() {
       return {
         display: 'flex',
+        flexDirection: 'column',
         gap: '8px',
-        alignItems: 'center',
+        alignItems: 'stretch',
         padding: '7px',
         borderRadius: '14px',
         background: 'rgba(255, 255, 255, 0.72)',
         border: '1px solid rgba(160, 180, 205, 0.3)',
         boxShadow: '0 8px 20px rgba(37, 64, 97, 0.08)',
         backdropFilter: 'blur(4px)'
+      };
+    },
+    mapToolRowStyle() {
+      return {
+        display: 'flex',
+        gap: '8px',
+        alignItems: 'center',
+        justifyContent: 'flex-end',
+        flexWrap: 'wrap'
+      };
+    },
+    mapToolFloorSectionStyle() {
+      return {
+        display: 'flex',
+        flexDirection: 'column',
+        gap: '4px',
+        paddingTop: '6px',
+        borderTop: '1px solid rgba(160, 180, 205, 0.22)'
+      };
+    },
+    mapToolSectionLabelStyle() {
+      return {
+        color: '#6a7f95',
+        fontSize: '10px',
+        lineHeight: '14px',
+        textAlign: 'right'
+      };
+    },
+    mapToolFloorListStyle() {
+      return {
+        display: 'flex',
+        flexDirection: 'column',
+        gap: '4px',
+        alignItems: 'stretch'
       };
     },
     mapToolFpsStyle() {
@@ -252,8 +343,30 @@
         whiteSpace: 'nowrap'
       };
     },
+    mapToolFloorButtonStyle(active) {
+      return {
+        appearance: 'none',
+        border: '1px solid ' + (active ? 'rgba(96, 132, 170, 0.36)' : 'rgba(160, 180, 205, 0.3)'),
+        background: active ? 'rgba(235, 243, 251, 0.96)' : 'rgba(255, 255, 255, 0.88)',
+        color: active ? '#27425c' : '#4d647d',
+        minWidth: '44px',
+        height: '26px',
+        padding: '0 10px',
+        borderRadius: '8px',
+        fontSize: '11px',
+        lineHeight: '26px',
+        cursor: 'pointer',
+        fontWeight: '700',
+        boxShadow: active ? '0 4px 12px rgba(37, 64, 97, 0.08)' : 'none',
+        whiteSpace: 'nowrap'
+      };
+    },
     toggleMapToolPanel() {
       this.showMapToolPanel = !this.showMapToolPanel;
+    },
+    selectFloorFromTool(lev) {
+      if (lev == null || lev === this.currentLev) { return; }
+      this.$emit('switch-lev', lev);
     },
     createMap() {
       this.pixiApp = new PIXI.Application({ backgroundColor: 0xF5F7F9, antialias: false, powerPreference: 'high-performance', autoDensity: true, resolution: Math.min(window.devicePixelRatio || 1, 2) });
@@ -270,9 +383,8 @@
       this.objectsContainer2 = new PIXI.Container();
       this.tracksContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false });
       this.tracksGraphics = new PIXI.Graphics();
-      this.shelvesContainer = new PIXI.ParticleContainer(10000, { scale: true, position: true, rotation: false, uvs: false, alpha: false });
+      this.shelvesContainer = new PIXI.Container();
       this.tracksContainer.autoResize = true;
-      this.shelvesContainer.autoResize = true;
       this.mapRoot = new PIXI.Container();
       this.pixiApp.stage.addChild(this.mapRoot);
       this.mapRoot.addChild(this.tracksGraphics);
@@ -320,6 +432,7 @@
           const dx = globalPos.x - mouseDownPoint[0];
           const dy = globalPos.y - mouseDownPoint[1];
           this.pixiApp.stage.position.set(stageOriginalPos[0] + dx, stageOriginalPos[1] + dy);
+          this.scheduleShelfChunkCulling();
         }
       });
       this.pixiApp.renderer.plugins.interaction.on('pointerup', () => { touchBlank = false; });
@@ -345,7 +458,8 @@
         const newPosX = sx - worldX * newZoomX;
         const newPosY = sy - worldY * newZoomY;
         this.pixiApp.stage.setTransform(newPosX, newPosY, newZoomX, newZoomY, 0, 0, 0, 0, 0);
-          this.scheduleAdjustLabels();
+        this.scheduleAdjustLabels();
+        this.scheduleShelfChunkCulling();
       });
       //*******************缂╂斁鐢诲竷*******************
 
@@ -388,6 +502,67 @@
       const rect = this.pixiApp.view ? this.pixiApp.view.getBoundingClientRect() : null;
       return { width: rect ? rect.width : 0, height: rect ? rect.height : 0 };
     },
+    getViewportPadding() {
+      return this.normalizeViewportPadding(this.viewportPadding);
+    },
+    normalizeViewportPadding(padding) {
+      const source = padding || {};
+      const normalize = (value) => {
+        const num = Number(value);
+        return isFinite(num) && num > 0 ? num : 0;
+      };
+      return {
+        top: normalize(source.top),
+        right: normalize(source.right),
+        bottom: normalize(source.bottom),
+        left: normalize(source.left)
+      };
+    },
+    getViewportCenter(viewport, padding) {
+      const normalized = this.normalizeViewportPadding(padding);
+      const availableW = Math.max(1, viewport.width - normalized.left - normalized.right);
+      const availableH = Math.max(1, viewport.height - normalized.top - normalized.bottom);
+      return {
+        x: normalized.left + availableW / 2,
+        y: normalized.top + availableH / 2
+      };
+    },
+    adjustStageForViewportPadding(oldPadding, newPadding) {
+      if (!this.pixiApp || !this.pixiApp.stage) { return; }
+      const viewport = this.getViewportSize();
+      if (viewport.width <= 0 || viewport.height <= 0) { return; }
+      const prevCenter = this.getViewportCenter(viewport, oldPadding);
+      const nextCenter = this.getViewportCenter(viewport, newPadding);
+      const deltaX = nextCenter.x - prevCenter.x;
+      const deltaY = nextCenter.y - prevCenter.y;
+      if (Math.abs(deltaX) < 0.5 && Math.abs(deltaY) < 0.5) {
+        return;
+      }
+      const targetX = this.pixiApp.stage.position.x + deltaX;
+      const targetY = this.pixiApp.stage.position.y + deltaY;
+      if (window.gsap) {
+        window.gsap.killTweensOf(this.pixiApp.stage.position);
+        window.gsap.to(this.pixiApp.stage.position, {
+          x: targetX,
+          y: targetY,
+          duration: 0.18,
+          ease: 'power1.out',
+          onUpdate: () => {
+            this.scheduleAdjustLabels();
+            this.scheduleShelfChunkCulling();
+          },
+          onComplete: () => {
+            this.scheduleAdjustLabels();
+            this.scheduleShelfChunkCulling();
+          }
+        });
+        return;
+      }
+      this.pixiApp.stage.position.x = targetX;
+      this.pixiApp.stage.position.y = targetY;
+      this.scheduleAdjustLabels();
+      this.scheduleShelfChunkCulling();
+    },
     resizeToContainer() {
       const w = this.$el.clientWidth || 0;
       const h = this.$el.clientHeight || 0;
@@ -419,7 +594,7 @@
       this.objectsContainer2.removeChildren();
       if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
       if (this.tracksGraphics) { this.tracksGraphics.clear(); }
-      if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); }
+      this.clearShelfChunks();
       this.crnList = [];
       this.dualCrnList = [];
       this.rgvList = [];
@@ -448,7 +623,7 @@
       this.objectsContainer2.removeChildren();
       if (this.tracksContainer) { this.tracksContainer.removeChildren(); }
       if (this.tracksGraphics) { this.tracksGraphics.clear(); }
-      if (this.shelvesContainer) { this.shelvesContainer.removeChildren(); }
+      this.clearShelfChunks();
       this.crnList = [];
       this.dualCrnList = [];
       this.rgvList = [];
@@ -573,15 +748,12 @@
             this.collectTrackItem(val);
             continue;
           }
+          if (val.type === 'shelf') { continue; }
           let sprite = this.getSprite(val, (e) => {
             //鍥炶皟
           });
           if (sprite == null) { continue; }
-          if (sprite._kind === 'shelf') {
-            this.shelvesContainer.addChild(sprite);
-          } else {
-            this.objectsContainer.addChild(sprite);
-          }
+          this.objectsContainer.addChild(sprite);
           this.pixiStageList[index][idx] = sprite;
         }
       });
@@ -698,6 +870,7 @@
         }
       }
       this.mapContentSize = { width: contentW, height: contentH };
+      this.buildShelfChunks(map, contentW, contentH);
       this.applyMapTransform(true);
       this.map = map;
       this.isSwitchingFloor = false;
@@ -743,7 +916,6 @@
       if (!sites) { return; }
       sites.forEach((item) => {
         let id = item.siteId != null ? item.siteId : item.stationId;
-        let status = item.siteStatus != null ? item.siteStatus : item.stationStatus;
         let workNo = item.workNo != null ? item.workNo : item.taskNo;
         if (id == null) { return; }
         let sta = this.pixiStaMap.get(parseInt(id));
@@ -754,7 +926,7 @@
           sta.statusObj = null;
           if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); }
         }
-        this.setStationBaseColor(sta, this.getStationStatusColor(status));
+        this.setStationBaseColor(sta, this.getStationStatusColor(this.resolveStationStatus(item)));
       });
     },
     getCrnInfo() {
@@ -1285,15 +1457,47 @@
       return brightness > 150 ? '#000000' : '#ffffff';
     },
     getStationStatusColor(status) {
-      if (status === "site-auto") { return 0x78ff81; }
-      if (status === "site-auto-run") { return 0xfa51f6; }
-      if (status === "site-auto-id") { return 0xc4c400; }
-      if (status === "site-auto-run-id") { return 0x30bffc; }
-      if (status === "site-unauto") { return 0xb8b8b8; }
-      if (status === "machine-pakin") { return 0x30bffc; }
-      if (status === "machine-pakout") { return 0x97b400; }
-      if (status === "site-run-block") { return 0xe69138; }
-      return 0xb8b8b8;
+      const colorMap = this.stationStatusColors || this.getDefaultStationStatusColors();
+      if (status && colorMap[status] != null) { return colorMap[status]; }
+      return colorMap['site-unauto'] != null ? colorMap['site-unauto'] : 0xb8b8b8;
+    },
+    resolveStationStatus(item) {
+      const status = item && (item.siteStatus != null ? item.siteStatus : item.stationStatus);
+      const taskNo = this.parseStationTaskNo(item && (item.workNo != null ? item.workNo : item.taskNo));
+      const autoing = !!(item && item.autoing);
+      const loading = !!(item && item.loading);
+      const runBlock = !!(item && item.runBlock);
+      const enableIn = !!(item && item.enableIn);
+      if (taskNo === 9998 || enableIn) { return 'site-enable-in'; }
+      if (autoing && loading && taskNo > 0 && !runBlock) {
+        const taskClass = this.getStationTaskClass(taskNo);
+        if (taskClass) { return taskClass; }
+      }
+      if (status) { return status; }
+      if (autoing && loading && taskNo > 0 && runBlock) { return 'site-run-block'; }
+      if (autoing && loading && taskNo > 0) { return 'site-auto-run-id'; }
+      if (autoing && loading) { return 'site-auto-run'; }
+      if (autoing && taskNo > 0) { return 'site-auto-id'; }
+      if (autoing) { return 'site-auto'; }
+      return 'site-unauto';
+    },
+    parseStationTaskNo(value) {
+      const taskNo = parseInt(value, 10);
+      return isNaN(taskNo) ? 0 : taskNo;
+    },
+    getStationTaskClass(taskNo) {
+      if (!(taskNo > 0)) { return null; }
+      const range = this.stationTaskRange || {};
+      if (this.isTaskNoInRange(taskNo, range.inbound)) { return 'machine-pakin'; }
+      if (this.isTaskNoInRange(taskNo, range.outbound)) { return 'machine-pakout'; }
+      return null;
+    },
+    isTaskNoInRange(taskNo, range) {
+      if (!range) { return false; }
+      const start = parseInt(range.start, 10);
+      const end = parseInt(range.end, 10);
+      if (isNaN(start) || isNaN(end)) { return false; }
+      return taskNo >= start && taskNo <= end;
     },
     getCrnStatusColor(status) {
       if (status === "machine-auto") { return 0x21BA45; }
@@ -1837,6 +2041,135 @@
         }
       }
     },
+    clearShelfChunks() {
+      if (this.shelfCullRaf) {
+        cancelAnimationFrame(this.shelfCullRaf);
+        this.shelfCullRaf = null;
+      }
+      this.shelfChunkList = [];
+      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 });
+        }
+      });
+    },
+    buildShelfChunks(map, contentW, contentH) {
+      this.clearShelfChunks();
+      if (!this.pixiApp || !this.pixiApp.renderer || !this.shelvesContainer || !Array.isArray(map)) { return; }
+      const chunkSize = Math.max(256, parseInt(this.shelfChunkSize, 10) || 2048);
+      const chunkMap = new Map();
+      for (let r = 0; r < map.length; r++) {
+        const row = map[r];
+        if (!row) { continue; }
+        for (let c = 0; c < row.length; c++) {
+          const cell = row[c];
+          if (!cell || cell.type !== 'shelf' || cell.type === 'merge') { continue; }
+          const startChunkX = Math.floor(cell.posX / chunkSize);
+          const endChunkX = Math.floor((cell.posX + Math.max(1, cell.width) - 0.01) / chunkSize);
+          const startChunkY = Math.floor(cell.posY / chunkSize);
+          const endChunkY = Math.floor((cell.posY + Math.max(1, cell.height) - 0.01) / chunkSize);
+          for (let chunkY = startChunkY; chunkY <= endChunkY; chunkY++) {
+            for (let chunkX = startChunkX; chunkX <= endChunkX; chunkX++) {
+              const key = chunkX + ',' + chunkY;
+              let list = chunkMap.get(key);
+              if (!list) {
+                list = [];
+                chunkMap.set(key, list);
+              }
+              list.push(cell);
+            }
+          }
+        }
+      }
+
+      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();
+        graphics.beginFill(0xb6e2e2);
+        graphics.lineStyle(1, 0xffffff, 1);
+        for (let i = 0; i < cells.length; i++) {
+          const cell = cells[i];
+          graphics.drawRect(cell.posX - chunkLeft, cell.posY - 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.shelfChunkList = chunkList;
+      this.updateVisibleShelfChunks();
+    },
+    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 };
+    },
+    updateVisibleShelfChunks() {
+      if (!this.shelfChunkList || this.shelfChunkList.length === 0) { return; }
+      const localBounds = this.getViewportLocalBounds(this.shelfCullPadding);
+      if (!localBounds) { return; }
+      for (let i = 0; i < this.shelfChunkList.length; i++) {
+        const sprite = this.shelfChunkList[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;
+        }
+      }
+    },
+    scheduleShelfChunkCulling() {
+      if (this.shelfCullRaf) { return; }
+      this.shelfCullRaf = requestAnimationFrame(() => {
+        this.shelfCullRaf = null;
+        this.updateVisibleShelfChunks();
+      });
+    },
     findIndexByOffsets(offsets, sizes, value) {
       if (!offsets || !sizes || offsets.length === 0) { return -1; }
       for (let i = 0; i < offsets.length; i++) {
@@ -2084,6 +2417,23 @@
       this.applyMapTransform(true);
       this.saveMapTransformConfig();
     },
+    openStationColorConfigPage() {
+      if (typeof window === 'undefined') { return; }
+      const url = (typeof baseUrl !== 'undefined' ? baseUrl : '') + '/views/watch/stationColorConfig.html';
+      const layerInstance = (window.top && window.top.layer) || window.layer;
+      if (layerInstance && typeof layerInstance.open === 'function') {
+        layerInstance.open({
+          type: 2,
+          title: '绔欑偣棰滆壊閰嶇疆',
+          maxmin: true,
+          area: ['980px', '760px'],
+          shadeClose: false,
+          content: url
+        });
+        return;
+      }
+      window.open(url, '_blank');
+    },
     parseRotation(value) {
       const num = parseInt(value, 10);
       if (!isFinite(num)) { return 0; }
@@ -2095,6 +2445,98 @@
       if (value == null) { return false; }
       const str = String(value).toLowerCase();
       return str === '1' || str === 'true' || str === 'y';
+    },
+    getDefaultStationStatusColors() {
+      return {
+        'site-auto': 0x78ff81,
+        'site-auto-run': 0xfa51f6,
+        'site-auto-id': 0xc4c400,
+        'site-auto-run-id': 0x30bffc,
+        'site-enable-in': 0x18c7b8,
+        'site-unauto': 0xb8b8b8,
+        'machine-pakin': 0x30bffc,
+        'machine-pakout': 0x97b400,
+        'site-run-block': 0xe69138
+      };
+    },
+    parseColorConfigValue(value, fallback) {
+      if (typeof value === 'number' && isFinite(value)) {
+        return value;
+      }
+      const str = String(value == null ? '' : value).trim();
+      if (!str) { return fallback; }
+      if (/^#[0-9a-fA-F]{6}$/.test(str)) { return parseInt(str.slice(1), 16); }
+      if (/^#[0-9a-fA-F]{3}$/.test(str)) {
+        const expanded = str.charAt(1) + str.charAt(1) + str.charAt(2) + str.charAt(2) + str.charAt(3) + str.charAt(3);
+        return parseInt(expanded, 16);
+      }
+      if (/^0x[0-9a-fA-F]{6}$/i.test(str)) { return parseInt(str.slice(2), 16); }
+      if (/^[0-9]+$/.test(str)) {
+        const num = parseInt(str, 10);
+        return isNaN(num) ? fallback : num;
+      }
+      return fallback;
+    },
+    loadStationColorConfig() {
+      if (!window.$ || typeof baseUrl === 'undefined') { return; }
+      $.ajax({
+        url: baseUrl + "/watch/stationColor/config/auth",
+        headers: { 'token': localStorage.getItem('token') },
+        dataType: 'json',
+        method: 'GET',
+        success: (res) => {
+          if (!res || res.code !== 200 || !res.data) {
+            if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; }
+            return;
+          }
+          this.applyStationColorConfigPayload(res.data);
+        }
+      });
+    },
+    applyStationColorConfigPayload(data) {
+      const defaults = this.getDefaultStationStatusColors();
+      const nextColors = Object.assign({}, defaults);
+      const items = Array.isArray(data.items) ? data.items : [];
+      items.forEach((item) => {
+        if (!item || !item.status || defaults[item.status] == null) { return; }
+        nextColors[item.status] = this.parseColorConfigValue(item.color, defaults[item.status]);
+      });
+      this.stationStatusColors = nextColors;
+    },
+    buildMissingMapConfigList(byCode) {
+      const createList = [];
+      if (!byCode[this.mapConfigCodes.rotate]) {
+        createList.push({
+          name: '鍦板浘鏃嬭浆',
+          code: this.mapConfigCodes.rotate,
+          value: String(this.mapRotation || 0),
+          type: 1,
+          status: 1,
+          selectType: 'map'
+        });
+      }
+      if (!byCode[this.mapConfigCodes.mirror]) {
+        createList.push({
+          name: '鍦板浘闀滃儚',
+          code: this.mapConfigCodes.mirror,
+          value: this.mapMirrorX ? '1' : '0',
+          type: 1,
+          status: 1,
+          selectType: 'map'
+        });
+      }
+      return createList;
+    },
+    createMapConfigs(createList) {
+      if (!window.$ || typeof baseUrl === 'undefined' || !Array.isArray(createList) || createList.length === 0) { return; }
+      createList.forEach((cfg) => {
+        $.ajax({
+          url: baseUrl + "/config/add/auth",
+          headers: { 'token': localStorage.getItem('token') },
+          method: 'POST',
+          data: cfg
+        });
+      });
     },
     loadMapTransformConfig() {
       if (!window.$ || typeof baseUrl === 'undefined') { return; }
@@ -2120,45 +2562,11 @@
           if (mirrorCfg && mirrorCfg.value != null) {
             this.mapMirrorX = this.parseMirror(mirrorCfg.value);
           }
-          if (rotateCfg == null || mirrorCfg == null) {
-            this.createMapTransformConfigIfMissing(rotateCfg, mirrorCfg);
-          }
+          this.createMapConfigs(this.buildMissingMapConfigList(byCode));
           if (this.mapContentSize && this.mapContentSize.width > 0) {
             this.applyMapTransform(true);
           }
         }
-      });
-    },
-    createMapTransformConfigIfMissing(rotateCfg, mirrorCfg) {
-      if (!window.$ || typeof baseUrl === 'undefined') { return; }
-      const createList = [];
-      if (!rotateCfg) {
-        createList.push({
-          name: '鍦板浘鏃嬭浆',
-          code: this.mapConfigCodes.rotate,
-          value: String(this.mapRotation || 0),
-          type: 1,
-          status: 1,
-          selectType: 'map'
-        });
-      }
-      if (!mirrorCfg) {
-        createList.push({
-          name: '鍦板浘闀滃儚',
-          code: this.mapConfigCodes.mirror,
-          value: this.mapMirrorX ? '1' : '0',
-          type: 1,
-          status: 1,
-          selectType: 'map'
-        });
-      }
-      createList.forEach((cfg) => {
-        $.ajax({
-          url: baseUrl + "/config/add/auth",
-          headers: { 'token': localStorage.getItem('token') },
-          method: 'POST',
-          data: cfg
-        });
       });
     },
     saveMapTransformConfig() {
@@ -2193,15 +2601,20 @@
       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, vw - padding.left - padding.right);
+      const availableH = Math.max(1, vh - padding.top - padding.bottom);
+      let scale = Math.min(availableW / contentW, availableH / contentH) * 0.95;
       if (!isFinite(scale) || scale <= 0) { scale = 1; }
       const baseW = this.mapContentSize.width || contentW;
       const baseH = this.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;
+      const centerX = padding.left + availableW / 2;
+      const centerY = padding.top + availableH / 2;
+      const posX = centerX - (baseW / 2) * scaleX;
+      const posY = centerY - (baseH / 2) * scaleY;
       this.pixiApp.stage.setTransform(posX, posY, scaleX, scaleY, 0, 0, 0, 0, 0);
     },
     applyMapTransform(fitToView) {
@@ -2215,6 +2628,7 @@
       this.mapRoot.scale.set(1, 1);
       if (fitToView) { this.fitStageToContent(); }
       this.scheduleAdjustLabels();
+      this.scheduleShelfChunkCulling();
     },
     scheduleAdjustLabels() {
       if (this.adjustLabelTimer) { clearTimeout(this.adjustLabelTimer); }
@@ -2226,22 +2640,6 @@
     }
   }
 });
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
 
 
 

--
Gitblit v1.9.1