From fd82105a3dfe347c4c9acb0410c117d8d67c9339 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期三, 18 三月 2026 10:40:16 +0800
Subject: [PATCH] #

---
 src/main/webapp/static/js/deviceLogs/deviceLogs.js | 1990 ++++++++++++++++++++++++++++++++++++++--------------------
 1 files changed, 1,301 insertions(+), 689 deletions(-)

diff --git a/src/main/webapp/static/js/deviceLogs/deviceLogs.js b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
index f28a6d7..62667fc 100644
--- a/src/main/webapp/static/js/deviceLogs/deviceLogs.js
+++ b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
@@ -1,99 +1,313 @@
 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: [],
+        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,
-        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,
+        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) {
@@ -102,581 +316,555 @@
             }
             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 = [];
+            (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)
+                            });
+                        }
                     });
                 });
-            });
-            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);
+        handleNodeClick: function (data) {
+            if (data && data.day) {
+                this.selectDay(data.day);
             }
         },
-
-        // --- 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('璇锋眰澶辫触');
-                }
-            });
+        handleRecentDayClick: function (day) {
+            this.selectDay(day);
         },
-
-        // --- 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;
-                        }
-                    };
-                    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);
-                    }
-                });
+        selectDay: function (day) {
+            if (!day) {
                 return;
             }
+            this.selectedDay = day;
+            this.searchDeviceNo = '';
+            this.pause();
+            this.summaryLoading = true;
+            this.resetSelectionState();
 
-            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.
-                            }
-                        }
+            var self = this;
+            $.ajax({
+                url: baseUrl + '/deviceLog/day/' + day + '/summary/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'GET',
+                success: function (res) {
+                    self.summaryLoading = false;
+                    if (res && res.code === 200) {
+                        self.deviceSummary = self.normalizeSummaryData(res.data);
+                        self.activeType = self.pickActiveType();
+                        self.resetSelectionState();
+                    } else if (res && res.code === 403) {
+                        top.location.href = baseUrl + '/';
                     } else {
-                        that.$message.error(res.msg);
-                        that.seekTargetTime = 0;
+                        self.deviceSummary = self.emptySummary();
+                        self.$message.error((res && res.msg) || '鍔犺浇璁惧鎽樿澶辫触');
                     }
                 },
-                error: function() {
-                    if (loadingInstance) loadingInstance.close();
-                    that.loadingLogs = false;
-                    that.seekTargetTime = 0;
-                    that.$message.error('璇锋眰澶辫触');
+                error: function () {
+                    self.summaryLoading = false;
+                    self.deviceSummary = self.emptySummary();
+                    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]);
+        normalizeSummaryData: function (data) {
+            var self = this;
+            var summary = this.emptySummary();
+            if (data && data.stats) {
+                summary.stats = {
+                    totalDevices: data.stats.totalDevices || 0,
+                    totalFiles: data.stats.totalFiles || 0,
+                    typeCounts: data.stats.typeCounts || {}
+                };
             }
+            var groupMap = {};
+            ((data && data.groups) || []).forEach(function (group) {
+                groupMap[group.type] = {
+                    type: group.type,
+                    typeLabel: group.typeLabel || self.typeLabels[group.type] || group.type,
+                    deviceCount: group.deviceCount || 0,
+                    totalFiles: group.totalFiles || 0,
+                    devices: (group.devices || []).map(function (device) {
+                        return {
+                            type: device.type,
+                            typeLabel: device.typeLabel || self.typeLabels[device.type] || device.type,
+                            deviceNo: String(device.deviceNo),
+                            fileCount: device.fileCount || 0,
+                            firstTime: device.firstTime || 0,
+                            lastTime: device.lastTime || 0
+                        };
+                    })
+                };
+            });
+            summary.groups = this.typeOrder.map(function (type) {
+                return groupMap[type] || {
+                    type: type,
+                    typeLabel: self.typeLabels[type] || type,
+                    deviceCount: 0,
+                    totalFiles: 0,
+                    devices: []
+                };
+            });
+            return summary;
         },
-        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();
+        pickActiveType: function () {
+            var existing = this.activeType;
+            if (existing) {
+                var existingGroup = this.getGroup(existing);
+                if (existingGroup && (existingGroup.devices || []).length) {
+                    return existing;
                 }
             }
-            
-            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();
+            for (var i = 0; i < this.typeOrder.length; i++) {
+                var group = this.getGroup(this.typeOrder[i]);
+                if (group && (group.devices || []).length) {
+                    return group.type;
+                }
+            }
+            return this.typeOrder[0] || '';
+        },
+        ensureDeviceSelection: function () {
+            var selected = this.selectedDeviceSummary;
+            if (selected && selected.type === this.activeType) {
+                return;
+            }
+            this.resetSelectionState();
+        },
+        selectTypeGroup: function (type) {
+            if (!type) {
+                return;
+            }
+            this.activeType = type;
+            var current = this.selectedDeviceSummary;
+            if (current && current.type === type) {
+                return;
+            }
+            this.resetSelectionState();
+        },
+        getGroup: function (type) {
+            var result = null;
+            (this.deviceGroups || []).forEach(function (group) {
+                if (group.type === type) {
+                    result = group;
+                }
+            });
+            return result;
+        },
+        resetSelectionState: function () {
+            this.viewMode = 'picker';
+            this.selectedType = '';
+            this.selectedDeviceNo = '';
+            this.activeDeviceKey = '';
+            this.detailTab = 'logs';
+            this.rawTab = 'wcs';
+            this.timelineMeta = this.createEmptyTimeline();
+            this.timelineLoading = false;
+            this.logLoading = false;
+            this.logLoadError = '';
+            this.logRows = [];
+            this.loadedOffsets = {};
+            this.loadingOffsets = {};
+            this.selectedTimestamp = 0;
+            this.logWindowAnchorOffset = -1;
+        },
+        selectDevice: function (device) {
+            if (!device) {
+                return;
+            }
+            var nextKey = this.buildDeviceKey(device.type, device.deviceNo);
+            if (this.activeDeviceKey === nextKey && this.logRows.length) {
+                return;
+            }
+            this.pause();
+            this.viewMode = 'viewer';
+            this.activeType = device.type;
+            this.selectedType = device.type;
+            this.selectedDeviceNo = String(device.deviceNo);
+            this.activeDeviceKey = nextKey;
+            this.detailTab = 'raw';
+            this.rawTab = 'wcs';
+            this.timelineMeta = this.createEmptyTimeline();
+            this.logRows = [];
+            this.loadedOffsets = {};
+            this.loadingOffsets = {};
+            this.selectedTimestamp = 0;
+            this.logLoadError = '';
+            this.loadTimeline();
+        },
+        returnToSelector: function () {
+            this.pause();
+            this.resetSelectionState();
+        },
+        loadTimeline: function () {
+            if (!this.selectedDay || !this.selectedType || !this.selectedDeviceNo) {
+                return;
+            }
+            this.timelineLoading = true;
+            this.logLoadError = '';
+            var self = this;
+            $.ajax({
+                url: baseUrl + '/deviceLog/day/' + this.selectedDay + '/timeline/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'GET',
+                data: {
+                    type: this.selectedType,
+                    deviceNo: this.selectedDeviceNo
+                },
+                success: function (res) {
+                    self.timelineLoading = false;
+                    if (res && res.code === 200) {
+                        self.timelineMeta = self.normalizeTimeline(res.data);
+                        if (self.timelineMeta.startTime) {
+                            self.selectedTimestamp = self.timelineMeta.startTime;
+                        }
+                        if (self.timelineMeta.totalFiles > 0) {
+                            self.loadSegmentWindow(0, {
+                                initialize: true,
+                                focusTimestamp: self.timelineMeta.startTime
+                            });
+                        }
+                    } else {
+                        self.timelineMeta = self.createEmptyTimeline();
+                        self.logLoadError = (res && res.msg) || '璇诲彇鏃堕棿杞村け璐�';
+                        self.$message.error(self.logLoadError);
                     }
-                    
-                    // 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;
+                },
+                error: function () {
+                    self.timelineLoading = false;
+                    self.timelineMeta = self.createEmptyTimeline();
+                    self.logLoadError = '璇诲彇鏃堕棿杞村け璐�';
+                    self.$message.error('璇诲彇鏃堕棿杞村け璐�');
                 }
-            }
-            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();
-                 }
+        normalizeTimeline: function (data) {
+            var timeline = this.createEmptyTimeline();
+            if (!data) {
+                return timeline;
             }
-        },
-        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);
+            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;
                     }
                 }
