From e9b531edd2917b01a80dfa14e917ec21ddad8882 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期四, 19 三月 2026 20:26:13 +0800
Subject: [PATCH] #
---
src/main/java/com/zy/core/model/command/StationCommand.java | 11
src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java | 517 ++++++++++++
src/main/webapp/components/MapCanvas.js | 3
src/main/java/com/zy/asrs/domain/vo/StationTaskTraceSegmentVo.java | 25
src/main/java/com/zy/asrs/domain/vo/StationTaskTraceVo.java | 45 +
src/main/webapp/views/watch/stationTrace.html | 903 ++++++++++++++++++++++
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentPlanner.java | 96 ++
src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java | 3
src/main/java/com/zy/asrs/domain/vo/StationTaskTraceEventVo.java | 25
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutionPlan.java | 19
src/main/java/com/zy/core/ServerBootstrap.java | 2
src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java | 312 +++++++
src/main/java/com/zy/asrs/controller/ConsoleController.java | 11
src/main/java/com/zy/core/network/ZyStationConnectDriver.java | 8
src/main/webapp/components/DevpCard.js | 6
src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java | 9
src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutor.java | 347 ++++++++
17 files changed, 2,335 insertions(+), 7 deletions(-)
diff --git a/src/main/java/com/zy/asrs/controller/ConsoleController.java b/src/main/java/com/zy/asrs/controller/ConsoleController.java
index e3c8f29..13029ed 100644
--- a/src/main/java/com/zy/asrs/controller/ConsoleController.java
+++ b/src/main/java/com/zy/asrs/controller/ConsoleController.java
@@ -16,6 +16,7 @@
import com.zy.asrs.domain.vo.FakeTaskTraceVo;
import com.zy.asrs.domain.vo.StationLatestDataVo;
import com.zy.asrs.domain.vo.RgvLatestDataVo;
+import com.zy.asrs.domain.vo.StationTaskTraceVo;
import com.zy.asrs.entity.*;
import com.zy.asrs.service.*;
import com.zy.common.CodeRes;
@@ -32,6 +33,7 @@
import com.zy.core.thread.RgvThread;
import com.zy.core.model.protocol.RgvProtocol;
import com.zy.core.network.fake.FakeTaskTraceRegistry;
+import com.zy.core.trace.StationTaskTraceRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@@ -65,6 +67,8 @@
private StationCycleCapacityService stationCycleCapacityService;
@Autowired
private FakeTaskTraceRegistry fakeTaskTraceRegistry;
+ @Autowired
+ private StationTaskTraceRegistry stationTaskTraceRegistry;
@PostMapping("/system/running/status")
@ManagerAuth(memo = "绯荤粺杩愯鐘舵��")
@@ -285,6 +289,13 @@
return R.ok().add(traceList);
}
+ @PostMapping("/latest/data/station/trace")
+ @ManagerAuth(memo = "杈撻�佷换鍔¤建杩瑰疄鏃舵暟鎹�")
+ public R stationTaskTraceLatestData() {
+ List<StationTaskTraceVo> traceList = stationTaskTraceRegistry.listLatestTraces();
+ return R.ok().add(traceList);
+ }
+
// @PostMapping("/latest/data/barcode")
// @ManagerAuth(memo = "鏉$爜鎵弿浠疄鏃舵暟鎹�")
// public R barcodeLatestData(){
diff --git a/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceEventVo.java b/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceEventVo.java
new file mode 100644
index 0000000..114ab22
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceEventVo.java
@@ -0,0 +1,25 @@
+package com.zy.asrs.domain.vo;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class StationTaskTraceEventVo {
+
+ private Long timestamp;
+
+ private String eventType;
+
+ private String message;
+
+ private String status;
+
+ private Integer currentStationId;
+
+ private Integer targetStationId;
+
+ private Integer traceVersion;
+
+ private Map<String, Object> details;
+}
diff --git a/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceSegmentVo.java b/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceSegmentVo.java
new file mode 100644
index 0000000..b6c3e34
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceSegmentVo.java
@@ -0,0 +1,25 @@
+package com.zy.asrs.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class StationTaskTraceSegmentVo {
+
+ private Integer segmentNo;
+
+ private Integer segmentCount;
+
+ private Integer stationId;
+
+ private Integer targetStationId;
+
+ private Integer segmentStartIndex;
+
+ private Integer segmentEndIndex;
+
+ private List<Integer> segmentPath;
+
+ private Boolean issued;
+}
diff --git a/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceVo.java b/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceVo.java
new file mode 100644
index 0000000..945c8e4
--- /dev/null
+++ b/src/main/java/com/zy/asrs/domain/vo/StationTaskTraceVo.java
@@ -0,0 +1,45 @@
+package com.zy.asrs.domain.vo;
+
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class StationTaskTraceVo {
+
+ private Integer taskNo;
+
+ private String threadImpl;
+
+ private String status;
+
+ private Integer traceVersion;
+
+ private Integer startStationId;
+
+ private Integer currentStationId;
+
+ private Integer finalTargetStationId;
+
+ private Integer blockedStationId;
+
+ private List<Integer> fullPathStationIds;
+
+ private List<Integer> issuedStationIds;
+
+ private List<Integer> passedStationIds;
+
+ private List<Integer> pendingStationIds;
+
+ private List<Integer> latestIssuedSegmentPath;
+
+ private List<StationTaskTraceSegmentVo> segmentList;
+
+ private Integer issuedSegmentCount;
+
+ private Integer totalSegmentCount;
+
+ private Long updatedAt;
+
+ private List<StationTaskTraceEventVo> events;
+}
diff --git a/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java b/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
index 7b16cae..a08d8c2 100644
--- a/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
+++ b/src/main/java/com/zy/asrs/ws/ConsoleWebSocket.java
@@ -109,6 +109,9 @@
} else if ("/console/latest/data/fake/trace".equals(url)) {
ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
resObj = consoleController.fakeTaskTraceLatestData();
+ } else if ("/console/latest/data/station/trace".equals(url)) {
+ ConsoleController consoleController = SpringUtils.getBean(ConsoleController.class);
+ resObj = consoleController.stationTaskTraceLatestData();
} 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/java/com/zy/core/ServerBootstrap.java b/src/main/java/com/zy/core/ServerBootstrap.java
index 68cc1a2..8fb3f02 100644
--- a/src/main/java/com/zy/core/ServerBootstrap.java
+++ b/src/main/java/com/zy/core/ServerBootstrap.java
@@ -137,6 +137,8 @@
thread = new ZyStationV3Thread(deviceConfig, redisUtil);
} else if (deviceConfig.getThreadImpl().equals("ZyStationV4Thread")) {
thread = new ZyStationV4Thread(deviceConfig, redisUtil);
+ } else if (deviceConfig.getThreadImpl().equals("ZyStationV5Thread")) {
+ thread = new ZyStationV5Thread(deviceConfig, redisUtil);
} else {
throw new CoolException("鏈煡鐨勭嚎绋嬪疄鐜�");
}
diff --git a/src/main/java/com/zy/core/model/command/StationCommand.java b/src/main/java/com/zy/core/model/command/StationCommand.java
index 44cf588..262abc8 100644
--- a/src/main/java/com/zy/core/model/command/StationCommand.java
+++ b/src/main/java/com/zy/core/model/command/StationCommand.java
@@ -29,4 +29,15 @@
// 浠跨湡妯″紡涓嬪彲閫夛細鎵嬪伐鎸囧畾鏉$爜
private String barcode;
+ // V5 杞ㄨ抗鍏冩暟鎹紝浠呬緵 WCS 鍐呴儴浣跨敤
+ private Integer segmentNo;
+
+ private Integer segmentCount;
+
+ private Integer segmentStartIndex;
+
+ private Integer segmentEndIndex;
+
+ private Integer traceVersion;
+
}
diff --git a/src/main/java/com/zy/core/network/ZyStationConnectDriver.java b/src/main/java/com/zy/core/network/ZyStationConnectDriver.java
index d986133..9b16ece 100644
--- a/src/main/java/com/zy/core/network/ZyStationConnectDriver.java
+++ b/src/main/java/com/zy/core/network/ZyStationConnectDriver.java
@@ -69,7 +69,8 @@
if (deviceConfig.getFake() == 0) {
if ("ZyStationV3Thread".equals(deviceConfig.getThreadImpl())) {
connectApi = new ZyStationV3RealConnect(deviceConfig, redisUtil);
- } else if ("ZyStationV4Thread".equals(deviceConfig.getThreadImpl())) {
+ } else if ("ZyStationV4Thread".equals(deviceConfig.getThreadImpl())
+ || "ZyStationV5Thread".equals(deviceConfig.getThreadImpl())) {
connectApi = new ZyStationV4RealConnect(deviceConfig, redisUtil);
} else {
connectApi = new ZyStationRealConnect(deviceConfig, redisUtil);
@@ -78,13 +79,14 @@
if ("ZyStationV3Thread".equals(deviceConfig.getThreadImpl())) {
zyStationFakeSegConnect.addFakeConnect(deviceConfig, redisUtil);
connectApi = zyStationFakeSegConnect;
- } else if ("ZyStationV4Thread".equals(deviceConfig.getThreadImpl())) {
+ } else if ("ZyStationV4Thread".equals(deviceConfig.getThreadImpl())
+ || "ZyStationV5Thread".equals(deviceConfig.getThreadImpl())) {
zyStationV4FakeSegConnect.addFakeConnect(deviceConfig, redisUtil);
connectApi = zyStationV4FakeSegConnect;
} else {
fakeConfigUnsupported = true;
zyStationConnectApi = null;
- log.error("鏃х増杈撻�佺珯 fake 宸茬Щ闄わ紝deviceNo={}, threadImpl={}, 璇峰垏鎹㈠埌 ZyStationV3Thread 鎴� ZyStationV4Thread",
+ log.error("鏃х増杈撻�佺珯 fake 宸茬Щ闄わ紝deviceNo={}, threadImpl={}, 璇峰垏鎹㈠埌 ZyStationV3Thread銆乑yStationV4Thread 鎴� ZyStationV5Thread",
deviceConfig.getDeviceNo(), deviceConfig.getThreadImpl());
return false;
}
diff --git a/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java b/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
index 2f2c744..0737eff 100644
--- a/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
+++ b/src/main/java/com/zy/core/network/fake/ZyStationFakeSegConnect.java
@@ -148,8 +148,13 @@
return false;
}
List<Integer> path = command.getNavigatePath();
- return (path == null || path.isEmpty()) && command.getStationId() != null
- && command.getStationId().equals(command.getTargetStaNo());
+ if (command.getStationId() == null || !command.getStationId().equals(command.getTargetStaNo())) {
+ return false;
+ }
+ if (path == null || path.isEmpty()) {
+ return true;
+ }
+ return path.size() == 1 && command.getStationId().equals(path.get(0));
}
private void handleDirectMoveCommand(Integer deviceNo, StationCommand command) {
diff --git a/src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java b/src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java
new file mode 100644
index 0000000..c59e7ef
--- /dev/null
+++ b/src/main/java/com/zy/core/thread/impl/ZyStationV5Thread.java
@@ -0,0 +1,312 @@
+package com.zy.core.thread.impl;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.common.Cools;
+import com.core.common.DateUtils;
+import com.core.common.SpringUtils;
+import com.zy.asrs.entity.BasDevp;
+import com.zy.asrs.entity.BasStationOpt;
+import com.zy.asrs.entity.DeviceConfig;
+import com.zy.asrs.entity.DeviceDataLog;
+import com.zy.asrs.service.BasDevpService;
+import com.zy.asrs.service.BasStationOptService;
+import com.zy.asrs.utils.Utils;
+import com.zy.common.model.NavigateNode;
+import com.zy.common.utils.NavigateUtils;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.cache.MessageQueue;
+import com.zy.core.cache.OutputQueue;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.enums.StationCommandType;
+import com.zy.core.model.CommandResponse;
+import com.zy.core.model.Task;
+import com.zy.core.model.command.StationCommand;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.network.DeviceConnectPool;
+import com.zy.core.network.ZyStationConnectDriver;
+import com.zy.core.network.entity.ZyStationStatusEntity;
+import com.zy.core.thread.impl.v5.StationV5SegmentExecutor;
+import com.zy.core.utils.DeviceLogRedisKeyBuilder;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+@Data
+@Slf4j
+public class ZyStationV5Thread implements Runnable, com.zy.core.thread.StationThread {
+
+ private List<StationProtocol> statusList = new ArrayList<>();
+ private DeviceConfig deviceConfig;
+ private RedisUtil redisUtil;
+ private ZyStationConnectDriver zyStationConnectDriver;
+ private int deviceLogCollectTime = 200;
+ private boolean initStatus = false;
+ private long deviceDataLogTime = System.currentTimeMillis();
+ private ExecutorService executor = Executors.newFixedThreadPool(9999);
+ private StationV5SegmentExecutor segmentExecutor;
+
+ public ZyStationV5Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) {
+ this.deviceConfig = deviceConfig;
+ this.redisUtil = redisUtil;
+ this.segmentExecutor = new StationV5SegmentExecutor(deviceConfig, redisUtil, this::sendCommand);
+ }
+
+ @Override
+ @SuppressWarnings("InfiniteLoopStatement")
+ public void run() {
+ this.connect();
+ deviceLogCollectTime = Utils.getDeviceLogCollectTime();
+
+ Thread readThread = new Thread(() -> {
+ while (true) {
+ try {
+ if (initStatus) {
+ deviceLogCollectTime = Utils.getDeviceLogCollectTime();
+ }
+ readStatus();
+ Thread.sleep(100);
+ } catch (Exception e) {
+ log.error("StationV5Thread Fail", e);
+ }
+ }
+ }, "DevpRead-" + deviceConfig.getDeviceNo());
+ readThread.start();
+
+ Thread processThread = new Thread(() -> {
+ while (true) {
+ try {
+ int step = 1;
+ Task task = MessageQueue.poll(SlaveType.Devp, deviceConfig.getDeviceNo());
+ if (task != null) {
+ step = task.getStep();
+ }
+ if (step == 2) {
+ StationCommand cmd = (StationCommand) task.getData();
+ executor.submit(() -> segmentExecutor.execute(cmd));
+ }
+ Thread.sleep(100);
+ } catch (Exception e) {
+ log.error("StationV5Process Fail", e);
+ }
+ }
+ }, "DevpProcess-" + deviceConfig.getDeviceNo());
+ processThread.start();
+ }
+
+ private void readStatus() {
+ if (zyStationConnectDriver == null) {
+ return;
+ }
+
+ if (statusList.isEmpty()) {
+ BasDevpService basDevpService = null;
+ try {
+ basDevpService = SpringUtils.getBean(BasDevpService.class);
+ } catch (Exception ignore) {
+ }
+ if (basDevpService == null) {
+ return;
+ }
+
+ BasDevp basDevp = basDevpService
+ .getOne(new QueryWrapper<BasDevp>().eq("devp_no", deviceConfig.getDeviceNo()));
+ if (basDevp == null) {
+ return;
+ }
+
+ List<ZyStationStatusEntity> list = JSONObject.parseArray(basDevp.getStationList(), ZyStationStatusEntity.class);
+ for (ZyStationStatusEntity entity : list) {
+ StationProtocol stationProtocol = new StationProtocol();
+ stationProtocol.setStationId(entity.getStationId());
+ statusList.add(stationProtocol);
+ }
+ initStatus = true;
+ }
+
+ List<ZyStationStatusEntity> zyStationStatusEntities = zyStationConnectDriver.getStatus();
+ for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) {
+ for (StationProtocol stationProtocol : statusList) {
+ if (stationProtocol.getStationId().equals(statusEntity.getStationId())) {
+ stationProtocol.setTaskNo(statusEntity.getTaskNo());
+ stationProtocol.setTargetStaNo(statusEntity.getTargetStaNo());
+ stationProtocol.setAutoing(statusEntity.isAutoing());
+ stationProtocol.setLoading(statusEntity.isLoading());
+ stationProtocol.setInEnable(statusEntity.isInEnable());
+ stationProtocol.setOutEnable(statusEntity.isOutEnable());
+ stationProtocol.setEmptyMk(statusEntity.isEmptyMk());
+ stationProtocol.setFullPlt(statusEntity.isFullPlt());
+ stationProtocol.setPalletHeight(statusEntity.getPalletHeight());
+ stationProtocol.setError(statusEntity.getError());
+ stationProtocol.setErrorMsg(statusEntity.getErrorMsg());
+ stationProtocol.setBarcode(statusEntity.getBarcode());
+ stationProtocol.setRunBlock(statusEntity.isRunBlock());
+ stationProtocol.setEnableIn(statusEntity.isEnableIn());
+ stationProtocol.setWeight(statusEntity.getWeight());
+ stationProtocol.setTaskWriteIdx(statusEntity.getTaskWriteIdx());
+ }
+
+ if (!Cools.isEmpty(stationProtocol.getSystemWarning())) {
+ if (stationProtocol.isAutoing() && !stationProtocol.isLoading()) {
+ stationProtocol.setSystemWarning("");
+ }
+ }
+ }
+ }
+
+ OutputQueue.DEVP.offer(MessageFormat.format("銆恵0}銆慬id:{1}] <<<<< 瀹炴椂鏁版嵁鏇存柊鎴愬姛",
+ DateUtils.convert(new Date()), deviceConfig.getDeviceNo()));
+
+ if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) {
+ DeviceDataLog deviceDataLog = new DeviceDataLog();
+ deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities));
+ deviceDataLog.setWcsData(JSON.toJSONString(statusList));
+ deviceDataLog.setType(String.valueOf(SlaveType.Devp));
+ deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo());
+ deviceDataLog.setCreateTime(new Date());
+
+ redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24);
+ deviceDataLogTime = System.currentTimeMillis();
+ }
+ }
+
+ @Override
+ public boolean connect() {
+ zyStationConnectDriver = new ZyStationConnectDriver(deviceConfig, redisUtil);
+ zyStationConnectDriver.start();
+ DeviceConnectPool.put(SlaveType.Devp, deviceConfig.getDeviceNo(), zyStationConnectDriver);
+ return true;
+ }
+
+ @Override
+ public void close() {
+ if (zyStationConnectDriver != null) {
+ zyStationConnectDriver.close();
+ }
+ if (executor != null) {
+ try {
+ executor.shutdownNow();
+ } catch (Exception ignore) {
+ }
+ }
+ }
+
+ @Override
+ public List<StationProtocol> getStatus() {
+ return statusList;
+ }
+
+ @Override
+ public Map<Integer, StationProtocol> getStatusMap() {
+ Map<Integer, StationProtocol> map = new HashMap<>();
+ for (StationProtocol stationProtocol : statusList) {
+ map.put(stationProtocol.getStationId(), stationProtocol);
+ }
+ return map;
+ }
+
+ @Override
+ public StationCommand getCommand(StationCommandType commandType,
+ Integer taskNo,
+ Integer stationId,
+ Integer targetStationId,
+ Integer palletSize) {
+ StationCommand stationCommand = new StationCommand();
+ stationCommand.setTaskNo(taskNo);
+ stationCommand.setStationId(stationId);
+ stationCommand.setTargetStaNo(targetStationId);
+ stationCommand.setPalletSize(palletSize);
+ stationCommand.setCommandType(commandType);
+
+ if (commandType == StationCommandType.MOVE && !stationId.equals(targetStationId)) {
+ List<NavigateNode> nodes = calcPathNavigateNodes(stationId, targetStationId);
+ List<Integer> path = new ArrayList<>();
+ List<Integer> liftTransferPath = new ArrayList<>();
+ for (NavigateNode n : nodes) {
+ JSONObject v = JSONObject.parseObject(n.getNodeValue());
+ if (v == null) {
+ continue;
+ }
+ Integer stationNo = v.getInteger("stationId");
+ if (stationNo == null) {
+ continue;
+ }
+ path.add(stationNo);
+ if (Boolean.TRUE.equals(n.getIsLiftTransferPoint())) {
+ liftTransferPath.add(stationNo);
+ }
+ }
+ stationCommand.setNavigatePath(path);
+ stationCommand.setLiftTransferPath(liftTransferPath);
+ }
+ return stationCommand;
+ }
+
+ @Override
+ public CommandResponse sendCommand(StationCommand command) {
+ CommandResponse commandResponse = null;
+ try {
+ commandResponse = zyStationConnectDriver.sendCommand(command);
+ } catch (Exception e) {
+ e.printStackTrace();
+ } finally {
+ BasStationOptService optService = SpringUtils.getBean(BasStationOptService.class);
+ List<ZyStationStatusEntity> statusListEntity = zyStationConnectDriver.getStatus();
+ ZyStationStatusEntity matched = null;
+ if (statusListEntity != null) {
+ for (ZyStationStatusEntity entity : statusListEntity) {
+ if (entity.getStationId() != null && entity.getStationId().equals(command.getStationId())) {
+ matched = entity;
+ break;
+ }
+ }
+ }
+ BasStationOpt basStationOpt = new BasStationOpt(
+ command.getTaskNo(),
+ command.getStationId(),
+ new Date(),
+ String.valueOf(command.getCommandType()),
+ command.getStationId(),
+ command.getTargetStaNo(),
+ null,
+ null,
+ null,
+ JSON.toJSONString(command),
+ JSON.toJSONString(matched),
+ 1,
+ JSON.toJSONString(commandResponse)
+ );
+ if (optService != null) {
+ optService.save(basStationOpt);
+ }
+ }
+ return commandResponse;
+ }
+
+ @Override
+ public CommandResponse sendOriginCommand(String address, short[] data) {
+ return zyStationConnectDriver.sendOriginCommand(address, data);
+ }
+
+ @Override
+ public byte[] readOriginCommand(String address, int length) {
+ return zyStationConnectDriver.readOriginCommand(address, length);
+ }
+
+ private List<NavigateNode> calcPathNavigateNodes(Integer startStationId, Integer targetStationId) {
+ NavigateUtils navigateUtils = SpringUtils.getBean(NavigateUtils.class);
+ if (navigateUtils == null) {
+ return new ArrayList<>();
+ }
+ return navigateUtils.calcByStationId(startStationId, targetStationId);
+ }
+}
diff --git a/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutionPlan.java b/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutionPlan.java
new file mode 100644
index 0000000..e2f6d06
--- /dev/null
+++ b/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutionPlan.java
@@ -0,0 +1,19 @@
+package com.zy.core.thread.impl.v5;
+
+import com.zy.core.model.command.StationCommand;
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+@Data
+public class StationV5SegmentExecutionPlan {
+
+ private List<Integer> fullPathStationIds = new ArrayList<>();
+
+ private List<StationCommand> segmentCommands = new ArrayList<>();
+
+ public int getTotalSegmentCount() {
+ return segmentCommands == null ? 0 : segmentCommands.size();
+ }
+}
diff --git a/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutor.java b/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutor.java
new file mode 100644
index 0000000..96320de
--- /dev/null
+++ b/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentExecutor.java
@@ -0,0 +1,347 @@
+package com.zy.core.thread.impl.v5;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.core.common.Cools;
+import com.core.common.SpringUtils;
+import com.zy.asrs.domain.vo.StationTaskTraceSegmentVo;
+import com.zy.asrs.entity.DeviceConfig;
+import com.zy.common.utils.RedisUtil;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.RedisKeyType;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.enums.StationCommandType;
+import com.zy.core.model.CommandResponse;
+import com.zy.core.model.command.StationCommand;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.trace.StationTaskTraceRegistry;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+
+public class StationV5SegmentExecutor {
+
+ private static final String CFG_STATION_COMMAND_SEGMENT_ADVANCE_RATIO = "stationCommandSegmentAdvanceRatio";
+ private static final double DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO = 0.3d;
+ private static final long CURRENT_STATION_TIMEOUT_MS = 1000L * 60L;
+
+ private final DeviceConfig deviceConfig;
+ private final RedisUtil redisUtil;
+ private final Function<StationCommand, CommandResponse> commandSender;
+ private final StationV5SegmentPlanner segmentPlanner = new StationV5SegmentPlanner();
+
+ public StationV5SegmentExecutor(DeviceConfig deviceConfig,
+ RedisUtil redisUtil,
+ Function<StationCommand, CommandResponse> commandSender) {
+ this.deviceConfig = deviceConfig;
+ this.redisUtil = redisUtil;
+ this.commandSender = commandSender;
+ }
+
+ public void execute(StationCommand original) {
+ if (original == null) {
+ return;
+ }
+ if (original.getCommandType() != StationCommandType.MOVE) {
+ commandSender.apply(original);
+ return;
+ }
+
+ StationV5SegmentExecutionPlan localPlan = segmentPlanner.buildPlan(original);
+ if (localPlan.getSegmentCommands().isEmpty()) {
+ return;
+ }
+
+ StationTaskTraceRegistry traceRegistry = SpringUtils.getBean(StationTaskTraceRegistry.class);
+ StationTaskTraceRegistry.TraceRegistration traceRegistration = traceRegistry == null
+ ? new StationTaskTraceRegistry.TraceRegistration()
+ : traceRegistry.registerPlan(original.getTaskNo(), deviceConfig.getThreadImpl(),
+ original.getStationId(), original.getStationId(), original.getTargetStaNo(),
+ localPlan.getFullPathStationIds(), buildTraceSegments(localPlan.getSegmentCommands()));
+ int traceVersion = traceRegistration.getTraceVersion() == null ? 1 : traceRegistration.getTraceVersion();
+ int pathOffset = traceRegistration.getPathOffset() == null ? 0 : traceRegistration.getPathOffset();
+ bindCommands(localPlan.getSegmentCommands(), traceVersion, pathOffset);
+ List<Integer> effectiveFullPath = traceRegistration.getFullPathStationIds() == null
+ || traceRegistration.getFullPathStationIds().isEmpty()
+ ? copyIntegerList(localPlan.getFullPathStationIds())
+ : copyIntegerList(traceRegistration.getFullPathStationIds());
+
+ StationCommand firstCommand = localPlan.getSegmentCommands().get(0);
+ if (!sendSegmentWithRetry(firstCommand)) {
+ return;
+ }
+ if (traceRegistry != null) {
+ traceRegistry.markSegmentIssued(original.getTaskNo(), traceVersion, firstCommand,
+ "FIRST_SEGMENT_SENT", "杈撻�佷换鍔¢娈典笅鍙戞垚鍔�", buildSegmentDetails(firstCommand));
+ }
+
+ long lastSeenAt = System.currentTimeMillis();
+ int segCursor = 0;
+ Integer lastCurrentStationId = null;
+ boolean firstRun = true;
+ double segmentAdvanceRatio = loadSegmentAdvanceRatio();
+ while (true) {
+ try {
+ Object cancel = redisUtil.get(RedisKeyType.DEVICE_STATION_MOVE_RESET.key + original.getTaskNo());
+ if (cancel != null) {
+ if (traceRegistry != null) {
+ traceRegistry.markCancelled(original.getTaskNo(), traceVersion, lastCurrentStationId,
+ buildDetails("reason", "redis_cancel_signal"));
+ }
+ break;
+ }
+
+ StationProtocol currentStation = findCurrentStationByTask(original.getTaskNo());
+ if (currentStation == null) {
+ if (System.currentTimeMillis() - lastSeenAt > CURRENT_STATION_TIMEOUT_MS) {
+ if (traceRegistry != null) {
+ traceRegistry.markTimeout(original.getTaskNo(), traceVersion, lastCurrentStationId,
+ buildDetails("timeoutMs", CURRENT_STATION_TIMEOUT_MS));
+ }
+ break;
+ }
+ Thread.sleep(500L);
+ continue;
+ }
+
+ lastSeenAt = System.currentTimeMillis();
+ Integer previousCurrentStationId = lastCurrentStationId;
+ if (traceRegistry != null) {
+ traceRegistry.updateProgress(original.getTaskNo(), traceVersion, currentStation.getStationId(),
+ equalsInteger(previousCurrentStationId, currentStation.getStationId()) ? null : "CURRENT_STATION_CHANGE",
+ "杈撻�佷换鍔″綋鍓嶄綅缃凡鏇存柊",
+ buildDetails("stationId", currentStation.getStationId()));
+ }
+ lastCurrentStationId = currentStation.getStationId();
+ if (!firstRun && currentStation.isRunBlock()) {
+ if (traceRegistry != null) {
+ traceRegistry.markBlocked(original.getTaskNo(), traceVersion, currentStation.getStationId(),
+ buildDetails("blockedStationId", currentStation.getStationId()));
+ }
+ break;
+ }
+
+ int currentIndex = effectiveFullPath.indexOf(currentStation.getStationId());
+ if (currentIndex < 0) {
+ Thread.sleep(500L);
+ firstRun = false;
+ continue;
+ }
+
+ int remaining = effectiveFullPath.size() - currentIndex - 1;
+ if (remaining <= 0) {
+ if (traceRegistry != null) {
+ traceRegistry.markFinished(original.getTaskNo(), traceVersion, currentStation.getStationId(),
+ buildDetails("targetStationId", original.getTargetStaNo()));
+ }
+ break;
+ }
+
+ StationCommand currentSegmentCommand = localPlan.getSegmentCommands().get(segCursor);
+ int currentSegEndIndex = safeIndex(currentSegmentCommand.getSegmentEndIndex());
+ int currentSegStartIndex = safeIndex(currentSegmentCommand.getSegmentStartIndex());
+ int segLen = Math.max(1, currentSegEndIndex - currentSegStartIndex + 1);
+ int remainingSegment = Math.max(0, currentSegEndIndex - currentIndex);
+ int thresholdSegment = (int) Math.ceil(segLen * segmentAdvanceRatio);
+ if (remainingSegment <= thresholdSegment && segCursor < localPlan.getSegmentCommands().size() - 1) {
+ StationCommand nextCommand = localPlan.getSegmentCommands().get(segCursor + 1);
+ if (sendSegmentWithRetry(nextCommand)) {
+ segCursor++;
+ if (traceRegistry != null) {
+ traceRegistry.markSegmentIssued(original.getTaskNo(), traceVersion, nextCommand,
+ "NEXT_SEGMENT_SENT", "杈撻�佷换鍔′笅涓�娈靛凡鎻愬墠涓嬪彂",
+ buildSegmentDetails(nextCommand));
+ }
+ }
+ }
+ Thread.sleep(500L);
+ firstRun = false;
+ } catch (Exception ignore) {
+ break;
+ }
+ }
+ }
+
+ private boolean sendSegmentWithRetry(StationCommand command) {
+ while (true) {
+ CommandResponse commandResponse = commandSender.apply(command);
+ if (commandResponse == null) {
+ sleepQuietly(200L);
+ continue;
+ }
+ if (commandResponse.getResult()) {
+ return true;
+ }
+ sleepQuietly(200L);
+ }
+ }
+
+ private double loadSegmentAdvanceRatio() {
+ try {
+ ConfigService configService = SpringUtils.getBean(ConfigService.class);
+ if (configService == null) {
+ return DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO;
+ }
+ Config config = configService.getOne(new QueryWrapper<Config>()
+ .eq("code", CFG_STATION_COMMAND_SEGMENT_ADVANCE_RATIO));
+ if (config == null || Cools.isEmpty(config.getValue())) {
+ return DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO;
+ }
+ return normalizeSegmentAdvanceRatio(config.getValue());
+ } catch (Exception ignore) {
+ return DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO;
+ }
+ }
+
+ private double normalizeSegmentAdvanceRatio(String valueText) {
+ if (valueText == null) {
+ return DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO;
+ }
+ String text = valueText.trim();
+ if (text.isEmpty()) {
+ return DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO;
+ }
+ if (text.endsWith("%")) {
+ text = text.substring(0, text.length() - 1).trim();
+ }
+ try {
+ double ratio = Double.parseDouble(text);
+ if (ratio > 1d && ratio <= 100d) {
+ ratio = ratio / 100d;
+ }
+ if (ratio < 0d) {
+ return 0d;
+ }
+ if (ratio > 1d) {
+ return 1d;
+ }
+ return ratio;
+ } catch (Exception ignore) {
+ return DEFAULT_STATION_COMMAND_SEGMENT_ADVANCE_RATIO;
+ }
+ }
+
+ private StationProtocol findCurrentStationByTask(Integer taskNo) {
+ try {
+ com.zy.asrs.service.DeviceConfigService deviceConfigService = SpringUtils.getBean(com.zy.asrs.service.DeviceConfigService.class);
+ if (deviceConfigService == null) {
+ return null;
+ }
+ List<DeviceConfig> devpList = deviceConfigService.list(new QueryWrapper<DeviceConfig>()
+ .eq("device_type", String.valueOf(SlaveType.Devp)));
+ for (DeviceConfig dc : devpList) {
+ com.zy.core.thread.StationThread thread = (com.zy.core.thread.StationThread) SlaveConnection.get(SlaveType.Devp, dc.getDeviceNo());
+ if (thread == null) {
+ continue;
+ }
+ Map<Integer, StationProtocol> statusMap = thread.getStatusMap();
+ if (statusMap == null || statusMap.isEmpty()) {
+ continue;
+ }
+ for (StationProtocol protocol : statusMap.values()) {
+ if (protocol.getTaskNo() != null && protocol.getTaskNo().equals(taskNo) && protocol.isLoading()) {
+ return protocol;
+ }
+ }
+ }
+ } catch (Exception ignore) {
+ return null;
+ }
+ return null;
+ }
+
+ private List<StationTaskTraceSegmentVo> buildTraceSegments(List<StationCommand> segmentCommands) {
+ List<StationTaskTraceSegmentVo> result = new ArrayList<>();
+ if (segmentCommands == null) {
+ return result;
+ }
+ for (StationCommand command : segmentCommands) {
+ if (command == null) {
+ continue;
+ }
+ StationTaskTraceSegmentVo item = new StationTaskTraceSegmentVo();
+ item.setSegmentNo(command.getSegmentNo());
+ item.setSegmentCount(command.getSegmentCount());
+ item.setStationId(command.getStationId());
+ item.setTargetStationId(command.getTargetStaNo());
+ item.setSegmentStartIndex(command.getSegmentStartIndex());
+ item.setSegmentEndIndex(command.getSegmentEndIndex());
+ item.setSegmentPath(copyIntegerList(command.getNavigatePath()));
+ item.setIssued(Boolean.FALSE);
+ result.add(item);
+ }
+ return result;
+ }
+
+ private void bindCommands(List<StationCommand> segmentCommands, int traceVersion, int pathOffset) {
+ if (segmentCommands == null) {
+ return;
+ }
+ for (StationCommand command : segmentCommands) {
+ if (command == null) {
+ continue;
+ }
+ command.setTraceVersion(traceVersion);
+ if (command.getSegmentStartIndex() != null) {
+ command.setSegmentStartIndex(command.getSegmentStartIndex() + pathOffset);
+ }
+ if (command.getSegmentEndIndex() != null) {
+ command.setSegmentEndIndex(command.getSegmentEndIndex() + pathOffset);
+ }
+ }
+ }
+
+ private Map<String, Object> buildSegmentDetails(StationCommand command) {
+ Map<String, Object> details = new LinkedHashMap<>();
+ if (command != null) {
+ details.put("segmentNo", command.getSegmentNo());
+ details.put("segmentCount", command.getSegmentCount());
+ details.put("segmentPath", copyIntegerList(command.getNavigatePath()));
+ details.put("segmentStartIndex", command.getSegmentStartIndex());
+ details.put("segmentEndIndex", command.getSegmentEndIndex());
+ details.put("traceVersion", command.getTraceVersion());
+ }
+ return details;
+ }
+
+ private Map<String, Object> buildDetails(Object... keyValues) {
+ Map<String, Object> details = new LinkedHashMap<>();
+ if (keyValues == null) {
+ return details;
+ }
+ for (int i = 0; i + 1 < keyValues.length; i += 2) {
+ Object key = keyValues[i];
+ if (key != null) {
+ details.put(String.valueOf(key), keyValues[i + 1]);
+ }
+ }
+ return details;
+ }
+
+ private List<Integer> copyIntegerList(List<Integer> source) {
+ List<Integer> result = new ArrayList<>();
+ if (source == null) {
+ return result;
+ }
+ result.addAll(source);
+ return result;
+ }
+
+ private int safeIndex(Integer value) {
+ return value == null ? -1 : value;
+ }
+
+ private boolean equalsInteger(Integer a, Integer b) {
+ return a != null && a.equals(b);
+ }
+
+ private void sleepQuietly(long millis) {
+ try {
+ Thread.sleep(millis);
+ } catch (Exception ignore) {
+ }
+ }
+}
diff --git a/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentPlanner.java b/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentPlanner.java
new file mode 100644
index 0000000..ece2fbf
--- /dev/null
+++ b/src/main/java/com/zy/core/thread/impl/v5/StationV5SegmentPlanner.java
@@ -0,0 +1,96 @@
+package com.zy.core.thread.impl.v5;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.zy.core.model.command.StationCommand;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+public class StationV5SegmentPlanner {
+
+ public StationV5SegmentExecutionPlan buildPlan(StationCommand original) {
+ StationV5SegmentExecutionPlan plan = new StationV5SegmentExecutionPlan();
+ if (original == null) {
+ return plan;
+ }
+
+ List<Integer> path = copyIntegerList(original.getNavigatePath());
+ List<Integer> liftTransferPath = copyIntegerList(original.getLiftTransferPath());
+ Integer startStationId = original.getStationId();
+ Integer targetStationId = original.getTargetStaNo();
+
+ if ((path == null || path.isEmpty()) && Objects.equals(startStationId, targetStationId) && startStationId != null) {
+ path = new ArrayList<>();
+ path.add(startStationId);
+ }
+
+ if (path == null || path.isEmpty()) {
+ return plan;
+ }
+
+ plan.setFullPathStationIds(copyIntegerList(path));
+
+ int total = path.size();
+ List<Integer> segmentEndIndices = new ArrayList<>();
+ if (liftTransferPath != null) {
+ for (Integer liftTransferStationId : liftTransferPath) {
+ int endIndex = path.indexOf(liftTransferStationId);
+ if (endIndex <= 0) {
+ continue;
+ }
+ if (segmentEndIndices.isEmpty() || endIndex > segmentEndIndices.get(segmentEndIndices.size() - 1)) {
+ segmentEndIndices.add(endIndex);
+ }
+ }
+ }
+ if (segmentEndIndices.isEmpty() || segmentEndIndices.get(segmentEndIndices.size() - 1) != total - 1) {
+ segmentEndIndices.add(total - 1);
+ }
+
+ List<StationCommand> segmentCommands = new ArrayList<>();
+ int buildStartIdx = 0;
+ for (Integer endIdx : segmentEndIndices) {
+ if (endIdx == null || endIdx < buildStartIdx) {
+ continue;
+ }
+ List<Integer> segmentPath = new ArrayList<>(path.subList(buildStartIdx, endIdx + 1));
+ if (segmentPath.isEmpty()) {
+ buildStartIdx = endIdx + 1;
+ continue;
+ }
+
+ StationCommand segmentCommand = new StationCommand();
+ segmentCommand.setTaskNo(original.getTaskNo());
+ segmentCommand.setCommandType(original.getCommandType());
+ segmentCommand.setPalletSize(original.getPalletSize());
+ segmentCommand.setBarcode(original.getBarcode());
+ segmentCommand.setOriginalNavigatePath(copyIntegerList(path));
+ segmentCommand.setNavigatePath(segmentPath);
+ segmentCommand.setStationId(segmentPath.get(0));
+ segmentCommand.setTargetStaNo(segmentPath.get(segmentPath.size() - 1));
+ segmentCommand.setSegmentStartIndex(buildStartIdx);
+ segmentCommand.setSegmentEndIndex(endIdx);
+ segmentCommands.add(segmentCommand);
+
+ buildStartIdx = endIdx;
+ }
+
+ int segmentCount = segmentCommands.size();
+ for (int i = 0; i < segmentCommands.size(); i++) {
+ StationCommand segmentCommand = segmentCommands.get(i);
+ segmentCommand.setSegmentNo(i + 1);
+ segmentCommand.setSegmentCount(segmentCount);
+ }
+ plan.setSegmentCommands(segmentCommands);
+ return plan;
+ }
+
+ private List<Integer> copyIntegerList(List<Integer> source) {
+ if (source == null) {
+ return new ArrayList<>();
+ }
+ return JSON.parseArray(JSON.toJSONString(source, SerializerFeature.DisableCircularReferenceDetect), Integer.class);
+ }
+}
diff --git a/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java b/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java
new file mode 100644
index 0000000..4c8e76b
--- /dev/null
+++ b/src/main/java/com/zy/core/trace/StationTaskTraceRegistry.java
@@ -0,0 +1,517 @@
+package com.zy.core.trace;
+
+import com.zy.asrs.domain.vo.StationTaskTraceEventVo;
+import com.zy.asrs.domain.vo.StationTaskTraceSegmentVo;
+import com.zy.asrs.domain.vo.StationTaskTraceVo;
+import com.zy.core.model.command.StationCommand;
+import lombok.Data;
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class StationTaskTraceRegistry {
+
+ private static final int MAX_EVENT_COUNT = 200;
+ private static final long TERMINAL_KEEP_MS = 30L * 60L * 1000L;
+
+ public static final String STATUS_WAITING = "WAITING";
+ public static final String STATUS_RUNNING = "RUNNING";
+ public static final String STATUS_BLOCKED = "BLOCKED";
+ public static final String STATUS_CANCELLED = "CANCELLED";
+ public static final String STATUS_TIMEOUT = "TIMEOUT";
+ public static final String STATUS_FINISHED = "FINISHED";
+ public static final String STATUS_REROUTED = "REROUTED";
+
+ private final Map<Integer, TraceTaskState> taskStateMap = new ConcurrentHashMap<>();
+
+ public TraceRegistration registerPlan(Integer taskNo,
+ String threadImpl,
+ Integer startStationId,
+ Integer currentStationId,
+ Integer finalTargetStationId,
+ List<Integer> localPathStationIds,
+ List<StationTaskTraceSegmentVo> localSegmentList) {
+ if (taskNo == null || taskNo <= 0) {
+ return new TraceRegistration();
+ }
+
+ cleanupExpired();
+ TraceTaskState taskState = taskStateMap.computeIfAbsent(taskNo, TraceTaskState::new);
+ return taskState.registerPlan(threadImpl, startStationId, currentStationId, finalTargetStationId,
+ copyIntegerList(localPathStationIds), copySegmentList(localSegmentList));
+ }
+
+ public void markSegmentIssued(Integer taskNo,
+ Integer traceVersion,
+ StationCommand command,
+ String eventType,
+ String message,
+ Map<String, Object> details) {
+ TraceTaskState state = taskStateMap.get(taskNo);
+ if (state == null) {
+ return;
+ }
+ state.markSegmentIssued(traceVersion, command, eventType, message, details);
+ }
+
+ public void updateProgress(Integer taskNo,
+ Integer traceVersion,
+ Integer currentStationId,
+ String eventType,
+ String message,
+ Map<String, Object> details) {
+ TraceTaskState state = taskStateMap.get(taskNo);
+ if (state == null) {
+ return;
+ }
+ state.updateProgress(traceVersion, currentStationId, eventType, message, details);
+ }
+
+ public void markBlocked(Integer taskNo,
+ Integer traceVersion,
+ Integer currentStationId,
+ Map<String, Object> details) {
+ TraceTaskState state = taskStateMap.get(taskNo);
+ if (state == null) {
+ return;
+ }
+ state.markTerminal(traceVersion, STATUS_BLOCKED, currentStationId, currentStationId,
+ "BLOCKED", "杈撻�佷换鍔¤繍琛屽牭濉�", details);
+ }
+
+ public void markCancelled(Integer taskNo,
+ Integer traceVersion,
+ Integer currentStationId,
+ Map<String, Object> details) {
+ TraceTaskState state = taskStateMap.get(taskNo);
+ if (state == null) {
+ return;
+ }
+ state.markTerminal(traceVersion, STATUS_CANCELLED, currentStationId, null,
+ "CANCELLED", "杈撻�佷换鍔℃敹鍒板彇娑堜俊鍙�", details);
+ }
+
+ public void markTimeout(Integer taskNo,
+ Integer traceVersion,
+ Integer currentStationId,
+ Map<String, Object> details) {
+ TraceTaskState state = taskStateMap.get(taskNo);
+ if (state == null) {
+ return;
+ }
+ state.markTerminal(traceVersion, STATUS_TIMEOUT, currentStationId, null,
+ "TIMEOUT", "杈撻�佷换鍔¢暱鏃堕棿鏃犳硶瀹氫綅褰撳墠浣嶇疆", details);
+ }
+
+ public void markFinished(Integer taskNo,
+ Integer traceVersion,
+ Integer currentStationId,
+ Map<String, Object> details) {
+ TraceTaskState state = taskStateMap.get(taskNo);
+ if (state == null) {
+ return;
+ }
+ state.markTerminal(traceVersion, STATUS_FINISHED, currentStationId, null,
+ "FINISHED", "杈撻�佷换鍔¤建杩瑰畬鎴�", details);
+ }
+
+ public List<StationTaskTraceVo> listLatestTraces() {
+ cleanupExpired();
+ List<StationTaskTraceVo> result = new ArrayList<>();
+ for (TraceTaskState state : taskStateMap.values()) {
+ if (state != null) {
+ result.add(state.toVo());
+ }
+ }
+ result.sort(new Comparator<StationTaskTraceVo>() {
+ @Override
+ public int compare(StationTaskTraceVo a, StationTaskTraceVo b) {
+ long av = a.getUpdatedAt() == null ? 0L : a.getUpdatedAt();
+ long bv = b.getUpdatedAt() == null ? 0L : b.getUpdatedAt();
+ return Long.compare(bv, av);
+ }
+ });
+ return result;
+ }
+
+ private void cleanupExpired() {
+ long now = System.currentTimeMillis();
+ for (Map.Entry<Integer, TraceTaskState> entry : taskStateMap.entrySet()) {
+ TraceTaskState state = entry.getValue();
+ if (state != null && state.shouldRemove(now)) {
+ taskStateMap.remove(entry.getKey(), state);
+ }
+ }
+ }
+
+ private static List<Integer> copyIntegerList(List<Integer> source) {
+ List<Integer> result = new ArrayList<>();
+ if (source == null) {
+ return result;
+ }
+ for (Integer item : source) {
+ if (item != null) {
+ result.add(item);
+ }
+ }
+ return result;
+ }
+
+ private static List<StationTaskTraceSegmentVo> copySegmentList(List<StationTaskTraceSegmentVo> source) {
+ List<StationTaskTraceSegmentVo> result = new ArrayList<>();
+ if (source == null) {
+ return result;
+ }
+ for (StationTaskTraceSegmentVo item : source) {
+ if (item == null) {
+ continue;
+ }
+ StationTaskTraceSegmentVo copy = new StationTaskTraceSegmentVo();
+ copy.setSegmentNo(item.getSegmentNo());
+ copy.setSegmentCount(item.getSegmentCount());
+ copy.setStationId(item.getStationId());
+ copy.setTargetStationId(item.getTargetStationId());
+ copy.setSegmentStartIndex(item.getSegmentStartIndex());
+ copy.setSegmentEndIndex(item.getSegmentEndIndex());
+ copy.setSegmentPath(copyIntegerList(item.getSegmentPath()));
+ copy.setIssued(Boolean.TRUE.equals(item.getIssued()));
+ result.add(copy);
+ }
+ return result;
+ }
+
+ private static Map<String, Object> copyDetails(Map<String, Object> source) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ if (source == null || source.isEmpty()) {
+ return result;
+ }
+ for (Map.Entry<String, Object> entry : source.entrySet()) {
+ Object value = entry.getValue();
+ if (value instanceof List) {
+ result.put(entry.getKey(), new ArrayList<>((List<?>) value));
+ } else if (value instanceof Map) {
+ result.put(entry.getKey(), new LinkedHashMap<>((Map<?, ?>) value));
+ } else {
+ result.put(entry.getKey(), value);
+ }
+ }
+ return result;
+ }
+
+ private static boolean isTerminalStatus(String status) {
+ return STATUS_BLOCKED.equals(status)
+ || STATUS_CANCELLED.equals(status)
+ || STATUS_TIMEOUT.equals(status)
+ || STATUS_FINISHED.equals(status);
+ }
+
+ @Data
+ public static class TraceRegistration {
+ private Integer traceVersion;
+ private Integer pathOffset;
+ private List<Integer> fullPathStationIds = new ArrayList<>();
+ private boolean rerouted;
+ }
+
+ private static class TraceTaskState {
+
+ private final Integer taskNo;
+ private String threadImpl;
+ private String status = STATUS_WAITING;
+ private Integer traceVersion = 0;
+ private Integer startStationId;
+ private Integer currentStationId;
+ private Integer finalTargetStationId;
+ private Integer blockedStationId;
+ private List<Integer> fullPathStationIds = new ArrayList<>();
+ private List<Integer> issuedStationIds = new ArrayList<>();
+ private List<Integer> passedStationIds = new ArrayList<>();
+ private List<Integer> pendingStationIds = new ArrayList<>();
+ private List<Integer> latestIssuedSegmentPath = new ArrayList<>();
+ private List<StationTaskTraceSegmentVo> segmentList = new ArrayList<>();
+ private Integer issuedSegmentCount = 0;
+ private Integer totalSegmentCount = 0;
+ private final List<StationTaskTraceEventVo> events = new ArrayList<>();
+ private Long updatedAt = System.currentTimeMillis();
+ private Long terminalExpireAt;
+
+ private TraceTaskState(Integer taskNo) {
+ this.taskNo = taskNo;
+ }
+
+ private synchronized TraceRegistration registerPlan(String threadImpl,
+ Integer startStationId,
+ Integer currentStationId,
+ Integer finalTargetStationId,
+ List<Integer> localPathStationIds,
+ List<StationTaskTraceSegmentVo> localSegmentList) {
+ TraceRegistration registration = new TraceRegistration();
+ boolean rerouted = !isTerminalStatus(this.status) && this.traceVersion != null && this.traceVersion > 0;
+ int nextTraceVersion = rerouted ? this.traceVersion + 1 : 1;
+ int pathOffset = rerouted ? this.passedStationIds.size() : 0;
+ Integer planCurrentStationId = currentStationId != null ? currentStationId : startStationId;
+ List<Integer> nextFullPath = buildFullPathForRegistration(rerouted, planCurrentStationId, localPathStationIds);
+ List<StationTaskTraceSegmentVo> nextSegmentList = shiftSegments(copySegmentList(localSegmentList), pathOffset);
+
+ this.threadImpl = threadImpl;
+ this.traceVersion = nextTraceVersion;
+ this.startStationId = rerouted && this.startStationId != null ? this.startStationId : startStationId;
+ this.currentStationId = planCurrentStationId;
+ this.finalTargetStationId = finalTargetStationId;
+ this.blockedStationId = null;
+ this.fullPathStationIds = nextFullPath;
+ this.segmentList = nextSegmentList;
+ this.issuedSegmentCount = 0;
+ this.totalSegmentCount = nextSegmentList.size();
+ this.issuedStationIds = new ArrayList<>();
+ this.latestIssuedSegmentPath = new ArrayList<>();
+ this.status = rerouted ? STATUS_REROUTED : STATUS_WAITING;
+ this.terminalExpireAt = null;
+ rebuildProgress(planCurrentStationId);
+ this.updatedAt = System.currentTimeMillis();
+
+ Map<String, Object> details = new LinkedHashMap<>();
+ details.put("traceVersion", nextTraceVersion);
+ details.put("fullPathStationIds", copyIntegerList(this.fullPathStationIds));
+ details.put("segmentCount", this.totalSegmentCount);
+ details.put("pathOffset", pathOffset);
+ details.put("currentStationId", this.currentStationId);
+ appendEvent(rerouted ? "REROUTED" : "PLAN_READY",
+ rerouted ? "杈撻�佷换鍔¤矾寰勫凡閲嶇畻骞剁画鎺ヨ建杩�" : "杈撻�佷换鍔″垎娈佃鍒掑凡寤虹珛",
+ details);
+
+ registration.setTraceVersion(nextTraceVersion);
+ registration.setPathOffset(pathOffset);
+ registration.setFullPathStationIds(copyIntegerList(this.fullPathStationIds));
+ registration.setRerouted(rerouted);
+ return registration;
+ }
+
+ private synchronized void markSegmentIssued(Integer traceVersion,
+ StationCommand command,
+ String eventType,
+ String message,
+ Map<String, Object> details) {
+ if (!acceptTraceVersion(traceVersion)) {
+ return;
+ }
+ this.status = STATUS_RUNNING;
+ this.blockedStationId = null;
+ int currentIssued = this.issuedSegmentCount == null ? 0 : this.issuedSegmentCount;
+ int nextIssued = command == null || command.getSegmentNo() == null ? currentIssued : command.getSegmentNo();
+ this.issuedSegmentCount = Math.max(currentIssued, nextIssued);
+ this.latestIssuedSegmentPath = copyIntegerList(command == null ? null : command.getNavigatePath());
+ int segmentEndIndex = command == null || command.getSegmentEndIndex() == null ? -1 : command.getSegmentEndIndex();
+ if (segmentEndIndex >= 0 && !this.fullPathStationIds.isEmpty()) {
+ int end = Math.min(segmentEndIndex + 1, this.fullPathStationIds.size());
+ this.issuedStationIds = copyIntegerList(this.fullPathStationIds.subList(0, end));
+ }
+ this.updatedAt = System.currentTimeMillis();
+
+ Map<String, Object> nextDetails = copyDetails(details);
+ if (command != null) {
+ nextDetails.put("segmentNo", command.getSegmentNo());
+ nextDetails.put("segmentCount", command.getSegmentCount());
+ nextDetails.put("commandStationId", command.getStationId());
+ nextDetails.put("commandTargetStationId", command.getTargetStaNo());
+ nextDetails.put("segmentPath", copyIntegerList(command.getNavigatePath()));
+ nextDetails.put("issuedSegmentCount", this.issuedSegmentCount);
+ nextDetails.put("totalSegmentCount", this.totalSegmentCount);
+ }
+ appendEvent(eventType, message, nextDetails);
+ }
+
+ private synchronized void updateProgress(Integer traceVersion,
+ Integer currentStationId,
+ String eventType,
+ String message,
+ Map<String, Object> details) {
+ if (!acceptTraceVersion(traceVersion)) {
+ return;
+ }
+ boolean changed = !Objects.equals(this.currentStationId, currentStationId);
+ rebuildProgress(currentStationId);
+ if (!isTerminalStatus(this.status)) {
+ this.status = STATUS_RUNNING;
+ this.blockedStationId = null;
+ }
+ this.updatedAt = System.currentTimeMillis();
+ if (changed && eventType != null) {
+ Map<String, Object> nextDetails = copyDetails(details);
+ nextDetails.put("currentStationId", currentStationId);
+ nextDetails.put("passedStationIds", copyIntegerList(this.passedStationIds));
+ nextDetails.put("pendingStationIds", copyIntegerList(this.pendingStationIds));
+ appendEvent(eventType, message, nextDetails);
+ }
+ }
+
+ private synchronized void markTerminal(Integer traceVersion,
+ String terminalStatus,
+ Integer currentStationId,
+ Integer blockedStationId,
+ String eventType,
+ String message,
+ Map<String, Object> details) {
+ if (!acceptTraceVersion(traceVersion)) {
+ return;
+ }
+ if (currentStationId != null) {
+ rebuildProgress(currentStationId);
+ }
+ this.status = terminalStatus;
+ this.blockedStationId = blockedStationId;
+ this.updatedAt = System.currentTimeMillis();
+ this.terminalExpireAt = this.updatedAt + TERMINAL_KEEP_MS;
+
+ Map<String, Object> nextDetails = copyDetails(details);
+ nextDetails.put("currentStationId", this.currentStationId);
+ nextDetails.put("blockedStationId", this.blockedStationId);
+ nextDetails.put("passedStationIds", copyIntegerList(this.passedStationIds));
+ nextDetails.put("pendingStationIds", copyIntegerList(this.pendingStationIds));
+ appendEvent(eventType, message, nextDetails);
+ }
+
+ private synchronized boolean shouldRemove(long now) {
+ return terminalExpireAt != null && terminalExpireAt <= now;
+ }
+
+ private synchronized StationTaskTraceVo toVo() {
+ StationTaskTraceVo vo = new StationTaskTraceVo();
+ vo.setTaskNo(taskNo);
+ vo.setThreadImpl(threadImpl);
+ vo.setStatus(status);
+ vo.setTraceVersion(traceVersion);
+ vo.setStartStationId(startStationId);
+ vo.setCurrentStationId(currentStationId);
+ vo.setFinalTargetStationId(finalTargetStationId);
+ vo.setBlockedStationId(blockedStationId);
+ vo.setFullPathStationIds(copyIntegerList(fullPathStationIds));
+ vo.setIssuedStationIds(copyIntegerList(issuedStationIds));
+ vo.setPassedStationIds(copyIntegerList(passedStationIds));
+ vo.setPendingStationIds(copyIntegerList(pendingStationIds));
+ vo.setLatestIssuedSegmentPath(copyIntegerList(latestIssuedSegmentPath));
+ vo.setSegmentList(copySegmentListWithIssued(segmentList, issuedSegmentCount));
+ vo.setIssuedSegmentCount(issuedSegmentCount);
+ vo.setTotalSegmentCount(totalSegmentCount);
+ vo.setUpdatedAt(updatedAt);
+ vo.setEvents(new ArrayList<>(events));
+ return vo;
+ }
+
+ private List<Integer> buildFullPathForRegistration(boolean rerouted,
+ Integer planCurrentStationId,
+ List<Integer> localPathStationIds) {
+ List<Integer> localPath = copyIntegerList(localPathStationIds);
+ if (!rerouted) {
+ if (localPath.isEmpty() && planCurrentStationId != null) {
+ localPath.add(planCurrentStationId);
+ }
+ return localPath;
+ }
+
+ List<Integer> result = new ArrayList<>(copyIntegerList(this.passedStationIds));
+ if (planCurrentStationId != null) {
+ result.add(planCurrentStationId);
+ }
+ if (!localPath.isEmpty()) {
+ int startIdx = 0;
+ if (planCurrentStationId != null && Objects.equals(localPath.get(0), planCurrentStationId)) {
+ startIdx = 1;
+ }
+ for (int i = startIdx; i < localPath.size(); i++) {
+ Integer stationId = localPath.get(i);
+ if (stationId != null) {
+ result.add(stationId);
+ }
+ }
+ }
+ return result;
+ }
+
+ private void rebuildProgress(Integer nextCurrentStationId) {
+ this.currentStationId = nextCurrentStationId;
+ List<Integer> fullPath = copyIntegerList(this.fullPathStationIds);
+ this.passedStationIds = new ArrayList<>();
+ this.pendingStationIds = copyIntegerList(fullPath);
+ int currentIndex = nextCurrentStationId == null ? -1 : fullPath.indexOf(nextCurrentStationId);
+ if (currentIndex < 0) {
+ return;
+ }
+ this.passedStationIds = copyIntegerList(fullPath.subList(0, currentIndex));
+ this.pendingStationIds = copyIntegerList(fullPath.subList(currentIndex + 1, fullPath.size()));
+ }
+
+ private boolean acceptTraceVersion(Integer incomingTraceVersion) {
+ return incomingTraceVersion != null
+ && this.traceVersion != null
+ && incomingTraceVersion.intValue() == this.traceVersion.intValue();
+ }
+
+ private void appendEvent(String eventType, String message, Map<String, Object> details) {
+ if (eventType == null) {
+ return;
+ }
+ StationTaskTraceEventVo event = new StationTaskTraceEventVo();
+ event.setTimestamp(System.currentTimeMillis());
+ event.setEventType(eventType);
+ event.setMessage(message);
+ event.setStatus(this.status);
+ event.setCurrentStationId(this.currentStationId);
+ event.setTargetStationId(this.finalTargetStationId);
+ event.setTraceVersion(this.traceVersion);
+ event.setDetails(copyDetails(details));
+ this.events.add(event);
+ if (this.events.size() > MAX_EVENT_COUNT) {
+ this.events.remove(0);
+ }
+ }
+
+ private List<StationTaskTraceSegmentVo> shiftSegments(List<StationTaskTraceSegmentVo> source, int pathOffset) {
+ List<StationTaskTraceSegmentVo> result = new ArrayList<>();
+ for (StationTaskTraceSegmentVo item : source) {
+ if (item == null) {
+ continue;
+ }
+ StationTaskTraceSegmentVo copy = new StationTaskTraceSegmentVo();
+ copy.setSegmentNo(item.getSegmentNo());
+ copy.setSegmentCount(item.getSegmentCount());
+ copy.setStationId(item.getStationId());
+ copy.setTargetStationId(item.getTargetStationId());
+ copy.setSegmentStartIndex(item.getSegmentStartIndex() == null ? null : item.getSegmentStartIndex() + pathOffset);
+ copy.setSegmentEndIndex(item.getSegmentEndIndex() == null ? null : item.getSegmentEndIndex() + pathOffset);
+ copy.setSegmentPath(copyIntegerList(item.getSegmentPath()));
+ copy.setIssued(Boolean.FALSE);
+ result.add(copy);
+ }
+ return result;
+ }
+
+ private List<StationTaskTraceSegmentVo> copySegmentListWithIssued(List<StationTaskTraceSegmentVo> source, Integer issuedSegmentCount) {
+ List<StationTaskTraceSegmentVo> result = new ArrayList<>();
+ int issuedCount = issuedSegmentCount == null ? 0 : issuedSegmentCount;
+ for (StationTaskTraceSegmentVo item : source) {
+ if (item == null) {
+ continue;
+ }
+ StationTaskTraceSegmentVo copy = new StationTaskTraceSegmentVo();
+ copy.setSegmentNo(item.getSegmentNo());
+ copy.setSegmentCount(item.getSegmentCount());
+ copy.setStationId(item.getStationId());
+ copy.setTargetStationId(item.getTargetStationId());
+ copy.setSegmentStartIndex(item.getSegmentStartIndex());
+ copy.setSegmentEndIndex(item.getSegmentEndIndex());
+ copy.setSegmentPath(copyIntegerList(item.getSegmentPath()));
+ copy.setIssued(item.getSegmentNo() != null && item.getSegmentNo() <= issuedCount);
+ result.add(copy);
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/main/webapp/components/DevpCard.js b/src/main/webapp/components/DevpCard.js
index d8daf18..4aecb86 100644
--- a/src/main/webapp/components/DevpCard.js
+++ b/src/main/webapp/components/DevpCard.js
@@ -1,3 +1,5 @@
+var stationTracePageVersion = "20260319_station_trace_layout_v2";
+
Vue.component("devp-card", {
template: `
<div class="mc-root">
@@ -32,6 +34,7 @@
<div class="mc-action-row">
<button type="button" class="mc-btn" @click="controlCommand">涓嬪彂</button>
<button type="button" class="mc-btn mc-btn-soft" @click="resetCommand">澶嶄綅</button>
+ <button type="button" class="mc-btn mc-btn-ghost" @click="openStationTracePage">杩愯杞ㄨ抗</button>
<button v-if="showFakeTraceEntry" type="button" class="mc-btn mc-btn-ghost" @click="openFakeTracePage">浠跨湡杞ㄨ抗</button>
</div>
</div>
@@ -252,6 +255,9 @@
}
window.open(baseUrl + "/views/watch/fakeTrace.html", "_blank");
},
+ openStationTracePage: function () {
+ window.open(baseUrl + "/views/watch/stationTrace.html?v=" + stationTracePageVersion, "_blank");
+ },
buildDetailEntries: function (item) {
return [
{ label: "缂栧彿", value: this.orDash(item.stationId) },
diff --git a/src/main/webapp/components/MapCanvas.js b/src/main/webapp/components/MapCanvas.js
index 06c06d3..3dfb358 100644
--- a/src/main/webapp/components/MapCanvas.js
+++ b/src/main/webapp/components/MapCanvas.js
@@ -1854,7 +1854,7 @@
blockedStationId: this.parseStationTaskNo(trace.blockedStationId),
passedStationIds: this.normalizeTraceStationIds(trace.passedStationIds),
pendingStationIds: this.normalizeTraceStationIds(trace.pendingStationIds),
- latestAppendedPath: this.normalizeTraceStationIds(trace.latestAppendedPath)
+ latestAppendedPath: this.normalizeTraceStationIds(trace.latestIssuedSegmentPath || trace.latestAppendedPath)
};
},
normalizeTraceStationIds(list) {
@@ -2794,7 +2794,6 @@
}
}
});
-
diff --git a/src/main/webapp/views/watch/stationTrace.html b/src/main/webapp/views/watch/stationTrace.html
new file mode 100644
index 0000000..ca08a37
--- /dev/null
+++ b/src/main/webapp/views/watch/stationTrace.html
@@ -0,0 +1,903 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>杈撻�佷换鍔¤建杩�</title>
+ <link rel="stylesheet" href="../../static/css/watch/console_vue.css">
+ <link rel="stylesheet" href="../../static/vue/element/element.css">
+ <style>
+ html, body, #app {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ }
+
+ body {
+ background: linear-gradient(180deg, #eef4f8 0%, #e7edf4 100%);
+ color: #27425c;
+ font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
+ }
+
+ #app {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .trace-page {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 18px;
+ box-sizing: border-box;
+ gap: 14px;
+ }
+
+ .trace-topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 14px 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);
+ }
+
+ .trace-title {
+ font-size: 18px;
+ font-weight: 700;
+ line-height: 1.2;
+ }
+
+ .trace-subtitle {
+ margin-top: 4px;
+ font-size: 12px;
+ color: #718399;
+ }
+
+ .trace-topbar-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-shrink: 0;
+ }
+
+ .trace-search {
+ width: 180px;
+ height: 34px;
+ padding: 0 12px;
+ border-radius: 999px;
+ border: 1px solid rgba(210, 221, 232, 0.98);
+ background: rgba(255, 255, 255, 0.9);
+ color: #31485f;
+ outline: none;
+ box-sizing: border-box;
+ }
+
+ .trace-status-pill {
+ padding: 6px 10px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 700;
+ background: rgba(111, 149, 189, 0.12);
+ color: #42617f;
+ }
+
+ .trace-main {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: 300px minmax(0, 1fr) 360px;
+ gap: 14px;
+ }
+
+ .trace-card {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ 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;
+ }
+
+ .trace-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid rgba(226, 232, 240, 0.72);
+ background: rgba(255, 255, 255, 0.24);
+ }
+
+ .trace-card-title {
+ font-size: 14px;
+ font-weight: 700;
+ color: #27425c;
+ }
+
+ .trace-card-body {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .trace-task-list,
+ .trace-timeline {
+ flex: 1;
+ min-height: 0;
+ padding: 10px;
+ overflow: auto;
+ }
+
+ .trace-task-item {
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+ gap: 7px;
+ margin-bottom: 10px;
+ padding: 12px;
+ border: 1px solid transparent;
+ border-radius: 14px;
+ background: rgba(247, 250, 252, 0.82);
+ text-align: left;
+ color: inherit;
+ cursor: pointer;
+ box-sizing: border-box;
+ }
+
+ .trace-task-item:last-child,
+ .trace-event:last-child {
+ margin-bottom: 0;
+ }
+
+ .trace-task-item.is-active {
+ border-color: rgba(111, 149, 189, 0.34);
+ background: rgba(235, 243, 251, 0.94);
+ }
+
+ .trace-task-line,
+ .trace-card-header,
+ .trace-event-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ }
+
+ .trace-task-title,
+ .trace-event-title {
+ font-size: 12px;
+ font-weight: 700;
+ color: #27425c;
+ }
+
+ .trace-task-meta,
+ .trace-event-detail {
+ font-size: 11px;
+ color: #748397;
+ line-height: 1.5;
+ word-break: break-all;
+ }
+
+ .trace-badge {
+ padding: 3px 8px;
+ border-radius: 999px;
+ font-size: 10px;
+ font-weight: 700;
+ white-space: nowrap;
+ }
+
+ .trace-badge.is-running {
+ background: rgba(59, 130, 246, 0.14);
+ color: #245baf;
+ }
+
+ .trace-badge.is-rerouted {
+ background: rgba(20, 184, 166, 0.16);
+ color: #0f766e;
+ }
+
+ .trace-badge.is-waiting {
+ background: rgba(148, 163, 184, 0.16);
+ color: #64748b;
+ }
+
+ .trace-badge.is-blocked {
+ background: rgba(239, 68, 68, 0.14);
+ color: #b42318;
+ }
+
+ .trace-badge.is-timeout {
+ background: rgba(249, 115, 22, 0.16);
+ color: #b45309;
+ }
+
+ .trace-badge.is-finished {
+ background: rgba(34, 197, 94, 0.14);
+ color: #15803d;
+ }
+
+ .trace-badge.is-cancelled {
+ background: rgba(148, 163, 184, 0.18);
+ color: #64748b;
+ }
+
+ .trace-map-card .trace-card-body {
+ gap: 10px;
+ padding: 12px;
+ box-sizing: border-box;
+ overflow: hidden;
+ }
+
+ .trace-detail-scroll {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ overflow: auto;
+ padding-right: 4px;
+ box-sizing: border-box;
+ }
+
+ .trace-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ }
+
+ .trace-summary-item,
+ .trace-path-row,
+ .trace-segment-strip {
+ padding: 10px 12px;
+ border-radius: 12px;
+ background: rgba(247, 250, 252, 0.88);
+ border: 1px solid rgba(233, 239, 244, 0.96);
+ }
+
+ .trace-summary-label,
+ .trace-path-label,
+ .trace-segment-head {
+ font-size: 11px;
+ font-weight: 700;
+ color: #6f8194;
+ }
+
+ .trace-summary-value {
+ margin-top: 5px;
+ font-size: 14px;
+ font-weight: 700;
+ color: #31485f;
+ word-break: break-all;
+ }
+
+ .trace-path-board {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 8px;
+ }
+
+ .trace-path-value {
+ margin-top: 6px;
+ font-size: 12px;
+ line-height: 1.5;
+ color: #31485f;
+ word-break: break-all;
+ }
+
+ .trace-segment-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+ }
+
+ .trace-segment-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 12px;
+ font-weight: 700;
+ color: #31485f;
+ }
+
+ .trace-segment-path {
+ margin-top: 6px;
+ font-size: 11px;
+ line-height: 1.5;
+ color: #6f8194;
+ word-break: break-all;
+ }
+
+ .trace-map-shell {
+ flex: 0 0 320px;
+ min-height: 320px;
+ border-radius: 16px;
+ overflow: hidden;
+ border: 1px solid rgba(224, 232, 239, 0.92);
+ background: rgba(255, 255, 255, 0.62);
+ }
+
+ .trace-map-shell map-canvas {
+ display: block;
+ width: 100%;
+ height: 100%;
+ }
+
+ .trace-event {
+ position: relative;
+ padding: 0 0 14px 18px;
+ margin-bottom: 14px;
+ border-left: 2px solid rgba(210, 221, 232, 0.96);
+ }
+
+ .trace-event::before {
+ content: "";
+ position: absolute;
+ left: -6px;
+ top: 2px;
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: #6f95bd;
+ box-shadow: 0 0 0 3px rgba(111, 149, 189, 0.12);
+ }
+
+ .trace-event-time {
+ font-size: 11px;
+ color: #8090a2;
+ white-space: nowrap;
+ }
+
+ .trace-event-message,
+ .trace-empty {
+ font-size: 12px;
+ line-height: 1.5;
+ color: #4a627a;
+ }
+
+ .trace-empty {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ text-align: center;
+ color: #8b9aad;
+ }
+
+ @media (max-width: 1440px) {
+ .trace-main {
+ grid-template-columns: 280px minmax(0, 1fr) 320px;
+ }
+
+ .trace-summary-grid,
+ .trace-segment-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+ }
+ </style>
+ <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.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>
+</head>
+<body>
+<div id="app">
+ <div class="trace-page">
+ <div class="trace-topbar">
+ <div>
+ <div class="trace-title">杈撻�佷换鍔¤建杩�</div>
+ <div class="trace-subtitle">鎸変换鍔″彿鏌ョ湅鏁存潯璺緞銆佸凡涓嬪彂娈点�佸綋鍓嶄綅缃�佸墿浣欒矾寰勫拰鎵ц鏃堕棿绾�</div>
+ </div>
+ <div class="trace-topbar-actions">
+ <div class="trace-status-pill">{{ wsStatusText }}</div>
+ <input class="trace-search" v-model.trim="searchTaskNo" placeholder="绛涢�変换鍔″彿" />
+ </div>
+ </div>
+
+ <div class="trace-main">
+ <div class="trace-card">
+ <div class="trace-card-header">
+ <div class="trace-card-title">杞ㄨ抗浠诲姟</div>
+ <div class="trace-status-pill">{{ filteredTraces.length }} 涓�</div>
+ </div>
+ <div class="trace-card-body">
+ <div v-if="filteredTraces.length === 0" class="trace-empty">褰撳墠娌℃湁杈撻�佷换鍔¤建杩�</div>
+ <div v-else class="trace-task-list">
+ <button
+ v-for="item in filteredTraces"
+ :key="item.taskNo + '-' + item.traceVersion"
+ type="button"
+ class="trace-task-item"
+ :class="{ 'is-active': selectedTaskNo === item.taskNo }"
+ @click="selectTrace(item.taskNo)">
+ <div class="trace-task-line">
+ <div class="trace-task-title">浠诲姟 {{ item.taskNo }}</div>
+ <span class="trace-badge" :class="'is-' + statusTone(item.status)">{{ item.status || '--' }}</span>
+ </div>
+ <div class="trace-task-meta">
+ 褰撳墠绔�: {{ orDash(item.currentStationId) }}<br>
+ 鐩爣绔�: {{ orDash(item.finalTargetStationId) }}<br>
+ 鍒嗘: {{ orDash(item.issuedSegmentCount) }} / {{ orDash(item.totalSegmentCount) }}<br>
+ 鏇存柊鏃堕棿: {{ formatTime(item.updatedAt) }}
+ </div>
+ </button>
+ </div>
+ </div>
+ </div>
+
+ <div class="trace-card trace-map-card">
+ <div class="trace-card-header">
+ <div class="trace-card-title">鍦板浘涓庤矾寰勬憳瑕�</div>
+ <div v-if="selectedTrace" class="trace-status-pill">妤煎眰 {{ currentLev }}F</div>
+ </div>
+ <div class="trace-card-body">
+ <div class="trace-map-shell">
+ <map-canvas
+ :lev="currentLev"
+ :lev-list="levList"
+ :crn-param="crnParam"
+ :rgv-param="rgvParam"
+ :devp-param="devpParam"
+ :station-task-range="stationTaskRange"
+ :trace-overlay="selectedTrace"
+ @switch-lev="switchLev">
+ </map-canvas>
+ </div>
+ <template v-if="selectedTrace">
+ <div class="trace-detail-scroll">
+ <div class="trace-summary-grid">
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">浠诲姟鍙�</div>
+ <div class="trace-summary-value">{{ selectedTrace.taskNo }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">鐘舵��</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.status) }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">褰撳墠绔欑偣</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.currentStationId) }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">鏈�缁堢洰鏍�</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.finalTargetStationId) }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">宸蹭笅鍙戞</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.issuedSegmentCount) }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">鎬绘鏁�</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.totalSegmentCount) }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">杞ㄨ抗鐗堟湰</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.traceVersion) }}</div>
+ </div>
+ <div class="trace-summary-item">
+ <div class="trace-summary-label">绾跨▼瀹炵幇</div>
+ <div class="trace-summary-value">{{ orDash(selectedTrace.threadImpl) }}</div>
+ </div>
+ </div>
+
+ <div class="trace-path-board">
+ <div class="trace-path-row">
+ <div class="trace-path-label">瀹屾暣璺緞</div>
+ <div class="trace-path-value">{{ formatPath(selectedTrace.fullPathStationIds) }}</div>
+ </div>
+ <div class="trace-path-row">
+ <div class="trace-path-label">宸蹭笅鍙戣矾寰�</div>
+ <div class="trace-path-value">{{ formatPath(selectedTrace.issuedStationIds) }}</div>
+ </div>
+ <div class="trace-path-row">
+ <div class="trace-path-label">宸茶蛋璺緞</div>
+ <div class="trace-path-value">{{ formatPath(selectedTrace.passedStationIds) }}</div>
+ </div>
+ <div class="trace-path-row">
+ <div class="trace-path-label">寰呰蛋璺緞</div>
+ <div class="trace-path-value">{{ formatPath(selectedTrace.pendingStationIds) }}</div>
+ </div>
+ <div class="trace-path-row">
+ <div class="trace-path-label">鏈�鏂颁笅鍙戞</div>
+ <div class="trace-path-value">{{ formatPath(selectedTrace.latestIssuedSegmentPath) }}</div>
+ </div>
+ </div>
+
+ <div class="trace-segment-grid" v-if="selectedSegments.length">
+ <div v-for="segment in selectedSegments" :key="'segment-' + segment.segmentNo + '-' + segment.segmentStartIndex" class="trace-segment-strip">
+ <div class="trace-segment-title">
+ <span>绗� {{ segment.segmentNo }} 娈�</span>
+ <span class="trace-badge" :class="segment.issued ? 'is-finished' : 'is-waiting'">{{ segment.issued ? '宸蹭笅鍙�' : '寰呬笅鍙�' }}</span>
+ </div>
+ <div class="trace-segment-path">{{ formatPath(segment.segmentPath) }}</div>
+ </div>
+ </div>
+ </div>
+ </template>
+ <div v-else class="trace-empty">璇烽�夋嫨涓�涓换鍔℃煡鐪嬫槑缁�</div>
+ </div>
+ </div>
+
+ <div class="trace-card">
+ <div class="trace-card-header">
+ <div class="trace-card-title">鎵ц鏃堕棿绾�</div>
+ <div v-if="selectedTrace" class="trace-status-pill">{{ selectedTraceEvents.length }} 鏉�</div>
+ </div>
+ <div class="trace-card-body">
+ <div v-if="!selectedTrace" class="trace-empty">璇烽�夋嫨涓�涓换鍔℃煡鐪嬫槑缁�</div>
+ <div v-else-if="selectedTraceEvents.length === 0" class="trace-empty">褰撳墠浠诲姟杩樻病鏈夎建杩逛簨浠�</div>
+ <div v-else class="trace-timeline">
+ <div v-for="(event, index) in selectedTraceEvents" :key="event.eventType + '-' + event.timestamp + '-' + index" class="trace-event">
+ <div class="trace-event-head">
+ <div class="trace-event-title">{{ event.eventType }}</div>
+ <div class="trace-event-time">{{ formatTime(event.timestamp) }}</div>
+ </div>
+ <div class="trace-event-message">{{ event.message || '--' }}</div>
+ <div v-for="detail in renderEventDetails(event)" :key="detail" class="trace-event-detail">{{ detail }}</div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script src="../../components/MapCanvas.js?v=20260319_station_trace_v1"></script>
+<script>
+ var stationTraceWs = null;
+
+ new Vue({
+ el: '#app',
+ data: {
+ traces: [],
+ selectedTaskNo: null,
+ searchTaskNo: '',
+ currentLev: 1,
+ levList: [],
+ stationTaskRange: {
+ inbound: null,
+ outbound: null
+ },
+ crnParam: { crnNo: 0 },
+ rgvParam: { rgvNo: 0 },
+ devpParam: { stationId: 0 },
+ wsReconnectTimer: null,
+ wsReconnectAttempts: 0,
+ wsReconnectBaseDelay: 1000,
+ wsReconnectMaxDelay: 15000,
+ tracePollTimer: null,
+ wsStatus: 'connecting'
+ },
+ computed: {
+ filteredTraces: function () {
+ var keyword = String(this.searchTaskNo || '').trim();
+ if (!keyword) {
+ return this.traces;
+ }
+ return this.traces.filter(function (item) {
+ return String(item.taskNo || '').indexOf(keyword) >= 0;
+ });
+ },
+ selectedTrace: function () {
+ if (!this.selectedTaskNo) {
+ return null;
+ }
+ for (var i = 0; i < this.traces.length; i++) {
+ if (this.traces[i] && this.traces[i].taskNo === this.selectedTaskNo) {
+ return this.traces[i];
+ }
+ }
+ return null;
+ },
+ selectedSegments: function () {
+ var trace = this.selectedTrace;
+ return trace && Array.isArray(trace.segmentList) ? trace.segmentList : [];
+ },
+ selectedTraceEvents: function () {
+ var trace = this.selectedTrace;
+ if (!trace || !Array.isArray(trace.events)) {
+ return [];
+ }
+ return trace.events.slice().sort(function (a, b) {
+ var va = a && a.timestamp ? a.timestamp : 0;
+ var vb = b && b.timestamp ? b.timestamp : 0;
+ return vb - va;
+ });
+ },
+ wsStatusText: function () {
+ if (this.wsStatus === 'open') {
+ return '宸茶繛鎺�';
+ }
+ if (this.wsStatus === 'closed') {
+ return '杩炴帴鏂紑';
+ }
+ if (this.wsStatus === 'error') {
+ return '杩炴帴寮傚父';
+ }
+ return '杩炴帴涓�';
+ }
+ },
+ watch: {
+ selectedTrace: {
+ deep: true,
+ immediate: true,
+ handler: function (trace) {
+ if (!trace) {
+ this.devpParam.stationId = 0;
+ return;
+ }
+ this.devpParam.stationId = this.resolveFocusStationId(trace);
+ this.applySelectedTraceFloor(trace);
+ }
+ }
+ },
+ created: function () {
+ this.init();
+ },
+ beforeDestroy: function () {
+ if (this.tracePollTimer) {
+ clearInterval(this.tracePollTimer);
+ this.tracePollTimer = null;
+ }
+ if (this.wsReconnectTimer) {
+ clearTimeout(this.wsReconnectTimer);
+ this.wsReconnectTimer = null;
+ }
+ if (stationTraceWs && (stationTraceWs.readyState === WebSocket.OPEN || stationTraceWs.readyState === WebSocket.CONNECTING)) {
+ try { stationTraceWs.close(); } catch (e) {}
+ }
+ },
+ methods: {
+ init: function () {
+ this.connectWs();
+ this.getLevList();
+ this.getStationTaskRange();
+ },
+ connectWs: function () {
+ if (stationTraceWs && (stationTraceWs.readyState === WebSocket.OPEN || stationTraceWs.readyState === WebSocket.CONNECTING)) {
+ return;
+ }
+ this.wsStatus = 'connecting';
+ stationTraceWs = new WebSocket('ws://' + window.location.host + baseUrl + '/console/websocket');
+ stationTraceWs.onopen = this.webSocketOnOpen;
+ stationTraceWs.onerror = this.webSocketOnError;
+ stationTraceWs.onmessage = this.webSocketOnMessage;
+ stationTraceWs.onclose = this.webSocketOnClose;
+ },
+ webSocketOnOpen: function () {
+ this.wsStatus = 'open';
+ this.wsReconnectAttempts = 0;
+ if (this.wsReconnectTimer) {
+ clearTimeout(this.wsReconnectTimer);
+ this.wsReconnectTimer = null;
+ }
+ this.refreshTrace();
+ if (!this.tracePollTimer) {
+ this.tracePollTimer = setInterval(function () {
+ this.refreshTrace();
+ }.bind(this), 1000);
+ }
+ },
+ webSocketOnError: function () {
+ this.wsStatus = 'error';
+ this.scheduleReconnect();
+ },
+ webSocketOnClose: function () {
+ this.wsStatus = 'closed';
+ this.scheduleReconnect();
+ },
+ webSocketOnMessage: function (event) {
+ var result = JSON.parse(event.data);
+ if (result.url === '/console/latest/data/station/trace') {
+ this.setTraceList(JSON.parse(result.data));
+ }
+ },
+ scheduleReconnect: function () {
+ if (this.wsReconnectTimer) {
+ return;
+ }
+ var attempt = this.wsReconnectAttempts + 1;
+ var jitter = Math.floor(Math.random() * 300);
+ var delay = Math.min(this.wsReconnectMaxDelay, this.wsReconnectBaseDelay * Math.pow(2, this.wsReconnectAttempts)) + jitter;
+ this.wsReconnectTimer = setTimeout(function () {
+ this.wsReconnectTimer = null;
+ this.wsReconnectAttempts = attempt;
+ this.connectWs();
+ }.bind(this), delay);
+ },
+ sendWs: function (payload) {
+ if (stationTraceWs && stationTraceWs.readyState === WebSocket.OPEN) {
+ stationTraceWs.send(payload);
+ }
+ },
+ refreshTrace: function () {
+ this.sendWs(JSON.stringify({
+ url: '/console/latest/data/station/trace',
+ data: {}
+ }));
+ },
+ setTraceList: function (res) {
+ if (!res) {
+ return;
+ }
+ if (res.code === 403) {
+ parent.location.href = baseUrl + '/login';
+ return;
+ }
+ if (res.code !== 200) {
+ return;
+ }
+ this.traces = Array.isArray(res.data) ? res.data : [];
+ if (this.selectedTaskNo != null) {
+ var matched = this.traces.some(function (item) {
+ return item && item.taskNo === this.selectedTaskNo;
+ }.bind(this));
+ if (!matched) {
+ this.selectedTaskNo = this.traces.length > 0 ? this.traces[0].taskNo : null;
+ }
+ } else if (this.traces.length > 0) {
+ this.selectedTaskNo = this.traces[0].taskNo;
+ }
+ },
+ selectTrace: function (taskNo) {
+ this.selectedTaskNo = taskNo;
+ },
+ switchLev: function (lev) {
+ this.currentLev = lev;
+ },
+ getLevList: function () {
+ $.ajax({
+ url: baseUrl + '/basMap/getLevList',
+ headers: { token: localStorage.getItem('token') },
+ method: 'get',
+ success: function (res) {
+ if (!res || res.code !== 200) {
+ return;
+ }
+ this.levList = res.data || [];
+ if ((!this.currentLev || this.currentLev === 0) && this.levList.length > 0) {
+ this.currentLev = this.levList[0];
+ }
+ }.bind(this)
+ });
+ },
+ getStationTaskRange: function () {
+ this.fetchWrkLastnoRange(1, 'inbound');
+ this.fetchWrkLastnoRange(101, 'outbound');
+ },
+ fetchWrkLastnoRange: function (id, key) {
+ $.ajax({
+ url: baseUrl + '/wrkLastno/' + id + '/auth',
+ headers: { token: localStorage.getItem('token') },
+ method: 'get',
+ success: function (res) {
+ if (!res || res.code !== 200 || !res.data) {
+ return;
+ }
+ var nextRange = Object.assign({}, this.stationTaskRange);
+ nextRange[key] = {
+ start: res.data.sNo,
+ end: res.data.eNo
+ };
+ this.stationTaskRange = nextRange;
+ }.bind(this)
+ });
+ },
+ resolveFocusStationId: function (trace) {
+ if (!trace) {
+ return 0;
+ }
+ return trace.currentStationId || trace.blockedStationId || trace.startStationId || 0;
+ },
+ applySelectedTraceFloor: function (trace) {
+ var floor = this.resolveTraceFloor(trace);
+ if (floor > 0 && floor !== this.currentLev) {
+ this.currentLev = floor;
+ }
+ },
+ resolveTraceFloor: function (trace) {
+ if (!trace) {
+ return this.currentLev || 1;
+ }
+ var stationId = trace.currentStationId || trace.blockedStationId || trace.startStationId;
+ if (!stationId) {
+ return this.currentLev || 1;
+ }
+ var floor = parseInt(String(stationId).charAt(0), 10);
+ return isNaN(floor) || floor <= 0 ? (this.currentLev || 1) : floor;
+ },
+ statusTone: function (status) {
+ var value = String(status || '').toUpperCase();
+ if (value === 'RUNNING') {
+ return 'running';
+ }
+ if (value === 'REROUTED') {
+ return 'rerouted';
+ }
+ if (value === 'BLOCKED') {
+ return 'blocked';
+ }
+ if (value === 'TIMEOUT') {
+ return 'timeout';
+ }
+ if (value === 'FINISHED') {
+ return 'finished';
+ }
+ if (value === 'CANCELLED') {
+ return 'cancelled';
+ }
+ return 'waiting';
+ },
+ renderEventDetails: function (event) {
+ if (!event || !event.details) {
+ return [];
+ }
+ var labelMap = {
+ traceVersion: '杞ㄨ抗鐗堟湰',
+ segmentNo: '鍒嗘鍙�',
+ segmentCount: '鎬绘鏁�',
+ segmentPath: '鍒嗘璺緞',
+ segmentStartIndex: '鍒嗘璧峰绱㈠紩',
+ segmentEndIndex: '鍒嗘缁撴潫绱㈠紩',
+ issuedSegmentCount: '宸蹭笅鍙戞鏁�',
+ totalSegmentCount: '鎬绘鏁�',
+ fullPathStationIds: '瀹屾暣璺緞',
+ issuedStationIds: '宸蹭笅鍙戣矾寰�',
+ passedStationIds: '宸茶蛋璺緞',
+ pendingStationIds: '寰呰蛋璺緞',
+ currentStationId: '褰撳墠绔欑偣',
+ blockedStationId: '鍫靛绔欑偣',
+ timeoutMs: '瓒呮椂鏃堕棿',
+ commandStationId: '鍛戒护璧风偣',
+ commandTargetStationId: '鍛戒护鐩爣',
+ targetStationId: '鐩爣绔�',
+ stationId: '绔欑偣',
+ pathOffset: '鍘嗗彶鍋忕Щ',
+ reason: '鍘熷洜'
+ };
+ var result = [];
+ Object.keys(event.details).forEach(function (key) {
+ var value = event.details[key];
+ if (value == null || value === '') {
+ return;
+ }
+ var text = Array.isArray(value) ? value.join(' -> ') : String(value);
+ result.push((labelMap[key] || key) + ': ' + text);
+ });
+ return result;
+ },
+ formatPath: function (path) {
+ if (!Array.isArray(path) || path.length === 0) {
+ return '--';
+ }
+ return path.join(' -> ');
+ },
+ formatTime: function (timestamp) {
+ if (!timestamp) {
+ return '--';
+ }
+ var date = new Date(timestamp);
+ var pad = function (value) {
+ return value < 10 ? '0' + value : '' + value;
+ };
+ return pad(date.getHours()) + ':' + pad(date.getMinutes()) + ':' + pad(date.getSeconds());
+ },
+ orDash: function (value) {
+ return value == null || value === '' ? '--' : value;
+ }
+ }
+ });
+</script>
+</body>
+</html>
--
Gitblit v1.9.1