<!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>
|