-            } catch (e) {
-                console.error('Error parsing wcsData', e);
+            }
+            if (!timeline.endTime && timeline.segments.length) {
+                for (var j = timeline.segments.length - 1; j >= 0; j--) {
+                    if (timeline.segments[j].endTime) {
+                        timeline.endTime = timeline.segments[j].endTime;
+                        break;
+                    }
+                }
+            }
+            return timeline;
+        },
+        loadSegmentWindow: function (offset, options) {
+            options = options || {};
+            if (offset == null || offset < 0 || offset >= (this.timelineMeta.totalFiles || 0)) {
+                return;
+            }
+            if (this.loadedOffsets[String(offset)] || this.loadingOffsets[String(offset)]) {
+                if (options.focusTimestamp) {
+                    this.selectedTimestamp = options.focusTimestamp;
+                    if (options.resetWindow !== false) {
+                        this.pruneLogRowsAroundOffset(options.anchorOffset != null ? options.anchorOffset : offset);
+                    }
+                    if (options.scrollIntoView !== false) {
+                        this.$nextTick(this.scrollCurrentRowIntoView);
+                    }
+                }
+                return;
+            }
+            var self = this;
+            var batchSize = 1;
+            this.logLoading = true;
+            this.$set(this.loadingOffsets, String(offset), true);
+            $.ajax({
+                url: baseUrl + '/deviceLog/day/' + this.selectedDay + '/preview/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'GET',
+                data: {
+                    type: this.selectedType,
+                    deviceNo: this.selectedDeviceNo,
+                    offset: offset,
+                    limit: batchSize
+                },
+                success: function (res) {
+                    self.logLoading = false;
+                    self.$delete(self.loadingOffsets, String(offset));
+                    if (res && res.code === 200) {
+                        self.markLoadedOffsets(offset, batchSize);
+                        var decorated = self.decorateLogs(res.data || [], offset);
+                        self.mergeLogRows(decorated);
+                        if (options.resetWindow !== false) {
+                            self.pruneLogRowsAroundOffset(options.anchorOffset != null ? options.anchorOffset : offset);
+                        }
+                        if (options.initialize && self.logRows.length) {
+                            self.selectedTimestamp = self.logRows[0]._ts || self.timelineMeta.startTime;
+                        } else if (options.focusTimestamp) {
+                            self.selectedTimestamp = options.focusTimestamp;
+                        } else if (!self.selectedTimestamp && self.logRows.length) {
+                            self.selectedTimestamp = self.logRows[0]._ts;
+                        }
+                        if (!decorated.length && options.initialize && offset + batchSize < self.timelineMeta.totalFiles) {
+                            self.loadSegmentWindow(offset + batchSize, options);
+                            return;
+                        }
+                        if (options.scrollIntoView !== false) {
+                            self.$nextTick(self.scrollCurrentRowIntoView);
+                        }
+                    } else {
+                        self.logLoadError = (res && res.msg) || '璇诲彇鏃ュ織澶辫触';
+                        self.$message.error(self.logLoadError);
+                    }
+                },
+                error: function () {
+                    self.logLoading = false;
+                    self.$delete(self.loadingOffsets, String(offset));
+                    self.logLoadError = '璇诲彇鏃ュ織澶辫触';
+                    self.$message.error('璇诲彇鏃ュ織澶辫触');
+                }
+            });
+        },
+        markLoadedOffsets: function (offset, limit) {
+            var total = this.timelineMeta.totalFiles || 0;
+            for (var i = offset; i < Math.min(total, offset + limit); i++) {
+                this.$set(this.loadedOffsets, String(i), true);
             }
         },
