From 0b1cc54f62919c5face9794208a0d24da0f97293 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期三, 04 三月 2026 13:53:50 +0800
Subject: [PATCH] #

---
 src/main/webapp/components/MapCanvas.js                                     |  158 +++++++++++-
 src/main/java/com/zy/asrs/controller/ConsoleController.java                 |    7 
 src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java             |   29 ++
 src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java |  466 +++++++++++++++++++++++++++++++++++
 src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java                          |    3 
 src/main/java/com/zy/asrs/service/StationCycleCapacityService.java          |   13 +
 src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java           |   20 +
 src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java                 |   28 ++
 8 files changed, 709 insertions(+), 15 deletions(-)

diff --git a/src/main/java/com/zy/asrs/controller/ConsoleController.java b/src/main/java/com/zy/asrs/controller/ConsoleController.java
index a786439..85d3af7 100644
--- a/src/main/java/com/zy/asrs/controller/ConsoleController.java
+++ b/src/main/java/com/zy/asrs/controller/ConsoleController.java
@@ -59,6 +59,8 @@
     private LocMastService locMastService;
     @Autowired
     private BasMapService basMapService;
+    @Autowired
+    private StationCycleCapacityService stationCycleCapacityService;
 
     @PostMapping("/system/running/status")
     @ManagerAuth(memo = "绯荤粺杩愯鐘舵��")
@@ -266,6 +268,11 @@
         return R.ok().add(vos);
     }
 
+    @GetMapping("/latest/data/station/cycle/capacity")
+    public R stationCycleCapacity() {
+        return R.ok().add(stationCycleCapacityService.getLatestSnapshot());
+    }
+
     // @PostMapping("/latest/data/barcode")
     // @ManagerAuth(memo = "鏉$爜鎵弿浠疄鏃舵暟鎹�")
     // public R barcodeLatestData(){
