From ce13e25ed685ba5c961832d023ceafecf4f30d47 Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期六, 10 一月 2026 15:27:33 +0800
Subject: [PATCH] #
---
src/main/webapp/static/js/deviceLogs/deviceLogs.js | 1039 ++++++++++++++++++++++++++++++++++++++++++++++-----------
1 files changed, 828 insertions(+), 211 deletions(-)
diff --git a/src/main/webapp/static/js/deviceLogs/deviceLogs.js b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
index 31246f0..9d44d8f 100644
--- a/src/main/webapp/static/js/deviceLogs/deviceLogs.js
+++ b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
@@ -1,228 +1,845 @@
-layui.use(['tree', 'layer', 'form', 'element'], function() {
- var tree = layui.tree;
- var $ = layui.jquery;
- var layer = layui.layer;
- var form = layui.form;
- var element = layui.element;
+var app = new Vue({
+ el: '#app',
+ data: {
+ // Sidebar Data
+ dateTreeData: [],
+ defaultProps: {
+ children: 'children',
+ label: 'title'
+ },
+ defaultExpandedKeys: [],
- var currentDay = null;
+ // Search & List Data
+ searchForm: {
+ day: '',
+ type: '',
+ deviceNo: '',
+ offset: 0,
+ limit: 200
+ },
+ deviceList: [],
+ loading: false,
- function 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 });
+ // Enums
+ deviceEnums: {},
+
+ // Visualization State
+ visualizationVisible: false,
+ visDeviceType: '',
+ visDeviceNo: '',
+ logs: [],
+ isPlaying: false,
+ playbackSpeed: 1,
+ sliderValue: 0,
+ startTime: 0,
+ endTime: 0,
+ 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,
+
+ // 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;
+ },
+ visualizationTitle() {
+ return `鏃ュ織鍙鍖� - ${this.visDeviceType} ${this.visDeviceNo} (${this.searchForm.day})`;
+ },
+ 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() {
+ this.loadDeviceEnums();
+ this.loadDateTree();
+ },
+ methods: {
+ // --- Initialization ---
+ loadDeviceEnums() {
+ let that = this;
+ $.ajax({
+ url: baseUrl + "/deviceLog/enums/auth",
+ headers: {'token': localStorage.getItem('token')},
+ method: 'GET',
+ success: function (res) {
+ if (res.code === 200) {
+ that.deviceEnums = res.data || {};
+ }
+ }
+ });
+ },
+
+ // --- Date Tree ---
+ loadDateTree() {
+ let that = this;
+ $.ajax({
+ 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];
+ }
+ } else if (res.code === 403) {
+ top.location.href = baseUrl + "/";
+ } else {
+ that.$message.error(res.msg || '鍔犺浇鏃ユ湡澶辫触');
+ }
+ }
+ });
+ },
+ 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 });
+ });
});
});
- });
- var result = [];
- Object.keys(monthMap).sort().forEach(function (month) {
- result.push({ title: month + '鏈�', id: month, children: monthMap[month] });
- });
- return result;
- }
-
- function loadDateTree() {
- $.ajax({
- url: baseUrl + "/deviceLog/dates/auth",
- headers: {'token': localStorage.getItem('token')},
- method: 'GET',
- beforeSend: function () {
- layer.load(1, {shade: [0.1,'#fff']});
- },
- success: function (res) {
- layer.closeAll('loading');
- if (res.code === 200) {
- var monthTree = buildMonthTree(res.data);
- tree.render({
- elem: '#date-tree',
- id: 'dateTree',
- data: monthTree,
- click: function(obj){
- var node = obj.data;
- if (node.id && node.id.length === 8) {
- currentDay = node.id;
- $('#selected-day').val(currentDay);
- loadDevices(currentDay);
- }
- }
- });
- } else if (res.code === 403) {
- top.location.href = baseUrl + "/";
- } else {
- layer.msg(res.msg || '鍔犺浇鏃ユ湡澶辫触', {icon: 2});
- }
+ var result = [];
+ Object.keys(monthMap).sort().reverse().forEach(function (month) {
+ result.push({ title: month + '鏈�', id: month, children: monthMap[month] });
+ });
+ return result;
+ },
+ handleNodeClick(data) {
+ if (data.day && data.day.length === 8) {
+ this.searchForm.day = data.day;
+ this.loadDevices(data.day);
}
- });
- }
+ },
- function loadDevices(day) {
- $('#device-list').html('');
- $.ajax({
- url: baseUrl + "/deviceLog/day/" + day + "/devices/auth",
- headers: {'token': localStorage.getItem('token')},
- method: 'GET',
- beforeSend: function () {
- layer.load(1, {shade: [0.1,'#fff']});
- },
- success: function (res) {
- layer.closeAll('loading');
- if (res.code === 200) {
- if (!res.data || res.data.length === 0) {
- $('#device-list').html('<div class="layui-text">褰撴棩鏈壘鍒拌澶囨棩蹇�</div>');
+ // --- Device List ---
+ loadDevices(day) {
+ this.loading = true;
+ this.deviceList = [];
+ let that = this;
+ $.ajax({
+ url: baseUrl + "/deviceLog/day/" + day + "/devices/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 + "/";
+ } else {
+ that.$message.error(res.msg || '鍔犺浇璁惧澶辫触');
+ }
+ },
+ error: function() {
+ that.loading = false;
+ that.$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 html = '';
- res.data.forEach(function(item){
- var types = item.types || [];
- var typeBtns = '';
- types.forEach(function(t){
- typeBtns += '<button class="layui-btn layui-btn-xs" data-type="' + t + '" data-device-no="' + item.deviceNo + '">涓嬭浇(' + t + ')</button>';
- });
- html += '<div class="layui-col-xs12" style="margin-bottom:8px;">' +
- '<div class="layui-card">' +
- '<div class="layui-card-body">' +
- '<span>璁惧缂栧彿锛�<b>' + item.deviceNo + '</b></span>' +
- '<span style="margin-left:20px;">绫诲瀷锛�' + types.join(',') + '</span>' +
- '<span style="margin-left:20px;">鏂囦欢鏁帮細' + item.fileCount + '</span>' +
- '<span style="margin-left:20px;">' + typeBtns + '</span>' +
- '</div>' +
- '</div>' +
- '</div>';
- });
- $('#device-list').html(html);
- } else if (res.code === 403) {
- top.location.href = baseUrl + "/";
- } else {
- layer.msg(res.msg || '鍔犺浇璁惧澶辫触', {icon: 2});
+ var pid = res.data.progressId;
+ that.startDownloadProgress(pid);
+ that.performDownloadRequest(day, type, deviceNo, offset, limit, pid);
}
- }
- });
- }
-
- function downloadDeviceLog(day, type, deviceNo) {
- if (!day) {
- layer.msg('璇峰厛閫夋嫨鏃ユ湡', {icon: 2});
- return;
- }
- if (!type) {
- layer.msg('璇烽�夋嫨璁惧绫诲瀷', {icon: 2});
- return;
- }
- if (!deviceNo) {
- layer.msg('璇疯緭鍏ヨ澶囩紪鍙�', {icon: 2});
- return;
- }
- var offsetVal = parseInt($('#file-offset').val());
- var limitVal = parseInt($('#file-limit').val());
- var offset = isNaN(offsetVal) || offsetVal < 0 ? 0 : offsetVal;
- var limit = isNaN(limitVal) || limitVal <= 0 ? 200 : limitVal;
- $.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) {
- layer.msg(res.msg || '鍒濆鍖栧け璐�', {icon: 2});
- return;
- }
- var pid = res.data.progressId;
- var progressIndex = layer.open({
- type: 1,
- title: '涓嬭浇涓�',
- area: ['520px', '200px'],
- content: '<div style="padding:16px;">' +
- '<div class="layui-text" style="margin-bottom:15px;">鍘嬬缉鐢熸垚杩涘害</div>' +
- '<div class="layui-progress" lay-showPercent="true" lay-filter="buildProgress">' +
- '<div class="layui-progress-bar" style="width:0%"><span class="layui-progress-text">0%</span></div>' +
- '</div>' +
- '<div class="layui-text" style="margin:12px 0 15px;">涓嬭浇鎺ユ敹杩涘害</div>' +
- '<div class="layui-progress" lay-showPercent="true" lay-filter="receiveProgress">' +
- '<div class="layui-progress-bar" style="width:0%"><span class="layui-progress-text">0%</span></div>' +
- '</div>' +
- '</div>'
- });
- var timer = 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;
- element.progress('buildProgress', percent + '%');
- // 闅愯棌瀹炴椂澶у皬锛屼笉鏇存柊鏂囧瓧
- }
- }
- });
- }, 500);
-
+ });
+ },
+ startDownloadProgress(pid) {
+ this.downloadDialogVisible = true;
+ this.buildProgress = 0;
+ this.receiveProgress = 0;
+ let that = this;
+ this.downloadTimer = setInterval(function(){
$.ajax({
- url: baseUrl + "/deviceLog/day/" + day + "/download/auth?type=" + encodeURIComponent(type) + "&deviceNo=" + encodeURIComponent(deviceNo) + "&offset=" + offset + "&limit=" + limit + "&progressId=" + encodeURIComponent(pid),
+ url: baseUrl + '/deviceLog/download/progress/auth',
headers: {'token': localStorage.getItem('token')},
method: 'GET',
- xhrFields: { responseType: 'blob' },
- xhr: function(){
- var xhr = new window.XMLHttpRequest();
- xhr.onprogress = function(e){
- var percent = 0;
- if (e.lengthComputable && e.total > 0) {
- percent = Math.floor(e.loaded / e.total * 100);
- element.progress('receiveProgress', percent + '%');
- }
- // 闅愯棌瀹炴椂澶у皬锛屼笉鏇存柊鏂囧瓧
- };
- 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]);
+ data: { id: pid },
+ success: function (p) {
+ if (p.code === 200) {
+ var percent = p.data.percent || 0;
+ that.buildProgress = percent;
}
- element.progress('buildProgress', '100%');
- element.progress('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(timer);
- setTimeout(function(){ layer.close(progressIndex); }, 300);
- },
- error: function () {
- clearInterval(timer);
- layer.close(progressIndex);
- layer.msg('涓嬭浇澶辫触鎴栨湭鎵惧埌鏃ュ織', {icon: 2});
}
});
+ }, 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;
+ }
+ };
+ 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);
+ },
+ error: function () {
+ clearInterval(that.downloadTimer);
+ that.downloadDialogVisible = false;
+ that.$message.error('涓嬭浇澶辫触鎴栨湭鎵惧埌鏃ュ織');
+ }
+ });
+ },
+
+ // --- 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();
+ },
+ 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: '姝e湪璺宠浆鑷崇洰鏍囨椂闂� (鍔犺浇涓�)...',
+ spinner: 'el-icon-loading',
+ background: 'rgba(255, 255, 255, 0.7)'
+ });
+ }
+ } 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)'
+ });
}
- });
+
+ 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.
+ }
+
+ // NEW LOGIC: If seeking, try to find offset first
+ if (this.seekTargetTime > 0 && this.needToSeekOffset && !this.seekingOffset) {
+ this.seekingOffset = true;
+ $.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 },
+ 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);
+ } else {
+ // Fallback to sequential load if seek fails
+ that.seekingOffset = false;
+ that.needToSeekOffset = false;
+ that.loadMoreLogsSequential(loadingInstance);
+ }
+ },
+ error: function() {
+ that.seekingOffset = false;
+ that.needToSeekOffset = false;
+ that.loadMoreLogsSequential(loadingInstance);
+ }
+ });
+ return;
+ }
+
+ this.loadMoreLogsSequential(loadingInstance);
+ },
+ loadMoreLogsSequential(loadingInstance) {
+ let that = this;
+ let currentLimit = this.seekTargetTime > 0 ? 10 : this.visLimit;
+
+ $.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 },
+ 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('鏁版嵁宸插叏閮ㄥ姞杞�');
+ }
+ 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]);
+ });
+ }
+ } 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;
+ }
+ },
+ error: function() {
+ if (loadingInstance) loadingInstance.close();
+ that.loadingLogs = false;
+ that.seekTargetTime = 0;
+ that.$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]);
+ }
+ },
+ 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);
+ },
+ 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();
+ }
+ }
+ },
+ 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();
+ }
+ }
+ },
+ syncState() {
+ var idx = this.binarySearch(this.currentTime);
+ if (idx >= 0) {
+ var targetLog = this.logs[idx];
+ this.updateDeviceState(targetLog);
+ }
+ },
+ 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;
+ }
+ }
+ return ans;
+ },
+ 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];
+ }
+
+ 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 || {};
+
+ 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'))
+ };
+ } else if (type === 'DualCrn') {
+ var vo = {
+ 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) 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 = {
+ rgvNo: protocol.rgvNo,
+ taskNo: protocol.taskNo,
+ mode: RgvModeType[protocol.mode] || '',
+ status: RgvStatusType[protocol.status] || '',
+ loading: protocol.loaded == 1 ? '鏈夌墿' : '鏃犵墿',
+ 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') {
+ 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;
+ },
+ 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();
+ },
+ initJumpTime() {
+ if (this.currentTime > 0) {
+ this.jumpTime = new Date(this.currentTime);
+ } else if (this.startTime > 0) {
+ this.jumpTime = new Date(this.startTime);
+ } 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 {
+ this.jumpTime = new Date();
+ }
+ }
+ },
+ 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('鐩爣鏃堕棿瓒呭嚭鏃ュ織鑼冨洿锛屽凡璺宠浆鑷崇粨鏉熸椂闂�');
+ }
+ }
+
+ 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();
+ }
+ }
}
-
- $(document).on('click', '#download-btn', function () {
- downloadDeviceLog(currentDay, $('#device-type-input').val(), $('#device-no-input').val());
- });
-
- $(document).on('click', '#device-list .layui-btn', function () {
- var deviceNo = $(this).attr('data-device-no');
- var type = $(this).attr('data-type');
- downloadDeviceLog(currentDay, type, deviceNo);
- });
-
- loadDateTree();
- limit();
-});
-
+});
\ No newline at end of file
--
Gitblit v1.9.1