-        transformData(protocol, type) {
-            if (!protocol) return {};
-            
-            // Enums from API
+        decorateLogs: function (logs, segmentOffset) {
+            var self = this;
+            return (logs || []).map(function (logItem) {
+                return self.decorateLog(logItem, segmentOffset);
+            });
+        },
+        decorateLog: function (logItem, segmentOffset) {
+            var protocol = this.safeParse(logItem && logItem.wcsData);
+            var visualItems = this.buildVisualItems(protocol, this.selectedType);
+            return Object.assign({}, logItem, {
+                _ts: this.parseTimestamp(logItem && logItem.createTime),
+                _key: this.buildLogRowKey(logItem),
+                _segmentOffset: segmentOffset,
+                _protocol: protocol,
+                _visualItems: visualItems,
+                _visualParam: this.buildVisualParam(this.selectedType, this.selectedDeviceNo),
+                _summary: this.buildLogSummary(this.selectedType, visualItems, protocol)
+            });
+        },
+        buildLogSummary: function (type, visualItems, protocol) {
+            var fallback = {
+                statusLabel: '绂荤嚎',
+                tone: 'muted',
+                title: '鍘熷鏃ュ織',
+                detail: '褰撳墠璁板綍缂哄皯鍙В鏋愮殑涓氬姟瀛楁',
+                hint: ''
+            };
+            if (!type) {
+                return fallback;
+            }
+            if (type === 'Crn') {
+                var crn = visualItems[0] || {};
+                var crnStatus = MonitorCardKit.deviceStatusLabel(crn.deviceStatus);
+                return {
+                    statusLabel: crnStatus,
+                    tone: MonitorCardKit.statusTone(crnStatus),
+                    title: '浠诲姟 ' + MonitorCardKit.orDash(crn.workNo) + ' 路 鎺� ' + MonitorCardKit.orDash(crn.bay) + ' 路 灞� ' + MonitorCardKit.orDash(crn.lev),
+                    detail: '妯″紡 ' + MonitorCardKit.orDash(crn.mode) + ' / 鐘舵�� ' + MonitorCardKit.orDash(crn.status) + ' / 璐у弶 ' + MonitorCardKit.orDash(crn.forkOffset),
+                    hint: crn.warnCode ? ('鎶ヨ浠g爜 ' + 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 ? ('鎶ヨ浠g爜 ' + 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 ? ('鎶ヨ浠g爜 ' + rgv.warnCode) : ''
+                };
+            }
+            if (type === 'Devp') {
+                var stations = visualItems || [];
+                var autoCount = 0;
+                var taskCount = 0;
+                var loadingCount = 0;
+                var errorStations = [];
+                var canInCount = 0;
+                for (var i = 0; i < stations.length; i++) {
+                    if (this.toBool(stations[i].autoing)) {
+                        autoCount += 1;
+                    }
+                    if (stations[i].taskNo != null && stations[i].taskNo !== '' && Number(stations[i].taskNo) !== 0) {
+                        taskCount += 1;
+                    }
+                    if (this.toBool(stations[i].loading)) {
+                        loadingCount += 1;
+                    }
+                    if (this.toBool(stations[i].inEnable)) {
+                        canInCount += 1;
+                    }
+                    if (stations[i].error || stations[i].errorMsg) {
+                        errorStations.push(stations[i].stationId);
+                    }
+                }
+                var statusLabel = errorStations.length ? '鏁呴殰' : (autoCount === stations.length && stations.length ? '鑷姩' : '鎵嬪姩');
+                return {
+                    statusLabel: statusLabel,
+                    tone: MonitorCardKit.statusTone(statusLabel),
+                    title: stations.length + ' 涓珯鐐� 路 浠诲姟 ' + taskCount + ' 路 鏈夌墿 ' + loadingCount,
+                    detail: '鑷姩 ' + autoCount + ' / 鎵嬪姩 ' + Math.max(0, stations.length - autoCount) + ' / 鍙叆 ' + canInCount,
+                    hint: errorStations.length ? ('寮傚父绔欑偣 ' + errorStations.slice(0, 6).join(', ')) : ('绔欑偣鏁扮粍澶у皬 ' + stations.length)
+                };
+            }
+            return fallback;
+        },
+        buildVisualItems: function (protocol, type) {
+            if (!protocol) {
+                return [];
+            }
+            if (type === 'Devp' && Array.isArray(protocol)) {
+                var self = this;
+                return protocol.map(function (item) {
+                    return self.transformData(item, type);
+                }).sort(function (a, b) {
+                    return (a.stationId || 0) - (b.stationId || 0);
+                });
+            }
+            if (type !== 'Devp') {
+                return [this.transformData(protocol, type)];
+            }
+            return [];
+        },
+        buildVisualParam: function (type, deviceNo) {
+            if (type === 'Crn' || type === 'DualCrn') {
+                return { crnNo: Number(deviceNo) };
+            }
+            if (type === 'Rgv') {
+                return { rgvNo: Number(deviceNo) };
+            }
+            return {};
+        },
+        transformData: function (protocol, type) {
+            if (!protocol) {
+                return {};
+            }
             var CrnModeType = this.deviceEnums.CrnModeType || {};
             var CrnStatusType = this.deviceEnums.CrnStatusType || {};
             var CrnForkPosType = this.deviceEnums.CrnForkPosType || {};
             var CrnLiftPosType = this.deviceEnums.CrnLiftPosType || {};
-            
             var DualCrnForkPosType = this.deviceEnums.DualCrnForkPosType || {};
             var DualCrnLiftPosType = this.deviceEnums.DualCrnLiftPosType || {};
-
             var RgvModeType = this.deviceEnums.RgvModeType || {};
             var RgvStatusType = this.deviceEnums.RgvStatusType || {};
 
@@ -691,19 +879,20 @@
                     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' : 
-                                  ((protocol.taskNo && protocol.taskNo > 0) ? 'WORKING' : 
-                                  (protocol.mode == 3 ? 'AUTO' : 'OFFLINE')) 
+                    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,
@@ -726,31 +915,30 @@
                     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') {
+                };
+                if (protocol.alarm && protocol.alarm > 0) dual.deviceStatus = 'ERROR';
+                else if ((protocol.taskNo && protocol.taskNo > 0) || (protocol.taskNoTwo && protocol.taskNoTwo > 0)) dual.deviceStatus = 'WORKING';
+                else if (protocol.mode == 3) dual.deviceStatus = 'AUTO';
+                else dual.deviceStatus = 'OFFLINE';
+                return dual;
+            }
+            if (type === 'Rgv') {
+                var rgv = {
+                    rgvNo: protocol.rgvNo,
+                    taskNo: protocol.taskNo,
+                    mode: RgvModeType[protocol.mode] || '',
+                    status: RgvStatusType[protocol.status] || '',
+                    loading: protocol.loaded == 1 ? '鏈夌墿' : '鏃犵墿',
+                    trackSiteNo: protocol.rgvPos,
+                    warnCode: protocol.alarm
+                };
+                if (protocol.alarm && protocol.alarm > 0) rgv.deviceStatus = 'ERROR';
+                else if (protocol.taskNo && protocol.taskNo > 0) rgv.deviceStatus = 'WORKING';
+                else if (protocol.mode == 3) rgv.deviceStatus = 'AUTO';
+                else rgv.deviceStatus = 'OFFLINE';
+                return rgv;
+            }
+            if (type === 'Devp') {
                 return {
                     stationId: protocol.stationId,
                     taskNo: protocol.taskNo,
@@ -773,93 +961,517 @@
             }
             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);
-            } 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');
+        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 {
-                    this.jumpTime = new Date();
+                    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
+                    });
                 }
             }
         },
