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