diff --git a/src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java b/src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java
new file mode 100644
index 0000000..fbcfda4
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/vo/StationCycleCapacityVo.java
@@ -0,0 +1,29 @@
+package com.zy.asrs.domain.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+@Data
+public class StationCycleCapacityVo {
+
+    // 寰幆鍦堟槑缁�
+    private List<StationCycleLoopVo> loopList = new ArrayList<>();
+
+    // 寰幆鍦堟暟閲�
+    private Integer loopCount = 0;
+
+    // 寰幆鍦堢珯鐐规�绘暟
+    private Integer totalStationCount = 0;
+
+    // 寰幆鍦堜腑鏈変换鍔$珯鐐规�绘暟
+    private Integer taskStationCount = 0;
+
+    // 褰撳墠鎵胯浇閲忥紙0-1锛夛細褰撳墠浠诲姟鏁� / 寰幆鍦堟�荤珯鐐规暟
+    private Double currentLoad = 0.0;
+
+    // 鏈�鏂板埛鏂版椂闂�
+    private Date refreshTime;
+}
diff --git a/src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java b/src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java
new file mode 100644
index 0000000..0c84768
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/vo/StationCycleLoopVo.java
@@ -0,0 +1,28 @@
+package com.zy.asrs.domain.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class StationCycleLoopVo {
+
+    // 寰幆鍦堝簭鍙凤紙浠�1寮�濮嬶級
+    private Integer loopNo;
+
+    // 寰幆鍦堝唴绔欑偣缂栧彿
+    private List<Integer> stationIdList = new ArrayList<>();
+
+    // 寰幆鍦堝唴瀛樺湪鐨勫伐浣滃彿
+    private List<Integer> workNoList = new ArrayList<>();
+
+    // 寰幆鍦堢珯鐐规�绘暟
+    private Integer stationCount = 0;
+
+    // 寰幆鍦堝唴鏈変换鍔$珯鐐规暟
+    private Integer taskCount = 0;
+
+    // 褰撳墠鎵胯浇閲忥紙0-1锛夛細褰撳墠浠诲姟鏁� / 褰撳墠寰幆鍦堟�荤珯鐐规暟
+    private Double currentLoad = 0.0;
+}
diff --git a/src/main/java/com/zy/asrs/service/StationCycleCapacityService.java b/src/main/java/com/zy/asrs/service/StationCycleCapacityService.java
new file mode 100644
index 0000000..4a29b3b
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/StationCycleCapacityService.java
@@ -0,0 +1,13 @@
+package com.zy.asrs.service;
+
+import com.zy.asrs.domain.vo.StationCycleCapacityVo;
+
+public interface StationCycleCapacityService {
+
+    // 绔嬪嵆鍒锋柊寰幆鍦堜笌鎵胯浇閲忓揩鐓�
+    void refreshSnapshot();
+
+    // 鑾峰彇鏈�鏂板惊鐜湀涓庢壙杞介噺蹇収
+    StationCycleCapacityVo getLatestSnapshot();
+}
+
diff --git a/src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java b/src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java
new file mode 100644
index 0000000..f79dda5
--- /dev/null
+++ b/src/main/java/com/zy/asrs/service/impl/StationCycleCapacityServiceImpl.java
@@ -0,0 +1,466 @@
+package com.zy.asrs.service.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.mapper.EntityWrapper;
+import com.zy.asrs.domain.vo.StationCycleCapacityVo;
+import com.zy.asrs.domain.vo.StationCycleLoopVo;
+import com.zy.asrs.entity.BasDevp;
+import com.zy.asrs.entity.DeviceConfig;
+import com.zy.asrs.service.BasMapService;
+import com.zy.asrs.service.BasDevpService;
+import com.zy.asrs.service.DeviceConfigService;
+import com.zy.asrs.service.StationCycleCapacityService;
+import com.zy.common.model.NavigateNode;
+import com.zy.common.utils.NavigateSolution;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.model.StationObjModel;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.thread.StationThread;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Service("stationCycleCapacityService")
+@Slf4j
+public class StationCycleCapacityServiceImpl implements StationCycleCapacityService {
+
+    @Autowired
+    private BasMapService basMapService;
+    @Autowired
+    private DeviceConfigService deviceConfigService;
+    @Autowired
+    private BasDevpService basDevpService;
+
+    private final AtomicReference<StationCycleCapacityVo> snapshotRef = new AtomicReference<>(new StationCycleCapacityVo());
+
+    @Override
+    public synchronized void refreshSnapshot() {
+        try {
+            StationCycleCapacityVo snapshot = buildSnapshot();
+            snapshotRef.set(snapshot);
+        } catch (Exception e) {
+            log.error("鍒锋柊寰幆鍦堟壙杞介噺澶辫触", e);
+        }
+    }
+
+    @Override
+    public StationCycleCapacityVo getLatestSnapshot() {
+        StationCycleCapacityVo snapshot = snapshotRef.get();
+        if (snapshot == null || snapshot.getRefreshTime() == null) {
+            refreshSnapshot();
+            snapshot = snapshotRef.get();
+        }
+        return snapshot == null ? new StationCycleCapacityVo() : snapshot;
+    }
+
+    private StationCycleCapacityVo buildSnapshot() {
+        GraphContext context = buildStationGraph();
+        Map<Integer, Integer> workNoMap = buildStationWorkNoMap();
+
+        Set<Integer> availableStationSet = new HashSet<>(context.graph.keySet());
+        availableStationSet.removeAll(context.excludeStationSet);
+
+        Map<Integer, Set<Integer>> filteredGraph = new HashMap<>();
+        for (Integer stationId : availableStationSet) {
+            Set<Integer> nextSet = context.graph.getOrDefault(stationId, Collections.emptySet());
+            Set<Integer> filteredNext = new HashSet<>();
+            for (Integer nextId : nextSet) {
+                if (availableStationSet.contains(nextId)) {
+                    filteredNext.add(nextId);
+                }
+            }
+            filteredGraph.put(stationId, filteredNext);
+        }
+
+        List<Set<Integer>> sccList = findStrongConnectedComponents(filteredGraph);
+        List<StationCycleLoopVo> loopList = new ArrayList<>();
+
+        int loopNo = 1;
+        int totalStationCount = 0;
+        int taskStationCount = 0;
+
+        for (Set<Integer> scc : sccList) {
+            if (!isCycleScc(scc, filteredGraph)) {
+                continue;
+            }
+
+            // 瀵� SCC 鍐嶅仛涓�娆♀�滅幆鏍稿績鈥濆墺绂伙紝鍓旈櫎鏋濇潏/姝昏儭鍚岃妭鐐�
+            List<Set<Integer>> coreLoopList = extractCoreLoopComponents(scc, filteredGraph);
+            for (Set<Integer> coreLoop : coreLoopList) {
+                List<Integer> stationIdList = new ArrayList<>(coreLoop);
+                Collections.sort(stationIdList);
+
+                List<Integer> workNoList = new ArrayList<>();
+                int currentLoopTaskCount = 0;
+                for (Integer stationId : stationIdList) {
+                    Integer workNo = workNoMap.get(stationId);
+                    if (workNo != null && workNo > 0) {
+                        workNoList.add(workNo);
+                        currentLoopTaskCount++;
+                    }
+                }
+
+                StationCycleLoopVo loopVo = new StationCycleLoopVo();
+                loopVo.setLoopNo(loopNo++);
+                loopVo.setStationIdList(stationIdList);
+                loopVo.setWorkNoList(workNoList);
+                loopVo.setStationCount(stationIdList.size());
+                loopVo.setTaskCount(currentLoopTaskCount);
+                loopVo.setCurrentLoad(calcCurrentLoad(currentLoopTaskCount, stationIdList.size()));
+                loopList.add(loopVo);
+
+                totalStationCount += stationIdList.size();
+                taskStationCount += currentLoopTaskCount;
+            }
+        }
+
+        StationCycleCapacityVo vo = new StationCycleCapacityVo();
+        vo.setLoopList(loopList);
+        vo.setLoopCount(loopList.size());
+        vo.setTotalStationCount(totalStationCount);
+        vo.setTaskStationCount(taskStationCount);
+        vo.setCurrentLoad(calcCurrentLoad(taskStationCount, totalStationCount));
+        vo.setRefreshTime(new Date());
+        return vo;
+    }
+
+    private double calcCurrentLoad(int taskCount, int stationCount) {
+        if (stationCount <= 0 || taskCount <= 0) {
+            return 0.0;
+        }
+        double value = (double) taskCount / (double) stationCount;
+        if (value < 0.0) {
+            return 0.0;
+        }
+        if (value > 1.0) {
+            return 1.0;
+        }
+        return value;
+    }
+
+    private GraphContext buildStationGraph() {
+        GraphContext context = new GraphContext();
+        List<Integer> levList = basMapService.getLevList();
+        if (levList == null || levList.isEmpty()) {
+            return context;
+        }
+
+        NavigateSolution navigateSolution = new NavigateSolution();
+        List<Integer> sortedLevList = new ArrayList<>(levList);
+        sortedLevList = new ArrayList<>(new HashSet<>(sortedLevList));
+        Collections.sort(sortedLevList);
+
+        for (Integer lev : sortedLevList) {
+            List<List<NavigateNode>> stationMap;
+            try {
+                stationMap = navigateSolution.getStationMap(lev);
+            } catch (Exception e) {
+                log.warn("鍔犺浇妤煎眰{}鍦板浘澶辫触锛岃烦杩囧惊鐜湀璁$畻", lev);
+                continue;
+            }
+            if (stationMap == null || stationMap.isEmpty()) {
+                continue;
+            }
+
+            for (List<NavigateNode> row : stationMap) {
+                for (NavigateNode node : row) {
+                    JSONObject valueObj = parseNodeValue(node.getNodeValue());
+                    if (valueObj == null) {
+                        continue;
+                    }
+                    Integer stationId = valueObj.getInteger("stationId");
+                    if (stationId == null) {
+                        continue;
+                    }
+
+                    context.graph.computeIfAbsent(stationId, k -> new HashSet<>());
+                    if (isExcludeStation(valueObj)) {
+                        context.excludeStationSet.add(stationId);
+                    }
+
+                    List<NavigateNode> nextNodeList = navigateSolution.extend_current_node(stationMap, node);
+                    if (nextNodeList == null || nextNodeList.isEmpty()) {
+                        continue;
+                    }
+                    for (NavigateNode nextNode : nextNodeList) {
+                        JSONObject nextValueObj = parseNodeValue(nextNode.getNodeValue());
+                        if (nextValueObj == null) {
+                            continue;
+                        }
+                        Integer nextStationId = nextValueObj.getInteger("stationId");
+                        if (nextStationId == null || stationId.equals(nextStationId)) {
+                            continue;
+                        }
+
+                        context.graph.computeIfAbsent(nextStationId, k -> new HashSet<>());
+                        context.graph.get(stationId).add(nextStationId);
+                    }
+                }
+            }
+        }
+
+        appendExcludeStationsFromDeviceConfig(context.excludeStationSet);
+
+        return context;
+    }
+
+    private void appendExcludeStationsFromDeviceConfig(Set<Integer> excludeStationSet) {
+        List<BasDevp> basDevpList = basDevpService.selectList(new EntityWrapper<>());
+        if (basDevpList == null || basDevpList.isEmpty()) {
+            return;
+        }
+
+        for (BasDevp basDevp : basDevpList) {
+            List<StationObjModel> inStationList = basDevp.getInStationList$();
+            for (StationObjModel stationObjModel : inStationList) {
+                if (stationObjModel != null && stationObjModel.getStationId() != null) {
+                    excludeStationSet.add(stationObjModel.getStationId());
+                }
+            }
+
+            List<StationObjModel> barcodeStationList = basDevp.getBarcodeStationList$();
+            for (StationObjModel stationObjModel : barcodeStationList) {
+                if (stationObjModel != null && stationObjModel.getStationId() != null) {
+                    excludeStationSet.add(stationObjModel.getStationId());
+                }
+            }
+        }
+    }
+
+    private JSONObject parseNodeValue(String nodeValue) {
+        if (nodeValue == null || nodeValue.trim().isEmpty()) {
+            return null;
+        }
+        try {
+            return JSON.parseObject(nodeValue);
+        } catch (Exception ignore) {
+            return null;
+        }
+    }
+
+    private boolean isExcludeStation(JSONObject valueObj) {
+        Integer isInStation = valueObj.getInteger("isInStation");
+        Integer isBarcodeStation = valueObj.getInteger("isBarcodeStation");
+        return (isInStation != null && isInStation == 1)
+                || (isBarcodeStation != null && isBarcodeStation == 1);
+    }
+
+    private Map<Integer, Integer> buildStationWorkNoMap() {
+        Map<Integer, Integer> workNoMap = new HashMap<>();
+        List<DeviceConfig> devpList = deviceConfigService.selectList(new EntityWrapper<DeviceConfig>()
+                .eq("device_type", String.valueOf(SlaveType.Devp)));
+        if (devpList == null || devpList.isEmpty()) {
+            return workNoMap;
+        }
+
+        for (DeviceConfig deviceConfig : devpList) {
+            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, deviceConfig.getDeviceNo());
+            if (stationThread == null) {
+                continue;
+            }
+            Map<Integer, StationProtocol> statusMap = stationThread.getStatusMap();
+            if (statusMap == null || statusMap.isEmpty()) {
+                continue;
+            }
+
+            for (StationProtocol protocol : statusMap.values()) {
+                if (protocol == null || protocol.getStationId() == null) {
+                    continue;
+                }
+                Integer taskNo = protocol.getTaskNo();
+                if (taskNo != null && taskNo > 0) {
+                    workNoMap.put(protocol.getStationId(), taskNo);
+                }
+            }
+        }
+        return workNoMap;
+    }
+
+    private List<Set<Integer>> findStrongConnectedComponents(Map<Integer, Set<Integer>> graph) {
+        List<Set<Integer>> result = new ArrayList<>();
+        if (graph == null || graph.isEmpty()) {
+            return result;
+        }
+
+        Map<Integer, Integer> indexMap = new HashMap<>();
+        Map<Integer, Integer> lowLinkMap = new HashMap<>();
+        Deque<Integer> stack = new ArrayDeque<>();
+        Set<Integer> inStack = new HashSet<>();
+        int[] index = new int[]{0};
+
+        List<Integer> nodeList = new ArrayList<>(graph.keySet());
+        Collections.sort(nodeList);
+        for (Integer node : nodeList) {
+            if (!indexMap.containsKey(node)) {
+                strongConnect(node, graph, indexMap, lowLinkMap, stack, inStack, index, result);
+            }
+        }
+        return result;
+    }
+
+    private void strongConnect(Integer node,
+                               Map<Integer, Set<Integer>> graph,
+                               Map<Integer, Integer> indexMap,
+                               Map<Integer, Integer> lowLinkMap,
+                               Deque<Integer> stack,
+                               Set<Integer> inStack,
+                               int[] index,
+                               List<Set<Integer>> result) {
+        indexMap.put(node, index[0]);
+        lowLinkMap.put(node, index[0]);
+        index[0]++;
+
+        stack.push(node);
+        inStack.add(node);
+
+        List<Integer> nextList = new ArrayList<>(graph.getOrDefault(node, Collections.emptySet()));
+        Collections.sort(nextList);
+        for (Integer next : nextList) {
+            if (!indexMap.containsKey(next)) {
+                strongConnect(next, graph, indexMap, lowLinkMap, stack, inStack, index, result);
+                lowLinkMap.put(node, Math.min(lowLinkMap.get(node), lowLinkMap.get(next)));
+            } else if (inStack.contains(next)) {
+                lowLinkMap.put(node, Math.min(lowLinkMap.get(node), indexMap.get(next)));
+            }
+        }
+
+        if (!lowLinkMap.get(node).equals(indexMap.get(node))) {
+            return;
+        }
+
+        Set<Integer> scc = new HashSet<>();
+        while (!stack.isEmpty()) {
+            Integer top = stack.pop();
+            inStack.remove(top);
+            scc.add(top);
+            if (top.equals(node)) {
+                break;
+            }
+        }
+        result.add(scc);
+    }
+
+    private boolean isCycleScc(Set<Integer> scc, Map<Integer, Set<Integer>> graph) {
+        if (scc == null || scc.isEmpty()) {
+            return false;
+        }
+        if (scc.size() > 1) {
+            return true;
+        }
+        Integer onlyNode = scc.iterator().next();
+        Set<Integer> nextSet = graph.getOrDefault(onlyNode, Collections.emptySet());
+        return nextSet.contains(onlyNode);
+    }
+
+    /**
+     * 浠� SCC 涓彁鍙栧惊鐜牳蹇冿細
+     * 1) 杞棤鍚戝浘
+     * 2) 閫掑綊鍓ョ搴︽暟<2鐨勮妭鐐癸紙2-core锛�
+     * 3) 灏嗗墿浣欒妭鐐规媶鎴愯繛閫氬垎閲忥紝姣忎釜鍒嗛噺>=3鎵嶈瀹氫负寰幆鍦�
+     */
+    private List<Set<Integer>> extractCoreLoopComponents(Set<Integer> scc, Map<Integer, Set<Integer>> graph) {
+        List<Set<Integer>> result = new ArrayList<>();
+        if (scc == null || scc.isEmpty()) {
+            return result;
+        }
+
+        // 鏋勫缓 SCC 鍐呮棤鍚戦偦鎺�
+        Map<Integer, Set<Integer>> undirectedMap = new HashMap<>();
+        for (Integer node : scc) {
+            undirectedMap.put(node, new HashSet<>());
+        }
+        for (Integer from : scc) {
+            Set<Integer> nextSet = graph.getOrDefault(from, Collections.emptySet());
+            for (Integer to : nextSet) {
+                if (!scc.contains(to) || from.equals(to)) {
+                    continue;
+                }
+                undirectedMap.get(from).add(to);
+                undirectedMap.get(to).add(from);
+            }
+        }
+
+        // 2-core 鍓ョ
+        Set<Integer> alive = new HashSet<>(scc);
+        Map<Integer, Integer> degreeMap = new HashMap<>();
+        ArrayDeque<Integer> queue = new ArrayDeque<>();
+        for (Integer node : scc) {
+            int degree = undirectedMap.getOrDefault(node, Collections.emptySet()).size();
+            degreeMap.put(node, degree);
+            if (degree < 2) {
+                queue.offer(node);
+            }
+        }
+
+        while (!queue.isEmpty()) {
+            Integer node = queue.poll();
+            if (!alive.remove(node)) {
+                continue;
+            }
+            for (Integer next : undirectedMap.getOrDefault(node, Collections.emptySet())) {
+                if (!alive.contains(next)) {
+                    continue;
+                }
+                int newDegree = degreeMap.getOrDefault(next, 0) - 1;
+                degreeMap.put(next, newDegree);
+                if (newDegree < 2) {
+                    queue.offer(next);
+                }
+            }
+        }
+
+        if (alive.size() < 3) {
+            return result;
+        }
+
+        // 鎷嗗垎杩為�氬垎閲�
+        Set<Integer> visited = new HashSet<>();
+        List<Integer> sortedAlive = new ArrayList<>(alive);
+        Collections.sort(sortedAlive);
+        for (Integer start : sortedAlive) {
+            if (!visited.add(start)) {
+                continue;
+            }
+            Set<Integer> component = new HashSet<>();
+            ArrayDeque<Integer> bfs = new ArrayDeque<>();
+            bfs.offer(start);
+            component.add(start);
+            while (!bfs.isEmpty()) {
+                Integer node = bfs.poll();
+                for (Integer next : undirectedMap.getOrDefault(node, Collections.emptySet())) {
+                    if (!alive.contains(next) || !visited.add(next)) {
+                        continue;
+                    }
+                    component.add(next);
+                    bfs.offer(next);
+                }
+            }
+
+            // 鑷冲皯3涓偣鎵嶈涓烘槸鐪熸鈥滃湀鈥�
+            if (component.size() >= 3) {
+                result.add(component);
+            }
+        }
+
+        return result;
+    }
+
+    private static class GraphContext {
+        private final Map<Integer, Set<Integer>> graph = new HashMap<>();
+        private final Set<Integer> excludeStationSet = new HashSet<>();
+    }
+}
diff --git a/src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java b/src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java
new file mode 100644
index 0000000..c8355fe
--- /dev/null
+++ b/src/main/java/com/zy/asrs/task/StationCycleCapacityScheduler.java
@@ -0,0 +1,20 @@
+package com.zy.asrs.task;
+
+import com.zy.asrs.service.StationCycleCapacityService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Component
+public class StationCycleCapacityScheduler {
+
+    @Autowired
+    private StationCycleCapacityService stationCycleCapacityService;
+
+    // 姣忕鍒锋柊涓�娆″惊鐜湀鎵胯浇閲�
+    @Scheduled(cron = "0/1 * * * * ? ")
+    public void refreshStationCycleCapacity() {
+        stationCycleCapacityService.refreshSnapshot();
+    }
+}
+
diff --git a/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java b/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
index 6d3a357..63ef73c 100644
--- a/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
+++ b/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
@@ -103,6 +103,9 @@
             } else if ("/console/latest/data/dualcrn".equals(url)) {
                 ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
                 resObj = consoleController.dualCrnLatestData();
+            } else if ("/console/latest/data/station/cycle/capacity".equals(url)) {
+                ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
+                resObj = consoleController.stationCycleCapacity();
             } else if ("/crn/table/crn/state".equals(url)) {
                 resObj = SpringUtils.getBean(CrnController.class).crnStateTable();
             } else if ("/rgv/table/rgv/state".equals(url)) {
diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index 795f549..f04f4ea 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -2,6 +2,20 @@
   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="display: flex; flex-direction: column; gap: 6px; align-items: flex-start;">
+          <div v-for="item in cycleCapacity.loopList"
+               :key="'loop-' + item.loopNo"
+               @mouseenter="handleLoopCardEnter(item)"
+               @mouseleave="handleLoopCardLeave(item)"
+               style="padding: 6px 10px; border-radius: 4px; background: rgba(11, 35, 58, 0.72); color: #fff; font-size: 12px; line-height: 1.4; white-space: nowrap; pointer-events: auto;">
+            鍦坽{ item.loopNo }} |
+            绔欑偣: {{ item.stationCount || 0 }} |
+            浠诲姟: {{ item.taskCount || 0 }} |
+            鎵胯浇: {{ formatLoadPercent(item.currentLoad) }}
+          </div>
+        </div>
+      </div>
       <div v-show="shelfTooltip.visible"
            :style="shelfTooltipStyle()">
         {{ shelfTooltip.text }}
