| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | private StationCycleCapacityService stationCycleCapacityService; |
| | | @Autowired |
| | | private FakeTaskTraceRegistry fakeTaskTraceRegistry; |
| | | @Autowired |
| | | private StationTaskTraceRegistry stationTaskTraceRegistry; |
| | | |
| | | @PostMapping("/system/running/status") |
| | | @ManagerAuth(memo = "系统运行状态") |
| | |
| | | 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(){ |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | |
| | | } 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)) { |
| | |
| | | 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("未知的线程实现"); |
| | | } |
| | |
| | | // 仿真模式下可选:手工指定条码 |
| | | private String barcode; |
| | | |
| | | // V5 轨迹元数据,仅供 WCS 内部使用 |
| | | private Integer segmentNo; |
| | | |
| | | private Integer segmentCount; |
| | | |
| | | private Integer segmentStartIndex; |
| | | |
| | | private Integer segmentEndIndex; |
| | | |
| | | private Integer traceVersion; |
| | | |
| | | } |
| | |
| | | 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); |
| | |
| | | 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、ZyStationV4Thread 或 ZyStationV5Thread", |
| | | deviceConfig.getDeviceNo(), deviceConfig.getThreadImpl()); |
| | | return false; |
| | | } |
| | |
| | | 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) { |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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(); |
| | | } |
| | | } |
| New file |
| | |
| | | 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) { |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | 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); |
| | | } |
| | | } |
| New file |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | var stationTracePageVersion = "20260319_station_trace_layout_v2"; |
| | | |
| | | Vue.component("devp-card", { |
| | | template: ` |
| | | <div class="mc-root"> |
| | |
| | | <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> |
| | |
| | | } |
| | | 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) }, |
| | |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | }); |
| | | |
| | | |
| | | |
| | | |
| New file |
| | |
| | | <!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> |