-        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);
+        findSegmentByTime: function (timestamp) {
+            var segments = this.timelineMeta.segments || [];
+            if (!segments.length) {
+                return null;
             }
-            
-            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('鐩爣鏃堕棿瓒呭嚭鏃ュ織鑼冨洿锛屽凡璺宠浆鑷崇粨鏉熸椂闂�');
+            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;
                 }
             }
-            
-            this.currentTime = targetTs;
-            this.sliderValue = this.currentTime - this.startTime;
-            this.syncState();
+            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;
-            
-            // Trigger load if needed
-            if (this.hasMoreLogs && !this.loadingLogs) {
-                 // Force load check
-                 this.loadMoreLogs();
+            this.seekToTimestamp(ts, { scrollIntoView: true });
+        },
+        seekToTimestamp: function (timestamp, options) {
+            options = options || {};
+            if (!timestamp || !this.selectedDay || !this.selectedType || !this.selectedDeviceNo) {
+                return;
             }
+            var self = this;
+            this.selectedTimestamp = timestamp;
+            $.ajax({
+                url: baseUrl + '/deviceLog/day/' + this.selectedDay + '/seek/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'GET',
+                data: {
+                    type: this.selectedType,
+                    deviceNo: this.selectedDeviceNo,
+                    timestamp: timestamp
+                },
+                success: function (res) {
+                    if (res && res.code === 200 && res.data && res.data.offset != null) {
+                        self.loadSegmentWindow(Number(res.data.offset), {
+                            focusTimestamp: timestamp,
+                            anchorOffset: Number(res.data.offset),
+                            resetWindow: true,
+                            scrollIntoView: options.scrollIntoView !== false
+                        });
+                        return;
+                    }
+                    self.ensureTimestampLoaded(timestamp);
+                    if (options.scrollIntoView !== false) {
+                        self.$nextTick(self.scrollCurrentRowIntoView);
+                    }
+                },
+                error: function () {
+                    self.ensureTimestampLoaded(timestamp);
+                    if (options.scrollIntoView !== false) {
+                        self.$nextTick(self.scrollCurrentRowIntoView);
+                    }
+                }
+            });
+        },
+        scrollCurrentRowIntoView: function () {
+            if (!this.selectedLogKey) {
+                return;
+            }
+            var el = document.getElementById('log-row-' + this.selectedLogKey);
+            if (el && typeof el.scrollIntoView === 'function') {
+                el.scrollIntoView({ block: 'nearest', behavior: 'auto' });
+            }
+        },
+        formatTooltip: function (value) {
+            if (!this.timelineMeta.startTime) {
+                return '--';
+            }
+            return this.formatTimestamp(this.timelineMeta.startTime + value, true);
+        },
+        handleCurrentDeviceDownload: function () {
+            this.doDownload(this.selectedDay, this.selectedType, this.selectedDeviceNo);
+        },
+        doDownload: function (day, type, deviceNo) {
+            if (!day || !type || !deviceNo) {
+                return;
+            }
+            var self = this;
+            $.ajax({
+                url: baseUrl + '/deviceLog/download/init/auth',
+                headers: { token: localStorage.getItem('token') },
+                method: 'POST',
+                data: JSON.stringify({
+                    day: day,
+                    type: type,
+                    deviceNo: deviceNo
+                }),
+                dataType: 'json',
+                contentType: 'application/json;charset=UTF-8',
+                success: function (res) {
+                    if (!res || res.code !== 200) {
+                        self.$message.error((res && res.msg) || '鍒濆鍖栧け璐�');
+                        return;
+                    }
+                    var pid = res.data.progressId;
+                    self.startDownloadProgress(pid);
+                    self.performDownloadRequest(day, type, deviceNo, pid);
+                },
+                error: function () {
+                    self.$message.error('鍒濆鍖栧け璐�');
+                }
+            });
+        },
+        startDownloadProgress: function (pid) {
+            if (this.downloadTimer) {
+                clearInterval(this.downloadTimer);
+                this.downloadTimer = null;
+            }
+            this.downloadDialogVisible = true;
+            this.buildProgress = 0;
+            this.receiveProgress = 0;
+            var self = this;
+            this.downloadTimer = setInterval(function () {
+                $.ajax({
+                    url: baseUrl + '/deviceLog/download/progress/auth',
+                    headers: { token: localStorage.getItem('token') },
+                    method: 'GET',
+                    data: { id: pid },
+                    success: function (res) {
+                        if (res && res.code === 200) {
+                            self.buildProgress = res.data.percent || 0;
+                        }
+                    }
+                });
+            }, 500);
+        },
+        performDownloadRequest: function (day, type, deviceNo, pid) {
+            var self = this;
+            $.ajax({
+                url: baseUrl + '/deviceLog/day/' + day + '/download/auth?type=' + encodeURIComponent(type) + '&deviceNo=' + encodeURIComponent(deviceNo) + '&progressId=' + encodeURIComponent(pid),
+                headers: { token: localStorage.getItem('token') },
+                method: 'GET',
+                xhrFields: { responseType: 'blob' },
+                xhr: function () {
+                    var xhr = new window.XMLHttpRequest();
+                    xhr.onprogress = function (e) {
+                        if (e.lengthComputable && e.total > 0) {
+                            self.receiveProgress = Math.floor(e.loaded / e.total * 100);
+                        }
+                    };
+                    return xhr;
+                },
+                success: function (data, status, xhr) {
+                    var disposition = xhr.getResponseHeader('Content-Disposition') || '';
+                    var filename = type + '_' + deviceNo + '_' + day + '.zip';
+                    var match = /filename=(.+)/.exec(disposition);
+                    if (match && match[1]) {
+                        filename = decodeURIComponent(match[1]);
+                    }
+                    self.buildProgress = 100;
+                    self.receiveProgress = 100;
+                    var blob = new Blob([data], { type: 'application/zip' });
+                    var link = document.createElement('a');
+                    var url = window.URL.createObjectURL(blob);
+                    link.href = url;
+                    link.download = filename;
+                    document.body.appendChild(link);
+                    link.click();
+                    document.body.removeChild(link);
+                    window.URL.revokeObjectURL(url);
+                    if (self.downloadTimer) {
+                        clearInterval(self.downloadTimer);
+                        self.downloadTimer = null;
+                    }
+                    setTimeout(function () {
+                        self.downloadDialogVisible = false;
+                    }, 900);
+                },
+                error: function () {
+                    if (self.downloadTimer) {
+                        clearInterval(self.downloadTimer);
+                        self.downloadTimer = null;
+                    }
+                    self.downloadDialogVisible = false;
+                    self.$message.error('涓嬭浇澶辫触鎴栨湭鎵惧埌鏃ュ織');
+                }
+            });
+        },
+        buildDeviceKey: function (type, deviceNo) {
+            return String(type || '') + ':' + String(deviceNo || '');
+        },
+        parseDeviceNo: function (deviceNo) {
+            var n = parseInt(deviceNo, 10);
+            return isNaN(n) ? Number.MAX_SAFE_INTEGER : n;
+        },
+        formatTimestamp: function (timestamp, withMillis) {
+            if (!timestamp) {
+                return '--';
+            }
+            var d = new Date(timestamp);
+            if (isNaN(d.getTime())) {
+                return '--';
+            }
+            var Y = d.getFullYear();
+            var M = String(d.getMonth() + 1).padStart(2, '0');
+            var D = String(d.getDate()).padStart(2, '0');
+            var h = String(d.getHours()).padStart(2, '0');
+            var m = String(d.getMinutes()).padStart(2, '0');
+            var s = String(d.getSeconds()).padStart(2, '0');
+            if (withMillis) {
+                return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s + '.' + String(d.getMilliseconds()).padStart(3, '0');
+            }
+            return Y + '-' + M + '-' + D + ' ' + h + ':' + m + ':' + s;
+        },
+        formatDayText: function (day) {
+            if (!day || day.length !== 8) {
+                return '--';
+            }
+            return day.substring(0, 4) + '-' + day.substring(4, 6) + '-' + day.substring(6, 8);
+        },
+        toBool: function (value) {
+            return value === true || value === 'Y' || value === 'y' || value === 1 || value === '1';
         }
     }
 });

--
Gitblit v1.9.1