@@ -83,7 +97,16 @@
       containerResizeObserver: null,
       timer: null,
       adjustLabelTimer: null,
-      isSwitchingFloor: false
+      isSwitchingFloor: false,
+      cycleCapacity: {
+        loopList: [],
+        totalStationCount: 0,
+        taskStationCount: 0,
+        currentLoad: 0
+      },
+      hoverLoopNo: null,
+      hoverLoopStationIdSet: new Set(),
+      loopHighlightColor: 0xfff34d
     }
   },
     mounted() {
@@ -102,6 +125,7 @@
       this.getCrnInfo();
       this.getDualCrnInfo();
       this.getSiteInfo();
+      this.getCycleCapacityInfo();
       this.getRgvInfo();
     }, 1000);
   },
@@ -314,6 +338,7 @@
     },
     changeFloor(lev) {
       this.currentLev = lev;
+      this.clearLoopStationHighlight();
       this.isSwitchingFloor = true;
       this.hideShelfTooltip();
       this.hoveredShelfCell = null;
@@ -338,6 +363,7 @@
       this.getMap();
     },
     createMapData(map) {
+      this.clearLoopStationHighlight();
       this.hideShelfTooltip();
       this.hoveredShelfCell = null;
       this.mapRowOffsets = [];
@@ -652,21 +678,21 @@
           sta.statusObj = null;
           if (sta.textObj.parent !== sta) { sta.addChild(sta.textObj); sta.textObj.position.set(sta.width / 2, sta.height / 2); }
         }
