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