var app = new Vue({ el: '#app', data: { dateTreeData: [], defaultProps: { children: 'children', label: 'title' }, defaultExpandedKeys: [], typeOrder: ['Crn', 'DualCrn', 'Rgv', 'Devp'], typeLabels: { Crn: '堆垛机', DualCrn: '双工位堆垛机', Rgv: 'RGV', Devp: '输送设备' }, selectedDay: '', searchDeviceNo: '', activeType: '', viewMode: 'picker', deviceSummary: { stats: { totalDevices: 0, totalFiles: 0, typeCounts: {} }, groups: [] }, summaryLoading: false, deviceEnums: {}, selectedType: '', selectedDeviceNo: '', activeDeviceKey: '', timelineMeta: { type: '', typeLabel: '', deviceNo: '', startTime: 0, endTime: 0, totalFiles: 0, segments: [] }, timelineLoading: false, logLoading: false, logLoadError: '', logRows: [], loadedOffsets: {}, loadingOffsets: {}, selectedTimestamp: 0, loadedSegmentRadius: 1, playbackTickMs: 120, logWindowAnchorOffset: -1, isPlaying: false, playbackSpeed: 1, timer: null, lastTick: 0, jumpVisible: false, jumpTime: null, detailTab: 'logs', rawTab: 'wcs', downloadDialogVisible: false, buildProgress: 0, receiveProgress: 0, downloadTimer: null }, computed: { summaryStats: function () { return this.deviceSummary && this.deviceSummary.stats ? this.deviceSummary.stats : { totalDevices: 0, totalFiles: 0, typeCounts: {} }; }, deviceGroups: function () { var self = this; var groups = {}; (this.deviceSummary.groups || []).forEach(function (group) { groups[group.type] = group; }); return this.typeOrder.map(function (type) { var group = groups[type] || { type: type, typeLabel: self.typeLabels[type] || type, deviceCount: 0, totalFiles: 0, devices: [] }; group.devices = (group.devices || []).slice().sort(function (a, b) { return self.parseDeviceNo(a.deviceNo) - self.parseDeviceNo(b.deviceNo); }); return group; }); }, recentDays: function () { var days = this.flattenDayNodes(this.dateTreeData); days.sort(function (a, b) { return b.day.localeCompare(a.day); }); return days.slice(0, 7); }, activeGroup: function () { var match = null; (this.deviceGroups || []).forEach(function (group) { if (group.type === this.activeType) { match = group; } }, this); if (match) { return match; } for (var i = 0; i < this.deviceGroups.length; i++) { if ((this.deviceGroups[i].devices || []).length > 0) { return this.deviceGroups[i]; } } return this.deviceGroups[0] || null; }, filteredDevices: function () { var group = this.activeGroup; var devices = group && group.devices ? group.devices.slice() : []; var keyword = String(this.searchDeviceNo || '').trim(); if (!keyword) { return devices; } return devices.filter(function (item) { return String(item.deviceNo).indexOf(keyword) >= 0; }); }, selectedDeviceSummary: function () { var key = this.activeDeviceKey; var found = null; (this.deviceGroups || []).forEach(function (group) { (group.devices || []).forEach(function (device) { if (this.buildDeviceKey(device.type, device.deviceNo) === key) { found = device; } }, this); }, this); return found; }, sliderMax: function () { if (!this.timelineMeta.startTime || !this.timelineMeta.endTime) { return 0; } return Math.max(0, this.timelineMeta.endTime - this.timelineMeta.startTime); }, sliderValue: function () { if (!this.timelineMeta.startTime || !this.selectedTimestamp) { return 0; } return Math.max(0, this.selectedTimestamp - this.timelineMeta.startTime); }, currentTimeStr: function () { if (!this.selectedTimestamp) { return '--'; } return this.formatTimestamp(this.selectedTimestamp, true); }, selectedLogRow: function () { if (!this.logRows.length) { return null; } var idx = this.binarySearch(this.selectedTimestamp); if (idx < 0) { return this.logRows[0]; } return this.logRows[idx]; }, selectedLogKey: function () { return this.selectedLogRow ? this.selectedLogRow._key : ''; }, currentStatusLabel: function () { if (!this.selectedLogRow) { return '未定位'; } return this.selectedLogRow._summary.statusLabel; }, currentStatusTone: function () { if (!this.selectedLogRow) { return 'muted'; } return this.selectedLogRow._summary.tone; }, currentLogTitle: function () { if (!this.selectedLogRow) { return '等待选择日志'; } return this.selectedLogRow._summary.title; }, currentLogDetail: function () { if (!this.selectedLogRow) { return this.selectedDeviceSummary ? '点击一条日志或拖动时间轴后,同步查看该时刻的状态。' : '请先选择设备。'; } return this.selectedLogRow._summary.detail + (this.selectedLogRow._summary.hint ? (' · ' + this.selectedLogRow._summary.hint) : ''); }, timelineRangeText: function () { var start = this.timelineMeta.startTime || (this.selectedDeviceSummary && this.selectedDeviceSummary.firstTime) || 0; var end = this.timelineMeta.endTime || (this.selectedDeviceSummary && this.selectedDeviceSummary.lastTime) || 0; if (!start && !end) { return '--'; } return this.formatTimestamp(start, false) + ' ~ ' + this.formatTimestamp(end, false); }, loadedSegmentCount: function () { return this.getLoadedOffsetNumbers().length; }, visualComponentName: function () { if (this.selectedType === 'Crn') { return 'watch-crn-card'; } if (this.selectedType === 'DualCrn') { return 'watch-dual-crn-card'; } if (this.selectedType === 'Rgv') { return 'watch-rgv-card'; } if (this.selectedType === 'Devp') { return 'devp-card'; } return ''; }, visualItems: function () { return this.selectedLogRow ? this.selectedLogRow._visualItems : []; }, visualParam: function () { return this.selectedLogRow ? this.selectedLogRow._visualParam : {}; }, activeRawText: function () { if (!this.selectedLogRow) { return ''; } return this.rawTab === 'origin' ? this.getOriginDataText(this.selectedLogRow) : this.getWcsDataText(this.selectedLogRow); }, activeRawHint: function () { if (!this.selectedLogRow) { return ''; } if (this.rawTab === 'origin') { if (!this.selectedLogRow.originData) { return '当前记录没有 originData。'; } return this.safeParse(this.selectedLogRow.originData) ? 'originData 已按 JSON 格式化展示。' : 'originData 不是合法 JSON,以下展示原始文本。'; } if (!this.selectedLogRow.wcsData) { return '当前记录没有 wcsData。'; } return this.selectedLogRow._protocol ? 'wcsData 已按 JSON 格式化展示。' : 'wcsData 不是合法 JSON,以下展示原始文本。'; }, canDownload: function () { return !!(this.selectedDay && this.selectedType && this.selectedDeviceNo); }, canPlay: function () { return !!(this.selectedDeviceSummary && this.timelineMeta.startTime && this.timelineMeta.endTime && this.sliderMax > 0); }, canLoadPreviousSegment: function () { if (!this.selectedDeviceSummary || !(this.timelineMeta.totalFiles > 0)) { return false; } var offsets = this.getLoadedOffsetNumbers(); if (!offsets.length) { return true; } return offsets[0] > 0; }, canLoadNextSegment: function () { if (!this.selectedDeviceSummary || !(this.timelineMeta.totalFiles > 0)) { return false; } var offsets = this.getLoadedOffsetNumbers(); if (!offsets.length) { return true; } return offsets[offsets.length - 1] < this.timelineMeta.totalFiles - 1; }, downloadDialogTitle: function () { return this.i18n('deviceLogs.downloadDialogTitle', '文件下载中'); } }, created: function () { this.loadDeviceEnums(); this.loadDateTree(); }, mounted: function () { if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') { var self = this; window.WCS_I18N.onReady(function () { self.$forceUpdate(); }); } }, beforeDestroy: function () { this.pause(); if (this.downloadTimer) { clearInterval(this.downloadTimer); this.downloadTimer = null; } }, methods: { i18n: function (key, fallback, params) { if (window.WCS_I18N && typeof window.WCS_I18N.t === 'function') { var translated = window.WCS_I18N.t(key, params); if (translated && translated !== key) { return translated; } } return fallback || key; }, emptySummary: function () { var self = this; return { stats: { totalDevices: 0, totalFiles: 0, typeCounts: {} }, groups: this.typeOrder.map(function (type) { return { type: type, typeLabel: self.typeLabels[type] || type, deviceCount: 0, totalFiles: 0, devices: [] }; }) }; }, createEmptyTimeline: function () { return { type: '', typeLabel: '', deviceNo: '', startTime: 0, endTime: 0, totalFiles: 0, segments: [] }; }, loadDeviceEnums: function () { var self = this; $.ajax({ url: baseUrl + '/deviceLog/enums/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', success: function (res) { if (res && res.code === 200) { self.deviceEnums = res.data || {}; } } }); }, loadDateTree: function () { var self = this; $.ajax({ url: baseUrl + '/deviceLog/dates/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', success: function (res) { if (res && res.code === 200) { self.dateTreeData = res.data || []; var latest = self.recentDays.length ? self.recentDays[0].day : ''; self.defaultExpandedKeys = self.resolveExpandedKeys(latest); if (latest) { self.selectDay(latest); } } else if (res && res.code === 403) { top.location.href = baseUrl + '/'; } else { self.$message.error((res && res.msg) || '加载日期失败'); } }, error: function () { self.$message.error('加载日期失败'); } }); }, resolveExpandedKeys: function (day) { if (!day || day.length !== 8) { return []; } return [day.substring(0, 4), day.substring(0, 4) + '-' + day.substring(4, 6)]; }, flattenDayNodes: function (nodes) { var result = []; (nodes || []).forEach(function (yearNode) { (yearNode.children || []).forEach(function (monthNode) { (monthNode.children || []).forEach(function (dayNode) { if (dayNode.day) { result.push({ day: dayNode.day, label: dayNode.day.substring(4, 6) + '-' + dayNode.day.substring(6, 8) }); } }); }); }); return result; }, handleNodeClick: function (data) { if (data && data.day) { this.selectDay(data.day); } }, handleRecentDayClick: function (day) { this.selectDay(day); }, selectDay: function (day) { if (!day) { return; } this.selectedDay = day; this.searchDeviceNo = ''; this.pause(); this.summaryLoading = true; this.resetSelectionState(); var self = this; $.ajax({ url: baseUrl + '/deviceLog/day/' + day + '/summary/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', success: function (res) { self.summaryLoading = false; if (res && res.code === 200) { self.deviceSummary = self.normalizeSummaryData(res.data); self.activeType = self.pickActiveType(); self.resetSelectionState(); } else if (res && res.code === 403) { top.location.href = baseUrl + '/'; } else { self.deviceSummary = self.emptySummary(); self.$message.error((res && res.msg) || '加载设备摘要失败'); } }, error: function () { self.summaryLoading = false; self.deviceSummary = self.emptySummary(); self.$message.error('加载设备摘要失败'); } }); }, normalizeSummaryData: function (data) { var self = this; var summary = this.emptySummary(); if (data && data.stats) { summary.stats = { totalDevices: data.stats.totalDevices || 0, totalFiles: data.stats.totalFiles || 0, typeCounts: data.stats.typeCounts || {} }; } var groupMap = {}; ((data && data.groups) || []).forEach(function (group) { groupMap[group.type] = { type: group.type, typeLabel: group.typeLabel || self.typeLabels[group.type] || group.type, deviceCount: group.deviceCount || 0, totalFiles: group.totalFiles || 0, devices: (group.devices || []).map(function (device) { return { type: device.type, typeLabel: device.typeLabel || self.typeLabels[device.type] || device.type, deviceNo: String(device.deviceNo), fileCount: device.fileCount || 0, firstTime: device.firstTime || 0, lastTime: device.lastTime || 0 }; }) }; }); summary.groups = this.typeOrder.map(function (type) { return groupMap[type] || { type: type, typeLabel: self.typeLabels[type] || type, deviceCount: 0, totalFiles: 0, devices: [] }; }); return summary; }, pickActiveType: function () { var existing = this.activeType; if (existing) { var existingGroup = this.getGroup(existing); if (existingGroup && (existingGroup.devices || []).length) { return existing; } } for (var i = 0; i < this.typeOrder.length; i++) { var group = this.getGroup(this.typeOrder[i]); if (group && (group.devices || []).length) { return group.type; } } return this.typeOrder[0] || ''; }, ensureDeviceSelection: function () { var selected = this.selectedDeviceSummary; if (selected && selected.type === this.activeType) { return; } this.resetSelectionState(); }, selectTypeGroup: function (type) { if (!type) { return; } this.activeType = type; var current = this.selectedDeviceSummary; if (current && current.type === type) { return; } this.resetSelectionState(); }, getGroup: function (type) { var result = null; (this.deviceGroups || []).forEach(function (group) { if (group.type === type) { result = group; } }); return result; }, resetSelectionState: function () { this.viewMode = 'picker'; this.selectedType = ''; this.selectedDeviceNo = ''; this.activeDeviceKey = ''; this.detailTab = 'logs'; this.rawTab = 'wcs'; this.timelineMeta = this.createEmptyTimeline(); this.timelineLoading = false; this.logLoading = false; this.logLoadError = ''; this.logRows = []; this.loadedOffsets = {}; this.loadingOffsets = {}; this.selectedTimestamp = 0; this.logWindowAnchorOffset = -1; }, selectDevice: function (device) { if (!device) { return; } var nextKey = this.buildDeviceKey(device.type, device.deviceNo); if (this.activeDeviceKey === nextKey && this.logRows.length) { return; } this.pause(); this.viewMode = 'viewer'; this.activeType = device.type; this.selectedType = device.type; this.selectedDeviceNo = String(device.deviceNo); this.activeDeviceKey = nextKey; this.detailTab = 'raw'; this.rawTab = 'wcs'; this.timelineMeta = this.createEmptyTimeline(); this.logRows = []; this.loadedOffsets = {}; this.loadingOffsets = {}; this.selectedTimestamp = 0; this.logLoadError = ''; this.loadTimeline(); }, returnToSelector: function () { this.pause(); this.resetSelectionState(); }, loadTimeline: function () { if (!this.selectedDay || !this.selectedType || !this.selectedDeviceNo) { return; } this.timelineLoading = true; this.logLoadError = ''; var self = this; $.ajax({ url: baseUrl + '/deviceLog/day/' + this.selectedDay + '/timeline/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', data: { type: this.selectedType, deviceNo: this.selectedDeviceNo }, success: function (res) { self.timelineLoading = false; if (res && res.code === 200) { self.timelineMeta = self.normalizeTimeline(res.data); if (self.timelineMeta.startTime) { self.selectedTimestamp = self.timelineMeta.startTime; } if (self.timelineMeta.totalFiles > 0) { self.loadSegmentWindow(0, { initialize: true, focusTimestamp: self.timelineMeta.startTime }); } } else { self.timelineMeta = self.createEmptyTimeline(); self.logLoadError = (res && res.msg) || '读取时间轴失败'; self.$message.error(self.logLoadError); } }, error: function () { self.timelineLoading = false; self.timelineMeta = self.createEmptyTimeline(); self.logLoadError = '读取时间轴失败'; self.$message.error('读取时间轴失败'); } }); }, normalizeTimeline: function (data) { var timeline = this.createEmptyTimeline(); if (!data) { return timeline; } timeline.type = data.type || this.selectedType; timeline.typeLabel = data.typeLabel || this.typeLabels[timeline.type] || timeline.type; timeline.deviceNo = String(data.deviceNo || this.selectedDeviceNo || ''); timeline.startTime = data.startTime || 0; timeline.endTime = data.endTime || 0; timeline.totalFiles = data.totalFiles || 0; timeline.segments = (data.segments || []).map(function (segment) { return { offset: segment.offset, startTime: segment.startTime || 0, endTime: segment.endTime || 0 }; }).sort(function (a, b) { return a.offset - b.offset; }); if (!timeline.startTime && timeline.segments.length) { for (var i = 0; i < timeline.segments.length; i++) { if (timeline.segments[i].startTime) { timeline.startTime = timeline.segments[i].startTime; break; } } } if (!timeline.endTime && timeline.segments.length) { for (var j = timeline.segments.length - 1; j >= 0; j--) { if (timeline.segments[j].endTime) { timeline.endTime = timeline.segments[j].endTime; break; } } } return timeline; }, loadSegmentWindow: function (offset, options) { options = options || {}; if (offset == null || offset < 0 || offset >= (this.timelineMeta.totalFiles || 0)) { return; } if (this.loadedOffsets[String(offset)] || this.loadingOffsets[String(offset)]) { if (options.focusTimestamp) { this.selectedTimestamp = options.focusTimestamp; if (options.resetWindow !== false) { this.pruneLogRowsAroundOffset(options.anchorOffset != null ? options.anchorOffset : offset); } if (options.scrollIntoView !== false) { this.$nextTick(this.scrollCurrentRowIntoView); } } return; } var self = this; var batchSize = 1; this.logLoading = true; this.$set(this.loadingOffsets, String(offset), true); $.ajax({ url: baseUrl + '/deviceLog/day/' + this.selectedDay + '/preview/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', data: { type: this.selectedType, deviceNo: this.selectedDeviceNo, offset: offset, limit: batchSize }, success: function (res) { self.logLoading = false; self.$delete(self.loadingOffsets, String(offset)); if (res && res.code === 200) { self.markLoadedOffsets(offset, batchSize); var decorated = self.decorateLogs(res.data || [], offset); self.mergeLogRows(decorated); if (options.resetWindow !== false) { self.pruneLogRowsAroundOffset(options.anchorOffset != null ? options.anchorOffset : offset); } if (options.initialize && self.logRows.length) { self.selectedTimestamp = self.logRows[0]._ts || self.timelineMeta.startTime; } else if (options.focusTimestamp) { self.selectedTimestamp = options.focusTimestamp; } else if (!self.selectedTimestamp && self.logRows.length) { self.selectedTimestamp = self.logRows[0]._ts; } if (!decorated.length && options.initialize && offset + batchSize < self.timelineMeta.totalFiles) { self.loadSegmentWindow(offset + batchSize, options); return; } if (options.scrollIntoView !== false) { self.$nextTick(self.scrollCurrentRowIntoView); } } else { self.logLoadError = (res && res.msg) || '读取日志失败'; self.$message.error(self.logLoadError); } }, error: function () { self.logLoading = false; self.$delete(self.loadingOffsets, String(offset)); self.logLoadError = '读取日志失败'; self.$message.error('读取日志失败'); } }); }, markLoadedOffsets: function (offset, limit) { var total = this.timelineMeta.totalFiles || 0; for (var i = offset; i < Math.min(total, offset + limit); i++) { this.$set(this.loadedOffsets, String(i), true); } }, decorateLogs: function (logs, segmentOffset) { var self = this; return (logs || []).map(function (logItem) { return self.decorateLog(logItem, segmentOffset); }); }, decorateLog: function (logItem, segmentOffset) { var protocol = this.safeParse(logItem && logItem.wcsData); var visualItems = this.buildVisualItems(protocol, this.selectedType); return Object.assign({}, logItem, { _ts: this.parseTimestamp(logItem && logItem.createTime), _key: this.buildLogRowKey(logItem), _segmentOffset: segmentOffset, _protocol: protocol, _visualItems: visualItems, _visualParam: this.buildVisualParam(this.selectedType, this.selectedDeviceNo), _summary: this.buildLogSummary(this.selectedType, visualItems, protocol) }); }, buildLogSummary: function (type, visualItems, protocol) { var fallback = { statusLabel: '离线', tone: 'muted', title: '原始日志', detail: '当前记录缺少可解析的业务字段', hint: '' }; if (!type) { return fallback; } if (type === 'Crn') { var crn = visualItems[0] || {}; var crnStatus = MonitorCardKit.deviceStatusLabel(crn.deviceStatus); return { statusLabel: crnStatus, tone: MonitorCardKit.statusTone(crnStatus), title: '任务 ' + MonitorCardKit.orDash(crn.workNo) + ' · 排 ' + MonitorCardKit.orDash(crn.bay) + ' · 层 ' + MonitorCardKit.orDash(crn.lev), detail: '模式 ' + MonitorCardKit.orDash(crn.mode) + ' / 状态 ' + MonitorCardKit.orDash(crn.status) + ' / 货叉 ' + MonitorCardKit.orDash(crn.forkOffset), hint: crn.warnCode ? ('报警代码 ' + crn.warnCode) : ('载货 ' + MonitorCardKit.orDash(crn.loading)) }; } if (type === 'DualCrn') { var dual = visualItems[0] || {}; var dualStatus = MonitorCardKit.deviceStatusLabel(dual.deviceStatus); return { statusLabel: dualStatus, tone: MonitorCardKit.statusTone(dualStatus), title: '工位1任务 ' + MonitorCardKit.orDash(dual.taskNo) + ' · 工位2任务 ' + MonitorCardKit.orDash(dual.taskNoTwo), detail: '排 ' + MonitorCardKit.orDash(dual.bay) + ' / 层 ' + MonitorCardKit.orDash(dual.lev) + ' / 状态 ' + MonitorCardKit.orDash(dual.status), hint: dual.warnCode ? ('报警代码 ' + dual.warnCode) : ('工位2状态 ' + MonitorCardKit.orDash(dual.statusTwo)) }; } if (type === 'Rgv') { var rgv = visualItems[0] || {}; var rgvStatus = MonitorCardKit.deviceStatusLabel(rgv.deviceStatus); return { statusLabel: rgvStatus, tone: MonitorCardKit.statusTone(rgvStatus), title: '任务 ' + MonitorCardKit.orDash(rgv.taskNo) + ' · 轨道位 ' + MonitorCardKit.orDash(rgv.trackSiteNo), detail: '模式 ' + MonitorCardKit.orDash(rgv.mode) + ' / 状态 ' + MonitorCardKit.orDash(rgv.status) + ' / 载货 ' + MonitorCardKit.orDash(rgv.loading), hint: rgv.warnCode ? ('报警代码 ' + rgv.warnCode) : '' }; } if (type === 'Devp') { var stations = visualItems || []; var autoCount = 0; var taskCount = 0; var loadingCount = 0; var errorStations = []; var canInCount = 0; for (var i = 0; i < stations.length; i++) { if (this.toBool(stations[i].autoing)) { autoCount += 1; } if (stations[i].taskNo != null && stations[i].taskNo !== '' && Number(stations[i].taskNo) !== 0) { taskCount += 1; } if (this.toBool(stations[i].loading)) { loadingCount += 1; } if (this.toBool(stations[i].inEnable)) { canInCount += 1; } if (stations[i].error || stations[i].errorMsg) { errorStations.push(stations[i].stationId); } } var statusLabel = errorStations.length ? '故障' : (autoCount === stations.length && stations.length ? '自动' : '手动'); return { statusLabel: statusLabel, tone: MonitorCardKit.statusTone(statusLabel), title: stations.length + ' 个站点 · 任务 ' + taskCount + ' · 有物 ' + loadingCount, detail: '自动 ' + autoCount + ' / 手动 ' + Math.max(0, stations.length - autoCount) + ' / 可入 ' + canInCount, hint: errorStations.length ? ('异常站点 ' + errorStations.slice(0, 6).join(', ')) : ('站点数组大小 ' + stations.length) }; } return fallback; }, buildVisualItems: function (protocol, type) { if (!protocol) { return []; } if (type === 'Devp' && Array.isArray(protocol)) { var self = this; return protocol.map(function (item) { return self.transformData(item, type); }).sort(function (a, b) { return (a.stationId || 0) - (b.stationId || 0); }); } if (type !== 'Devp') { return [this.transformData(protocol, type)]; } return []; }, buildVisualParam: function (type, deviceNo) { if (type === 'Crn' || type === 'DualCrn') { return { crnNo: Number(deviceNo) }; } if (type === 'Rgv') { return { rgvNo: Number(deviceNo) }; } return {}; }, transformData: function (protocol, type) { if (!protocol) { return {}; } var CrnModeType = this.deviceEnums.CrnModeType || {}; var CrnStatusType = this.deviceEnums.CrnStatusType || {}; var CrnForkPosType = this.deviceEnums.CrnForkPosType || {}; var CrnLiftPosType = this.deviceEnums.CrnLiftPosType || {}; var DualCrnForkPosType = this.deviceEnums.DualCrnForkPosType || {}; var DualCrnLiftPosType = this.deviceEnums.DualCrnLiftPosType || {}; var RgvModeType = this.deviceEnums.RgvModeType || {}; var RgvStatusType = this.deviceEnums.RgvStatusType || {}; if (type === 'Crn') { return { crnNo: protocol.crnNo, workNo: protocol.taskNo || 0, mode: CrnModeType[protocol.mode] || '-', status: CrnStatusType[protocol.status] || '-', loading: protocol.loaded == 1 ? '有物' : '无物', bay: protocol.bay, lev: protocol.level, forkOffset: CrnForkPosType[protocol.forkPos] || '-', liftPos: CrnLiftPosType[protocol.liftPos] || '-', walkPos: protocol.walkPos == 1 ? '不在定位' : '在定位', xspeed: protocol.xSpeed || 0, yspeed: protocol.ySpeed || 0, zspeed: protocol.zSpeed || 0, xdistance: protocol.xDistance || 0, ydistance: protocol.yDistance || 0, warnCode: protocol.alarm, deviceStatus: protocol.alarm && protocol.alarm > 0 ? 'ERROR' : ((protocol.taskNo && protocol.taskNo > 0) ? 'WORKING' : (protocol.mode == 3 ? 'AUTO' : 'OFFLINE')) }; } if (type === 'DualCrn') { var dual = { crnNo: protocol.crnNo, taskNo: protocol.taskNo || 0, taskNoTwo: protocol.taskNoTwo || 0, mode: CrnModeType[protocol.mode] || '-', status: CrnStatusType[protocol.status] || '-', statusTwo: CrnStatusType[protocol.statusTwo] || '-', loading: protocol.loaded == 1 ? '有物' : '无物', loadingTwo: protocol.loadedTwo == 1 ? '有物' : '无物', bay: protocol.bay, lev: protocol.level, forkOffset: DualCrnForkPosType[protocol.forkPos] || '-', forkOffsetTwo: DualCrnForkPosType[protocol.forkPosTwo] || '-', liftPos: DualCrnLiftPosType[protocol.liftPos] || '-', walkPos: protocol.walkPos == 0 ? '在定位' : '不在定位', taskReceive: protocol.taskReceive == 1 ? '接收' : '无任务', taskReceiveTwo: protocol.taskReceiveTwo == 1 ? '接收' : '无任务', xspeed: protocol.xSpeed, yspeed: protocol.ySpeed, zspeed: protocol.zSpeed, xdistance: protocol.xDistance, ydistance: protocol.yDistance, warnCode: protocol.alarm }; if (protocol.alarm && protocol.alarm > 0) dual.deviceStatus = 'ERROR'; else if ((protocol.taskNo && protocol.taskNo > 0) || (protocol.taskNoTwo && protocol.taskNoTwo > 0)) dual.deviceStatus = 'WORKING'; else if (protocol.mode == 3) dual.deviceStatus = 'AUTO'; else dual.deviceStatus = 'OFFLINE'; return dual; } if (type === 'Rgv') { var rgv = { rgvNo: protocol.rgvNo, taskNo: protocol.taskNo, mode: RgvModeType[protocol.mode] || '', status: RgvStatusType[protocol.status] || '', loading: protocol.loaded == 1 ? '有物' : '无物', trackSiteNo: protocol.rgvPos, warnCode: protocol.alarm }; if (protocol.alarm && protocol.alarm > 0) rgv.deviceStatus = 'ERROR'; else if (protocol.taskNo && protocol.taskNo > 0) rgv.deviceStatus = 'WORKING'; else if (protocol.mode == 3) rgv.deviceStatus = 'AUTO'; else rgv.deviceStatus = 'OFFLINE'; return rgv; } if (type === 'Devp') { return { stationId: protocol.stationId, taskNo: protocol.taskNo, targetStaNo: protocol.targetStaNo, autoing: protocol.autoing, loading: protocol.loading, inEnable: protocol.inEnable, outEnable: protocol.outEnable, emptyMk: protocol.emptyMk, fullPlt: protocol.fullPlt, runBlock: protocol.runBlock, enableIn: protocol.enableIn, palletHeight: protocol.palletHeight, barcode: protocol.barcode, weight: protocol.weight, error: protocol.error, errorMsg: protocol.errorMsg, extend: protocol.extend }; } return protocol; }, safeParse: function (text) { if (!text) { return null; } try { return JSON.parse(text); } catch (e) { return null; } }, mergeLogRows: function (newRows) { var map = {}; var merged = []; this.logRows.concat(newRows || []).forEach(function (row) { if (!row || !row._key || map[row._key]) { return; } map[row._key] = true; merged.push(row); }); merged.sort(function (a, b) { return a._ts - b._ts; }); this.logRows = merged; }, pruneLogRowsAroundOffset: function (anchorOffset) { if (anchorOffset == null || anchorOffset < 0) { return; } var minOffset = Math.max(0, anchorOffset - this.loadedSegmentRadius); var maxOffset = anchorOffset + this.loadedSegmentRadius; var loadedKeys = Object.keys(this.loadedOffsets); if (this.logWindowAnchorOffset === anchorOffset && loadedKeys.length <= (this.loadedSegmentRadius * 2 + 1)) { return; } var nextLoaded = {}; loadedKeys.forEach(function (key) { var offset = Number(key); if (offset >= minOffset && offset <= maxOffset) { nextLoaded[key] = true; } }); this.loadedOffsets = nextLoaded; this.logRows = (this.logRows || []).filter(function (row) { return row && row._segmentOffset >= minOffset && row._segmentOffset <= maxOffset; }); this.logWindowAnchorOffset = anchorOffset; }, buildLogRowKey: function (logItem) { return [ this.selectedType, this.selectedDeviceNo, logItem && logItem.createTime ? logItem.createTime : '', this.hashString(logItem && logItem.originData ? logItem.originData : ''), this.hashString(logItem && logItem.wcsData ? logItem.wcsData : '') ].join('|'); }, hashString: function (text) { var str = String(text || ''); var hash = 0; for (var i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return hash; }, parseTimestamp: function (value) { var ts = new Date(value).getTime(); return isNaN(ts) ? 0 : ts; }, binarySearch: function (time) { var list = this.logRows; var left = 0; var right = list.length - 1; var answer = -1; while (left <= right) { var mid = Math.floor((left + right) / 2); if ((list[mid]._ts || 0) <= time) { answer = mid; left = mid + 1; } else { right = mid - 1; } } return answer; }, handleLogRowClick: function (row) { if (!row) { return; } this.pause(); this.selectedTimestamp = row._ts; if (row._segmentOffset != null) { this.pruneLogRowsAroundOffset(row._segmentOffset); } this.$nextTick(this.scrollCurrentRowIntoView); }, buildLogMetaLine: function (summary) { if (!summary) { return ''; } return summary.detail + (summary.hint ? (' · ' + summary.hint) : ''); }, getWcsDataText: function (row) { if (!row || !row.wcsData) { return '当前记录没有 wcsData。'; } if (row._protocol) { return this.prettyPrintJson(row._protocol); } return String(row.wcsData); }, getOriginDataText: function (row) { if (!row || !row.originData) { return '当前记录没有 originData。'; } var parsed = this.safeParse(row.originData); return parsed ? this.prettyPrintJson(parsed) : String(row.originData); }, prettyPrintJson: function (value) { if (value == null || value === '') { return ''; } try { if (typeof value === 'string') { return JSON.stringify(JSON.parse(value), null, 2); } return JSON.stringify(value, null, 2); } catch (e) { return String(value); } }, handleSliderInput: function (value) { if (!this.selectedDeviceSummary) { return; } var next = (this.timelineMeta.startTime || 0) + value; this.selectedTimestamp = next; }, handleSliderChange: function (value) { if (!this.selectedDeviceSummary) { return; } var next = (this.timelineMeta.startTime || 0) + value; this.seekToTimestamp(next, { scrollIntoView: true }); }, ensureTimestampLoaded: function (timestamp) { if (!timestamp || !this.timelineMeta.segments.length) { return; } var segment = this.findSegmentByTime(timestamp); if (!segment) { return; } if (this.loadedOffsets[String(segment.offset)]) { this.pruneLogRowsAroundOffset(segment.offset); } if (!this.loadedOffsets[String(segment.offset)] && !this.loadingOffsets[String(segment.offset)]) { this.loadSegmentWindow(segment.offset, { focusTimestamp: timestamp, anchorOffset: segment.offset, resetWindow: true, scrollIntoView: false }); return; } var nextOffset = segment.offset + 1; if (nextOffset < this.timelineMeta.totalFiles && !this.loadedOffsets[String(nextOffset)] && !this.loadingOffsets[String(nextOffset)]) { var segmentEnd = segment.endTime || this.timelineMeta.endTime; if (segmentEnd && timestamp >= segmentEnd - Math.max(2000, this.playbackSpeed * 500)) { this.loadSegmentWindow(nextOffset, { anchorOffset: segment.offset, resetWindow: true, scrollIntoView: false }); } } }, findSegmentByTime: function (timestamp) { var segments = this.timelineMeta.segments || []; if (!segments.length) { return null; } var fallback = segments[0]; for (var i = 0; i < segments.length; i++) { var segment = segments[i]; var start = segment.startTime || (i === 0 ? this.timelineMeta.startTime : segments[i - 1].endTime); var end = segment.endTime || (i === segments.length - 1 ? this.timelineMeta.endTime : segments[i + 1].startTime); if (start && timestamp < start) { return fallback; } fallback = segment; if ((!start || timestamp >= start) && (!end || timestamp <= end)) { return segment; } } return fallback; }, getLoadedOffsetNumbers: function () { var self = this; return Object.keys(this.loadedOffsets) .filter(function (key) { return !!self.loadedOffsets[key]; }) .map(function (key) { return Number(key); }) .sort(function (a, b) { return a - b; }); }, loadPreviousSegment: function () { if (!this.canLoadPreviousSegment) { return; } var offsets = this.getLoadedOffsetNumbers(); if (!offsets.length) { this.loadSegmentWindow(0); return; } this.loadSegmentWindow(offsets[0] - 1); }, loadNextSegment: function () { if (!this.canLoadNextSegment) { return; } var offsets = this.getLoadedOffsetNumbers(); if (!offsets.length) { this.loadSegmentWindow(0); return; } this.loadSegmentWindow(offsets[offsets.length - 1] + 1); }, play: function () { if (!this.canPlay) { return; } if (!this.selectedTimestamp) { this.selectedTimestamp = this.timelineMeta.startTime; } this.isPlaying = true; this.lastTick = Date.now(); this.tick(); }, tick: function () { if (!this.isPlaying) { return; } var now = Date.now(); var delta = Math.max(0, now - this.lastTick); this.lastTick = now; var endTime = this.timelineMeta.endTime || this.selectedTimestamp; if (!endTime) { this.pause(); return; } var next = this.selectedTimestamp + delta * this.playbackSpeed; if (next >= endTime) { this.selectedTimestamp = endTime; this.ensureTimestampLoaded(endTime); this.pause(); this.$nextTick(this.scrollCurrentRowIntoView); return; } this.selectedTimestamp = next; this.ensureTimestampLoaded(next); var self = this; this.timer = setTimeout(function () { self.tick(); }, this.playbackTickMs); }, pause: function () { this.isPlaying = false; if (this.timer) { clearTimeout(this.timer); cancelAnimationFrame(this.timer); this.timer = null; } }, resetPlayback: function () { this.pause(); if (this.timelineMeta.startTime) { this.selectedTimestamp = this.timelineMeta.startTime; this.ensureTimestampLoaded(this.selectedTimestamp); this.$nextTick(this.scrollCurrentRowIntoView); } }, initJumpTime: function () { if (this.selectedTimestamp) { this.jumpTime = new Date(this.selectedTimestamp); return; } if (this.selectedDay && this.selectedDay.length === 8) { this.jumpTime = new Date(this.selectedDay.substring(0, 4) + '/' + this.selectedDay.substring(4, 6) + '/' + this.selectedDay.substring(6, 8) + ' 00:00:00'); return; } this.jumpTime = new Date(); }, confirmJump: function () { if (!this.jumpTime || !this.selectedDay || !this.timelineMeta.startTime) { return; } this.pause(); var baseDate = new Date(this.selectedDay.substring(0, 4) + '/' + this.selectedDay.substring(4, 6) + '/' + this.selectedDay.substring(6, 8) + ' 00:00:00'); var target = new Date(this.jumpTime); baseDate.setHours(target.getHours()); baseDate.setMinutes(target.getMinutes()); baseDate.setSeconds(target.getSeconds()); baseDate.setMilliseconds(0); var ts = baseDate.getTime(); if (this.timelineMeta.endTime && ts > this.timelineMeta.endTime) { ts = this.timelineMeta.endTime; this.$message.warning('目标时间超出日志范围,已跳转到结束时间'); } if (ts < this.timelineMeta.startTime) { ts = this.timelineMeta.startTime; this.$message.warning('目标时间早于日志起点,已跳转到起始时间'); } this.jumpVisible = false; this.seekToTimestamp(ts, { scrollIntoView: true }); }, seekToTimestamp: function (timestamp, options) { options = options || {}; if (!timestamp || !this.selectedDay || !this.selectedType || !this.selectedDeviceNo) { return; } var self = this; this.selectedTimestamp = timestamp; $.ajax({ url: baseUrl + '/deviceLog/day/' + this.selectedDay + '/seek/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', data: { type: this.selectedType, deviceNo: this.selectedDeviceNo, timestamp: timestamp }, success: function (res) { if (res && res.code === 200 && res.data && res.data.offset != null) { self.loadSegmentWindow(Number(res.data.offset), { focusTimestamp: timestamp, anchorOffset: Number(res.data.offset), resetWindow: true, scrollIntoView: options.scrollIntoView !== false }); return; } self.ensureTimestampLoaded(timestamp); if (options.scrollIntoView !== false) { self.$nextTick(self.scrollCurrentRowIntoView); } }, error: function () { self.ensureTimestampLoaded(timestamp); if (options.scrollIntoView !== false) { self.$nextTick(self.scrollCurrentRowIntoView); } } }); }, scrollCurrentRowIntoView: function () { if (!this.selectedLogKey) { return; } var el = document.getElementById('log-row-' + this.selectedLogKey); if (el && typeof el.scrollIntoView === 'function') { el.scrollIntoView({ block: 'nearest', behavior: 'auto' }); } }, formatTooltip: function (value) { if (!this.timelineMeta.startTime) { return '--'; } return this.formatTimestamp(this.timelineMeta.startTime + value, true); }, handleCurrentDeviceDownload: function () { this.doDownload(this.selectedDay, this.selectedType, this.selectedDeviceNo); }, doDownload: function (day, type, deviceNo) { if (!day || !type || !deviceNo) { return; } var self = this; $.ajax({ url: baseUrl + '/deviceLog/download/init/auth', headers: { token: localStorage.getItem('token') }, method: 'POST', data: JSON.stringify({ day: day, type: type, deviceNo: deviceNo }), dataType: 'json', contentType: 'application/json;charset=UTF-8', success: function (res) { if (!res || res.code !== 200) { self.$message.error((res && res.msg) || '初始化失败'); return; } var pid = res.data.progressId; self.startDownloadProgress(pid); self.performDownloadRequest(day, type, deviceNo, pid); }, error: function () { self.$message.error('初始化失败'); } }); }, startDownloadProgress: function (pid) { if (this.downloadTimer) { clearInterval(this.downloadTimer); this.downloadTimer = null; } this.downloadDialogVisible = true; this.buildProgress = 0; this.receiveProgress = 0; var self = this; this.downloadTimer = setInterval(function () { $.ajax({ url: baseUrl + '/deviceLog/download/progress/auth', headers: { token: localStorage.getItem('token') }, method: 'GET', data: { id: pid }, success: function (res) { if (res && res.code === 200) { self.buildProgress = res.data.percent || 0; } } }); }, 500); }, performDownloadRequest: function (day, type, deviceNo, pid) { var self = this; $.ajax({ url: baseUrl + '/deviceLog/day/' + day + '/download/auth?type=' + encodeURIComponent(type) + '&deviceNo=' + encodeURIComponent(deviceNo) + '&progressId=' + encodeURIComponent(pid), headers: { token: localStorage.getItem('token') }, method: 'GET', xhrFields: { responseType: 'blob' }, xhr: function () { var xhr = new window.XMLHttpRequest(); xhr.onprogress = function (e) { if (e.lengthComputable && e.total > 0) { self.receiveProgress = Math.floor(e.loaded / e.total * 100); } }; return xhr; }, success: function (data, status, xhr) { var disposition = xhr.getResponseHeader('Content-Disposition') || ''; var filename = type + '_' + deviceNo + '_' + day + '.zip'; var match = /filename=(.+)/.exec(disposition); if (match && match[1]) { filename = decodeURIComponent(match[1]); } self.buildProgress = 100; self.receiveProgress = 100; var blob = new Blob([data], { type: 'application/zip' }); var link = document.createElement('a'); var url = window.URL.createObjectURL(blob); link.href = url; link.download = filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); window.URL.revokeObjectURL(url); if (self.downloadTimer) { clearInterval(self.downloadTimer); self.downloadTimer = null; } setTimeout(function () { self.downloadDialogVisible = false; }, 900); }, error: function () { if (self.downloadTimer) { clearInterval(self.downloadTimer); self.downloadTimer = null; } self.downloadDialogVisible = false; self.$message.error('下载失败或未找到日志'); } }); }, buildDeviceKey: function (type, deviceNo) { return String(type || '') + ':' + String(deviceNo || ''); }, parseDeviceNo: function (deviceNo) { var n = parseInt(deviceNo, 10); return isNaN(n) ? Number.MAX_SAFE_INTEGER : n; }, formatTimestamp: function (timestamp, withMillis) { if (!timestamp) { return '--'; } var d = new Date(timestamp); if (isNaN(d.getTime())) { return '--'; } var Y = d.getFullYear(); var M = String(d.getMonth() + 1).padStart(2, '0'); var D = String(d.getDate()).padStart(2, '0'); var h = String(d.getHours()).padStart(2, '0'); var m = String(d.getMinutes()).padStart(2, '0'); var s = String(d.getSeconds()).padStart(2, '0'); if (withMillis) { return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s + '.' + String(d.getMilliseconds()).padStart(3, '0'); } return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s; }, formatDayText: function (day) { if (!day || day.length !== 8) { return '--'; } return day.substring(0, 4) + '-' + day.substring(4, 6) + '-' + day.substring(6, 8); }, toBool: function (value) { return value === true || value === 'Y' || value === 'y' || value === 1 || value === '1'; } } });