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/components/WatchRgvCard.js | 6
src/main/java/com/zy/asrs/controller/DeviceLogController.java | 436 +++++
src/main/webapp/components/DevpCard.js | 6
src/main/webapp/components/WatchCrnCard.js | 6
src/main/webapp/static/js/deviceLogs/deviceLogs.js | 1990 +++++++++++++++++++----------
src/main/webapp/components/WatchDualCrnCard.js | 6
src/main/webapp/views/deviceLogs/deviceLogs.html | 1504 ++++++++++++++++++++--
7 files changed, 3,028 insertions(+), 926 deletions(-)
diff --git a/src/main/java/com/zy/asrs/controller/DeviceLogController.java b/src/main/java/com/zy/asrs/controller/DeviceLogController.java
index a498bd5..d5804a2 100644
--- a/src/main/java/com/zy/asrs/controller/DeviceLogController.java
+++ b/src/main/java/com/zy/asrs/controller/DeviceLogController.java
@@ -14,6 +14,8 @@
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletResponse;
+import java.io.ByteArrayOutputStream;
+import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -27,6 +29,16 @@
@RestController
public class DeviceLogController extends BaseController {
+ private static final List<String> DEVICE_TYPE_ORDER = Arrays.asList("Crn", "DualCrn", "Rgv", "Devp");
+ private static final Map<String, String> DEVICE_TYPE_LABELS = new LinkedHashMap<>();
+
+ static {
+ DEVICE_TYPE_LABELS.put("Crn", "鍫嗗灈鏈�");
+ DEVICE_TYPE_LABELS.put("DualCrn", "鍙屽伐浣嶅爢鍨涙満");
+ DEVICE_TYPE_LABELS.put("Rgv", "RGV");
+ DEVICE_TYPE_LABELS.put("Devp", "杈撻�佽澶�");
+ }
+
@Value("${deviceLogStorage.loggingPath}")
private String loggingPath;
@@ -36,6 +48,31 @@
int totalCount;
int processedCount;
boolean finished;
+ }
+
+ private static class FileNameInfo {
+ String type;
+ String deviceNo;
+ String day;
+ int index;
+ }
+
+ private static class FileTimeRange {
+ Long startTime;
+ Long endTime;
+ }
+
+ private static class DeviceAggregate {
+ String type;
+ String typeLabel;
+ String deviceNo;
+ int fileCount;
+ Long firstTime;
+ Long lastTime;
+ Integer firstIndex;
+ Integer lastIndex;
+ Path firstFile;
+ Path lastFile;
}
private static final Map<String, ProgressInfo> DOWNLOAD_PROGRESS = new ConcurrentHashMap<>();
@@ -135,6 +172,142 @@
return R.ok(res);
} catch (Exception e) {
return R.error("璇诲彇璁惧鍒楄〃澶辫触");
+ }
+ }
+
+ @RequestMapping(value = "/deviceLog/day/{day}/summary/auth")
+ @ManagerAuth
+ public R summary(@PathVariable("day") String day) {
+ try {
+ String dayClean = normalizeDay(day);
+ if (dayClean == null) {
+ return R.error("鏃ユ湡鏍煎紡閿欒");
+ }
+ Path dayDir = Paths.get(loggingPath, dayClean);
+ if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) {
+ return R.ok(buildEmptySummary());
+ }
+
+ Map<String, DeviceAggregate> aggregateMap = new LinkedHashMap<>();
+ try (Stream<Path> stream = Files.list(dayDir)) {
+ List<Path> files = stream
+ .filter(p -> !Files.isDirectory(p) && p.getFileName().toString().endsWith(".log"))
+ .collect(Collectors.toList());
+ for (Path file : files) {
+ FileNameInfo info = parseFileName(file.getFileName().toString());
+ if (info == null || !dayClean.equals(info.day) || !DEVICE_TYPE_LABELS.containsKey(info.type)) {
+ continue;
+ }
+ String key = info.type + ":" + info.deviceNo;
+ DeviceAggregate aggregate = aggregateMap.computeIfAbsent(key, k -> {
+ DeviceAggregate x = new DeviceAggregate();
+ x.type = info.type;
+ x.typeLabel = DEVICE_TYPE_LABELS.get(info.type);
+ x.deviceNo = info.deviceNo;
+ return x;
+ });
+ aggregate.fileCount += 1;
+ if (aggregate.firstIndex == null || info.index < aggregate.firstIndex) {
+ aggregate.firstIndex = info.index;
+ aggregate.firstFile = file;
+ }
+ if (aggregate.lastIndex == null || info.index > aggregate.lastIndex) {
+ aggregate.lastIndex = info.index;
+ aggregate.lastFile = file;
+ }
+ }
+ }
+ for (DeviceAggregate aggregate : aggregateMap.values()) {
+ if (aggregate.firstFile != null) {
+ FileTimeRange firstRange = readFileTimeRange(aggregate.firstFile);
+ aggregate.firstTime = firstRange.startTime != null ? firstRange.startTime : firstRange.endTime;
+ }
+ if (aggregate.lastFile != null) {
+ FileTimeRange lastRange = readFileTimeRange(aggregate.lastFile);
+ aggregate.lastTime = lastRange.endTime != null ? lastRange.endTime : lastRange.startTime;
+ }
+ }
+ return R.ok(buildSummaryResponse(aggregateMap.values()));
+ } catch (Exception e) {
+ log.error("璇诲彇璁惧鏃ュ織鎽樿澶辫触", e);
+ return R.error("璇诲彇璁惧鏃ュ織鎽樿澶辫触");
+ }
+ }
+
+ @RequestMapping(value = "/deviceLog/day/{day}/timeline/auth")
+ @ManagerAuth
+ public R timeline(@PathVariable("day") String day,
+ @RequestParam("type") String type,
+ @RequestParam("deviceNo") String deviceNo) {
+ try {
+ String dayClean = normalizeDay(day);
+ if (dayClean == null) {
+ 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("鏈壘鍒版棩蹇楁枃浠�");
+ }
+ List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo);
+ if (files.isEmpty()) {
+ return R.error("鏈壘鍒版棩蹇楁枃浠�");
+ }
+
+ List<Map<String, Object>> segments = new ArrayList<>();
+ Long startTime = null;
+ for (int i = 0; i < files.size(); i++) {
+ Long segmentStart = getFileStartTime(files.get(i));
+ if (segmentStart != null && (startTime == null || segmentStart < startTime)) {
+ startTime = segmentStart;
+ }
+ Map<String, Object> segment = new LinkedHashMap<>();
+ segment.put("offset", i);
+ segment.put("startTime", segmentStart);
+ segment.put("endTime", null);
+ segments.add(segment);
+ }
+ Long endTime = getFileEndTime(files.get(files.size() - 1));
+ if (endTime == null) {
+ for (int i = segments.size() - 1; i >= 0; i--) {
+ Long segmentStart = (Long) segments.get(i).get("startTime");
+ if (segmentStart != null) {
+ endTime = segmentStart;
+ break;
+ }
+ }
+ }
+ for (int i = 0; i < segments.size(); i++) {
+ Long segmentEnd = null;
+ if (i < segments.size() - 1) {
+ segmentEnd = (Long) segments.get(i + 1).get("startTime");
+ }
+ if (segmentEnd == null && i == segments.size() - 1) {
+ segmentEnd = endTime;
+ }
+ if (segmentEnd == null) {
+ segmentEnd = (Long) segments.get(i).get("startTime");
+ }
+ segments.get(i).put("endTime", segmentEnd);
+ }
+
+ Map<String, Object> result = new HashMap<>();
+ result.put("type", type);
+ result.put("typeLabel", DEVICE_TYPE_LABELS.getOrDefault(type, type));
+ result.put("deviceNo", deviceNo);
+ result.put("startTime", startTime);
+ result.put("endTime", endTime);
+ result.put("totalFiles", files.size());
+ result.put("segments", segments);
+ return R.ok(result);
+ } catch (Exception e) {
+ log.error("璇诲彇璁惧鏃ュ織鏃堕棿杞村け璐�", e);
+ return R.error("璇诲彇璁惧鏃ュ織鏃堕棿杞村け璐�");
}
}
@@ -306,13 +479,21 @@
private Long getFileStartTime(Path file) {
try {
- String firstLine = null;
- try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
- firstLine = lines.findFirst().orElse(null);
- }
+ String firstLine = readFirstNonBlankLine(file);
if (firstLine == null) return null;
DeviceDataLog firstLog = JSON.parseObject(firstLine, DeviceDataLog.class);
return firstLog.getCreateTime().getTime();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private Long getFileEndTime(Path file) {
+ try {
+ String lastLine = readLastNonBlankLine(file);
+ if (lastLine == null) return null;
+ DeviceDataLog lastLog = JSON.parseObject(lastLine, DeviceDataLog.class);
+ return lastLog.getCreateTime().getTime();
} catch (Exception e) {
return null;
}
@@ -346,31 +527,8 @@
response.setStatus(404);
return;
}
- List<Path> files = Files.list(dayDir)
- .filter(p -> {
- String name = p.getFileName().toString();
- String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
- return name.endsWith(".log") && name.startsWith(prefix);
- }).collect(Collectors.toList());
- // 鎺掑簭锛堟寜鏂囦欢涓殑绱㈠紩鍙烽�掑锛�
- String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
- 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 ? 200 : limit;
- int to = Math.min(files.size(), from + max);
- if (from >= files.size()) {
- response.setStatus(404);
- return;
- }
- files = files.subList(from, to);
+ List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo);
+ files = sliceDownloadFiles(files, offset, limit);
if (files.isEmpty()) {
response.setStatus(404);
return;
@@ -447,29 +605,14 @@
if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) {
return R.error("褰撴棩鐩綍涓嶅瓨鍦�");
}
- List<Path> files = Files.list(dayDir)
- .filter(p -> {
- String name = p.getFileName().toString();
- String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
- return name.endsWith(".log") && name.startsWith(prefix);
- }).collect(Collectors.toList());
- String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
- 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 ? 200 : limit;
- int to = Math.min(files.size(), from + max);
- if (from >= files.size()) {
+ List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo);
+ if ((offset != null && offset >= files.size())) {
return R.error("璧峰搴忓彿瓒呭嚭鑼冨洿");
}
- files = files.subList(from, to);
+ files = sliceDownloadFiles(files, offset, limit);
+ if (files.isEmpty()) {
+ return R.error("鏈壘鍒版棩蹇楁枃浠�");
+ }
String id = UUID.randomUUID().toString();
ProgressInfo info = new ProgressInfo();
info.totalCount = files.size();
@@ -552,4 +695,197 @@
return R.ok(enums);
}
+
+ private Map<String, Object> buildEmptySummary() {
+ return buildSummaryResponse(Collections.emptyList());
+ }
+
+ private Map<String, Object> buildSummaryResponse(Collection<DeviceAggregate> aggregates) {
+ List<DeviceAggregate> aggregateList = new ArrayList<>(aggregates);
+ Map<String, Object> stats = new LinkedHashMap<>();
+ Map<String, Object> typeCounts = new LinkedHashMap<>();
+ int totalFiles = 0;
+ for (String type : DEVICE_TYPE_ORDER) {
+ int count = (int) aggregateList.stream().filter(item -> type.equals(item.type)).count();
+ typeCounts.put(type, count);
+ }
+ for (DeviceAggregate aggregate : aggregateList) {
+ totalFiles += aggregate.fileCount;
+ }
+ stats.put("totalDevices", aggregateList.size());
+ stats.put("totalFiles", totalFiles);
+ stats.put("typeCounts", typeCounts);
+
+ List<Map<String, Object>> groups = new ArrayList<>();
+ for (String type : DEVICE_TYPE_ORDER) {
+ List<DeviceAggregate> devices = aggregateList.stream()
+ .filter(item -> type.equals(item.type))
+ .sorted(Comparator.comparingInt(item -> parseDeviceNo(item.deviceNo)))
+ .collect(Collectors.toList());
+ Map<String, Object> group = new LinkedHashMap<>();
+ group.put("type", type);
+ group.put("typeLabel", DEVICE_TYPE_LABELS.get(type));
+ group.put("deviceCount", devices.size());
+ group.put("totalFiles", devices.stream().mapToInt(item -> item.fileCount).sum());
+ group.put("devices", devices.stream().map(item -> {
+ Map<String, Object> x = new LinkedHashMap<>();
+ x.put("type", item.type);
+ x.put("typeLabel", item.typeLabel);
+ x.put("deviceNo", item.deviceNo);
+ x.put("fileCount", item.fileCount);
+ x.put("firstTime", item.firstTime);
+ x.put("lastTime", item.lastTime);
+ return x;
+ }).collect(Collectors.toList()));
+ groups.add(group);
+ }
+
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("stats", stats);
+ result.put("groups", groups);
+ return result;
+ }
+
+ private String normalizeDay(String day) {
+ String dayClean = day == null ? null : day.replaceAll("\\D", "");
+ if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) {
+ return null;
+ }
+ return dayClean;
+ }
+
+ private List<Path> findDeviceFiles(Path dayDir, String dayClean, String type, String deviceNo) throws Exception {
+ String prefix = type + "_" + deviceNo + "_" + dayClean + "_";
+ List<Path> files;
+ try (Stream<Path> stream = Files.list(dayDir)) {
+ files = stream
+ .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;
+ }
+ }));
+ return files;
+ }
+
+ private List<Path> sliceDownloadFiles(List<Path> files, Integer offset, Integer limit) {
+ if (files == null || files.isEmpty()) {
+ return Collections.emptyList();
+ }
+ int from = offset == null || offset < 0 ? 0 : offset;
+ if (from >= files.size()) {
+ return Collections.emptyList();
+ }
+ if (offset == null && limit == null) {
+ return new ArrayList<>(files);
+ }
+ int to;
+ if (limit == null || limit <= 0) {
+ to = files.size();
+ } else {
+ to = Math.min(files.size(), from + limit);
+ }
+ return new ArrayList<>(files.subList(from, to));
+ }
+
+ private FileNameInfo parseFileName(String fileName) {
+ if (fileName == null || !fileName.endsWith(".log")) {
+ return null;
+ }
+ String[] parts = fileName.split("_", 4);
+ if (parts.length < 4) {
+ return null;
+ }
+ FileNameInfo info = new FileNameInfo();
+ info.type = parts[0];
+ info.deviceNo = parts[1];
+ info.day = parts[2];
+ try {
+ info.index = Integer.parseInt(parts[3].replace(".log", ""));
+ } catch (Exception e) {
+ return null;
+ }
+ return info;
+ }
+
+ private int parseDeviceNo(String deviceNo) {
+ try {
+ return Integer.parseInt(String.valueOf(deviceNo));
+ } catch (Exception e) {
+ return Integer.MAX_VALUE;
+ }
+ }
+
+ private FileTimeRange readFileTimeRange(Path file) {
+ FileTimeRange range = new FileTimeRange();
+ try {
+ String firstLine = readFirstNonBlankLine(file);
+ String lastLine = readLastNonBlankLine(file);
+ range.startTime = parseLogTime(firstLine);
+ range.endTime = parseLogTime(lastLine);
+ return range;
+ } catch (Exception e) {
+ return range;
+ }
+ }
+
+ private Long parseLogTime(String line) {
+ try {
+ if (line == null || line.trim().isEmpty()) {
+ return null;
+ }
+ DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class);
+ return logItem != null && logItem.getCreateTime() != null ? logItem.getCreateTime().getTime() : null;
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private String readFirstNonBlankLine(Path file) {
+ try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
+ return lines.filter(line -> line != null && !line.trim().isEmpty()).findFirst().orElse(null);
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ private String readLastNonBlankLine(Path file) {
+ try (RandomAccessFile randomAccessFile = new RandomAccessFile(file.toFile(), "r")) {
+ long length = randomAccessFile.length();
+ if (length <= 0) {
+ return null;
+ }
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ for (long pointer = length - 1; pointer >= 0; pointer--) {
+ randomAccessFile.seek(pointer);
+ int read = randomAccessFile.read();
+ if (read == '\n' || read == '\r') {
+ if (baos.size() > 0) {
+ break;
+ }
+ continue;
+ }
+ baos.write(read);
+ }
+ byte[] bytes = baos.toByteArray();
+ for (int i = 0, j = bytes.length - 1; i < j; i++, j--) {
+ byte tmp = bytes[i];
+ bytes[i] = bytes[j];
+ bytes[j] = tmp;
+ }
+ String line = new String(bytes, StandardCharsets.UTF_8).trim();
+ return line.isEmpty() ? null : line;
+ } catch (Exception e) {
+ return null;
+ }
+ }
}
diff --git a/src/main/webapp/components/DevpCard.js b/src/main/webapp/components/DevpCard.js
index d699cfc..c2a89b3 100644
--- a/src/main/webapp/components/DevpCard.js
+++ b/src/main/webapp/components/DevpCard.js
@@ -3,7 +3,7 @@
<div class="mc-root">
<div class="mc-toolbar">
<div class="mc-title">杈撻�佺洃鎺�</div>
- <div class="mc-search">
+ <div v-if="!readOnly" class="mc-search">
<input class="mc-input" v-model="searchStationId" placeholder="璇疯緭鍏ョ珯鍙�" />
<button type="button" class="mc-btn mc-btn-ghost" @click="getDevpStateInfo">鏌ヨ</button>
</div>
@@ -88,7 +88,7 @@
<div v-if="displayStationList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑绔欑偣鏁版嵁</div>
</div>
- <div class="mc-footer">
+ <div v-if="!readOnly || totalPages > 1" class="mc-footer">
<button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
@@ -113,7 +113,7 @@
targetStationId: ""
},
barcodePreviewCache: {},
- pageSize: 12,
+ pageSize: this.readOnly ? 24 : 12,
currentPage: 1,
timer: null
};
diff --git a/src/main/webapp/components/WatchCrnCard.js b/src/main/webapp/components/WatchCrnCard.js
index 0d7e292..7e20467 100644
--- a/src/main/webapp/components/WatchCrnCard.js
+++ b/src/main/webapp/components/WatchCrnCard.js
@@ -3,7 +3,7 @@
<div class="mc-root">
<div class="mc-toolbar">
<div class="mc-title">鍫嗗灈鏈虹洃鎺�</div>
- <div class="mc-search">
+ <div v-if="!readOnly" class="mc-search">
<input class="mc-input" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�" />
<button type="button" class="mc-btn mc-btn-ghost" @click="getCrnStateInfo">鏌ヨ</button>
</div>
@@ -67,7 +67,7 @@
<div v-if="displayCrnList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑鍫嗗灈鏈烘暟鎹�</div>
</div>
- <div class="mc-footer">
+ <div v-if="!readOnly || totalPages > 1" class="mc-footer">
<button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
@@ -91,7 +91,7 @@
sourceLocNo: "",
targetLocNo: ""
},
- pageSize: 12,
+ pageSize: this.readOnly ? 24 : 12,
currentPage: 1,
timer: null
};
diff --git a/src/main/webapp/components/WatchDualCrnCard.js b/src/main/webapp/components/WatchDualCrnCard.js
index 886db1e..dd96e2a 100644
--- a/src/main/webapp/components/WatchDualCrnCard.js
+++ b/src/main/webapp/components/WatchDualCrnCard.js
@@ -3,7 +3,7 @@
<div class="mc-root">
<div class="mc-toolbar">
<div class="mc-title">鍙屽伐浣嶅爢鍨涙満鐩戞帶</div>
- <div class="mc-search">
+ <div v-if="!readOnly" class="mc-search">
<input class="mc-input" v-model="searchCrnNo" placeholder="璇疯緭鍏ュ爢鍨涙満鍙�" />
<button type="button" class="mc-btn mc-btn-ghost" @click="getDualCrnStateInfo">鏌ヨ</button>
</div>
@@ -80,7 +80,7 @@
<div v-if="displayCrnList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑鍙屽伐浣嶅爢鍨涙満鏁版嵁</div>
</div>
- <div class="mc-footer">
+ <div v-if="!readOnly || totalPages > 1" class="mc-footer">
<button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
@@ -105,7 +105,7 @@
targetLocNo: "",
station: 1
},
- pageSize: 12,
+ pageSize: this.readOnly ? 24 : 12,
currentPage: 1,
timer: null
};
diff --git a/src/main/webapp/components/WatchRgvCard.js b/src/main/webapp/components/WatchRgvCard.js
index 3c19e76..80324b1 100644
--- a/src/main/webapp/components/WatchRgvCard.js
+++ b/src/main/webapp/components/WatchRgvCard.js
@@ -3,7 +3,7 @@
<div class="mc-root">
<div class="mc-toolbar">
<div class="mc-title">RGV鐩戞帶</div>
- <div class="mc-search">
+ <div v-if="!readOnly" class="mc-search">
<input class="mc-input" v-model="searchRgvNo" placeholder="璇疯緭鍏GV鍙�" />
<button type="button" class="mc-btn mc-btn-ghost" @click="getRgvStateInfo">鏌ヨ</button>
</div>
@@ -67,7 +67,7 @@
<div v-if="displayRgvList.length === 0" class="mc-empty">褰撳墠娌℃湁鍙睍绀虹殑RGV鏁版嵁</div>
</div>
- <div class="mc-footer">
+ <div v-if="!readOnly || totalPages > 1" class="mc-footer">
<button type="button" class="mc-page-btn" :disabled="currentPage <= 1" @click="handlePageChange(currentPage - 1)">涓婁竴椤�</button>
<span>{{ currentPage }} / {{ totalPages }}</span>
<button type="button" class="mc-page-btn" :disabled="currentPage >= totalPages" @click="handlePageChange(currentPage + 1)">涓嬩竴椤�</button>
@@ -91,7 +91,7 @@
sourcePos: "",
targetPos: ""
},
- pageSize: 12,
+ pageSize: this.readOnly ? 24 : 12,
currentPage: 1,
timer: null
};
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';
}
}
});
diff --git a/src/main/webapp/views/deviceLogs/deviceLogs.html b/src/main/webapp/views/deviceLogs/deviceLogs.html
index 2af2915..5d2d451 100644
--- a/src/main/webapp/views/deviceLogs/deviceLogs.html
+++ b/src/main/webapp/views/deviceLogs/deviceLogs.html
@@ -6,192 +6,1346 @@
<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">
- <!-- 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; }
+ :root {
+ --dl-bg: linear-gradient(180deg, #edf3f7 0%, #e7edf4 100%);
+ --dl-panel-bg: rgba(248, 251, 253, 0.94);
+ --dl-panel-border: rgba(223, 232, 240, 0.96);
+ --dl-panel-shadow: 0 12px 26px rgba(114, 136, 164, 0.08);
+ --dl-text-main: #22384f;
+ --dl-text-sub: #708396;
+ --dl-accent: #6f95bd;
+ --dl-accent-strong: #557ca7;
+ --dl-success: #52b17e;
+ --dl-warning: #c78a3f;
+ --dl-danger: #c96660;
+ }
+
+ html, body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+ }
+
+ body {
+ background: var(--dl-bg);
+ color: var(--dl-text-main);
+ }
+
+ #app {
+ width: 100%;
+ height: 100%;
+ }
+
+ .dl-shell {
+ width: 100%;
+ height: 100%;
+ padding: 16px;
+ box-sizing: border-box;
+ display: flex;
+ gap: 14px;
+ }
+
+ .dl-sidebar {
+ width: 320px;
+ min-width: 320px;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ min-height: 0;
+ }
+
+ .dl-workbench {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(520px, 1.28fr) minmax(340px, 0.92fr);
+ gap: 14px;
+ }
+
+ .dl-center,
+ .dl-visual {
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .dl-center {
+ order: 2;
+ }
+
+ .dl-visual {
+ order: 1;
+ }
+
+ .dl-panel {
+ border-radius: 20px;
+ border: 1px solid var(--dl-panel-border);
+ background: var(--dl-panel-bg);
+ box-shadow: var(--dl-panel-shadow);
+ overflow: hidden;
+ min-height: 0;
+ }
+
+ .dl-panel-head {
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid rgba(228, 236, 243, 0.92);
+ background: rgba(255, 255, 255, 0.26);
+ }
+
+ .dl-panel-title {
+ font-size: 15px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ line-height: 1.25;
+ }
+
+ .dl-panel-desc {
+ margin-top: 5px;
+ font-size: 12px;
+ color: var(--dl-text-sub);
+ line-height: 1.5;
+ }
+
+ .dl-panel-body {
+ padding: 14px 16px 16px;
+ box-sizing: border-box;
+ min-height: 0;
+ height: 100%;
+ }
+
+ .dl-hero {
+ padding: 16px;
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.88) 0%, rgba(240, 246, 251, 0.82) 100%);
+ }
+
+ .dl-hero-title {
+ font-size: 19px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ line-height: 1.2;
+ }
+
+ .dl-hero-desc {
+ margin-top: 8px;
+ font-size: 12px;
+ line-height: 1.6;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-hero-stats {
+ margin-top: 14px;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 10px;
+ }
+
+ .dl-stat {
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(225, 233, 240, 0.94);
+ background: rgba(255, 255, 255, 0.7);
+ }
+
+ .dl-stat-value {
+ font-size: 18px;
+ font-weight: 700;
+ color: var(--dl-accent-strong);
+ }
+
+ .dl-stat-label {
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-date-panel,
+ .dl-device-panel,
+ .dl-log-panel,
+ .dl-raw-panel,
+ .dl-visual-card,
+ .dl-device-summary,
+ .dl-timeline-panel {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ }
+
+ .dl-date-panel {
+ flex: 0 0 260px;
+ }
+
+ .dl-device-panel {
+ flex: 1;
+ }
+
+ .dl-quick-days {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-bottom: 14px;
+ }
+
+ .dl-day-chip {
+ padding: 0 10px;
+ height: 30px;
+ border-radius: 999px;
+ border: 1px solid rgba(219, 228, 236, 0.96);
+ background: rgba(255, 255, 255, 0.84);
+ color: #47627d;
+ cursor: pointer;
+ transition: all .16s ease;
+ }
+
+ .dl-day-chip.is-active {
+ border-color: rgba(111, 149, 189, 0.44);
+ background: rgba(111, 149, 189, 0.14);
+ color: var(--dl-accent-strong);
+ box-shadow: 0 8px 18px rgba(111, 149, 189, 0.16);
+ }
+
+ .dl-tree-wrap {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding-right: 4px;
+ scrollbar-gutter: stable;
+ }
+
+ .dl-type-tabs {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+ margin-bottom: 12px;
+ }
+
+ .dl-type-tab {
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(222, 231, 239, 0.96);
+ background: rgba(255, 255, 255, 0.72);
+ cursor: pointer;
+ text-align: left;
+ transition: all .16s ease;
+ }
+
+ .dl-type-tab.is-active {
+ border-color: rgba(111, 149, 189, 0.44);
+ background: rgba(111, 149, 189, 0.14);
+ box-shadow: 0 10px 20px rgba(111, 149, 189, 0.12);
+ }
+
+ .dl-type-tab-label {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ }
+
+ .dl-type-tab-meta {
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-device-search {
+ margin-bottom: 12px;
+ }
+
+ .dl-device-list {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding-right: 4px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ scrollbar-gutter: stable;
+ }
+
+ .dl-device-item {
+ padding: 12px 13px;
+ border-radius: 16px;
+ border: 1px solid rgba(223, 232, 240, 0.94);
+ background: rgba(255, 255, 255, 0.72);
+ cursor: pointer;
+ transition: all .16s ease;
+ }
+
+ .dl-device-item.is-active {
+ border-color: rgba(111, 149, 189, 0.48);
+ background: rgba(111, 149, 189, 0.12);
+ box-shadow: 0 12px 24px rgba(111, 149, 189, 0.14);
+ }
+
+ .dl-device-item:hover {
+ transform: translateY(-1px);
+ }
+
+ .dl-device-name {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 14px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ }
+
+ .dl-device-badge {
+ padding: 3px 8px;
+ border-radius: 999px;
+ background: rgba(111, 149, 189, 0.12);
+ color: var(--dl-accent-strong);
+ font-size: 10px;
+ font-weight: 700;
+ }
+
+ .dl-device-meta {
+ margin-top: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-date-panel .dl-panel-body,
+ .dl-device-panel .dl-panel-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ height: auto;
+ }
+
+ .dl-device-summary .dl-panel-head {
+ padding: 12px 16px 8px;
+ }
+
+ .dl-device-summary .dl-panel-desc {
+ margin-top: 3px;
+ }
+
+ .dl-device-summary .dl-panel-body,
+ .dl-timeline-panel .dl-panel-body {
+ padding: 12px 16px 14px;
+ }
+
+ .dl-timeline-panel .dl-panel-head {
+ padding: 8px 14px 4px;
+ }
+
+ .dl-timeline-panel .dl-panel-desc {
+ display: none;
+ }
+
+ .dl-timeline-panel .dl-panel-body {
+ padding: 8px 14px 10px;
+ }
+
+ .dl-timeline-panel .dl-btn {
+ height: 30px;
+ padding: 0 12px;
+ border-radius: 10px;
+ }
+
+ .dl-raw-panel .dl-panel-body {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ height: auto;
+ }
+
+ .dl-summary-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ margin-bottom: 10px;
+ }
+
+ .dl-device-summary .dl-btn {
+ height: 30px;
+ padding: 0 12px;
+ border-radius: 10px;
+ }
+
+ .dl-summary-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 6px;
+ }
+
+ .dl-summary-cell {
+ padding: 9px 10px;
+ border-radius: 12px;
+ border: 1px solid rgba(224, 232, 239, 0.96);
+ background: rgba(255, 255, 255, 0.78);
+ }
+
+ .dl-summary-cell-label {
+ font-size: 10px;
+ line-height: 1.2;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-summary-cell-value {
+ margin-top: 4px;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ line-height: 1.35;
+ word-break: normal;
+ overflow-wrap: anywhere;
+ }
+
+ .dl-summary-cell-sub {
+ margin-top: 2px;
+ font-size: 11px;
+ line-height: 1.3;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-summary-cell.is-range,
+ .dl-summary-cell.is-current {
+ grid-column: 1 / -1;
+ }
+
+ .dl-summary-cell.is-range .dl-summary-cell-value,
+ .dl-summary-cell.is-range .dl-summary-cell-sub {
+ font-family: monospace;
+ }
+
+ .dl-inline-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .dl-btn {
+ height: 34px;
+ padding: 0 14px;
+ border-radius: 12px;
+ border: none;
+ background: var(--dl-accent);
+ color: #fff;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: transform .16s ease, box-shadow .16s ease;
+ box-shadow: 0 10px 18px rgba(111, 149, 189, 0.16);
+ }
+
+ .dl-btn:hover {
+ transform: translateY(-1px);
+ }
+
+ .dl-btn.is-ghost {
+ border: 1px solid rgba(219, 228, 236, 0.96);
+ background: rgba(255, 255, 255, 0.84);
+ color: #4b667f;
+ box-shadow: none;
+ }
+
+ .dl-btn[disabled] {
+ opacity: .5;
+ cursor: not-allowed;
+ transform: none;
+ box-shadow: none;
+ }
+
+ .dl-log-workspace {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .dl-detail-panel {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .dl-detail-toolbar {
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid rgba(228, 236, 243, 0.92);
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+ }
+
+ .dl-detail-tabs {
+ display: inline-flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ }
+
+ .dl-detail-body {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .dl-log-status {
+ display: inline-flex;
+ align-items: center;
+ height: 24px;
+ padding: 0 9px;
+ border-radius: 999px;
+ font-size: 11px;
+ font-weight: 700;
+ margin-right: 8px;
+ }
+
+ .dl-tone-success { background: rgba(82, 177, 126, 0.12); color: #2d7650; }
+ .dl-tone-working { background: rgba(111, 149, 189, 0.12); color: #3f6286; }
+ .dl-tone-warning { background: rgba(214, 162, 94, 0.14); color: #9b6a24; }
+ .dl-tone-danger { background: rgba(201, 102, 96, 0.14); color: #a74d47; }
+ .dl-tone-muted { background: rgba(148, 163, 184, 0.14); color: #748397; }
+
+ .dl-log-list {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 12px 16px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ scrollbar-gutter: stable;
+ }
+
+ .dl-raw-content {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ padding: 12px 16px 16px;
+ box-sizing: border-box;
+ }
+
+ .dl-log-row {
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(223, 232, 240, 0.94);
+ background: rgba(255, 255, 255, 0.72);
+ cursor: pointer;
+ transition: all .16s ease;
+ }
+
+ .dl-log-row.is-active {
+ border-color: rgba(111, 149, 189, 0.5);
+ background: rgba(111, 149, 189, 0.12);
+ box-shadow: 0 12px 22px rgba(111, 149, 189, 0.12);
+ }
+
+ .dl-log-row-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ }
+
+ .dl-log-time {
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--dl-accent-strong);
+ font-family: monospace;
+ }
+
+ .dl-log-title {
+ margin-top: 6px;
+ font-size: 12px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ line-height: 1.35;
+ }
+
+ .dl-log-meta-line {
+ margin-top: 4px;
+ font-size: 11px;
+ line-height: 1.4;
+ color: var(--dl-text-sub);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .dl-log-hint {
+ margin-top: 5px;
+ display: inline-flex;
+ align-items: center;
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-picker-main {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .dl-picker-panel {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ }
+
+ .dl-picker-panel .dl-panel-body {
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ height: auto;
+ }
+
+ .dl-picker-toolbar {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 280px;
+ gap: 12px;
+ align-items: start;
+ margin-bottom: 12px;
+ }
+
+ .dl-picker-side-tools {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .dl-picker-current-day {
+ padding: 10px 12px;
+ border-radius: 14px;
+ border: 1px solid rgba(223, 232, 240, 0.94);
+ background: rgba(255, 255, 255, 0.72);
+ font-size: 12px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-picker-current-day strong {
+ display: block;
+ margin-top: 3px;
+ font-size: 13px;
+ color: var(--dl-text-main);
+ }
+
+ .dl-picker-device-grid {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 12px;
+ padding-right: 4px;
+ scrollbar-gutter: stable;
+ }
+
+ .dl-picker-device-grid .dl-device-item {
+ min-height: 112px;
+ text-align: left;
+ }
+
+ .dl-picker-span-all {
+ grid-column: 1 / -1;
+ }
+
+ .dl-viewer-shell {
+ flex: 1;
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ }
+
+ .dl-viewer-header .dl-panel-body {
+ height: auto;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px 16px;
+ }
+
+ .dl-viewer-header-main {
+ min-width: 0;
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ }
+
+ .dl-viewer-copy {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ }
+
+ .dl-viewer-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+
+ .dl-viewer-meta-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 5px 10px;
+ border-radius: 999px;
+ border: 1px solid rgba(223, 232, 240, 0.94);
+ background: rgba(255, 255, 255, 0.72);
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-viewer-meta-item strong {
+ color: var(--dl-text-main);
+ font-weight: 700;
+ }
+
+ .dl-viewer-grid {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(620px, 1.28fr) minmax(360px, 0.92fr);
+ gap: 12px;
+ }
+
+ .dl-viewer-main,
+ .dl-viewer-side {
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+ }
+
+ .dl-viewer-side .dl-timeline-panel {
+ flex: 0 0 auto;
+ }
+
+ .dl-raw-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 12px;
+ }
+
+ .dl-raw-meta {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 12px;
+ font-size: 12px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-raw-tabs {
+ display: inline-flex;
+ gap: 8px;
+ }
+
+ .dl-tab-btn {
+ height: 30px;
+ padding: 0 12px;
+ border-radius: 999px;
+ border: 1px solid rgba(219, 228, 236, 0.96);
+ background: rgba(255, 255, 255, 0.84);
+ color: #4b667f;
+ font-size: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ transition: all .16s ease;
+ }
+
+ .dl-tab-btn.is-active {
+ border-color: rgba(111, 149, 189, 0.44);
+ background: rgba(111, 149, 189, 0.14);
+ color: var(--dl-accent-strong);
+ }
+
+ .dl-json-box {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 14px;
+ border-radius: 16px;
+ border: 1px solid rgba(223, 232, 240, 0.94);
+ background: rgba(244, 248, 252, 0.88);
+ scrollbar-gutter: stable;
+ }
+
+ .dl-json-pre {
+ margin: 0;
+ font-size: 12px;
+ line-height: 1.55;
+ color: #2b425a;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
+ white-space: pre-wrap;
+ word-break: break-word;
+ }
+
+ .dl-json-note {
+ margin-top: 8px;
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-timeline-toolbar {
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 8px 10px;
+ margin-bottom: 6px;
+ }
+
+ .dl-timeline-range {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ }
+
+ .dl-timeline-range .el-slider {
+ flex: 1 1 220px;
+ width: 100%;
+ min-width: 180px;
+ }
+
+ .dl-time-readout {
+ min-width: 196px;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--dl-text-main);
+ font-family: monospace;
+ text-align: right;
+ flex-shrink: 0;
+ }
+
+ .dl-timeline-side {
+ display: inline-flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 8px;
+ min-width: 0;
+ }
+
+ .dl-range-meta {
+ margin-top: 4px;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 4px 10px;
+ font-size: 11px;
+ color: var(--dl-text-sub);
+ }
+
+ .dl-visual-card {
+ flex: 1;
+ min-height: 0;
+ }
+
+ .dl-visual-card .dl-panel-head {
+ padding: 12px 16px 8px;
+ }
+
+ .dl-visual-card .dl-panel-body {
+ height: 100%;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ padding: 10px 12px 12px;
+ }
+
+ .dl-visual-card .mc-toolbar {
+ margin-bottom: 6px;
+ }
+
+ .dl-visual-card .mc-title {
+ font-size: 13px;
+ }
+
+ .dl-visual-card .mc-head {
+ padding: 8px 10px;
+ }
+
+ .dl-visual-card .mc-head-title {
+ font-size: 12px;
+ }
+
+ .dl-visual-card .mc-head-subtitle {
+ display: none;
+ }
+
+ .dl-visual-card .mc-body {
+ padding: 0 10px 8px;
+ }
+
+ .dl-visual-card .mc-detail-grid {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 5px;
+ margin-top: 6px;
+ }
+
+ .dl-visual-card .mc-detail-cell {
+ padding: 7px 8px;
+ border-radius: 9px;
+ }
+
+ .dl-visual-card .mc-detail-label {
+ font-size: 10px;
+ }
+
+ .dl-visual-card .mc-detail-value {
+ margin-top: 2px;
+ font-size: 10px;
+ line-height: 1.25;
+ }
+
+ .dl-visual-card .mc-footer {
+ margin-top: 6px;
+ }
+
+ .dl-tree-wrap,
+ .dl-device-list,
+ .dl-log-list,
+ .dl-json-box,
+ .dl-visual-card .mc-collapse {
+ scrollbar-width: auto;
+ scrollbar-color: rgba(111, 149, 189, 0.9) rgba(225, 233, 241, 0.72);
+ }
+
+ .dl-visual-card .mc-collapse {
+ padding-right: 6px;
+ scrollbar-gutter: stable;
+ }
+
+ .dl-tree-wrap::-webkit-scrollbar,
+ .dl-device-list::-webkit-scrollbar,
+ .dl-log-list::-webkit-scrollbar,
+ .dl-json-box::-webkit-scrollbar,
+ .dl-visual-card .mc-collapse::-webkit-scrollbar {
+ width: 12px;
+ height: 12px;
+ }
+
+ .dl-tree-wrap::-webkit-scrollbar-track,
+ .dl-device-list::-webkit-scrollbar-track,
+ .dl-log-list::-webkit-scrollbar-track,
+ .dl-json-box::-webkit-scrollbar-track,
+ .dl-visual-card .mc-collapse::-webkit-scrollbar-track {
+ background: rgba(225, 233, 241, 0.78);
+ border-radius: 999px;
+ border: 2px solid rgba(248, 251, 253, 0.96);
+ }
+
+ .dl-tree-wrap::-webkit-scrollbar-thumb,
+ .dl-device-list::-webkit-scrollbar-thumb,
+ .dl-log-list::-webkit-scrollbar-thumb,
+ .dl-json-box::-webkit-scrollbar-thumb,
+ .dl-visual-card .mc-collapse::-webkit-scrollbar-thumb {
+ background: linear-gradient(180deg, rgba(111, 149, 189, 0.96) 0%, rgba(85, 124, 167, 0.96) 100%);
+ border-radius: 999px;
+ border: 2px solid rgba(248, 251, 253, 0.96);
+ }
+
+ .dl-tree-wrap::-webkit-scrollbar-thumb:hover,
+ .dl-device-list::-webkit-scrollbar-thumb:hover,
+ .dl-log-list::-webkit-scrollbar-thumb:hover,
+ .dl-json-box::-webkit-scrollbar-thumb:hover,
+ .dl-visual-card .mc-collapse::-webkit-scrollbar-thumb:hover {
+ background: linear-gradient(180deg, rgba(98, 136, 177, 0.98) 0%, rgba(74, 112, 154, 0.98) 100%);
+ }
+
+ .dl-empty,
+ .dl-loading {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 100%;
+ min-height: 160px;
+ color: var(--dl-text-sub);
+ font-size: 13px;
+ text-align: center;
+ padding: 0 20px;
+ box-sizing: border-box;
+ }
+
+ .dl-loading i {
+ font-size: 22px;
+ margin-right: 8px;
+ }
+
+ @media (max-width: 1440px) {
+ .dl-workbench {
+ grid-template-columns: minmax(460px, 1.18fr) minmax(320px, 0.92fr);
+ }
+
+ .dl-picker-toolbar {
+ grid-template-columns: 1fr;
+ }
+
+ .dl-viewer-grid {
+ grid-template-columns: minmax(520px, 1.15fr) minmax(320px, 0.92fr);
+ }
+ }
+
+ @media (max-width: 1180px) {
+ .dl-workbench {
+ grid-template-columns: 1fr;
+ }
+
+ .dl-center,
+ .dl-visual {
+ order: initial;
+ }
+
+ .dl-summary-grid,
+ .dl-visual-card .mc-detail-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .dl-viewer-grid {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ @media (max-width: 1560px) {
+ .dl-timeline-toolbar {
+ grid-template-columns: auto auto;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .dl-timeline-range {
+ grid-column: 1 / -1;
+ width: 100%;
+ }
+
+ .dl-timeline-side {
+ justify-content: flex-end;
+ }
+ }
+
+ @media (max-width: 1320px) {
+ .dl-timeline-range {
+ flex-wrap: wrap;
+ }
+
+ .dl-time-readout {
+ min-width: 0;
+ width: 100%;
+ text-align: left;
+ }
+ }
</style>
</head>
<body>
-
<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="dl-shell">
+ <template v-if="viewMode === 'picker'">
+ <aside class="dl-sidebar">
+ <section class="dl-panel dl-hero">
+ <div class="dl-hero-title">璁惧鏃ュ織宸ヤ綔鍙�</div>
+ <div class="dl-hero-desc">鍏堥�夋嫨鏃ュ織鏃ユ湡鍜岃澶囷紝鍐嶈繘鍏ョ嫭绔嬬殑鏁版嵁鏌ョ湅椤点�傜瓫閫夊叆鍙e拰鏌ョ湅鎬佹媶寮�锛岄伩鍏嶈澶囧垪琛ㄨ鏁版嵁鐪嬫澘鎸ゅ帇銆�</div>
+ <div class="dl-hero-stats">
+ <div class="dl-stat">
+ <div class="dl-stat-value">{{ summaryStats.totalDevices || 0 }}</div>
+ <div class="dl-stat-label">璁惧鏁�</div>
+ </div>
+ <div class="dl-stat">
+ <div class="dl-stat-value">{{ summaryStats.totalFiles || 0 }}</div>
+ <div class="dl-stat-label">鏃ュ織鏂囦欢</div>
+ </div>
+ <div class="dl-stat">
+ <div class="dl-stat-value">{{ recentDays.length }}</div>
+ <div class="dl-stat-label">鏈�杩戞棩鏈�</div>
+ </div>
+ </div>
+ </section>
- <!-- 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>
+ <section class="dl-panel dl-date-panel">
+ <div class="dl-panel-head">
+ <div class="dl-panel-title">鏃ユ湡瀵艰埅</div>
+ <div class="dl-panel-desc">鍏堥�夋棩蹇楁棩鏈燂紝鍐嶈繘鍏ヨ澶囬�夋嫨椤点��</div>
+ </div>
+ <div class="dl-panel-body">
+ <div class="dl-quick-days">
+ <button
+ v-for="day in recentDays"
+ :key="day.day"
+ type="button"
+ class="dl-day-chip"
+ :class="{ 'is-active': selectedDay === day.day }"
+ @click="handleRecentDayClick(day.day)"
+ >{{ day.label }}</button>
+ </div>
+ <div class="dl-tree-wrap">
+ <el-tree
+ ref="dateTree"
+ :data="dateTreeData"
+ :props="defaultProps"
+ node-key="id"
+ :default-expanded-keys="defaultExpandedKeys"
+ highlight-current
+ accordion
+ @node-click="handleNodeClick">
+ <span slot-scope="{ node, data }">
+ <i v-if="data.children" class="el-icon-folder"></i>
+ <i v-else class="el-icon-date"></i>
+ <span style="margin-left: 6px;">{{ node.label }}</span>
+ </span>
+ </el-tree>
+ </div>
+ </div>
+ </section>
+ </aside>
- <!-- 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>
+ <section class="dl-picker-main">
+ <section class="dl-panel dl-picker-panel">
+ <div class="dl-panel-head">
+ <div class="dl-panel-title">璁惧閫夋嫨</div>
+ <div class="dl-panel-desc">{{ selectedDay ? ('鏃ュ織鏃ユ湡 ' + formatDayText(selectedDay) + '锛屽厛绛涢�夎澶囩被鍨嬶紝鍐嶇偣鍑讳竴鍙拌澶囪繘鍏ョ姸鎬佹煡鐪嬮〉銆�') : '鍏堜粠宸︿晶閫夋嫨涓�涓棩蹇楁棩鏈熴��' }}</div>
+ </div>
+ <div class="dl-panel-body">
+ <div class="dl-picker-toolbar">
+ <div class="dl-type-tabs">
+ <button
+ v-for="group in deviceGroups"
+ :key="group.type"
+ type="button"
+ class="dl-type-tab"
+ :class="{ 'is-active': activeType === group.type }"
+ @click="selectTypeGroup(group.type)">
+ <div class="dl-type-tab-label">{{ group.typeLabel }}</div>
+ <div class="dl-type-tab-meta">{{ group.deviceCount }} 鍙� / {{ group.totalFiles }} 鏂囦欢</div>
+ </button>
+ </div>
+ <div class="dl-picker-side-tools">
+ <div class="dl-picker-current-day">
+ 褰撳墠鏃ユ湡
+ <strong>{{ selectedDay ? formatDayText(selectedDay) : '鏈�夋嫨' }}</strong>
</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 class="dl-device-search" style="margin-bottom: 0;">
+ <el-input
+ v-model.trim="searchDeviceNo"
+ size="small"
+ clearable
+ placeholder="鎸夎澶囩紪鍙风瓫閫�">
+ <i slot="prefix" class="el-input__icon el-icon-search"></i>
+ </el-input>
</div>
</div>
</div>
+ <div class="dl-picker-device-grid">
+ <div v-if="summaryLoading" class="dl-loading dl-picker-span-all">
+ <i class="el-icon-loading"></i><span>姝e湪鍔犺浇璁惧鎽樿...</span>
+ </div>
+ <div v-else-if="!selectedDay" class="dl-empty dl-picker-span-all">鍏堜粠宸︿晶閫夋嫨涓�涓棩鏈熴��</div>
+ <div v-else-if="filteredDevices.length === 0" class="dl-empty dl-picker-span-all">褰撳墠鍒嗙粍涓嬫病鏈夊尮閰嶇殑璁惧銆�</div>
+ <button
+ v-else
+ v-for="device in filteredDevices"
+ :key="buildDeviceKey(device.type, device.deviceNo)"
+ type="button"
+ class="dl-device-item"
+ @click="selectDevice(device)">
+ <div class="dl-device-name">
+ <span>{{ device.deviceNo }} 鍙穥{ device.typeLabel }}</span>
+ <span class="dl-device-badge">{{ device.fileCount }} 鏂囦欢</span>
+ </div>
+ <div class="dl-device-meta">
+ <span>棣栨潯: {{ formatTimestamp(device.firstTime, false) }}</span>
+ <span>鏈潯: {{ formatTimestamp(device.lastTime, false) }}</span>
+ </div>
+ </button>
+ </div>
+ </div>
+ </section>
+ </section>
+ </template>
+
+ <section v-else class="dl-viewer-shell">
+ <section class="dl-panel dl-viewer-header">
+ <div class="dl-panel-body">
+ <div class="dl-viewer-header-main">
+ <button type="button" class="dl-btn is-ghost" @click="returnToSelector">杩斿洖绛涢��</button>
+ <div class="dl-viewer-copy">
+ <div class="dl-panel-title">{{ selectedDeviceSummary ? (selectedDeviceSummary.typeLabel + ' ' + selectedDeviceSummary.deviceNo + '鍙�') : '璁惧鐘舵�佹煡鐪�' }}</div>
+ <div class="dl-panel-desc">{{ selectedDay ? ('鏃ュ織鏃ユ湡 ' + formatDayText(selectedDay)) : '璇烽�夋嫨鏃ユ湡鍜岃澶�' }}</div>
+ <div v-if="selectedDeviceSummary" class="dl-viewer-meta">
+ <span class="dl-viewer-meta-item">绫诲瀷 <strong>{{ selectedDeviceSummary.typeLabel }}</strong></span>
+ <span class="dl-viewer-meta-item">鏂囦欢 <strong>{{ selectedDeviceSummary.fileCount }}</strong></span>
+ <span class="dl-viewer-meta-item">宸茶浇 <strong>{{ loadedSegmentCount }}</strong></span>
+ <span class="dl-viewer-meta-item">鑼冨洿 <strong>{{ timelineRangeText }}</strong></span>
+ </div>
+ </div>
+ </div>
+ <div class="dl-inline-actions">
+ <span class="dl-log-status" :class="'dl-tone-' + currentStatusTone">{{ currentStatusLabel }}</span>
+ <button type="button" class="dl-btn is-ghost" @click="loadPreviousSegment" :disabled="!canLoadPreviousSegment">鍔犺浇鏇存棭鐗囨</button>
+ <button type="button" class="dl-btn is-ghost" @click="loadNextSegment" :disabled="!canLoadNextSegment">鍔犺浇鏇存柊鐗囨</button>
+ <button type="button" class="dl-btn" @click="handleCurrentDeviceDownload" :disabled="!canDownload">涓嬭浇褰撴棩鏃ュ織</button>
</div>
</div>
- </el-card>
- </div>
+ </section>
+
+ <div class="dl-viewer-grid">
+ <section class="dl-viewer-main">
+ <section class="dl-panel dl-visual-card">
+ <div class="dl-panel-head">
+ <div class="dl-panel-title">鐘舵�佸彲瑙嗗寲</div>
+ <div class="dl-panel-desc">{{ selectedLogRow ? selectedLogRow._summary.detail : '鎷栧姩鏃堕棿杞存垨鐐瑰嚮鏃ュ織鍚庯紝鍦ㄨ繖閲屾煡鐪嬭鏃跺埢鐨勮澶囩姸鎬併��' }}</div>
+ </div>
+ <div class="dl-panel-body">
+ <component
+ v-if="visualComponentName"
+ :is="visualComponentName"
+ :key="activeDeviceKey"
+ :items="visualItems"
+ :param="visualParam"
+ :auto-refresh="false"
+ :read-only="true"></component>
+ <div v-else class="dl-empty">鍏堥�夋嫨涓�涓澶囷紝骞跺姞杞借嚦灏戜竴鏉℃棩蹇楄褰曘��</div>
+ </div>
+ </section>
+ </section>
+
+ <section class="dl-viewer-side">
+ <section class="dl-panel dl-timeline-panel">
+ <div class="dl-panel-head">
+ <div class="dl-panel-title">鏃堕棿杞翠笌鍥炴斁</div>
+ <div class="dl-panel-desc">鎷栧姩鏃堕棿杞存垨璺宠浆鏃堕棿鐐规煡鐪嬭璁惧鐨勭姸鎬佸彉鍖栥��</div>
+ </div>
+ <div class="dl-panel-body">
+ <div class="dl-timeline-toolbar">
+ <div class="dl-inline-actions">
+ <button type="button" class="dl-btn" v-if="!isPlaying" @click="play" :disabled="!canPlay">鎾斁</button>
+ <button type="button" class="dl-btn" v-else @click="pause">鏆傚仠</button>
+ <button type="button" class="dl-btn is-ghost" @click="resetPlayback" :disabled="!selectedDeviceSummary">閲嶇疆</button>
+ </div>
+ <div class="dl-timeline-range">
+ <el-slider
+ :value="sliderValue"
+ :max="sliderMax"
+ :disabled="!selectedDeviceSummary || sliderMax <= 0"
+ @input="handleSliderInput"
+ @change="handleSliderChange"
+ :format-tooltip="formatTooltip"></el-slider>
+ <div class="dl-time-readout">{{ currentTimeStr }}</div>
+ </div>
+ <div class="dl-timeline-side">
+ <el-popover
+ placement="bottom"
+ width="220"
+ trigger="click"
+ v-model="jumpVisible"
+ @show="initJumpTime">
+ <div style="display: flex; flex-direction: column; gap: 10px;">
+ <el-time-picker
+ v-model="jumpTime"
+ size="small"
+ placeholder="閫夋嫨鏃堕棿"
+ style="width: 100%;"
+ :picker-options="{ selectableRange: '00:00:00 - 23:59:59' }">
+ </el-time-picker>
+ <button type="button" class="dl-btn" style="width: 100%;" @click="confirmJump">璺宠浆</button>
+ </div>
+ <button slot="reference" type="button" class="dl-btn is-ghost" :disabled="!selectedDeviceSummary">璺宠浆</button>
+ </el-popover>
+ <el-select v-model="playbackSpeed" size="small" style="width: 92px;" :disabled="!selectedDeviceSummary">
+ <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-select>
+ </div>
+ </div>
+ <div class="dl-range-meta">
+ <span>瀹屾暣鑼冨洿: {{ timelineRangeText }}</span>
+ <span>宸插姞杞界墖娈�: {{ loadedSegmentCount }} / {{ timelineMeta.totalFiles || 0 }}</span>
+ <span v-if="selectedLogRow">褰撳墠璁板綍: {{ selectedLogRow._summary.title }}</span>
+ </div>
+ </div>
+ </section>
+
+ <section class="dl-panel dl-detail-panel">
+ <div class="dl-detail-toolbar">
+ <div>
+ <div class="dl-panel-title">{{ detailTab === 'raw' ? '鍘熷鏁版嵁' : '鏃ュ織鍒楄〃' }}</div>
+ <div v-if="detailTab === 'raw'" class="dl-panel-desc">{{ selectedLogRow ? '褰撳墠璁板綍鐨� wcsData JSON 宸叉牸寮忓寲灞曠ず锛屽繀瑕佹椂鍙垏鎹㈡煡鐪嬪師濮嬫枃鏈��' : '鍏堥�夋嫨涓�鏉℃棩蹇楋紝鍐嶆煡鐪嬪師濮嬫暟鎹��' }}</div>
+ </div>
+ <div class="dl-detail-tabs">
+ <button type="button" class="dl-tab-btn" :class="{ 'is-active': detailTab === 'logs' }" @click="detailTab = 'logs'">鏃ュ織鍒楄〃</button>
+ <button type="button" class="dl-tab-btn" :class="{ 'is-active': detailTab === 'raw' }" @click="detailTab = 'raw'" :disabled="!selectedLogRow">鍘熷鏁版嵁</button>
+ </div>
+ </div>
+ <div class="dl-detail-body">
+ <div v-show="detailTab === 'logs'" class="dl-log-list">
+ <div v-if="timelineLoading || logLoading" class="dl-loading">
+ <i class="el-icon-loading"></i><span>{{ timelineLoading ? '姝e湪鏋勫缓璁惧鏃堕棿杞�...' : '姝e湪鍔犺浇鏃ュ織鐗囨...' }}</span>
+ </div>
+ <div v-else-if="!selectedDeviceSummary" class="dl-empty">鍏堣繑鍥炵瓫閫夐〉閫夋嫨璁惧銆�</div>
+ <div v-else-if="logRows.length === 0" class="dl-empty">{{ logLoadError || '褰撳墠璁惧鏆傛棤鍙睍绀烘棩蹇椼��' }}</div>
+ <div
+ v-else
+ v-for="row in logRows"
+ :key="row._key"
+ :id="'log-row-' + row._key"
+ class="dl-log-row"
+ :class="{ 'is-active': row._key === selectedLogKey }"
+ @click="handleLogRowClick(row)">
+ <div class="dl-log-row-head">
+ <span class="dl-log-time">{{ formatTimestamp(row._ts, true) }}</span>
+ <span class="dl-log-status" :class="'dl-tone-' + row._summary.tone">{{ row._summary.statusLabel }}</span>
+ </div>
+ <div class="dl-log-title">{{ row._summary.title }}</div>
+ <div class="dl-log-meta-line">{{ buildLogMetaLine(row._summary) }}</div>
+ </div>
+ </div>
+ <div v-show="detailTab === 'raw'" class="dl-raw-content">
+ <template v-if="selectedLogRow">
+ <div class="dl-raw-toolbar">
+ <div class="dl-raw-meta">
+ <span>{{ formatTimestamp(selectedLogRow._ts, true) }}</span>
+ <span>{{ currentLogTitle }}</span>
+ </div>
+ <div class="dl-raw-tabs">
+ <button type="button" class="dl-tab-btn" :class="{ 'is-active': rawTab === 'wcs' }" @click="rawTab = 'wcs'">WCS JSON</button>
+ <button type="button" class="dl-tab-btn" :class="{ 'is-active': rawTab === 'origin' }" @click="rawTab = 'origin'">originData</button>
+ </div>
+ </div>
+ <div class="dl-json-box">
+ <pre class="dl-json-pre">{{ activeRawText }}</pre>
+ </div>
+ <div class="dl-json-note">{{ activeRawHint }}</div>
+ </template>
+ <div v-else class="dl-empty">鍏堝湪鏃ュ織鍒楄〃鐐瑰嚮涓�鏉¤褰曪紝鍐嶆煡鐪嬪師濮嬫暟鎹��</div>
+ </div>
+ </div>
+ </section>
+ </section>
+ </div>
+ </section>
</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="downloadDialogTitle" :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>
@@ -206,11 +1360,11 @@
<script type="text/javascript" src="../../static/js/common.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/MonitorCardKit.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?v=20260309_i18n_pagefix1" charset="utf-8"></script>
+<script src="../../components/MonitorCardKit.js?v=20260318_monitor_v2"></script>
+<script src="../../components/WatchCrnCard.js?v=20260318_monitor_v2"></script>
+<script src="../../components/WatchRgvCard.js?v=20260318_monitor_v2"></script>
+<script src="../../components/WatchDualCrnCard.js?v=20260318_monitor_v2"></script>
+<script src="../../components/DevpCard.js?v=20260318_monitor_v2"></script>
+<script type="text/javascript" src="../../static/js/deviceLogs/deviceLogs.js?v=20260318_workbench_v9" charset="utf-8"></script>
</body>
</html>
--
Gitblit v1.9.1