From ce13e25ed685ba5c961832d023ceafecf4f30d47 Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期六, 10 一月 2026 15:27:33 +0800
Subject: [PATCH] #
---
src/main/webapp/components/WatchRgvCard.js | 33
src/main/java/com/zy/asrs/controller/DeviceLogController.java | 219 +++++++
src/main/webapp/views/index.html | 13
src/main/webapp/components/DevpCard.js | 46 +
src/main/webapp/components/WatchCrnCard.js | 33
src/main/webapp/static/js/deviceLogs/deviceLogs.js | 1039 +++++++++++++++++++++++++++++-------
src/main/webapp/components/WatchDualCrnCard.js | 33
src/main/webapp/views/deviceLogs/deviceLogs.html | 257 ++++++--
8 files changed, 1,364 insertions(+), 309 deletions(-)
diff --git a/src/main/java/com/zy/asrs/controller/DeviceLogController.java b/src/main/java/com/zy/asrs/controller/DeviceLogController.java
index d5537e9..6640886 100644
--- a/src/main/java/com/zy/asrs/controller/DeviceLogController.java
+++ b/src/main/java/com/zy/asrs/controller/DeviceLogController.java
@@ -1,24 +1,29 @@
package com.zy.asrs.controller;
+import com.alibaba.fastjson.JSON;
import com.core.annotations.ManagerAuth;
import com.core.common.Cools;
import com.core.common.R;
+import com.zy.asrs.entity.DeviceDataLog;
import com.zy.common.web.BaseController;
import com.zy.core.enums.SlaveType;
+import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
-
import javax.servlet.http.HttpServletResponse;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
+@Slf4j
@RestController
public class DeviceLogController extends BaseController {
@@ -130,6 +135,186 @@
return R.ok(res);
} catch (Exception e) {
return R.error("璇诲彇璁惧鍒楄〃澶辫触");
+ }
+ }
+
+ @RequestMapping(value = "/deviceLog/day/{day}/preview/auth")
+ @ManagerAuth
+ public R preview(@PathVariable("day") String day,
+ @RequestParam("type") String type,
+ @RequestParam("deviceNo") String deviceNo,
+ @RequestParam(value = "offset", required = false) Integer offset,
+ @RequestParam(value = "limit", required = false) Integer limit) {
+ try {
+ String dayClean = day == null ? null : day.replaceAll("\\D", "");
+ if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) {
+ return R.error("鏃ユ湡鏍煎紡閿欒");
+ }
+ if (type == null || SlaveType.findInstance(type) == null) {
+ return R.error("璁惧绫诲瀷閿欒");
+ }
+ if (deviceNo == null || !deviceNo.chars().allMatch(Character::isDigit)) {
+ return R.error("璁惧缂栧彿閿欒");
+ }
+ Path dayDir = Paths.get(loggingPath, dayClean);
+ if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) {
+ return R.ok(new ArrayList<>());
+ }
+ String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
+ List<Path> files = Files.list(dayDir)
+ .filter(p -> {
+ String name = p.getFileName().toString();
+ return name.endsWith(".log") && name.startsWith(prefix);
+ }).collect(Collectors.toList());
+
+ files.sort(Comparator.comparingInt(p -> {
+ String n = p.getFileName().toString();
+ try {
+ String suf = n.substring(prefix.length(), n.length() - 4);
+ return Integer.parseInt(suf);
+ } catch (Exception e) {
+ return Integer.MAX_VALUE;
+ }
+ }));
+
+ int from = offset == null || offset < 0 ? 0 : offset;
+ int max = limit == null || limit <= 0 ? 5 : limit; // 榛樿璇诲彇5涓枃浠�
+ if (max > 10) max = 10; // 闄愬埗鏈�澶ф枃浠舵暟锛岄槻姝㈣秴鏃�
+ int to = Math.min(files.size(), from + max);
+
+ if (from >= files.size()) {
+ return R.ok(new ArrayList<>());
+ }
+
+ List<Path> targetFiles = files.subList(from, to);
+ List<DeviceDataLog> resultLogs = new ArrayList<>();
+
+ for (Path f : targetFiles) {
+ try (Stream<String> lines = Files.lines(f, StandardCharsets.UTF_8)) {
+ lines.forEach(line -> {
+ if (line != null && !line.trim().isEmpty()) {
+ try {
+ DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class);
+ resultLogs.add(logItem);
+ } catch (Exception e) {
+ // 蹇界暐瑙f瀽閿欒
+ }
+ }
+ });
+ } catch (Exception e) {
+ log.error("璇诲彇鏃ュ織鏂囦欢澶辫触: " + f, e);
+ }
+ }
+ // 鎸夋椂闂存帓搴�
+ resultLogs.sort(Comparator.comparing(DeviceDataLog::getCreateTime, Comparator.nullsLast(Date::compareTo)));
+
+ return R.ok(resultLogs);
+ } catch (Exception e) {
+ log.error("棰勮鏃ュ織澶辫触", e);
+ return R.error("棰勮鏃ュ織澶辫触");
+ }
+ }
+
+ @RequestMapping(value = "/deviceLog/day/{day}/seek/auth")
+ @ManagerAuth
+ public R seek(@PathVariable("day") String day,
+ @RequestParam("type") String type,
+ @RequestParam("deviceNo") String deviceNo,
+ @RequestParam("timestamp") Long timestamp) {
+ try {
+ String dayClean = day == null ? null : day.replaceAll("\\D", "");
+ if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) {
+ return R.error("鏃ユ湡鏍煎紡閿欒");
+ }
+ if (type == null || SlaveType.findInstance(type) == null) {
+ return R.error("璁惧绫诲瀷閿欒");
+ }
+ if (deviceNo == null || !deviceNo.chars().allMatch(Character::isDigit)) {
+ return R.error("璁惧缂栧彿閿欒");
+ }
+ Path dayDir = Paths.get(loggingPath, dayClean);
+ if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) {
+ return R.error("鏈壘鍒版棩蹇楁枃浠�");
+ }
+
+ String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
+ List<Path> files = Files.list(dayDir)
+ .filter(p -> {
+ String name = p.getFileName().toString();
+ return name.endsWith(".log") && name.startsWith(prefix);
+ }).collect(Collectors.toList());
+
+ files.sort(Comparator.comparingInt(p -> {
+ String n = p.getFileName().toString();
+ try {
+ String suf = n.substring(prefix.length(), n.length() - 4);
+ return Integer.parseInt(suf);
+ } catch (Exception e) {
+ return Integer.MAX_VALUE;
+ }
+ }));
+
+ if (files.isEmpty()) {
+ return R.error("鏈壘鍒版棩蹇楁枃浠�");
+ }
+
+ // Binary search for the file containing the timestamp
+ // We want to find the LAST file that has startTime <= targetTime.
+ // Because files are sequential: [t0, t1), [t1, t2), ...
+ // If we find file[i].startTime <= target < file[i+1].startTime, then target is in file[i].
+
+ int low = 0;
+ int high = files.size() - 1;
+ int foundIndex = -1;
+
+ while (low <= high) {
+ int mid = (low + high) >>> 1;
+ Path midFile = files.get(mid);
+
+ // Read start time of this file
+ Long midStart = getFileStartTime(midFile);
+ if (midStart == null) {
+ low = mid + 1;
+ continue;
+ }
+
+ if (midStart <= timestamp) {
+ // This file starts before or at target. It COULD be the one.
+ // But maybe a later file also starts before target?
+ foundIndex = mid;
+ low = mid + 1; // Try to find a later start time
+ } else {
+ // This file starts AFTER target. So target must be in an earlier file.
+ high = mid - 1;
+ }
+ }
+
+ if (foundIndex == -1) {
+ foundIndex = 0;
+ }
+
+ // Return the file index (offset)
+ Map<String, Object> result = new HashMap<>();
+ result.put("offset", foundIndex);
+ return R.ok(result);
+
+ } catch (Exception e) {
+ log.error("瀵诲潃澶辫触", e);
+ return R.error("瀵诲潃澶辫触");
+ }
+ }
+
+ private Long getFileStartTime(Path file) {
+ try {
+ String firstLine = null;
+ try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
+ firstLine = lines.findFirst().orElse(null);
+ }
+ if (firstLine == null) return null;
+ DeviceDataLog firstLog = JSON.parseObject(firstLine, DeviceDataLog.class);
+ return firstLog.getCreateTime().getTime();
+ } catch (Exception e) {
+ return null;
}
}
@@ -335,4 +520,36 @@
res.put("finished", info.finished);
return R.ok(res);
}
+
+ @RequestMapping(value = "/deviceLog/enums/auth")
+ @ManagerAuth
+ public R getEnums() {
+ Map<String, Map<String, String>> enums = new HashMap<>();
+
+ enums.put("CrnModeType", Arrays.stream(com.zy.core.enums.CrnModeType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("CrnStatusType", Arrays.stream(com.zy.core.enums.CrnStatusType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("CrnForkPosType", Arrays.stream(com.zy.core.enums.CrnForkPosType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("CrnLiftPosType", Arrays.stream(com.zy.core.enums.CrnLiftPosType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("DualCrnForkPosType", Arrays.stream(com.zy.core.enums.DualCrnForkPosType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("DualCrnLiftPosType", Arrays.stream(com.zy.core.enums.DualCrnLiftPosType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("RgvModeType", Arrays.stream(com.zy.core.enums.RgvModeType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ enums.put("RgvStatusType", Arrays.stream(com.zy.core.enums.RgvStatusType.values())
+ .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc)));
+
+ return R.ok(enums);
+ }
}
diff --git a/src/main/webapp/components/DevpCard.js b/src/main/webapp/components/DevpCard.js
index 9a4fa5c..24308c3 100644
--- a/src/main/webapp/components/DevpCard.js
+++ b/src/main/webapp/components/DevpCard.js
@@ -5,7 +5,7 @@
<div style="width: 100%;">杈撻�佺洃鎺�</div>
<div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchStationId" placeholder="璇疯緭鍏ョ珯鍙�"></el-input><el-button @click="getDevpStateInfo" size="mini">鏌ヨ</el-button></div>
</div>
- <div style="margin-bottom: 10px;">
+ <div style="margin-bottom: 10px;" v-if="!readOnly">
<div style="margin-bottom: 5px;">
<el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
<el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
@@ -65,10 +65,24 @@
</div>
</div>
`,
- props: ["param"],
+ props: {
+ param: {
+ type: Object,
+ default: () => ({})
+ },
+ autoRefresh: {
+ type: Boolean,
+ default: true
+ },
+ readOnly: {
+ type: Boolean,
+ default: false
+ }
+ },
data() {
return {
stationList: [],
+ fullStationList: [],
activeNames: "",
searchStationId: "",
showControl: false,
@@ -79,12 +93,20 @@
},
pageSize: 25,
currentPage: 1,
+ timer: null
};
},
created() {
- setInterval(() => {
- this.getDevpStateInfo();
- }, 1000);
+ if (this.autoRefresh) {
+ this.timer = setInterval(() => {
+ this.getDevpStateInfo();
+ }, 1000);
+ }
+ },
+ beforeDestroy() {
+ if (this.timer) {
+ clearInterval(this.timer);
+ }
},
computed: {
displayStationList() {
@@ -96,7 +118,7 @@
watch: {
param: {
handler(newVal, oldVal) {
- if (newVal.stationId != 0) {
+ if (newVal && newVal.stationId && newVal.stationId != 0) {
this.activeNames = newVal.stationId;
this.searchStationId = newVal.stationId;
}
@@ -114,7 +136,15 @@
this.currentPage = 1;
},
getDevpStateInfo() {
- if (this.$root.sendWs) {
+ if (this.readOnly) {
+ // Frontend filtering for readOnly mode
+ if (this.searchStationId == "") {
+ this.stationList = this.fullStationList;
+ } else {
+ this.stationList = this.fullStationList.filter(item => item.stationId == this.searchStationId);
+ this.currentPage = 1;
+ }
+ } else if (this.$root.sendWs) {
this.$root.sendWs(JSON.stringify({
"url": "/console/latest/data/station",
"data": {}
@@ -125,7 +155,7 @@
let that = this;
if (res.code == 200) {
let list = res.data;
-
+ that.fullStationList = list;
if (that.searchStationId == "") {
that.stationList = list;
} else {
diff --git a/src/main/webapp/components/WatchCrnCard.js b/src/main/webapp/components/WatchCrnCard.js
index e088d98..d760b97 100644
--- a/src/main/webapp/components/WatchCrnCard.js
+++ b/src/main/webapp/components/WatchCrnCard.js
@@ -5,7 +5,7 @@
<div style="width: 100%;">鍫嗗灈鏈虹洃鎺�</div>
<div style="width: 100%;text-align: right;display: flex;"><el-input size="mini" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�"></el-input><el-button @click="getCrnStateInfo" size="mini">鏌ヨ</el-button></div>
</div>
- <div style="margin-bottom: 10px;">
+ <div style="margin-bottom: 10px;" v-if="!readOnly">
<div style="margin-bottom: 5px;">
<el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
<el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
@@ -75,7 +75,20 @@
</div>
</div>
`,
- props: ["param"],
+ props: {
+ param: {
+ type: Object,
+ default: () => ({})
+ },
+ autoRefresh: {
+ type: Boolean,
+ default: true
+ },
+ readOnly: {
+ type: Boolean,
+ default: false
+ }
+ },
data() {
return {
crnList: [],
@@ -89,12 +102,20 @@
},
pageSize: 25,
currentPage: 1,
+ timer: null
};
},
created() {
- setInterval(() => {
- this.getCrnStateInfo();
- }, 1000);
+ if (this.autoRefresh) {
+ this.timer = setInterval(() => {
+ this.getCrnStateInfo();
+ }, 1000);
+ }
+ },
+ beforeDestroy() {
+ if (this.timer) {
+ clearInterval(this.timer);
+ }
},
computed: {
displayCrnList() {
@@ -106,7 +127,7 @@
watch: {
param: {
handler(newVal, oldVal) {
- if (newVal.crnNo != 0) {
+ if (newVal && newVal.crnNo && newVal.crnNo != 0) {
this.activeNames = newVal.crnNo;
this.searchCrnNo = newVal.crnNo;
const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo);
diff --git a/src/main/webapp/components/WatchDualCrnCard.js b/src/main/webapp/components/WatchDualCrnCard.js
index 4191450..d221857 100644
--- a/src/main/webapp/components/WatchDualCrnCard.js
+++ b/src/main/webapp/components/WatchDualCrnCard.js
@@ -8,7 +8,7 @@
<el-button @click="getDualCrnStateInfo" size="mini">鏌ヨ</el-button>
</div>
</div>
- <div style="margin-bottom: 10px;">
+ <div style="margin-bottom: 10px;" v-if="!readOnly">
<div style="margin-bottom: 5px;">
<el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
<el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
@@ -86,7 +86,20 @@
</div>
</div>
`,
- props: ["param"],
+ props: {
+ param: {
+ type: Object,
+ default: () => ({})
+ },
+ autoRefresh: {
+ type: Boolean,
+ default: true
+ },
+ readOnly: {
+ type: Boolean,
+ default: false
+ }
+ },
data() {
return {
crnList: [],
@@ -101,12 +114,20 @@
},
pageSize: 25,
currentPage: 1,
+ timer: null
};
},
created() {
- setInterval(() => {
- this.getDualCrnStateInfo();
- }, 1000);
+ if (this.autoRefresh) {
+ this.timer = setInterval(() => {
+ this.getDualCrnStateInfo();
+ }, 1000);
+ }
+ },
+ beforeDestroy() {
+ if (this.timer) {
+ clearInterval(this.timer);
+ }
},
computed: {
displayCrnList() {
@@ -118,7 +139,7 @@
watch: {
param: {
handler(newVal) {
- if (newVal.crnNo != 0) {
+ if (newVal && newVal.crnNo && newVal.crnNo != 0) {
this.activeNames = newVal.crnNo;
this.searchCrnNo = newVal.crnNo;
const idx = this.crnList.findIndex(i => i.crnNo == newVal.crnNo);
diff --git a/src/main/webapp/components/WatchRgvCard.js b/src/main/webapp/components/WatchRgvCard.js
index 57e12c5..86cf8ee 100644
--- a/src/main/webapp/components/WatchRgvCard.js
+++ b/src/main/webapp/components/WatchRgvCard.js
@@ -8,7 +8,7 @@
<el-button @click="getRgvStateInfo" size="mini">鏌ヨ</el-button>
</div>
</div>
- <div style="margin-bottom: 10px;">
+ <div style="margin-bottom: 10px;" v-if="!readOnly">
<div style="margin-bottom: 5px;">
<el-button v-if="showControl" @click="openControl" size="mini">鍏抽棴鎺у埗涓績</el-button>
<el-button v-else @click="openControl" size="mini">鎵撳紑鎺у埗涓績</el-button>
@@ -63,7 +63,20 @@
</div>
</div>
`,
- props: ["param"],
+ props: {
+ param: {
+ type: Object,
+ default: () => ({})
+ },
+ autoRefresh: {
+ type: Boolean,
+ default: true
+ },
+ readOnly: {
+ type: Boolean,
+ default: false
+ }
+ },
data() {
return {
rgvList: [],
@@ -77,12 +90,20 @@
},
pageSize: 25,
currentPage: 1,
+ timer: null
};
},
created() {
- setInterval(() => {
- this.getRgvStateInfo();
- }, 1000);
+ if (this.autoRefresh) {
+ this.timer = setInterval(() => {
+ this.getRgvStateInfo();
+ }, 1000);
+ }
+ },
+ beforeDestroy() {
+ if (this.timer) {
+ clearInterval(this.timer);
+ }
},
computed: {
displayRgvList() {
@@ -94,7 +115,7 @@
watch: {
param: {
handler(newVal) {
- if (newVal && newVal.rgvNo != 0) {
+ if (newVal && newVal.rgvNo && newVal.rgvNo != 0) {
this.activeNames = newVal.rgvNo;
const idx = this.rgvList.findIndex(i => i.rgvNo == newVal.rgvNo);
if (idx >= 0) { this.currentPage = Math.floor(idx / this.pageSize) + 1; }
diff --git a/src/main/webapp/static/js/deviceLogs/deviceLogs.js b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
index 31246f0..9d44d8f 100644
--- a/src/main/webapp/static/js/deviceLogs/deviceLogs.js
+++ b/src/main/webapp/static/js/deviceLogs/deviceLogs.js
@@ -1,228 +1,845 @@
-layui.use(['tree', 'layer', 'form', 'element'], function() {
- var tree = layui.tree;
- var $ = layui.jquery;
- var layer = layui.layer;
- var form = layui.form;
- var element = layui.element;
+var app = new Vue({
+ el: '#app',
+ data: {
+ // Sidebar Data
+ dateTreeData: [],
+ defaultProps: {
+ children: 'children',
+ label: 'title'
+ },
+ defaultExpandedKeys: [],
- var currentDay = null;
+ // Search & List Data
+ searchForm: {
+ day: '',
+ type: '',
+ deviceNo: '',
+ offset: 0,
+ limit: 200
+ },
+ deviceList: [],
+ loading: false,
- function buildMonthTree(data) {
- var monthMap = {};
- (data || []).forEach(function (y) {
- (y.children || []).forEach(function (m) {
- var month = m.title;
- var arr = monthMap[month] || (monthMap[month] = []);
- (m.children || []).forEach(function (d) {
- arr.push({ title: d.title, id: d.id });
+ // Enums
+ deviceEnums: {},
+
+ // Visualization State
+ visualizationVisible: false,
+ visDeviceType: '',
+ visDeviceNo: '',
+ logs: [],
+ isPlaying: false,
+ playbackSpeed: 1,
+ sliderValue: 0,
+ startTime: 0,
+ endTime: 0,
+ timer: null,
+ currentTime: 0,
+ lastTick: 0,
+
+ // Jump Time
+ jumpVisible: false,
+ jumpTime: null,
+ seekTargetTime: 0, // Target time we are trying to reach via loading
+ seekingOffset: false,
+ needToSeekOffset: false,
+
+ // Download State
+ downloadDialogVisible: false,
+ buildProgress: 0,
+ receiveProgress: 0,
+ downloadTimer: null
+ },
+ computed: {
+ filteredDeviceList() {
+ // Currently just returns the full list loaded for the day
+ return this.deviceList;
+ },
+ visualizationTitle() {
+ return `鏃ュ織鍙鍖� - ${this.visDeviceType} ${this.visDeviceNo} (${this.searchForm.day})`;
+ },
+ maxSliderValue() {
+ return Math.max(0, this.endTime - this.startTime);
+ },
+ currentTimeStr() {
+ if (!this.currentTime) return '';
+ var d = new Date(this.currentTime);
+ var Y = d.getFullYear() + '-';
+ var M = (d.getMonth() + 1 < 10 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1) + '-';
+ var D = (d.getDate() < 10 ? '0' + d.getDate() : d.getDate()) + ' ';
+ var h = d.getHours().toString().padStart(2, '0');
+ var m = d.getMinutes().toString().padStart(2, '0');
+ var s = d.getSeconds().toString().padStart(2, '0');
+ var ms = d.getMilliseconds().toString().padStart(3, '0');
+ return Y + M + D + h + ':' + m + ':' + s + '.' + ms;
+ },
+ canDownload() {
+ return this.searchForm.day && this.searchForm.type && this.searchForm.deviceNo;
+ }
+ },
+ created() {
+ this.loadDeviceEnums();
+ this.loadDateTree();
+ },
+ methods: {
+ // --- Initialization ---
+ loadDeviceEnums() {
+ let that = this;
+ $.ajax({
+ url: baseUrl + "/deviceLog/enums/auth",
+ headers: {'token': localStorage.getItem('token')},
+ method: 'GET',
+ success: function (res) {
+ if (res.code === 200) {
+ that.deviceEnums = res.data || {};
+ }
+ }
+ });
+ },
+
+ // --- Date Tree ---
+ loadDateTree() {
+ let that = this;
+ $.ajax({
+ url: baseUrl + "/deviceLog/dates/auth",
+ headers: {'token': localStorage.getItem('token')},
+ method: 'GET',
+ success: function (res) {
+ if (res.code === 200) {
+ that.dateTreeData = that.buildMonthTree(res.data);
+ // Auto-expand current year/month if needed, or just root
+ if (that.dateTreeData.length > 0) {
+ that.defaultExpandedKeys = [that.dateTreeData[0].id];
+ }
+ } else if (res.code === 403) {
+ top.location.href = baseUrl + "/";
+ } else {
+ that.$message.error(res.msg || '鍔犺浇鏃ユ湡澶辫触');
+ }
+ }
+ });
+ },
+ buildMonthTree(data) {
+ var monthMap = {};
+ (data || []).forEach(function (y) {
+ (y.children || []).forEach(function (m) {
+ var month = m.title;
+ var arr = monthMap[month] || (monthMap[month] = []);
+ (m.children || []).forEach(function (d) {
+ arr.push({ title: d.title + '鏃�', id: d.id, day: d.id });
+ });
});
});
- });
- var result = [];
- Object.keys(monthMap).sort().forEach(function (month) {
- result.push({ title: month + '鏈�', id: month, children: monthMap[month] });
- });
- return result;
- }
-
- function loadDateTree() {
- $.ajax({
- url: baseUrl + "/deviceLog/dates/auth",
- headers: {'token': localStorage.getItem('token')},
- method: 'GET',
- beforeSend: function () {
- layer.load(1, {shade: [0.1,'#fff']});
- },
- success: function (res) {
- layer.closeAll('loading');
- if (res.code === 200) {
- var monthTree = buildMonthTree(res.data);
- tree.render({
- elem: '#date-tree',
- id: 'dateTree',
- data: monthTree,
- click: function(obj){
- var node = obj.data;
- if (node.id && node.id.length === 8) {
- currentDay = node.id;
- $('#selected-day').val(currentDay);
- loadDevices(currentDay);
- }
- }
- });
- } else if (res.code === 403) {
- top.location.href = baseUrl + "/";
- } else {
- layer.msg(res.msg || '鍔犺浇鏃ユ湡澶辫触', {icon: 2});
- }
+ var result = [];
+ Object.keys(monthMap).sort().reverse().forEach(function (month) {
+ result.push({ title: month + '鏈�', id: month, children: monthMap[month] });
+ });
+ return result;
+ },
+ handleNodeClick(data) {
+ if (data.day && data.day.length === 8) {
+ this.searchForm.day = data.day;
+ this.loadDevices(data.day);
}
- });
- }
+ },
- function loadDevices(day) {
- $('#device-list').html('');
- $.ajax({
- url: baseUrl + "/deviceLog/day/" + day + "/devices/auth",
- headers: {'token': localStorage.getItem('token')},
- method: 'GET',
- beforeSend: function () {
- layer.load(1, {shade: [0.1,'#fff']});
- },
- success: function (res) {
- layer.closeAll('loading');
- if (res.code === 200) {
- if (!res.data || res.data.length === 0) {
- $('#device-list').html('<div class="layui-text">褰撴棩鏈壘鍒拌澶囨棩蹇�</div>');
+ // --- Device List ---
+ loadDevices(day) {
+ this.loading = true;
+ this.deviceList = [];
+ let that = this;
+ $.ajax({
+ url: baseUrl + "/deviceLog/day/" + day + "/devices/auth",
+ headers: {'token': localStorage.getItem('token')},
+ method: 'GET',
+ success: function (res) {
+ that.loading = false;
+ if (res.code === 200) {
+ that.deviceList = res.data || [];
+ } else if (res.code === 403) {
+ top.location.href = baseUrl + "/";
+ } else {
+ that.$message.error(res.msg || '鍔犺浇璁惧澶辫触');
+ }
+ },
+ error: function() {
+ that.loading = false;
+ that.$message.error('璇锋眰澶辫触');
+ }
+ });
+ },
+
+ // --- Download ---
+ handleBatchDownload() {
+ this.doDownload(this.searchForm.day, this.searchForm.type, this.searchForm.deviceNo);
+ },
+ downloadLog(deviceNo, type) {
+ this.doDownload(this.searchForm.day, type, deviceNo);
+ },
+ doDownload(day, type, deviceNo) {
+ if (!day) return this.$message.warning('璇峰厛閫夋嫨鏃ユ湡');
+ if (!type) return this.$message.warning('璇烽�夋嫨璁惧绫诲瀷');
+ if (!deviceNo) return this.$message.warning('璇疯緭鍏ヨ澶囩紪鍙�');
+
+ let offset = this.searchForm.offset || 0;
+ let limit = this.searchForm.limit || 200;
+ let that = this;
+
+ $.ajax({
+ url: baseUrl + "/deviceLog/download/init/auth",
+ headers: {'token': localStorage.getItem('token')},
+ method: 'POST',
+ data: JSON.stringify({ day: day, type: type, deviceNo: deviceNo, offset: offset, limit: limit }),
+ dataType:'json',
+ contentType:'application/json;charset=UTF-8',
+ success: function (res) {
+ if (res.code !== 200) {
+ that.$message.error(res.msg || '鍒濆鍖栧け璐�');
return;
}
- var html = '';
- res.data.forEach(function(item){
- var types = item.types || [];
- var typeBtns = '';
- types.forEach(function(t){
- typeBtns += '<button class="layui-btn layui-btn-xs" data-type="' + t + '" data-device-no="' + item.deviceNo + '">涓嬭浇(' + t + ')</button>';
- });
- html += '<div class="layui-col-xs12" style="margin-bottom:8px;">' +
- '<div class="layui-card">' +
- '<div class="layui-card-body">' +
- '<span>璁惧缂栧彿锛�<b>' + item.deviceNo + '</b></span>' +
- '<span style="margin-left:20px;">绫诲瀷锛�' + types.join(',') + '</span>' +
- '<span style="margin-left:20px;">鏂囦欢鏁帮細' + item.fileCount + '</span>' +
- '<span style="margin-left:20px;">' + typeBtns + '</span>' +
- '</div>' +
- '</div>' +
- '</div>';
- });
- $('#device-list').html(html);
- } else if (res.code === 403) {
- top.location.href = baseUrl + "/";
- } else {
- layer.msg(res.msg || '鍔犺浇璁惧澶辫触', {icon: 2});
+ var pid = res.data.progressId;
+ that.startDownloadProgress(pid);
+ that.performDownloadRequest(day, type, deviceNo, offset, limit, pid);
}
- }
- });
- }
-
- function downloadDeviceLog(day, type, deviceNo) {
- if (!day) {
- layer.msg('璇峰厛閫夋嫨鏃ユ湡', {icon: 2});
- return;
- }
- if (!type) {
- layer.msg('璇烽�夋嫨璁惧绫诲瀷', {icon: 2});
- return;
- }
- if (!deviceNo) {
- layer.msg('璇疯緭鍏ヨ澶囩紪鍙�', {icon: 2});
- return;
- }
- var offsetVal = parseInt($('#file-offset').val());
- var limitVal = parseInt($('#file-limit').val());
- var offset = isNaN(offsetVal) || offsetVal < 0 ? 0 : offsetVal;
- var limit = isNaN(limitVal) || limitVal <= 0 ? 200 : limitVal;
- $.ajax({
- url: baseUrl + "/deviceLog/download/init/auth",
- headers: {'token': localStorage.getItem('token')},
- method: 'POST',
- data: JSON.stringify({ day: day, type: type, deviceNo: deviceNo, offset: offset, limit: limit }),
- dataType:'json',
- contentType:'application/json;charset=UTF-8',
- success: function (res) {
- if (res.code !== 200) {
- layer.msg(res.msg || '鍒濆鍖栧け璐�', {icon: 2});
- return;
- }
- var pid = res.data.progressId;
- var progressIndex = layer.open({
- type: 1,
- title: '涓嬭浇涓�',
- area: ['520px', '200px'],
- content: '<div style="padding:16px;">' +
- '<div class="layui-text" style="margin-bottom:15px;">鍘嬬缉鐢熸垚杩涘害</div>' +
- '<div class="layui-progress" lay-showPercent="true" lay-filter="buildProgress">' +
- '<div class="layui-progress-bar" style="width:0%"><span class="layui-progress-text">0%</span></div>' +
- '</div>' +
- '<div class="layui-text" style="margin:12px 0 15px;">涓嬭浇鎺ユ敹杩涘害</div>' +
- '<div class="layui-progress" lay-showPercent="true" lay-filter="receiveProgress">' +
- '<div class="layui-progress-bar" style="width:0%"><span class="layui-progress-text">0%</span></div>' +
- '</div>' +
- '</div>'
- });
- var timer = setInterval(function(){
- $.ajax({
- url: baseUrl + '/deviceLog/download/progress/auth',
- headers: {'token': localStorage.getItem('token')},
- method: 'GET',
- data: { id: pid },
- success: function (p) {
- if (p.code === 200) {
- var percent = p.data.percent || 0;
- element.progress('buildProgress', percent + '%');
- // 闅愯棌瀹炴椂澶у皬锛屼笉鏇存柊鏂囧瓧
- }
- }
- });
- }, 500);
-
+ });
+ },
+ startDownloadProgress(pid) {
+ this.downloadDialogVisible = true;
+ this.buildProgress = 0;
+ this.receiveProgress = 0;
+ let that = this;
+ this.downloadTimer = setInterval(function(){
$.ajax({
- url: baseUrl + "/deviceLog/day/" + day + "/download/auth?type=" + encodeURIComponent(type) + "&deviceNo=" + encodeURIComponent(deviceNo) + "&offset=" + offset + "&limit=" + limit + "&progressId=" + encodeURIComponent(pid),
+ url: baseUrl + '/deviceLog/download/progress/auth',
headers: {'token': localStorage.getItem('token')},
method: 'GET',
- xhrFields: { responseType: 'blob' },
- xhr: function(){
- var xhr = new window.XMLHttpRequest();
- xhr.onprogress = function(e){
- var percent = 0;
- if (e.lengthComputable && e.total > 0) {
- percent = Math.floor(e.loaded / e.total * 100);
- element.progress('receiveProgress', percent + '%');
- }
- // 闅愯棌瀹炴椂澶у皬锛屼笉鏇存柊鏂囧瓧
- };
- return xhr;
- },
- success: function (data, status, xhr) {
- var disposition = xhr.getResponseHeader('Content-Disposition') || '';
- var filename = type + '_' + deviceNo + '_' + day + '.zip';
- var match = /filename=(.+)/.exec(disposition);
- if (match && match[1]) {
- filename = decodeURIComponent(match[1]);
+ data: { id: pid },
+ success: function (p) {
+ if (p.code === 200) {
+ var percent = p.data.percent || 0;
+ that.buildProgress = percent;
}
- element.progress('buildProgress', '100%');
- element.progress('receiveProgress', '100%');
- var blob = new Blob([data], {type: 'application/zip'});
- var link = document.createElement('a');
- var url = window.URL.createObjectURL(blob);
- link.href = url;
- link.download = filename;
- document.body.appendChild(link);
- link.click();
- document.body.removeChild(link);
- window.URL.revokeObjectURL(url);
- clearInterval(timer);
- setTimeout(function(){ layer.close(progressIndex); }, 300);
- },
- error: function () {
- clearInterval(timer);
- layer.close(progressIndex);
- layer.msg('涓嬭浇澶辫触鎴栨湭鎵惧埌鏃ュ織', {icon: 2});
}
});
+ }, 500);
+ },
+ performDownloadRequest(day, type, deviceNo, offset, limit, pid) {
+ let that = this;
+ $.ajax({
+ url: baseUrl + "/deviceLog/day/" + day + "/download/auth?type=" + encodeURIComponent(type) + "&deviceNo=" + encodeURIComponent(deviceNo) + "&offset=" + offset + "&limit=" + limit + "&progressId=" + encodeURIComponent(pid),
+ headers: {'token': localStorage.getItem('token')},
+ method: 'GET',
+ xhrFields: { responseType: 'blob' },
+ xhr: function(){
+ var xhr = new window.XMLHttpRequest();
+ xhr.onprogress = function(e){
+ if (e.lengthComputable && e.total > 0) {
+ var percent = Math.floor(e.loaded / e.total * 100);
+ that.receiveProgress = percent;
+ }
+ };
+ return xhr;
+ },
+ success: function (data, status, xhr) {
+ var disposition = xhr.getResponseHeader('Content-Disposition') || '';
+ var filename = type + '_' + deviceNo + '_' + day + '.zip';
+ var match = /filename=(.+)/.exec(disposition);
+ if (match && match[1]) {
+ filename = decodeURIComponent(match[1]);
+ }
+ that.buildProgress = 100;
+ that.receiveProgress = 100;
+
+ var blob = new Blob([data], {type: 'application/zip'});
+ var link = document.createElement('a');
+ var url = window.URL.createObjectURL(blob);
+ link.href = url;
+ link.download = filename;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ window.URL.revokeObjectURL(url);
+
+ clearInterval(that.downloadTimer);
+ setTimeout(() => { that.downloadDialogVisible = false; }, 1000);
+ },
+ error: function () {
+ clearInterval(that.downloadTimer);
+ that.downloadDialogVisible = false;
+ that.$message.error('涓嬭浇澶辫触鎴栨湭鎵惧埌鏃ュ織');
+ }
+ });
+ },
+
+ // --- Visualization ---
+ visualizeLog(deviceNo, type) {
+ this.visDeviceType = type;
+ this.visDeviceNo = deviceNo;
+ this.visOffset = this.searchForm.offset || 0;
+ // Optimization: Load fewer files per request to speed up response
+ // searchForm.limit might be large (for download), so we force a small batch for visualization
+ this.visLimit = 2;
+
+ this.logs = [];
+ this.hasMoreLogs = true;
+ this.loadingLogs = false;
+ this.startTime = 0;
+ this.endTime = 0;
+ this.currentTime = 0;
+ this.sliderValue = 0;
+ this.isPlaying = false;
+ this.playbackSpeed = 1;
+
+ this.visualizationVisible = true;
+ this.loadMoreLogs();
+ },
+ loadMoreLogs() {
+ if (this.loadingLogs || !this.hasMoreLogs) return;
+ this.loadingLogs = true;
+
+ // Use Vue loading service if available, or element UI loading
+ let loadingInstance = null;
+
+ // Show loading if explicitly seeking (jumping far ahead) or normal load
+ if (this.seekTargetTime > 0) {
+ if (this.$loading) {
+ loadingInstance = this.$loading({
+ target: '.vis-container',
+ lock: true,
+ text: '姝e湪璺宠浆鑷崇洰鏍囨椂闂� (鍔犺浇涓�)...',
+ spinner: 'el-icon-loading',
+ background: 'rgba(255, 255, 255, 0.7)'
+ });
+ }
+ } else if (this.$loading && !this.isPlaying) {
+ loadingInstance = this.$loading({
+ target: '.vis-container',
+ lock: true,
+ text: '鍔犺浇鏁版嵁涓�...',
+ spinner: 'el-icon-loading',
+ background: 'rgba(255, 255, 255, 0.7)'
+ });
}
- });
+
+ let that = this;
+
+ // If seeking and we have no idea where the target time is in terms of files,
+ // we should ask the server for the correct offset first!
+ if (this.seekTargetTime > 0 && this.visOffset === (this.searchForm.offset || 0)) {
+ // First time seeking or reset? No, this condition is tricky.
+ // Actually, if we are seeking, we can call the new /seek endpoint first.
+ // BUT, loadMoreLogs is recursive for seek. We need to be careful.
+
+ // Let's modify logic:
+ // If seekTargetTime is set, and we suspect it's far away (e.g. not in next batch),
+ // we should use the seek endpoint.
+ // For simplicity, let's ALWAYS try seek endpoint if seeking far ahead?
+ // Or just if we are seeking.
+
+ // However, loadMoreLogs is currently designed to just load NEXT batch.
+ // We should probably intercept the flow here.
+ }
+
+ // NEW LOGIC: If seeking, try to find offset first
+ if (this.seekTargetTime > 0 && this.needToSeekOffset && !this.seekingOffset) {
+ this.seekingOffset = true;
+ $.ajax({
+ url: baseUrl + "/deviceLog/day/" + this.searchForm.day + "/seek/auth",
+ headers: {'token': localStorage.getItem('token')},
+ data: { type: this.visDeviceType, deviceNo: this.visDeviceNo, timestamp: this.seekTargetTime },
+ success: function(res) {
+ if (res.code === 200) {
+ var targetOffset = res.data.offset;
+ // Update offset directly
+ that.visOffset = targetOffset;
+ // Clear logs because we jumped
+ that.logs = [];
+ that.seekingOffset = false;
+ that.needToSeekOffset = false;
+
+ // Now continue to load logs from this new offset
+ // We set seekTargetTime still > 0 so it will check if we arrived.
+ // But we need to call the actual load now.
+ // We recurse (but we need to reset loadingLogs flag first or it returns)
+ // that.loadingLogs = false; // Do not reset loadingLogs here as we are still "loading"
+ // that.loadMoreLogs(); // Recursive call is risky if not careful
+
+ // Better: call sequential load directly
+ that.loadMoreLogsSequential(loadingInstance);
+ } else {
+ // Fallback to sequential load if seek fails
+ that.seekingOffset = false;
+ that.needToSeekOffset = false;
+ that.loadMoreLogsSequential(loadingInstance);
+ }
+ },
+ error: function() {
+ that.seekingOffset = false;
+ that.needToSeekOffset = false;
+ that.loadMoreLogsSequential(loadingInstance);
+ }
+ });
+ return;
+ }
+
+ this.loadMoreLogsSequential(loadingInstance);
+ },
+ loadMoreLogsSequential(loadingInstance) {
+ let that = this;
+ let currentLimit = this.seekTargetTime > 0 ? 10 : this.visLimit;
+
+ $.ajax({
+ url: baseUrl + "/deviceLog/day/" + this.searchForm.day + "/preview/auth",
+ headers: {'token': localStorage.getItem('token')},
+ data: { type: this.visDeviceType, deviceNo: this.visDeviceNo, offset: this.visOffset, limit: currentLimit },
+ success: function(res) {
+ if (loadingInstance) loadingInstance.close();
+ that.loadingLogs = false;
+ if (res.code === 200) {
+ var newLogs = res.data || [];
+
+ if (newLogs.length === 0) {
+ that.hasMoreLogs = false;
+ if (that.seekTargetTime > 0) {
+ that.$message.warning('宸插埌杈炬棩蹇楁湯灏撅紝鏃犳硶鍒拌揪鐩爣鏃堕棿');
+ that.seekTargetTime = 0;
+ } else {
+ if (that.logs.length === 0) that.$message.warning('娌℃湁鎵惧埌鏃ュ織鏁版嵁');
+ else that.$message.info('鏁版嵁宸插叏閮ㄥ姞杞�');
+ }
+ return;
+ }
+
+ // If we cleared logs (jumped), we need to set start time again maybe?
+ // If logs is empty, it means we jumped or initial load.
+ var isJump = that.logs.length === 0;
+
+ that.logs = that.logs.concat(newLogs);
+ that.visOffset += currentLimit;
+
+ if (that.logs.length > 0) {
+ if (isJump) {
+ // If we jumped, we need to ensure we don't break startTime if possible,
+ // OR we update startTime if it was 0.
+ // If we jumped to middle, startTime of the whole day is still 0?
+ // No, startTime usually is the beginning of the visualized session.
+ // If we jump, we might want to keep the "view" consistent?
+ // Actually, if we jump, we effectively discard previous logs.
+ // So the slider range might change?
+ // The user expects slider to represent the WHOLE day?
+ // Currently slider represents [startTime, endTime] of LOADED logs.
+ // If we jump, we might lose the "start".
+ // To support "Whole Day" slider, we need startTime of the FIRST log of the day.
+ // But we don't have that if we jump.
+ // For now, let's just update endTime.
+ // If it's a jump, we might need to adjust startTime if it's the first chunk we have.
+ if (that.startTime === 0) {
+ that.startTime = new Date(that.logs[0].createTime).getTime();
+ that.currentTime = that.startTime;
+ that.$nextTick(() => {
+ that.updateDeviceState(that.logs[0]);
+ });
+ }
+ } else {
+ // Normal load (initial or sequential)
+ // If initial load (startTime is 0)
+ if (that.startTime === 0) {
+ that.startTime = new Date(that.logs[0].createTime).getTime();
+ that.currentTime = that.startTime;
+ that.$nextTick(() => {
+ that.updateDeviceState(that.logs[0]);
+ });
+ }
+ }
+
+ // Update end time
+ that.endTime = new Date(that.logs[that.logs.length - 1].createTime).getTime();
+
+ // Handle Seek Logic
+ if (that.seekTargetTime > 0) {
+ // If we jumped, we should be close.
+ // Check if target is in current range
+ var lastLogTime = new Date(that.logs[that.logs.length - 1].createTime).getTime();
+ if (lastLogTime >= that.seekTargetTime) {
+ that.currentTime = that.seekTargetTime;
+ that.sliderValue = that.currentTime - that.startTime;
+ that.syncState();
+ that.seekTargetTime = 0;
+ that.$message.success('宸茶烦杞嚦鐩爣鏃堕棿');
+ } else {
+ // Still not there?
+ // If we used /seek, we should be there or very close.
+ // Maybe the file we found ends before target?
+ // We continue loading.
+ setTimeout(() => {
+ that.loadMoreLogs();
+ }, 50);
+ }
+ } else if (isJump) {
+ // If not seeking (just loaded via jump?), but we cleared logs...
+ // Wait, we only clear logs if seekTargetTime > 0 in the new logic.
+ // So this else is for normal load.
+ }
+ }
+ } else {
+ that.$message.error(res.msg);
+ that.seekTargetTime = 0;
+ }
+ },
+ error: function() {
+ if (loadingInstance) loadingInstance.close();
+ that.loadingLogs = false;
+ that.seekTargetTime = 0;
+ that.$message.error('璇锋眰澶辫触');
+ }
+ });
+ },
+ handleVisualizationClose() {
+ this.pause();
+ this.visualizationVisible = false;
+ },
+
+ // --- Playback Logic ---
+ play() {
+ this.isPlaying = true;
+ this.lastTick = Date.now();
+ this.tick();
+ },
+ pause() {
+ this.isPlaying = false;
+ if (this.timer) cancelAnimationFrame(this.timer);
+ },
+ reset() {
+ this.pause();
+ this.currentTime = this.startTime;
+ this.sliderValue = 0;
+ if (this.logs.length > 0) {
+ this.updateDeviceState(this.logs[0]);
+ }
+ },
+ tick() {
+ if (!this.isPlaying) return;
+ var now = Date.now();
+ var delta = now - this.lastTick;
+ this.lastTick = now;
+
+ // Auto-load more logs if we are close to the end (prefetch)
+ if (this.hasMoreLogs && !this.loadingLogs) {
+ var idx = this.binarySearch(this.currentTime);
+ // If within last 20 frames
+ if (this.logs.length > 0 && (this.logs.length - 1 - idx) < 20) {
+ this.loadMoreLogs();
+ }
+ }
+
+ var nextTime = this.currentTime + delta * this.playbackSpeed;
+ if (nextTime >= this.endTime) {
+ if (this.hasMoreLogs) {
+ // Reached end of buffer, but more data available
+ // Clamp to endTime
+ nextTime = this.endTime;
+
+ // Ensure loading is triggered
+ if (!this.loadingLogs) {
+ this.loadMoreLogs();
+ }
+
+ // Update state but do NOT pause
+ this.currentTime = nextTime;
+ this.sliderValue = this.currentTime - this.startTime;
+ this.syncState();
+
+ // Continue loop to check again next frame
+ this.timer = requestAnimationFrame(this.tick);
+ return;
+ } else {
+ // Truly finished
+ nextTime = this.endTime;
+ this.currentTime = nextTime;
+ this.sliderValue = this.currentTime - this.startTime;
+ this.syncState();
+ this.pause();
+ return;
+ }
+ }
+ this.currentTime = nextTime;
+ this.sliderValue = this.currentTime - this.startTime;
+
+ this.syncState();
+
+ this.timer = requestAnimationFrame(this.tick);
+ },
+ sliderChange(val) {
+ this.currentTime = this.startTime + val;
+ this.syncState();
+
+ // If dragged near the end, load more
+ if (this.hasMoreLogs && !this.loadingLogs) {
+ var idx = this.binarySearch(this.currentTime);
+ if (this.logs.length > 0 && (this.logs.length - 1 - idx) < 20) {
+ this.loadMoreLogs();
+ }
+ }
+ },
+ sliderInput(val) {
+ this.currentTime = this.startTime + val;
+ this.syncState();
+ // If dragged near the end, load more
+ if (this.hasMoreLogs && !this.loadingLogs) {
+ var idx = this.binarySearch(this.currentTime);
+ if (this.logs.length > 0 && (this.logs.length - 1 - idx) < 20) {
+ this.loadMoreLogs();
+ }
+ }
+ },
+ syncState() {
+ var idx = this.binarySearch(this.currentTime);
+ if (idx >= 0) {
+ var targetLog = this.logs[idx];
+ this.updateDeviceState(targetLog);
+ }
+ },
+ binarySearch(time) {
+ let l = 0, r = this.logs.length - 1;
+ let ans = -1;
+ while (l <= r) {
+ let mid = Math.floor((l + r) / 2);
+ let logTime = new Date(this.logs[mid].createTime).getTime();
+ if (logTime <= time) {
+ ans = mid;
+ l = mid + 1;
+ } else {
+ r = mid - 1;
+ }
+ }
+ return ans;
+ },
+ updateDeviceState(logItem) {
+ if (!logItem || !logItem.wcsData) return;
+ try {
+ var protocol = JSON.parse(logItem.wcsData);
+ var list = [];
+
+ if (this.visDeviceType === 'Devp' && Array.isArray(protocol)) {
+ list = protocol.map(p => this.transformData(p, this.visDeviceType));
+ list.sort((a, b) => (a.stationId || 0) - (b.stationId || 0));
+ } else {
+ var data = this.transformData(protocol, this.visDeviceType);
+ list = [data];
+ }
+
+ var res = { code: 200, data: list };
+
+ if (this.$refs.card) {
+ if (this.visDeviceType === 'Crn') {
+ this.$refs.card.setCrnList(res);
+ } else if (this.visDeviceType === 'Rgv') {
+ this.$refs.card.setRgvList(res);
+ } else if (this.visDeviceType === 'DualCrn') {
+ this.$refs.card.setDualCrnList(res);
+ } else if (this.visDeviceType === 'Devp') {
+ this.$refs.card.setStationList(res);
+ }
+ }
+ } catch (e) {
+ console.error('Error parsing wcsData', e);
+ }
+ },
+ transformData(protocol, type) {
+ if (!protocol) return {};
+
+ // Enums from API
+ var CrnModeType = this.deviceEnums.CrnModeType || {};
+ var CrnStatusType = this.deviceEnums.CrnStatusType || {};
+ var CrnForkPosType = this.deviceEnums.CrnForkPosType || {};
+ var CrnLiftPosType = this.deviceEnums.CrnLiftPosType || {};
+
+ var DualCrnForkPosType = this.deviceEnums.DualCrnForkPosType || {};
+ var DualCrnLiftPosType = this.deviceEnums.DualCrnLiftPosType || {};
+
+ var RgvModeType = this.deviceEnums.RgvModeType || {};
+ var RgvStatusType = this.deviceEnums.RgvStatusType || {};
+
+ if (type === 'Crn') {
+ return {
+ crnNo: protocol.crnNo,
+ workNo: protocol.taskNo || 0,
+ mode: CrnModeType[protocol.mode] || '-',
+ status: CrnStatusType[protocol.status] || '-',
+ loading: protocol.loaded == 1 ? '鏈夌墿' : '鏃犵墿',
+ bay: protocol.bay,
+ lev: protocol.level,
+ forkOffset: CrnForkPosType[protocol.forkPos] || '-',
+ liftPos: CrnLiftPosType[protocol.liftPos] || '-',
+ walkPos: (protocol.walkPos == 1) ? '涓嶅湪瀹氫綅' : '鍦ㄥ畾浣�',
+ xspeed: protocol.xSpeed || 0,
+ yspeed: protocol.ySpeed || 0,
+ zspeed: protocol.zSpeed || 0,
+ xdistance: protocol.xDistance || 0,
+ ydistance: protocol.yDistance || 0,
+ warnCode: protocol.alarm,
+ deviceStatus: (protocol.alarm && protocol.alarm > 0) ? 'ERROR' :
+ ((protocol.taskNo && protocol.taskNo > 0) ? 'WORKING' :
+ (protocol.mode == 3 ? 'AUTO' : 'OFFLINE'))
+ };
+ } else if (type === 'DualCrn') {
+ var vo = {
+ crnNo: protocol.crnNo,
+ taskNo: protocol.taskNo || 0,
+ taskNoTwo: protocol.taskNoTwo || 0,
+ mode: CrnModeType[protocol.mode] || '-',
+ status: CrnStatusType[protocol.status] || '-',
+ statusTwo: CrnStatusType[protocol.statusTwo] || '-',
+ loading: protocol.loaded == 1 ? '鏈夌墿' : '鏃犵墿',
+ loadingTwo: protocol.loadedTwo == 1 ? '鏈夌墿' : '鏃犵墿',
+ bay: protocol.bay,
+ lev: protocol.level,
+ forkOffset: DualCrnForkPosType[protocol.forkPos] || '-',
+ forkOffsetTwo: DualCrnForkPosType[protocol.forkPosTwo] || '-',
+ liftPos: DualCrnLiftPosType[protocol.liftPos] || '-',
+ walkPos: protocol.walkPos == 0 ? '鍦ㄥ畾浣�' : '涓嶅湪瀹氫綅',
+ taskReceive: protocol.taskReceive == 1 ? '鎺ユ敹' : '鏃犱换鍔�',
+ taskReceiveTwo: protocol.taskReceiveTwo == 1 ? '鎺ユ敹' : '鏃犱换鍔�',
+ xspeed: protocol.xSpeed,
+ yspeed: protocol.ySpeed,
+ zspeed: protocol.zSpeed,
+ xdistance: protocol.xDistance,
+ ydistance: protocol.yDistance,
+ warnCode: protocol.alarm
+ };
+ if (protocol.alarm && protocol.alarm > 0) vo.deviceStatus = 'ERROR';
+ else if ((protocol.taskNo && protocol.taskNo > 0) || (protocol.taskNoTwo && protocol.taskNoTwo > 0)) vo.deviceStatus = 'WORKING';
+ else if (protocol.mode == 3) vo.deviceStatus = 'AUTO';
+ else vo.deviceStatus = 'OFFLINE';
+ return vo;
+ } else if (type === 'Rgv') {
+ var vo = {
+ rgvNo: protocol.rgvNo,
+ taskNo: protocol.taskNo,
+ mode: RgvModeType[protocol.mode] || '',
+ status: RgvStatusType[protocol.status] || '',
+ loading: protocol.loaded == 1 ? '鏈夌墿' : '鏃犵墿',
+ trackSiteNo: protocol.rgvPos,
+ warnCode: protocol.alarm
+ };
+
+ var deviceStatus = "";
+ if (protocol.mode == 3) deviceStatus = "AUTO";
+ if (protocol.taskNo && protocol.taskNo > 0) deviceStatus = "WORKING";
+ if (protocol.alarm && protocol.alarm > 0) deviceStatus = "ERROR";
+ vo.deviceStatus = deviceStatus;
+
+ return vo;
+ } else if (type === 'Devp') {
+ return {
+ stationId: protocol.stationId,
+ taskNo: protocol.taskNo,
+ targetStaNo: protocol.targetStaNo,
+ autoing: protocol.autoing,
+ loading: protocol.loading,
+ inEnable: protocol.inEnable,
+ outEnable: protocol.outEnable,
+ emptyMk: protocol.emptyMk,
+ fullPlt: protocol.fullPlt,
+ runBlock: protocol.runBlock,
+ enableIn: protocol.enableIn,
+ palletHeight: protocol.palletHeight,
+ barcode: protocol.barcode,
+ weight: protocol.weight,
+ error: protocol.error,
+ errorMsg: protocol.errorMsg,
+ extend: protocol.extend
+ };
+ }
+ return protocol;
+ },
+ formatTooltip(val) {
+ var t = this.startTime + val;
+ var d = new Date(t);
+ var Y = d.getFullYear() + '-';
+ var M = (d.getMonth() + 1 < 10 ? '0' + (d.getMonth() + 1) : d.getMonth() + 1) + '-';
+ var D = (d.getDate() < 10 ? '0' + d.getDate() : d.getDate()) + ' ';
+ return Y + M + D + d.toLocaleTimeString() + '.' + d.getMilliseconds();
+ },
+ initJumpTime() {
+ if (this.currentTime > 0) {
+ this.jumpTime = new Date(this.currentTime);
+ } else if (this.startTime > 0) {
+ this.jumpTime = new Date(this.startTime);
+ } else {
+ // Try to parse from searchForm.day
+ if (this.searchForm.day && this.searchForm.day.length === 8) {
+ var y = this.searchForm.day.substring(0, 4);
+ var m = this.searchForm.day.substring(4, 6);
+ var d = this.searchForm.day.substring(6, 8);
+ // Default to 00:00:00 of that day
+ this.jumpTime = new Date(y + '/' + m + '/' + d + ' 00:00:00');
+ } else {
+ this.jumpTime = new Date();
+ }
+ }
+ },
+ confirmJump() {
+ if (!this.jumpTime) return;
+
+ // Construct target timestamp
+ // jumpTime from el-time-picker is a Date object (if not using value-format)
+ // or string/timestamp if using value-format.
+ // We didn't set value-format, so it should be Date object (default in ElementUI 2.x?)
+ // Actually, in default_api:Read above, I saw:
+ // <el-time-picker v-model="jumpTime" ... :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }">
+ // Default v-model for el-time-picker is Date object.
+
+ let targetDate = this.jumpTime;
+ if (typeof targetDate === 'string' || typeof targetDate === 'number') {
+ targetDate = new Date(targetDate);
+ }
+
+ let baseDate = new Date(this.startTime > 0 ? this.startTime : Date.now());
+
+ baseDate.setHours(targetDate.getHours());
+ baseDate.setMinutes(targetDate.getMinutes());
+ baseDate.setSeconds(targetDate.getSeconds());
+ // Picker usually 0 ms
+ baseDate.setMilliseconds(0);
+
+ let targetTs = baseDate.getTime();
+
+ if (this.startTime > 0 && targetTs < this.startTime) {
+ targetTs = this.startTime;
+ }
+
+ // Check if beyond endTime
+ if (this.endTime > 0 && targetTs > this.endTime) {
+ // If we have more logs, we try to go as far as we can (endTime)
+ // and trigger loading
+ if (this.hasMoreLogs) {
+ this.seekTargetTime = targetTs;
+ this.needToSeekOffset = true;
+ // Trigger load immediately
+ if (!this.loadingLogs) {
+ this.loadMoreLogs();
+ } else {
+ // Already loading, just set the target and let callback handle it
+ }
+ this.jumpVisible = false;
+ return; // Don't update current time yet, wait for load
+ } else {
+ targetTs = this.endTime;
+ this.$message.warning('鐩爣鏃堕棿瓒呭嚭鏃ュ織鑼冨洿锛屽凡璺宠浆鑷崇粨鏉熸椂闂�');
+ }
+ }
+
+ this.currentTime = targetTs;
+ this.sliderValue = this.currentTime - this.startTime;
+ this.syncState();
+ this.jumpVisible = false;
+
+ // Trigger load if needed
+ if (this.hasMoreLogs && !this.loadingLogs) {
+ // Force load check
+ this.loadMoreLogs();
+ }
+ }
}
-
- $(document).on('click', '#download-btn', function () {
- downloadDeviceLog(currentDay, $('#device-type-input').val(), $('#device-no-input').val());
- });
-
- $(document).on('click', '#device-list .layui-btn', function () {
- var deviceNo = $(this).attr('data-device-no');
- var type = $(this).attr('data-type');
- downloadDeviceLog(currentDay, type, deviceNo);
- });
-
- loadDateTree();
- limit();
-});
-
+});
\ No newline at end of file
diff --git a/src/main/webapp/views/deviceLogs/deviceLogs.html b/src/main/webapp/views/deviceLogs/deviceLogs.html
index c09cc42..56f8d78 100644
--- a/src/main/webapp/views/deviceLogs/deviceLogs.html
+++ b/src/main/webapp/views/deviceLogs/deviceLogs.html
@@ -6,91 +6,210 @@
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
- <link rel="stylesheet" href="../../static/layui/css/layui.css" media="all">
- <link rel="stylesheet" href="../../static/css/admin.css?v=318" media="all">
- <link rel="stylesheet" href="../../static/css/cool.css" media="all">
+ <!-- CSS -->
+ <link rel="stylesheet" href="../../static/vue/element/element.css">
+ <link rel="stylesheet" href="../../static/css/common.css">
+ <style>
+ body { margin: 0; padding: 0; background-color: #f0f2f5; height: 100vh; overflow: hidden; }
+ #app { height: 100%; padding: 10px; box-sizing: border-box; display: flex; flex-direction: column; }
+ .main-container { flex: 1; display: flex; overflow: hidden; }
+ .sidebar { width: 260px; margin-right: 10px; display: flex; flex-direction: column; }
+ .content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
+
+ .box-card { height: 100%; display: flex; flex-direction: column; border: none; box-shadow: 0 1px 4px rgba(0,21,41,.08); }
+ .box-card .el-card__header { padding: 10px 15px; border-bottom: 1px solid #ebeef5; background: #fff; font-weight: bold; font-size: 15px; }
+ .box-card .el-card__body { flex: 1; overflow: auto; padding: 15px; }
+
+ .device-item { margin-bottom: 10px; }
+ .device-card { background-color: #fff; border: 1px solid #e6ebf5; border-radius: 4px; transition: all .3s; }
+ .device-card:hover { box-shadow: 0 2px 12px 0 rgba(0,0,0,.1); }
+ .device-info { display: flex; justify-content: space-between; align-items: center; padding: 15px; }
+ .device-info .info-text { font-size: 14px; color: #606266; }
+ .device-info .info-text b { color: #303133; margin-right: 5px; }
+ .device-info .tag-group { margin-left: 15px; }
+
+ .control-bar { margin-bottom: 15px; padding: 15px; background: #fff; border-radius: 4px; box-shadow: 0 1px 4px rgba(0,21,41,.08); }
+
+ /* Visualization styles */
+ .vis-control-panel { margin-bottom: 10px; display: flex; align-items: center; background: #f5f7fa; padding: 10px; border-radius: 4px; }
+ .vis-container { border: 1px solid #ebeef5; padding: 10px; border-radius: 4px; min-height: 400px; height: calc(80vh - 100px); overflow-y: auto; }
+ </style>
</head>
<body>
-<div class="layui-fluid">
- <div class="layui-row">
- <div class="layui-col-md3">
- <div class="layui-card">
- <div class="layui-card-header">鏃ユ湡</div>
- <div class="layui-card-body">
- <div id="date-tree"></div>
- </div>
- </div>
+<div id="app" v-cloak>
+ <div class="main-container">
+ <!-- Sidebar: Date Tree -->
+ <div class="sidebar">
+ <el-card class="box-card" :body-style="{padding: '10px'}">
+ <div slot="header">鏃ユ湡閫夋嫨</div>
+ <el-tree
+ ref="dateTree"
+ :data="dateTreeData"
+ :props="defaultProps"
+ node-key="id"
+ :default-expanded-keys="defaultExpandedKeys"
+ @node-click="handleNodeClick"
+ highlight-current
+ accordion>
+ <span class="custom-tree-node" slot-scope="{ node, data }">
+ <i v-if="data.children" class="el-icon-folder"></i>
+ <i v-else class="el-icon-document"></i>
+ <span style="margin-left: 5px;">{{ node.label }}</span>
+ </span>
+ </el-tree>
+ </el-card>
</div>
- <div class="layui-col-md9">
- <div class="layui-card">
- <div class="layui-card-header">鏃ュ織涓嬭浇</div>
- <div class="layui-card-body">
- <form class="layui-form toolbar" id="search-box">
- <div class="layui-form-item">
- <div class="layui-inline">
- <label class="layui-form-label">閫変腑鏃ユ湡锛�</label>
- <div class="layui-input-inline">
- <input id="selected-day" class="layui-input" type="text" placeholder="yyyyMMdd" readonly>
- </div>
- </div>
- <div class="layui-inline">
- <label class="layui-form-label">璁惧绫诲瀷锛�</label>
- <div class="layui-input-inline">
- <select id="device-type-input" class="layui-input">
- <option value="">璇烽�夋嫨</option>
- <option value="Crn">Crn</option>
- <option value="Devp">Devp</option>
- <option value="Rgv">Rgv</option>
- </select>
- </div>
- </div>
- <div class="layui-inline">
- <label class="layui-form-label">璁惧缂栧彿锛�</label>
- <div class="layui-input-inline">
- <input id="device-no-input" class="layui-input" type="text" placeholder="璇疯緭鍏ヨ澶囩紪鍙�">
- </div>
- </div>
- <div class="layui-inline">
- <label class="layui-form-label">璧峰搴忓彿锛�</label>
- <div class="layui-input-inline">
- <input id="file-offset" class="layui-input" type="text" placeholder="榛樿0">
- </div>
- </div>
- <div class="layui-inline">
- <label class="layui-form-label">鏈�澶ф枃浠舵暟锛�</label>
- <div class="layui-input-inline">
- <input id="file-limit" class="layui-input" type="text" placeholder="榛樿200">
- </div>
- </div>
- <div class="layui-inline">
- <button id="download-btn" type="button" class="layui-btn layui-btn-normal">涓嬭浇</button>
- </div>
- </div>
- </form>
- <hr class="layui-bg-gray">
+ <!-- Main Content -->
+ <div class="content">
+ <!-- Search Bar -->
+ <div class="control-bar">
+ <el-form :inline="true" :model="searchForm" size="small" style="margin-bottom: -18px;">
+ <el-form-item label="閫変腑鏃ユ湡">
+ <el-input v-model="searchForm.day" placeholder="yyyyMMdd" readonly style="width: 120px;" disabled></el-input>
+ </el-form-item>
+ <el-form-item label="璁惧绫诲瀷">
+ <el-select v-model="searchForm.type" placeholder="鍏ㄩ儴" clearable style="width: 100px;">
+ <el-option label="Crn" value="Crn"></el-option>
+ <el-option label="Devp" value="Devp"></el-option>
+ <el-option label="Rgv" value="Rgv"></el-option>
+ </el-select>
+ </el-form-item>
+ <el-form-item label="璁惧缂栧彿">
+ <el-input v-model="searchForm.deviceNo" placeholder="璇疯緭鍏ョ紪鍙�" style="width: 120px;" clearable></el-input>
+ </el-form-item>
+ <el-form-item label="璧峰搴忓彿">
+ <el-input-number v-model="searchForm.offset" :min="0" controls-position="right" style="width: 100px;"></el-input-number>
+ </el-form-item>
+ <el-form-item label="鏈�澶ф枃浠�">
+ <el-input-number v-model="searchForm.limit" :min="1" :max="1000" controls-position="right" style="width: 100px;"></el-input-number>
+ </el-form-item>
+ <el-form-item>
+ <el-button type="primary" icon="el-icon-download" @click="handleBatchDownload" :disabled="!canDownload">涓嬭浇</el-button>
+ </el-form-item>
+ </el-form>
+ </div>
- <div class="layui-row">
- <div class="layui-col-xs12">
- <div class="layui-card">
- <div class="layui-card-header">璇ユ棩璁惧鍒楄〃</div>
- <div class="layui-card-body">
- <div id="device-list" class="layui-row"></div>
+ <!-- Device List -->
+ <el-card class="box-card">
+ <div slot="header" class="clearfix">
+ <span>璁惧鍒楄〃</span>
+ <span style="float: right; color: #909399; font-size: 12px;">鍏� {{ filteredDeviceList.length }} 涓澶�</span>
+ </div>
+
+ <div v-if="loading" style="text-align: center; padding: 20px;">
+ <i class="el-icon-loading" style="font-size: 24px;"></i>
+ </div>
+ <div v-else-if="filteredDeviceList.length === 0" style="text-align: center; color: #909399; padding: 50px;">
+ <i class="el-icon-info" style="margin-right: 5px;"></i>鏆傛棤鏁版嵁锛岃鍏堥�夋嫨鏃ユ湡
+ </div>
+ <div v-else>
+ <div v-for="(item, index) in filteredDeviceList" :key="index" class="device-item">
+ <div class="device-card">
+ <div class="device-info">
+ <div>
+ <span class="info-text"><b>璁惧缂栧彿:</b> {{ item.deviceNo }}</span>
+ <span class="info-text tag-group"><b>绫诲瀷:</b> {{ item.types.join(', ') }}</span>
+ <span class="info-text tag-group"><b>鏂囦欢鏁�:</b> {{ item.fileCount }}</span>
+ </div>
+ <div>
+ <template v-for="t in item.types">
+ <el-button size="mini" icon="el-icon-download" @click="downloadLog(item.deviceNo, t)">涓嬭浇({{t}})</el-button>
+ <el-button size="mini" type="success" icon="el-icon-view" @click="visualizeLog(item.deviceNo, t)">鍙鍖�({{t}})</el-button>
+ </template>
</div>
</div>
</div>
</div>
</div>
- </div>
+ </el-card>
</div>
</div>
+
+ <!-- Visualization Dialog -->
+ <el-dialog
+ :title="visualizationTitle"
+ :visible.sync="visualizationVisible"
+ width="90%"
+ top="5vh"
+ :close-on-click-modal="false"
+ @close="handleVisualizationClose">
+
+ <div class="vis-control-panel">
+ <el-button-group>
+ <el-button type="primary" icon="el-icon-video-play" @click="play" v-if="!isPlaying" size="small">鎾斁</el-button>
+ <el-button type="primary" icon="el-icon-video-pause" @click="pause" v-else size="small">鏆傚仠</el-button>
+ <el-button type="warning" icon="el-icon-refresh-left" @click="reset" size="small">閲嶇疆</el-button>
+ </el-button-group>
+ <div style="margin-left: 20px; flex: 1; padding-right: 20px;">
+ <el-slider v-model="sliderValue" :max="maxSliderValue" @change="sliderChange" @input="sliderInput" :format-tooltip="formatTooltip"></el-slider>
+ </div>
+ <div style="width: 210px; font-size: 14px; font-weight: bold; font-family: monospace; display: flex; align-items: center;">
+ {{ currentTimeStr }}
+ <el-popover
+ placement="bottom"
+ width="200"
+ trigger="click"
+ v-model="jumpVisible"
+ @show="initJumpTime">
+ <div style="text-align: center;">
+ <el-time-picker
+ v-model="jumpTime"
+ size="small"
+ placeholder="閫夋嫨鏃堕棿"
+ style="width: 100%; margin-bottom: 10px;"
+ :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }">
+ </el-time-picker>
+ <el-button type="primary" size="mini" @click="confirmJump" style="width: 100%;">璺宠浆</el-button>
+ </div>
+ <el-button type="text" slot="reference" icon="el-icon-edit" style="margin-left: 5px; padding: 0;" title="璺宠浆鏃堕棿"></el-button>
+ </el-popover>
+ </div>
+ <div style="margin-left: 10px;">
+ <el-select v-model="playbackSpeed" style="width: 100px;" size="small" placeholder="鍊嶉��">
+ <el-option :value="1" label="1x"></el-option>
+ <el-option :value="5" label="5x"></el-option>
+ <el-option :value="10" label="10x"></el-option>
+ <el-option :value="50" label="50x"></el-option>
+ <el-option :value="100" label="100x"></el-option>
+ <el-option :value="200" label="200x"></el-option>
+ <el-option :value="500" label="500x"></el-option>
+ <el-option :value="1000" label="1000x"></el-option>
+ </el-select>
+ </div>
+ </div>
+
+ <div class="vis-container">
+ <watch-crn-card v-if="visDeviceType === 'Crn'" ref="card" :auto-refresh="false" :read-only="true"></watch-crn-card>
+ <watch-rgv-card v-else-if="visDeviceType === 'Rgv'" ref="card" :auto-refresh="false" :read-only="true"></watch-rgv-card>
+ <watch-dual-crn-card v-else-if="visDeviceType === 'DualCrn'" ref="card" :auto-refresh="false" :read-only="true"></watch-dual-crn-card>
+ <devp-card v-else-if="visDeviceType === 'Devp'" ref="card" :auto-refresh="false" :read-only="true"></devp-card>
+ <div v-else style="text-align: center; padding: 50px; color: #909399;">
+ 鏈煡璁惧绫诲瀷: {{ visDeviceType }}
+ </div>
+ </div>
+ </el-dialog>
+
+ <!-- Download Progress Dialog -->
+ <el-dialog title="鏂囦欢涓嬭浇涓�" :visible.sync="downloadDialogVisible" width="400px" :close-on-click-modal="false" :show-close="false">
+ <div style="padding: 10px;">
+ <div style="margin-bottom: 5px; font-size: 14px;">鍘嬬缉鐢熸垚杩涘害</div>
+ <el-progress :percentage="buildProgress" :text-inside="true" :stroke-width="18"></el-progress>
+ <div style="margin: 20px 0 5px; font-size: 14px;">涓嬭浇鎺ユ敹杩涘害</div>
+ <el-progress :percentage="receiveProgress" :text-inside="true" :stroke-width="18" status="success"></el-progress>
+ </div>
+ </el-dialog>
</div>
<script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script>
-<script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script>
<script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
-<script type="text/javascript" src="../../static/js/cool.js" charset="utf-8"></script>
+<script src="../../static/vue/js/vue.min.js"></script>
+<script src="../../static/vue/element/element.js"></script>
+<script src="../../components/WatchCrnCard.js"></script>
+<script src="../../components/WatchRgvCard.js"></script>
+<script src="../../components/WatchDualCrnCard.js"></script>
+<script src="../../components/DevpCard.js"></script>
<script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js" charset="utf-8"></script>
</body>
-</html>
+</html>
\ No newline at end of file
diff --git a/src/main/webapp/views/index.html b/src/main/webapp/views/index.html
index 90597c8..6c52b6f 100644
--- a/src/main/webapp/views/index.html
+++ b/src/main/webapp/views/index.html
@@ -208,7 +208,8 @@
}
let fakeRunning = false
- setInterval(function () {
+ let fakeStatusInterval = null
+ function checkFakeStatus() {
$.ajax({
url: baseUrl + "/openapi/getFakeSystemRunStatus",
headers: {'token': localStorage.getItem('token')},
@@ -224,15 +225,23 @@
$("#fakeShowText").text("浠跨湡妯℃嫙鏈繍琛�")
}
fakeRunning = running
+ if (!fakeStatusInterval) {
+ fakeStatusInterval = setInterval(checkFakeStatus, 1000);
+ }
}else {
$("#fakeShow").hide()
+ if (fakeStatusInterval) {
+ clearInterval(fakeStatusInterval);
+ fakeStatusInterval = null;
+ }
}
}else {
top.location.href = baseUrl + "/login";
}
}
});
- }, 1000);
+ }
+ checkFakeStatus();
$("#fakeShow").on("click", function () {
if (fakeRunning) {
--
Gitblit v1.9.1