| | |
| | | var app = new Vue({ |
| | | el: '#app', |
| | | data: { |
| | | // Sidebar Data |
| | | dateTreeData: [], |
| | | defaultProps: { |
| | | children: 'children', |
| | | label: 'title' |
| | | }, |
| | | defaultExpandedKeys: [], |
| | | |
| | | // Search & List Data |
| | | searchForm: { |
| | | day: '', |
| | | type: '', |
| | | deviceNo: '', |
| | | offset: 0, |
| | | limit: 200 |
| | | typeOrder: ['Crn', 'DualCrn', 'Rgv', 'Devp'], |
| | | typeLabels: { |
| | | Crn: '堆垛机', |
| | | DualCrn: '双工位堆垛机', |
| | | Rgv: 'RGV', |
| | | Devp: '输送设备' |
| | | }, |
| | | deviceList: [], |
| | | loading: false, |
| | | |
| | | // Enums |
| | | selectedDay: '', |
| | | searchDeviceNo: '', |
| | | activeType: '', |
| | | viewMode: 'picker', |
| | | deviceSummary: { |
| | | stats: { |
| | | totalDevices: 0, |
| | | totalFiles: 0, |
| | | typeCounts: {} |
| | | }, |
| | | groups: [] |
| | | }, |
| | | summaryLoading: false, |
| | | deviceEnums: {}, |
| | | |
| | | // Visualization State |
| | | visualizationVisible: false, |
| | | visDeviceType: '', |
| | | visDeviceNo: '', |
| | | logs: [], |
| | | isPlaying: false, |
| | | playbackSpeed: 1, |
| | | sliderValue: 0, |
| | | 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, |
| | | currentTime: 0, |
| | | lastTick: 0, |
| | | |
| | | // Jump Time |
| | | jumpVisible: false, |
| | | jumpTime: null, |
| | | seekTargetTime: 0, // Target time we are trying to reach via loading |
| | | seekingOffset: false, |
| | | needToSeekOffset: false, |
| | | detailTab: 'logs', |
| | | rawTab: 'wcs', |
| | | |
| | | // Download State |
| | | downloadDialogVisible: false, |
| | | buildProgress: 0, |
| | | receiveProgress: 0, |
| | | downloadTimer: null |
| | | }, |
| | | computed: { |
| | | filteredDeviceList() { |
| | | // Currently just returns the full list loaded for the day |
| | | return this.deviceList; |
| | | summaryStats: function () { |
| | | return this.deviceSummary && this.deviceSummary.stats ? this.deviceSummary.stats : { |
| | | totalDevices: 0, |
| | | totalFiles: 0, |
| | | typeCounts: {} |
| | | }; |
| | | }, |
| | | visualizationTitle() { |
| | | return this.i18n('deviceLogs.visualizationPrefix', '日志可视化 - ') + this.visDeviceType + ' ' + this.visDeviceNo + ' (' + this.searchForm.day + ')'; |
| | | 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; |
| | | }); |
| | | }, |
| | | downloadDialogTitle() { |
| | | 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', '文件下载中'); |
| | | }, |
| | | maxSliderValue() { |
| | | return Math.max(0, this.endTime - this.startTime); |
| | | }, |
| | | currentTimeStr() { |
| | | if (!this.currentTime) return ''; |
| | | var d = new Date(this.currentTime); |
| | | var Y = d.getFullYear() + '-'; |
| | | var M = (d.getMonth() + 1 < 10 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1) + '-'; |
| | | var D = (d.getDate() < 10 ? '0' + d.getDate() : d.getDate()) + ' '; |
| | | var h = d.getHours().toString().padStart(2, '0'); |
| | | var m = d.getMinutes().toString().padStart(2, '0'); |
| | | var s = d.getSeconds().toString().padStart(2, '0'); |
| | | var ms = d.getMilliseconds().toString().padStart(3, '0'); |
| | | return Y + M + D + h + ':' + m + ':' + s + '.' + ms; |
| | | }, |
| | | canDownload() { |
| | | return this.searchForm.day && this.searchForm.type && this.searchForm.deviceNo; |
| | | } |
| | | }, |
| | | created() { |
| | | created: function () { |
| | | this.loadDeviceEnums(); |
| | | this.loadDateTree(); |
| | | }, |
| | | mounted() { |
| | | mounted: function () { |
| | | if (window.WCS_I18N && typeof window.WCS_I18N.onReady === 'function') { |
| | | let that = this; |
| | | var self = this; |
| | | window.WCS_I18N.onReady(function () { |
| | | that.$forceUpdate(); |
| | | self.$forceUpdate(); |
| | | }); |
| | | } |
| | | }, |
| | | beforeDestroy: function () { |
| | | this.pause(); |
| | | if (this.downloadTimer) { |
| | | clearInterval(this.downloadTimer); |
| | | this.downloadTimer = null; |
| | | } |
| | | }, |
| | | methods: { |
| | | i18n(key, fallback, params) { |
| | | 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 fallback || key; |
| | | }, |
| | | // --- Initialization --- |
| | | loadDeviceEnums() { |
| | | let that = this; |
| | | 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')}, |
| | | url: baseUrl + '/deviceLog/enums/auth', |
| | | headers: { token: localStorage.getItem('token') }, |
| | | method: 'GET', |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | that.deviceEnums = res.data || {}; |
| | | if (res && res.code === 200) { |
| | | self.deviceEnums = res.data || {}; |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // --- Date Tree --- |
| | | loadDateTree() { |
| | | let that = this; |
| | | loadDateTree: function () { |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/deviceLog/dates/auth", |
| | | headers: {'token': localStorage.getItem('token')}, |
| | | url: baseUrl + '/deviceLog/dates/auth', |
| | | headers: { token: localStorage.getItem('token') }, |
| | | method: 'GET', |
| | | success: function (res) { |
| | | if (res.code === 200) { |
| | | that.dateTreeData = that.buildMonthTree(res.data); |
| | | // Auto-expand current year/month if needed, or just root |
| | | if (that.dateTreeData.length > 0) { |
| | | that.defaultExpandedKeys = [that.dateTreeData[0].id]; |
| | | 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.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | } else if (res && res.code === 403) { |
| | | top.location.href = baseUrl + '/'; |
| | | } else { |
| | | that.$message.error(res.msg || '加载日期失败'); |
| | | self.$message.error((res && res.msg) || '加载日期失败'); |
| | | } |
| | | }, |
| | | error: function () { |
| | | self.$message.error('加载日期失败'); |
| | | } |
| | | }); |
| | | }, |
| | | buildMonthTree(data) { |
| | | var monthMap = {}; |
| | | (data || []).forEach(function (y) { |
| | | (y.children || []).forEach(function (m) { |
| | | var month = m.title; |
| | | var arr = monthMap[month] || (monthMap[month] = []); |
| | | (m.children || []).forEach(function (d) { |
| | | arr.push({ title: d.title + '日', id: d.id, day: d.id }); |
| | | }); |
| | | }); |
| | | }); |
| | | 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 = []; |
| | | Object.keys(monthMap).sort().reverse().forEach(function (month) { |
| | | result.push({ title: month + '月', id: month, children: monthMap[month] }); |
| | | (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(data) { |
| | | if (data.day && data.day.length === 8) { |
| | | this.searchForm.day = data.day; |
| | | this.loadDevices(data.day); |
| | | 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(); |
| | | |
| | | // --- Device List --- |
| | | loadDevices(day) { |
| | | this.loading = true; |
| | | this.deviceList = []; |
| | | let that = this; |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/deviceLog/day/" + day + "/devices/auth", |
| | | headers: {'token': localStorage.getItem('token')}, |
| | | url: baseUrl + '/deviceLog/day/' + day + '/summary/auth', |
| | | headers: { token: localStorage.getItem('token') }, |
| | | method: 'GET', |
| | | success: function (res) { |
| | | that.loading = false; |
| | | if (res.code === 200) { |
| | | that.deviceList = res.data || []; |
| | | } else if (res.code === 403) { |
| | | top.location.href = baseUrl + "/"; |
| | | 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 { |
| | | that.$message.error(res.msg || '加载设备失败'); |
| | | self.deviceSummary = self.emptySummary(); |
| | | self.$message.error((res && res.msg) || '加载设备摘要失败'); |
| | | } |
| | | }, |
| | | error: function() { |
| | | that.loading = false; |
| | | that.$message.error('请求失败'); |
| | | self.summaryLoading = false; |
| | | self.deviceSummary = self.emptySummary(); |
| | | self.$message.error('加载设备摘要失败'); |
| | | } |
| | | }); |
| | | }, |
| | | |
| | | // --- Download --- |
| | | handleBatchDownload() { |
| | | this.doDownload(this.searchForm.day, this.searchForm.type, this.searchForm.deviceNo); |
| | | }, |
| | | downloadLog(deviceNo, type) { |
| | | this.doDownload(this.searchForm.day, type, deviceNo); |
| | | }, |
| | | doDownload(day, type, deviceNo) { |
| | | if (!day) return this.$message.warning('请先选择日期'); |
| | | if (!type) return this.$message.warning('请选择设备类型'); |
| | | if (!deviceNo) return this.$message.warning('请输入设备编号'); |
| | | |
| | | let offset = this.searchForm.offset || 0; |
| | | let limit = this.searchForm.limit || 200; |
| | | let that = this; |
| | | |
| | | $.ajax({ |
| | | url: baseUrl + "/deviceLog/download/init/auth", |
| | | headers: {'token': localStorage.getItem('token')}, |
| | | method: 'POST', |
| | | data: JSON.stringify({ day: day, type: type, deviceNo: deviceNo, offset: offset, limit: limit }), |
| | | dataType:'json', |
| | | contentType:'application/json;charset=UTF-8', |
| | | success: function (res) { |
| | | if (res.code !== 200) { |
| | | that.$message.error(res.msg || '初始化失败'); |
| | | return; |
| | | } |
| | | var pid = res.data.progressId; |
| | | that.startDownloadProgress(pid); |
| | | that.performDownloadRequest(day, type, deviceNo, offset, limit, pid); |
| | | } |
| | | }); |
| | | }, |
| | | startDownloadProgress(pid) { |
| | | this.downloadDialogVisible = true; |
| | | this.buildProgress = 0; |
| | | this.receiveProgress = 0; |
| | | let that = this; |
| | | this.downloadTimer = setInterval(function(){ |
| | | $.ajax({ |
| | | url: baseUrl + '/deviceLog/download/progress/auth', |
| | | headers: {'token': localStorage.getItem('token')}, |
| | | method: 'GET', |
| | | data: { id: pid }, |
| | | success: function (p) { |
| | | if (p.code === 200) { |
| | | var percent = p.data.percent || 0; |
| | | that.buildProgress = percent; |
| | | } |
| | | } |
| | | }); |
| | | }, 500); |
| | | }, |
| | | performDownloadRequest(day, type, deviceNo, offset, limit, pid) { |
| | | let that = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/deviceLog/day/" + day + "/download/auth?type=" + encodeURIComponent(type) + "&deviceNo=" + encodeURIComponent(deviceNo) + "&offset=" + offset + "&limit=" + limit + "&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) { |
| | | var percent = Math.floor(e.loaded / e.total * 100); |
| | | that.receiveProgress = percent; |
| | | } |
| | | 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 || {} |
| | | }; |
| | | 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]); |
| | | } |
| | | that.buildProgress = 100; |
| | | that.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); |
| | | |
| | | clearInterval(that.downloadTimer); |
| | | setTimeout(() => { that.downloadDialogVisible = false; }, 1000); |
| | | 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; |
| | | }, |
| | | error: function () { |
| | | clearInterval(that.downloadTimer); |
| | | that.downloadDialogVisible = false; |
| | | that.$message.error('下载失败或未找到日志'); |
| | | 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; |
| | | }, |
| | | |
| | | // --- Visualization --- |
| | | visualizeLog(deviceNo, type) { |
| | | this.visDeviceType = type; |
| | | this.visDeviceNo = deviceNo; |
| | | this.visOffset = this.searchForm.offset || 0; |
| | | // Optimization: Load fewer files per request to speed up response |
| | | // searchForm.limit might be large (for download), so we force a small batch for visualization |
| | | this.visLimit = 2; |
| | | |
| | | this.logs = []; |
| | | this.hasMoreLogs = true; |
| | | this.loadingLogs = false; |
| | | this.startTime = 0; |
| | | this.endTime = 0; |
| | | this.currentTime = 0; |
| | | this.sliderValue = 0; |
| | | this.isPlaying = false; |
| | | this.playbackSpeed = 1; |
| | | |
| | | this.visualizationVisible = true; |
| | | this.loadMoreLogs(); |
| | | 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; |
| | | }, |
| | | loadMoreLogs() { |
| | | if (this.loadingLogs || !this.hasMoreLogs) return; |
| | | this.loadingLogs = true; |
| | | |
| | | // Use Vue loading service if available, or element UI loading |
| | | let loadingInstance = null; |
| | | |
| | | // Show loading if explicitly seeking (jumping far ahead) or normal load |
| | | if (this.seekTargetTime > 0) { |
| | | if (this.$loading) { |
| | | loadingInstance = this.$loading({ |
| | | target: '.vis-container', |
| | | lock: true, |
| | | text: '正在跳转至目标时间 (加载中)...', |
| | | spinner: 'el-icon-loading', |
| | | background: 'rgba(255, 255, 255, 0.7)' |
| | | }); |
| | | selectDevice: function (device) { |
| | | if (!device) { |
| | | return; |
| | | } |
| | | } else if (this.$loading && !this.isPlaying) { |
| | | loadingInstance = this.$loading({ |
| | | target: '.vis-container', |
| | | lock: true, |
| | | text: '加载数据中...', |
| | | spinner: 'el-icon-loading', |
| | | background: 'rgba(255, 255, 255, 0.7)' |
| | | }); |
| | | var nextKey = this.buildDeviceKey(device.type, device.deviceNo); |
| | | if (this.activeDeviceKey === nextKey && this.logRows.length) { |
| | | return; |
| | | } |
| | | |
| | | let that = this; |
| | | |
| | | // If seeking and we have no idea where the target time is in terms of files, |
| | | // we should ask the server for the correct offset first! |
| | | if (this.seekTargetTime > 0 && this.visOffset === (this.searchForm.offset || 0)) { |
| | | // First time seeking or reset? No, this condition is tricky. |
| | | // Actually, if we are seeking, we can call the new /seek endpoint first. |
| | | // BUT, loadMoreLogs is recursive for seek. We need to be careful. |
| | | |
| | | // Let's modify logic: |
| | | // If seekTargetTime is set, and we suspect it's far away (e.g. not in next batch), |
| | | // we should use the seek endpoint. |
| | | // For simplicity, let's ALWAYS try seek endpoint if seeking far ahead? |
| | | // Or just if we are seeking. |
| | | |
| | | // However, loadMoreLogs is currently designed to just load NEXT batch. |
| | | // We should probably intercept the flow here. |
| | | 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; |
| | | } |
| | | |
| | | // NEW LOGIC: If seeking, try to find offset first |
| | | if (this.seekTargetTime > 0 && this.needToSeekOffset && !this.seekingOffset) { |
| | | this.seekingOffset = true; |
| | | this.timelineLoading = true; |
| | | this.logLoadError = ''; |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/deviceLog/day/" + this.searchForm.day + "/seek/auth", |
| | | headers: {'token': localStorage.getItem('token')}, |
| | | data: { type: this.visDeviceType, deviceNo: this.visDeviceNo, timestamp: this.seekTargetTime }, |
| | | 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) { |
| | | if (res.code === 200) { |
| | | var targetOffset = res.data.offset; |
| | | // Update offset directly |
| | | that.visOffset = targetOffset; |
| | | // Clear logs because we jumped |
| | | that.logs = []; |
| | | that.seekingOffset = false; |
| | | that.needToSeekOffset = false; |
| | | |
| | | // Now continue to load logs from this new offset |
| | | // We set seekTargetTime still > 0 so it will check if we arrived. |
| | | // But we need to call the actual load now. |
| | | // We recurse (but we need to reset loadingLogs flag first or it returns) |
| | | // that.loadingLogs = false; // Do not reset loadingLogs here as we are still "loading" |
| | | // that.loadMoreLogs(); // Recursive call is risky if not careful |
| | | |
| | | // Better: call sequential load directly |
| | | that.loadMoreLogsSequential(loadingInstance); |
| | | 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 { |
| | | // Fallback to sequential load if seek fails |
| | | that.seekingOffset = false; |
| | | that.needToSeekOffset = false; |
| | | that.loadMoreLogsSequential(loadingInstance); |
| | | self.timelineMeta = self.createEmptyTimeline(); |
| | | self.logLoadError = (res && res.msg) || '读取时间轴失败'; |
| | | self.$message.error(self.logLoadError); |
| | | } |
| | | }, |
| | | error: function() { |
| | | that.seekingOffset = false; |
| | | that.needToSeekOffset = false; |
| | | that.loadMoreLogsSequential(loadingInstance); |
| | | 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; |
| | | } |
| | | |
| | | this.loadMoreLogsSequential(loadingInstance); |
| | | }, |
| | | loadMoreLogsSequential(loadingInstance) { |
| | | let that = this; |
| | | let currentLimit = this.seekTargetTime > 0 ? 10 : this.visLimit; |
| | | |
| | | 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.searchForm.day + "/preview/auth", |
| | | headers: {'token': localStorage.getItem('token')}, |
| | | data: { type: this.visDeviceType, deviceNo: this.visDeviceNo, offset: this.visOffset, limit: currentLimit }, |
| | | 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) { |
| | | if (loadingInstance) loadingInstance.close(); |
| | | that.loadingLogs = false; |
| | | if (res.code === 200) { |
| | | var newLogs = res.data || []; |
| | | |
| | | if (newLogs.length === 0) { |
| | | that.hasMoreLogs = false; |
| | | if (that.seekTargetTime > 0) { |
| | | that.$message.warning('已到达日志末尾,无法到达目标时间'); |
| | | that.seekTargetTime = 0; |
| | | } else { |
| | | if (that.logs.length === 0) that.$message.warning('没有找到日志数据'); |
| | | else that.$message.info('数据已全部加载'); |
| | | 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 we cleared logs (jumped), we need to set start time again maybe? |
| | | // If logs is empty, it means we jumped or initial load. |
| | | var isJump = that.logs.length === 0; |
| | | |
| | | that.logs = that.logs.concat(newLogs); |
| | | that.visOffset += currentLimit; |
| | | |
| | | if (that.logs.length > 0) { |
| | | if (isJump) { |
| | | // If we jumped, we need to ensure we don't break startTime if possible, |
| | | // OR we update startTime if it was 0. |
| | | // If we jumped to middle, startTime of the whole day is still 0? |
| | | // No, startTime usually is the beginning of the visualized session. |
| | | // If we jump, we might want to keep the "view" consistent? |
| | | // Actually, if we jump, we effectively discard previous logs. |
| | | // So the slider range might change? |
| | | // The user expects slider to represent the WHOLE day? |
| | | // Currently slider represents [startTime, endTime] of LOADED logs. |
| | | // If we jump, we might lose the "start". |
| | | // To support "Whole Day" slider, we need startTime of the FIRST log of the day. |
| | | // But we don't have that if we jump. |
| | | // For now, let's just update endTime. |
| | | // If it's a jump, we might need to adjust startTime if it's the first chunk we have. |
| | | if (that.startTime === 0) { |
| | | that.startTime = new Date(that.logs[0].createTime).getTime(); |
| | | that.currentTime = that.startTime; |
| | | that.$nextTick(() => { |
| | | that.updateDeviceState(that.logs[0]); |
| | | }); |
| | | if (options.scrollIntoView !== false) { |
| | | self.$nextTick(self.scrollCurrentRowIntoView); |
| | | } |
| | | } else { |
| | | // Normal load (initial or sequential) |
| | | // If initial load (startTime is 0) |
| | | if (that.startTime === 0) { |
| | | that.startTime = new Date(that.logs[0].createTime).getTime(); |
| | | that.currentTime = that.startTime; |
| | | that.$nextTick(() => { |
| | | that.updateDeviceState(that.logs[0]); |
| | | }); |
| | | } |
| | | } |
| | | |
| | | // Update end time |
| | | that.endTime = new Date(that.logs[that.logs.length - 1].createTime).getTime(); |
| | | |
| | | // Handle Seek Logic |
| | | if (that.seekTargetTime > 0) { |
| | | // If we jumped, we should be close. |
| | | // Check if target is in current range |
| | | var lastLogTime = new Date(that.logs[that.logs.length - 1].createTime).getTime(); |
| | | if (lastLogTime >= that.seekTargetTime) { |
| | | that.currentTime = that.seekTargetTime; |
| | | that.sliderValue = that.currentTime - that.startTime; |
| | | that.syncState(); |
| | | that.seekTargetTime = 0; |
| | | that.$message.success('已跳转至目标时间'); |
| | | } else { |
| | | // Still not there? |
| | | // If we used /seek, we should be there or very close. |
| | | // Maybe the file we found ends before target? |
| | | // We continue loading. |
| | | setTimeout(() => { |
| | | that.loadMoreLogs(); |
| | | }, 50); |
| | | } |
| | | } else if (isJump) { |
| | | // If not seeking (just loaded via jump?), but we cleared logs... |
| | | // Wait, we only clear logs if seekTargetTime > 0 in the new logic. |
| | | // So this else is for normal load. |
| | | } |
| | | } |
| | | } else { |
| | | that.$message.error(res.msg); |
| | | that.seekTargetTime = 0; |
| | | self.logLoadError = (res && res.msg) || '读取日志失败'; |
| | | self.$message.error(self.logLoadError); |
| | | } |
| | | }, |
| | | error: function() { |
| | | if (loadingInstance) loadingInstance.close(); |
| | | that.loadingLogs = false; |
| | | that.seekTargetTime = 0; |
| | | that.$message.error('请求失败'); |
| | | self.logLoading = false; |
| | | self.$delete(self.loadingOffsets, String(offset)); |
| | | self.logLoadError = '读取日志失败'; |
| | | self.$message.error('读取日志失败'); |
| | | } |
| | | }); |
| | | }, |
| | | handleVisualizationClose() { |
| | | this.pause(); |
| | | this.visualizationVisible = false; |
| | | }, |
| | | |
| | | // --- Playback Logic --- |
| | | play() { |
| | | this.isPlaying = true; |
| | | this.lastTick = Date.now(); |
| | | this.tick(); |
| | | }, |
| | | pause() { |
| | | this.isPlaying = false; |
| | | if (this.timer) cancelAnimationFrame(this.timer); |
| | | }, |
| | | reset() { |
| | | this.pause(); |
| | | this.currentTime = this.startTime; |
| | | this.sliderValue = 0; |
| | | if (this.logs.length > 0) { |
| | | this.updateDeviceState(this.logs[0]); |
| | | 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); |
| | | } |
| | | }, |
| | | tick() { |
| | | if (!this.isPlaying) return; |
| | | var now = Date.now(); |
| | | var delta = now - this.lastTick; |
| | | this.lastTick = now; |
| | | |
| | | // Auto-load more logs if we are close to the end (prefetch) |
| | | if (this.hasMoreLogs && !this.loadingLogs) { |
| | | var idx = this.binarySearch(this.currentTime); |
| | | // If within last 20 frames |
| | | if (this.logs.length > 0 && (this.logs.length - 1 - idx) < 20) { |
| | | this.loadMoreLogs(); |
| | | } |
| | | } |
| | | |
| | | var nextTime = this.currentTime + delta * this.playbackSpeed; |
| | | if (nextTime >= this.endTime) { |
| | | if (this.hasMoreLogs) { |
| | | // Reached end of buffer, but more data available |
| | | // Clamp to endTime |
| | | nextTime = this.endTime; |
| | | |
| | | // Ensure loading is triggered |
| | | if (!this.loadingLogs) { |
| | | this.loadMoreLogs(); |
| | | } |
| | | |
| | | // Update state but do NOT pause |
| | | this.currentTime = nextTime; |
| | | this.sliderValue = this.currentTime - this.startTime; |
| | | this.syncState(); |
| | | |
| | | // Continue loop to check again next frame |
| | | this.timer = requestAnimationFrame(this.tick); |
| | | return; |
| | | } else { |
| | | // Truly finished |
| | | nextTime = this.endTime; |
| | | this.currentTime = nextTime; |
| | | this.sliderValue = this.currentTime - this.startTime; |
| | | this.syncState(); |
| | | this.pause(); |
| | | return; |
| | | } |
| | | } |
| | | this.currentTime = nextTime; |
| | | this.sliderValue = this.currentTime - this.startTime; |
| | | |
| | | this.syncState(); |
| | | |
| | | this.timer = requestAnimationFrame(this.tick); |
| | | decorateLogs: function (logs, segmentOffset) { |
| | | var self = this; |
| | | return (logs || []).map(function (logItem) { |
| | | return self.decorateLog(logItem, segmentOffset); |
| | | }); |
| | | }, |
| | | sliderChange(val) { |
| | | this.currentTime = this.startTime + val; |
| | | this.syncState(); |
| | | |
| | | // If dragged near the end, load more |
| | | if (this.hasMoreLogs && !this.loadingLogs) { |
| | | var idx = this.binarySearch(this.currentTime); |
| | | if (this.logs.length > 0 && (this.logs.length - 1 - idx) < 20) { |
| | | this.loadMoreLogs(); |
| | | } |
| | | } |
| | | 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) |
| | | }); |
| | | }, |
| | | sliderInput(val) { |
| | | this.currentTime = this.startTime + val; |
| | | this.syncState(); |
| | | // If dragged near the end, load more |
| | | if (this.hasMoreLogs && !this.loadingLogs) { |
| | | var idx = this.binarySearch(this.currentTime); |
| | | if (this.logs.length > 0 && (this.logs.length - 1 - idx) < 20) { |
| | | this.loadMoreLogs(); |
| | | 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; |
| | | }, |
| | | syncState() { |
| | | var idx = this.binarySearch(this.currentTime); |
| | | if (idx >= 0) { |
| | | var targetLog = this.logs[idx]; |
| | | this.updateDeviceState(targetLog); |
| | | 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 []; |
| | | }, |
| | | binarySearch(time) { |
| | | let l = 0, r = this.logs.length - 1; |
| | | let ans = -1; |
| | | while (l <= r) { |
| | | let mid = Math.floor((l + r) / 2); |
| | | let logTime = new Date(this.logs[mid].createTime).getTime(); |
| | | if (logTime <= time) { |
| | | ans = mid; |
| | | l = mid + 1; |
| | | } else { |
| | | r = mid - 1; |
| | | buildVisualParam: function (type, deviceNo) { |
| | | if (type === 'Crn' || type === 'DualCrn') { |
| | | return { crnNo: Number(deviceNo) }; |
| | | } |
| | | if (type === 'Rgv') { |
| | | return { rgvNo: Number(deviceNo) }; |
| | | } |
| | | return ans; |
| | | return {}; |
| | | }, |
| | | updateDeviceState(logItem) { |
| | | if (!logItem || !logItem.wcsData) return; |
| | | try { |
| | | var protocol = JSON.parse(logItem.wcsData); |
| | | var list = []; |
| | | |
| | | if (this.visDeviceType === 'Devp' && Array.isArray(protocol)) { |
| | | list = protocol.map(p => this.transformData(p, this.visDeviceType)); |
| | | list.sort((a, b) => (a.stationId || 0) - (b.stationId || 0)); |
| | | } else { |
| | | var data = this.transformData(protocol, this.visDeviceType); |
| | | list = [data]; |
| | | transformData: function (protocol, type) { |
| | | if (!protocol) { |
| | | return {}; |
| | | } |
| | | |
| | | var res = { code: 200, data: list }; |
| | | |
| | | if (this.$refs.card) { |
| | | if (this.visDeviceType === 'Crn') { |
| | | this.$refs.card.setCrnList(res); |
| | | } else if (this.visDeviceType === 'Rgv') { |
| | | this.$refs.card.setRgvList(res); |
| | | } else if (this.visDeviceType === 'DualCrn') { |
| | | this.$refs.card.setDualCrnList(res); |
| | | } else if (this.visDeviceType === 'Devp') { |
| | | this.$refs.card.setStationList(res); |
| | | } |
| | | } |
| | | } catch (e) { |
| | | console.error('Error parsing wcsData', e); |
| | | } |
| | | }, |
| | | transformData(protocol, type) { |
| | | if (!protocol) return {}; |
| | | |
| | | // Enums from API |
| | | 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 || {}; |
| | | |
| | |
| | | lev: protocol.level, |
| | | forkOffset: CrnForkPosType[protocol.forkPos] || '-', |
| | | liftPos: CrnLiftPosType[protocol.liftPos] || '-', |
| | | walkPos: (protocol.walkPos == 1) ? '不在定位' : '在定位', |
| | | 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' : |
| | | deviceStatus: protocol.alarm && protocol.alarm > 0 ? 'ERROR' : |
| | | ((protocol.taskNo && protocol.taskNo > 0) ? 'WORKING' : |
| | | (protocol.mode == 3 ? 'AUTO' : 'OFFLINE')) |
| | | }; |
| | | } else if (type === 'DualCrn') { |
| | | var vo = { |
| | | } |
| | | if (type === 'DualCrn') { |
| | | var dual = { |
| | | crnNo: protocol.crnNo, |
| | | taskNo: protocol.taskNo || 0, |
| | | taskNoTwo: protocol.taskNoTwo || 0, |
| | |
| | | ydistance: protocol.yDistance, |
| | | warnCode: protocol.alarm |
| | | }; |
| | | if (protocol.alarm && protocol.alarm > 0) vo.deviceStatus = 'ERROR'; |
| | | else if ((protocol.taskNo && protocol.taskNo > 0) || (protocol.taskNoTwo && protocol.taskNoTwo > 0)) vo.deviceStatus = 'WORKING'; |
| | | else if (protocol.mode == 3) vo.deviceStatus = 'AUTO'; |
| | | else vo.deviceStatus = 'OFFLINE'; |
| | | return vo; |
| | | } else if (type === 'Rgv') { |
| | | var vo = { |
| | | 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] || '', |
| | |
| | | trackSiteNo: protocol.rgvPos, |
| | | warnCode: protocol.alarm |
| | | }; |
| | | |
| | | var deviceStatus = ""; |
| | | if (protocol.mode == 3) deviceStatus = "AUTO"; |
| | | if (protocol.taskNo && protocol.taskNo > 0) deviceStatus = "WORKING"; |
| | | if (protocol.alarm && protocol.alarm > 0) deviceStatus = "ERROR"; |
| | | vo.deviceStatus = deviceStatus; |
| | | |
| | | return vo; |
| | | } else if (type === 'Devp') { |
| | | 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, |
| | |
| | | } |
| | | return protocol; |
| | | }, |
| | | formatTooltip(val) { |
| | | var t = this.startTime + val; |
| | | var d = new Date(t); |
| | | var Y = d.getFullYear() + '-'; |
| | | var M = (d.getMonth() + 1 < 10 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1) + '-'; |
| | | var D = (d.getDate() < 10 ? '0' + d.getDate() : d.getDate()) + ' '; |
| | | return Y + M + D + d.toLocaleTimeString() + '.' + d.getMilliseconds(); |
| | | safeParse: function (text) { |
| | | if (!text) { |
| | | return null; |
| | | } |
| | | try { |
| | | return JSON.parse(text); |
| | | } catch (e) { |
| | | return null; |
| | | } |
| | | }, |
| | | initJumpTime() { |
| | | if (this.currentTime > 0) { |
| | | this.jumpTime = new Date(this.currentTime); |
| | | } else if (this.startTime > 0) { |
| | | this.jumpTime = new Date(this.startTime); |
| | | 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 { |
| | | // Try to parse from searchForm.day |
| | | if (this.searchForm.day && this.searchForm.day.length === 8) { |
| | | var y = this.searchForm.day.substring(0, 4); |
| | | var m = this.searchForm.day.substring(4, 6); |
| | | var d = this.searchForm.day.substring(6, 8); |
| | | // Default to 00:00:00 of that day |
| | | this.jumpTime = new Date(y + '/' + m + '/' + d + ' 00:00:00'); |
| | | } 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); |
| | | } |
| | | }, |
| | | confirmJump() { |
| | | if (!this.jumpTime) return; |
| | | |
| | | // Construct target timestamp |
| | | // jumpTime from el-time-picker is a Date object (if not using value-format) |
| | | // or string/timestamp if using value-format. |
| | | // We didn't set value-format, so it should be Date object (default in ElementUI 2.x?) |
| | | // Actually, in default_api:Read above, I saw: |
| | | // <el-time-picker v-model="jumpTime" ... :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }"> |
| | | // Default v-model for el-time-picker is Date object. |
| | | |
| | | let targetDate = this.jumpTime; |
| | | if (typeof targetDate === 'string' || typeof targetDate === 'number') { |
| | | targetDate = new Date(targetDate); |
| | | } |
| | | |
| | | let baseDate = new Date(this.startTime > 0 ? this.startTime : Date.now()); |
| | | |
| | | baseDate.setHours(targetDate.getHours()); |
| | | baseDate.setMinutes(targetDate.getMinutes()); |
| | | baseDate.setSeconds(targetDate.getSeconds()); |
| | | // Picker usually 0 ms |
| | | baseDate.setMilliseconds(0); |
| | | |
| | | let targetTs = baseDate.getTime(); |
| | | |
| | | if (this.startTime > 0 && targetTs < this.startTime) { |
| | | targetTs = this.startTime; |
| | | } |
| | | |
| | | // Check if beyond endTime |
| | | if (this.endTime > 0 && targetTs > this.endTime) { |
| | | // If we have more logs, we try to go as far as we can (endTime) |
| | | // and trigger loading |
| | | if (this.hasMoreLogs) { |
| | | this.seekTargetTime = targetTs; |
| | | this.needToSeekOffset = true; |
| | | // Trigger load immediately |
| | | if (!this.loadingLogs) { |
| | | this.loadMoreLogs(); |
| | | } else { |
| | | // Already loading, just set the target and let callback handle it |
| | | } |
| | | this.jumpVisible = false; |
| | | return; // Don't update current time yet, wait for load |
| | | } else { |
| | | targetTs = this.endTime; |
| | | this.$message.warning('目标时间超出日志范围,已跳转至结束时间'); |
| | | error: function () { |
| | | self.ensureTimestampLoaded(timestamp); |
| | | if (options.scrollIntoView !== false) { |
| | | self.$nextTick(self.scrollCurrentRowIntoView); |
| | | } |
| | | } |
| | | |
| | | this.currentTime = targetTs; |
| | | this.sliderValue = this.currentTime - this.startTime; |
| | | this.syncState(); |
| | | this.jumpVisible = false; |
| | | |
| | | // Trigger load if needed |
| | | if (this.hasMoreLogs && !this.loadingLogs) { |
| | | // Force load check |
| | | this.loadMoreLogs(); |
| | | }); |
| | | }, |
| | | 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'; |
| | | } |
| | | } |
| | | }); |