+        let baseColor = 0xb8b8b8;
         if (status === "site-auto") {
-          this.updateColor(sta, 0x78ff81);
+          baseColor = 0x78ff81;
         } else if (status === "site-auto-run" || status === "site-auto-id" || status === "site-auto-run-id") {
-          this.updateColor(sta, 0xfa51f6);
+          baseColor = 0xfa51f6;
         } else if (status === "site-unauto") {
-          this.updateColor(sta, 0xb8b8b8);
+          baseColor = 0xb8b8b8;
         } else if (status === "machine-pakin") {
-          this.updateColor(sta, 0x30bffc);
+          baseColor = 0x30bffc;
         } else if (status === "machine-pakout") {
-          this.updateColor(sta, 0x97b400);
+          baseColor = 0x97b400;
         } else if (status === "site-run-block") {
-          this.updateColor(sta, 0xe69138);
-        } else {
-          this.updateColor(sta, 0xb8b8b8);
+          baseColor = 0xe69138;
         }
+        this.setStationBaseColor(sta, baseColor);
       });
     },
     getCrnInfo() {
@@ -684,6 +710,38 @@
     getRgvInfo() {
       if (this.isSwitchingFloor) { return; }
       this.sendWs(JSON.stringify({ url: "/console/latest/data/rgv", data: {} }));
+    },
+    getCycleCapacityInfo() {
+      if (this.isSwitchingFloor) { return; }
+      this.sendWs(JSON.stringify({ url: "/console/latest/data/station/cycle/capacity", data: {} }));
+    },
+    setCycleCapacityInfo(res) {
+      const payload = res && res.code === 200 ? res.data : null;
+      if (res && res.code === 403) { parent.location.href = baseUrl + "/login"; return; }
+      if (!payload) { return; }
+      const loopList = Array.isArray(payload.loopList) ? payload.loopList : [];
+      this.cycleCapacity = {
+        loopList: loopList,
+        totalStationCount: payload.totalStationCount || 0,
+        taskStationCount: payload.taskStationCount || 0,
+        currentLoad: typeof payload.currentLoad === 'number' ? payload.currentLoad : parseFloat(payload.currentLoad || 0)
+      };
+      if (this.hoverLoopNo != null) {
+        const targetLoop = loopList.find(v => v && v.loopNo === this.hoverLoopNo);
+        if (targetLoop) {
+          this.hoverLoopStationIdSet = this.buildStationIdSet(targetLoop.stationIdList);
+          this.applyLoopStationHighlight();
+        } else {
+          this.clearLoopStationHighlight();
+        }
+      }
+    },
+    formatLoadPercent(load) {
+      let value = typeof load === 'number' ? load : parseFloat(load || 0);
+      if (!isFinite(value)) { value = 0; }
+      if (value < 0) { value = 0; }
+      if (value > 1) { value = 1; }
+      return (value * 100).toFixed(1) + "%";
     },
     setCrnInfo(res) {
       let crns = Array.isArray(res) ? res : (res && res.code === 200 ? res.data : null);
@@ -839,6 +897,7 @@
       if (this.wsReconnectTimer) { clearTimeout(this.wsReconnectTimer); this.wsReconnectTimer = null; }
       this.wsReconnectAttempts = 0;
       this.getMap(this.currentLev);
+      this.getCycleCapacityInfo();
     },
     webSocketOnError(e) {
       this.scheduleReconnect();
@@ -853,6 +912,8 @@
         this.setDualCrnInfo(JSON.parse(result.data));
       } else if (result.url === "/console/latest/data/rgv") {
         this.setRgvInfo(JSON.parse(result.data));
+      } else if (result.url === "/console/latest/data/station/cycle/capacity") {
+        this.setCycleCapacityInfo(JSON.parse(result.data));
       } else if (typeof result.url === "string" && result.url.indexOf("/basMap/lev/") === 0) {
         this.setMap(JSON.parse(result.data));
       }
@@ -1200,7 +1261,11 @@
         text.position.set(sprite.width / 2, sprite.height / 2);
         sprite.addChild(text);
         sprite.textObj = text;
-        if (siteId != null && siteId !== -1) { this.pixiStaMap.set(parseInt(siteId), sprite); }
+        const stationIdInt = parseInt(siteId, 10);
+        if (!isNaN(stationIdInt)) { this.pixiStaMap.set(stationIdInt, sprite); }
+        sprite._stationId = isNaN(stationIdInt) ? null : stationIdInt;
+        sprite._baseColor = 0x00ff7f;
+        sprite._loopHighlighted = false;
         sprite.interactive = true;
         sprite.buttonMode = true;
         sprite.on('pointerdown', () => {
@@ -1401,6 +1466,74 @@
         return;
       }
       sprite.tint = color;
+    },
+    setStationBaseColor(sprite, color) {
+      if (!sprite) { return; }
+      sprite._baseColor = color;
+      if (this.isStationInHoverLoop(sprite)) {
+        this.applyHighlightColor(sprite);
+      } else {
+        this.updateColor(sprite, color);
+        sprite._loopHighlighted = false;
+      }
+    },
+    applyHighlightColor(sprite) {
+      if (!sprite) { return; }
+      this.updateColor(sprite, this.loopHighlightColor);
+      sprite._loopHighlighted = true;
+    },
+    isStationInHoverLoop(sprite) {
+      if (!sprite || sprite._stationId == null || !this.hoverLoopStationIdSet) { return false; }
+      return this.hoverLoopStationIdSet.has(sprite._stationId);
+    },
+    buildStationIdSet(stationIdList) {
+      const set = new Set();
+      if (!Array.isArray(stationIdList)) { return set; }
+      stationIdList.forEach((id) => {
+        const v = parseInt(id, 10);
+        if (!isNaN(v)) { set.add(v); }
+      });
+      return set;
+    },
+    applyLoopStationHighlight() {
+      if (!this.pixiStaMap) { return; }
+      this.pixiStaMap.forEach((sprite) => {
+        if (!sprite) { return; }
+        if (this.isStationInHoverLoop(sprite)) {
+          this.applyHighlightColor(sprite);
+        } else if (sprite._loopHighlighted) {
+          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
+          this.updateColor(sprite, baseColor);
+          sprite._loopHighlighted = false;
+        }
+      });
+    },
+    clearLoopStationHighlight() {
+      if (this.pixiStaMap) {
+        this.pixiStaMap.forEach((sprite) => {
+          if (!sprite || !sprite._loopHighlighted) { return; }
+          const baseColor = (typeof sprite._baseColor === 'number') ? sprite._baseColor : 0xb8b8b8;
+          this.updateColor(sprite, baseColor);
+          sprite._loopHighlighted = false;
+        });
+      }
+      this.hoverLoopNo = null;
+      this.hoverLoopStationIdSet = new Set();
+    },
+    handleLoopCardEnter(loopItem) {
+      if (!loopItem) { return; }
+      this.hoverLoopNo = loopItem.loopNo;
+      this.hoverLoopStationIdSet = this.buildStationIdSet(loopItem.stationIdList);
+      this.applyLoopStationHighlight();
+    },
+    handleLoopCardLeave(loopItem) {
+      if (!loopItem) {
+        this.clearLoopStationHighlight();
+        return;
+      }
+      if (this.hoverLoopNo === loopItem.loopNo) {
+        this.clearLoopStationHighlight();
+      }
     },
     isJson(str) {
       try { JSON.parse(str); return true; } catch (e) { return false; }
@@ -1883,11 +2016,6 @@
     }
   }
 });
-
-
-
-
-
 
 
 

--
Gitblit v1.9.1