| New file |
| | |
| | | package com.zy.asrs.controller; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.R; |
| | | import com.zy.asrs.entity.DeviceConfig; |
| | | import com.zy.asrs.service.DeviceConfigService; |
| | | import com.zy.asrs.service.DevicePingFileStorageService; |
| | | import com.zy.common.web.BaseController; |
| | | import com.zy.core.enums.SlaveType; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | |
| | | @RestController |
| | | public class DevicePingLogController extends BaseController { |
| | | |
| | | @Autowired |
| | | private DeviceConfigService deviceConfigService; |
| | | |
| | | @Autowired |
| | | private DevicePingFileStorageService devicePingFileStorageService; |
| | | |
| | | @Value("${devicePingStorage.intervalMs:1000}") |
| | | private int intervalMs; |
| | | |
| | | @Value("${devicePingStorage.timeoutMs:800}") |
| | | private int timeoutMs; |
| | | |
| | | @Value("${devicePingStorage.probeCount:3}") |
| | | private int probeCount; |
| | | |
| | | @Value("${devicePingStorage.packetSize:-1}") |
| | | private int packetSize; |
| | | |
| | | @RequestMapping("/devicePingLog/options/auth") |
| | | @ManagerAuth |
| | | public R options() { |
| | | List<DeviceConfig> configs = listConfigs(); |
| | | if (configs == null) { |
| | | return R.error("读取设备配置失败"); |
| | | } |
| | | List<Map<String, Object>> list = new ArrayList<>(); |
| | | for (DeviceConfig config : configs) { |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("deviceType", config.getDeviceType()); |
| | | item.put("deviceNo", config.getDeviceNo()); |
| | | item.put("ip", config.getIp()); |
| | | item.put("port", config.getPort()); |
| | | item.put("label", config.getDeviceType() + "-" + config.getDeviceNo() + " (" + config.getIp() + ")"); |
| | | list.add(item); |
| | | } |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("devices", list); |
| | | result.put("days", devicePingFileStorageService.listDays()); |
| | | Map<String, Object> samplingConfig = new LinkedHashMap<>(); |
| | | samplingConfig.put("intervalMs", intervalMs); |
| | | samplingConfig.put("timeoutMs", timeoutMs); |
| | | samplingConfig.put("probeCount", probeCount); |
| | | samplingConfig.put("packetSize", packetSize); |
| | | result.put("samplingConfig", samplingConfig); |
| | | return R.ok(result); |
| | | } |
| | | |
| | | @RequestMapping("/devicePingLog/overview/auth") |
| | | @ManagerAuth |
| | | public R overview() { |
| | | List<DeviceConfig> configs = listConfigs(); |
| | | if (configs == null) { |
| | | return R.error("读取设备配置失败"); |
| | | } |
| | | return R.ok(devicePingFileStorageService.queryOverview(configs)); |
| | | } |
| | | |
| | | @RequestMapping("/devicePingLog/trend/auth") |
| | | @ManagerAuth |
| | | public R trend(@RequestParam String deviceType, |
| | | @RequestParam Integer deviceNo, |
| | | @RequestParam Long startTime, |
| | | @RequestParam Long endTime, |
| | | @RequestParam(required = false) Integer bucketSec) { |
| | | if (SlaveType.findInstance(deviceType) == null) { |
| | | return R.error("设备类型错误"); |
| | | } |
| | | if (deviceNo == null) { |
| | | return R.error("设备编号不能为空"); |
| | | } |
| | | if (startTime == null || endTime == null || startTime <= 0 || endTime <= 0 || endTime < startTime) { |
| | | return R.error("时间范围错误"); |
| | | } |
| | | DeviceConfig config = deviceConfigService.getOne(new QueryWrapper<DeviceConfig>() |
| | | .eq("device_type", deviceType) |
| | | .eq("device_no", deviceNo) |
| | | .last("limit 1")); |
| | | if (config == null) { |
| | | return R.error("未找到对应设备配置"); |
| | | } |
| | | return R.ok(devicePingFileStorageService.queryTrend(config, startTime, endTime, bucketSec)); |
| | | } |
| | | |
| | | private List<DeviceConfig> listConfigs() { |
| | | try { |
| | | return deviceConfigService.list(new QueryWrapper<DeviceConfig>() |
| | | .isNotNull("ip") |
| | | .ne("ip", "") |
| | | .orderBy(true, true, "device_type", "device_no")); |
| | | } catch (Exception ex) { |
| | | return null; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.asrs.domain; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.util.Date; |
| | | |
| | | @Data |
| | | public class DevicePingSample { |
| | | |
| | | private String deviceType; |
| | | private Integer deviceNo; |
| | | private String ip; |
| | | private Integer port; |
| | | private Date createTime; |
| | | private Boolean reachable; |
| | | private Long latencyMs; |
| | | private Long avgLatencyMs; |
| | | private Long minLatencyMs; |
| | | private Long maxLatencyMs; |
| | | private Integer packetSize; |
| | | private Integer probeCount; |
| | | private Integer successProbeCount; |
| | | private String status; |
| | | private String message; |
| | | } |
| New file |
| | |
| | | package com.zy.asrs.service; |
| | | |
| | | import com.alibaba.fastjson.JSON; |
| | | import com.alibaba.fastjson.serializer.SerializerFeature; |
| | | import com.zy.asrs.domain.DevicePingSample; |
| | | import com.zy.asrs.entity.DeviceConfig; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | import java.io.BufferedWriter; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | import java.nio.file.StandardOpenOption; |
| | | import java.time.Instant; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.Collections; |
| | | import java.util.Comparator; |
| | | import java.util.Date; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.stream.Stream; |
| | | |
| | | @Service |
| | | public class DevicePingFileStorageService { |
| | | |
| | | private static final ZoneId ZONE_ID = ZoneId.systemDefault(); |
| | | private static final DateTimeFormatter DAY_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); |
| | | private static final DateTimeFormatter HOUR_FORMAT = DateTimeFormatter.ofPattern("HH"); |
| | | private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | |
| | | @Value("${devicePingStorage.loggingPath}") |
| | | private String loggingPath; |
| | | |
| | | @Value("${devicePingStorage.expireDays:7}") |
| | | private Integer expireDays; |
| | | |
| | | @Value("${devicePingStorage.packetSize:-1}") |
| | | private Integer packetSize; |
| | | |
| | | public void appendSamples(List<DevicePingSample> samples) { |
| | | if (samples == null || samples.isEmpty()) { |
| | | return; |
| | | } |
| | | Map<Path, List<DevicePingSample>> grouped = new LinkedHashMap<>(); |
| | | for (DevicePingSample sample : samples) { |
| | | if (sample == null || sample.getCreateTime() == null) { |
| | | continue; |
| | | } |
| | | grouped.computeIfAbsent(resolveFilePath(sample), k -> new ArrayList<>()).add(sample); |
| | | } |
| | | for (Map.Entry<Path, List<DevicePingSample>> entry : grouped.entrySet()) { |
| | | Path path = entry.getKey(); |
| | | List<DevicePingSample> fileSamples = entry.getValue(); |
| | | fileSamples.sort(Comparator.comparing(DevicePingSample::getCreateTime, Comparator.nullsLast(Date::compareTo))); |
| | | try { |
| | | Files.createDirectories(path.getParent()); |
| | | try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8, |
| | | StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { |
| | | for (DevicePingSample sample : fileSamples) { |
| | | writer.write(JSON.toJSONStringWithDateFormat(sample, "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteDateUseDateFormat)); |
| | | writer.newLine(); |
| | | } |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } |
| | | |
| | | public List<String> listDays() { |
| | | Path baseDir = Paths.get(loggingPath); |
| | | if (!Files.exists(baseDir)) { |
| | | return new ArrayList<>(); |
| | | } |
| | | try { |
| | | List<String> days = new ArrayList<>(); |
| | | try (Stream<Path> stream = Files.list(baseDir)) { |
| | | stream |
| | | .filter(Files::isDirectory) |
| | | .forEach(path -> { |
| | | String day = path.getFileName().toString(); |
| | | if (day.length() == 8 && day.chars().allMatch(Character::isDigit)) { |
| | | days.add(day); |
| | | } |
| | | }); |
| | | } |
| | | days.sort(Comparator.reverseOrder()); |
| | | return days; |
| | | } catch (Exception ignored) { |
| | | return new ArrayList<>(); |
| | | } |
| | | } |
| | | |
| | | public Map<String, Object> queryTrend(DeviceConfig deviceConfig, long startTime, long endTime, Integer bucketSec) { |
| | | if (deviceConfig == null || startTime <= 0 || endTime <= 0 || endTime < startTime) { |
| | | return Collections.emptyMap(); |
| | | } |
| | | TrendAccumulator summary = new TrendAccumulator(); |
| | | List<Map<String, Object>> series = new ArrayList<>(); |
| | | List<Map<String, Object>> alerts = new ArrayList<>(); |
| | | |
| | | for (Path file : resolveRangeFiles(deviceConfig.getDeviceType(), deviceConfig.getDeviceNo(), startTime, endTime)) { |
| | | if (!Files.exists(file)) { |
| | | continue; |
| | | } |
| | | try { |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | lines.forEach(line -> { |
| | | if (line == null || line.trim().isEmpty()) { |
| | | return; |
| | | } |
| | | DevicePingSample sample; |
| | | try { |
| | | sample = JSON.parseObject(line, DevicePingSample.class); |
| | | } catch (Exception ex) { |
| | | return; |
| | | } |
| | | if (sample == null || sample.getCreateTime() == null) { |
| | | return; |
| | | } |
| | | long ts = sample.getCreateTime().getTime(); |
| | | if (ts < startTime || ts > endTime) { |
| | | return; |
| | | } |
| | | summary.add(sample); |
| | | Map<String, Object> point = new LinkedHashMap<>(); |
| | | point.put("time", ts); |
| | | point.put("timeLabel", formatDateTime(ts)); |
| | | point.put("reachable", Boolean.TRUE.equals(sample.getReachable())); |
| | | point.put("status", sample.getStatus()); |
| | | point.put("message", sample.getMessage()); |
| | | point.put("latencyMs", sample.getLatencyMs()); |
| | | point.put("avgLatencyMs", sample.getAvgLatencyMs()); |
| | | point.put("minLatencyMs", sample.getMinLatencyMs()); |
| | | point.put("maxLatencyMs", sample.getMaxLatencyMs()); |
| | | point.put("packetSize", resolvePacketSize(sample.getPacketSize())); |
| | | point.put("probeCount", sample.getProbeCount()); |
| | | point.put("successProbeCount", sample.getSuccessProbeCount()); |
| | | point.put("successRate", round2(summaryRate(sample.getSuccessProbeCount(), sample.getProbeCount()))); |
| | | point.put("failProbeCount", Math.max(0, safeInt(sample.getProbeCount()) - safeInt(sample.getSuccessProbeCount()))); |
| | | series.add(point); |
| | | if (!"OK".equalsIgnoreCase(safeText(sample.getStatus(), "")) && alerts.size() < 120) { |
| | | Map<String, Object> alert = new LinkedHashMap<>(); |
| | | alert.put("time", ts); |
| | | alert.put("timeLabel", formatDateTime(ts)); |
| | | alert.put("status", sample.getStatus()); |
| | | alert.put("message", safeText(sample.getMessage(), "探测失败")); |
| | | alert.put("ip", sample.getIp()); |
| | | alerts.add(alert); |
| | | } |
| | | }); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | |
| | | series.sort(Comparator.comparingLong(item -> Long.parseLong(String.valueOf(item.get("time"))))); |
| | | |
| | | Map<String, Object> device = new LinkedHashMap<>(); |
| | | device.put("deviceType", deviceConfig.getDeviceType()); |
| | | device.put("deviceNo", deviceConfig.getDeviceNo()); |
| | | device.put("ip", deviceConfig.getIp()); |
| | | device.put("port", deviceConfig.getPort()); |
| | | device.put("label", buildDeviceLabel(deviceConfig)); |
| | | device.put("packetSize", resolvePacketSize(summary.latestPacketSize)); |
| | | |
| | | Map<String, Object> summaryMap = new LinkedHashMap<>(); |
| | | summaryMap.put("totalSamples", summary.totalCount); |
| | | summaryMap.put("successSamples", summary.successCount); |
| | | summaryMap.put("failSamples", summary.failCount); |
| | | summaryMap.put("successRate", round2(summary.successRate())); |
| | | summaryMap.put("avgLatencyMs", summary.latestAvgLatency); |
| | | summaryMap.put("minLatencyMs", summary.latestMinLatency); |
| | | summaryMap.put("maxLatencyMs", summary.latestMaxLatency); |
| | | summaryMap.put("latestStatus", summary.latestStatus); |
| | | summaryMap.put("latestTime", summary.latestTime); |
| | | summaryMap.put("latestTimeLabel", summary.latestTime <= 0 ? "" : formatDateTime(summary.latestTime)); |
| | | summaryMap.put("packetSize", resolvePacketSize(summary.latestPacketSize)); |
| | | summaryMap.put("bucketSec", 1); |
| | | summaryMap.put("startTime", startTime); |
| | | summaryMap.put("endTime", endTime); |
| | | summaryMap.put("startTimeLabel", formatDateTime(startTime)); |
| | | summaryMap.put("endTimeLabel", formatDateTime(endTime)); |
| | | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("device", device); |
| | | result.put("summary", summaryMap); |
| | | result.put("series", series); |
| | | result.put("alerts", alerts); |
| | | return result; |
| | | } |
| | | |
| | | public Map<String, Object> queryOverview(List<DeviceConfig> deviceConfigs) { |
| | | List<Map<String, Object>> devices = new ArrayList<>(); |
| | | if (deviceConfigs == null || deviceConfigs.isEmpty()) { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("summary", buildOverviewSummary(devices)); |
| | | result.put("devices", devices); |
| | | return result; |
| | | } |
| | | |
| | | for (DeviceConfig config : deviceConfigs) { |
| | | DevicePingSample latestSample = findLatestSample(config); |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("deviceType", config.getDeviceType()); |
| | | item.put("deviceNo", config.getDeviceNo()); |
| | | item.put("ip", config.getIp()); |
| | | item.put("port", config.getPort()); |
| | | item.put("label", buildDeviceLabel(config)); |
| | | item.put("packetSize", resolvePacketSize(latestSample == null ? null : latestSample.getPacketSize())); |
| | | if (latestSample == null) { |
| | | item.put("status", "NO_DATA"); |
| | | item.put("statusText", "暂无数据"); |
| | | item.put("statusLevel", 3); |
| | | item.put("reachable", false); |
| | | item.put("successRate", null); |
| | | item.put("avgLatencyMs", null); |
| | | item.put("minLatencyMs", null); |
| | | item.put("maxLatencyMs", null); |
| | | item.put("latestTime", null); |
| | | item.put("latestTimeLabel", "--"); |
| | | item.put("message", ""); |
| | | } else { |
| | | item.put("status", safeText(latestSample.getStatus(), "UNKNOWN")); |
| | | item.put("statusText", resolveStatusText(latestSample.getStatus())); |
| | | item.put("statusLevel", resolveStatusLevel(latestSample.getStatus())); |
| | | item.put("reachable", Boolean.TRUE.equals(latestSample.getReachable())); |
| | | item.put("successRate", round2(summaryRate(latestSample.getSuccessProbeCount(), latestSample.getProbeCount()))); |
| | | item.put("avgLatencyMs", latestSample.getAvgLatencyMs()); |
| | | item.put("minLatencyMs", latestSample.getMinLatencyMs()); |
| | | item.put("maxLatencyMs", latestSample.getMaxLatencyMs()); |
| | | item.put("latestTime", latestSample.getCreateTime() == null ? null : latestSample.getCreateTime().getTime()); |
| | | item.put("latestTimeLabel", latestSample.getCreateTime() == null ? "--" : formatDateTime(latestSample.getCreateTime().getTime())); |
| | | item.put("message", safeText(latestSample.getMessage(), "")); |
| | | } |
| | | devices.add(item); |
| | | } |
| | | |
| | | devices.sort((a, b) -> { |
| | | int levelDiff = toInt(a.get("statusLevel")) - toInt(b.get("statusLevel")); |
| | | if (levelDiff != 0) { |
| | | return levelDiff; |
| | | } |
| | | int typeDiff = safeText(String.valueOf(a.get("deviceType")), "").compareTo(safeText(String.valueOf(b.get("deviceType")), "")); |
| | | if (typeDiff != 0) { |
| | | return typeDiff; |
| | | } |
| | | return Integer.compare(toInt(a.get("deviceNo")), toInt(b.get("deviceNo"))); |
| | | }); |
| | | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("summary", buildOverviewSummary(devices)); |
| | | result.put("devices", devices); |
| | | return result; |
| | | } |
| | | |
| | | public void cleanupExpired() { |
| | | Path baseDir = Paths.get(loggingPath); |
| | | if (!Files.exists(baseDir) || expireDays == null || expireDays <= 0) { |
| | | return; |
| | | } |
| | | long cutoff = System.currentTimeMillis() - expireDays * 24L * 60L * 60L * 1000L; |
| | | try { |
| | | try (Stream<Path> stream = Files.list(baseDir)) { |
| | | stream |
| | | .filter(Files::isDirectory) |
| | | .forEach(path -> { |
| | | String day = path.getFileName().toString(); |
| | | if (day.length() != 8 || !day.chars().allMatch(Character::isDigit)) { |
| | | return; |
| | | } |
| | | try { |
| | | LocalDate date = LocalDate.parse(day, DAY_FORMAT); |
| | | long startOfDay = date.atStartOfDay(ZONE_ID).toInstant().toEpochMilli(); |
| | | if (startOfDay >= cutoff) { |
| | | return; |
| | | } |
| | | try (Stream<Path> walk = Files.walk(path)) { |
| | | walk.sorted(Comparator.reverseOrder()) |
| | | .forEach(p -> { |
| | | try { |
| | | Files.deleteIfExists(p); |
| | | } catch (Exception ignored) { |
| | | } |
| | | }); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | }); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | |
| | | private Path resolveFilePath(DevicePingSample sample) { |
| | | LocalDateTime dateTime = LocalDateTime.ofInstant(sample.getCreateTime().toInstant(), ZONE_ID); |
| | | String day = DAY_FORMAT.format(dateTime); |
| | | String hour = HOUR_FORMAT.format(dateTime); |
| | | String fileName = sample.getDeviceType() + "_" + sample.getDeviceNo() + "_" + day + "_" + hour + ".log"; |
| | | return Paths.get(loggingPath, day, fileName); |
| | | } |
| | | |
| | | private List<Path> resolveRangeFiles(String deviceType, Integer deviceNo, long startTime, long endTime) { |
| | | List<Path> paths = new ArrayList<>(); |
| | | LocalDateTime current = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZONE_ID) |
| | | .withMinute(0).withSecond(0).withNano(0); |
| | | LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTime), ZONE_ID) |
| | | .withMinute(0).withSecond(0).withNano(0); |
| | | while (!current.isAfter(end)) { |
| | | String day = DAY_FORMAT.format(current); |
| | | String hour = HOUR_FORMAT.format(current); |
| | | String fileName = deviceType + "_" + deviceNo + "_" + day + "_" + hour + ".log"; |
| | | paths.add(Paths.get(loggingPath, day, fileName)); |
| | | current = current.plusHours(1); |
| | | } |
| | | return paths; |
| | | } |
| | | |
| | | private String formatDateTime(long timestamp) { |
| | | return TIME_FORMAT.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZONE_ID)); |
| | | } |
| | | |
| | | private String buildDeviceLabel(DeviceConfig config) { |
| | | return String.format(Locale.ROOT, "%s-%s (%s)", safeText(config.getDeviceType(), "-"), |
| | | config.getDeviceNo() == null ? "-" : String.valueOf(config.getDeviceNo()), |
| | | safeText(config.getIp(), "-")); |
| | | } |
| | | |
| | | private DevicePingSample findLatestSample(DeviceConfig config) { |
| | | if (config == null || config.getDeviceNo() == null || safeText(config.getDeviceType(), "").isEmpty()) { |
| | | return null; |
| | | } |
| | | List<String> days = listDays(); |
| | | for (String day : days) { |
| | | Path dayDir = Paths.get(loggingPath, day); |
| | | if (!Files.exists(dayDir)) { |
| | | continue; |
| | | } |
| | | String prefix = config.getDeviceType() + "_" + config.getDeviceNo() + "_" + day + "_"; |
| | | List<Path> candidates = new ArrayList<>(); |
| | | try (Stream<Path> stream = Files.list(dayDir)) { |
| | | stream.filter(path -> !Files.isDirectory(path)) |
| | | .filter(path -> { |
| | | String name = path.getFileName().toString(); |
| | | return name.startsWith(prefix) && name.endsWith(".log"); |
| | | }) |
| | | .forEach(candidates::add); |
| | | } catch (Exception ignored) { |
| | | } |
| | | candidates.sort((a, b) -> b.getFileName().toString().compareTo(a.getFileName().toString())); |
| | | for (Path candidate : candidates) { |
| | | DevicePingSample sample = readLastSample(candidate); |
| | | if (sample != null) { |
| | | return sample; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private DevicePingSample readLastSample(Path file) { |
| | | try { |
| | | List<String> lines = Files.readAllLines(file, StandardCharsets.UTF_8); |
| | | for (int i = lines.size() - 1; i >= 0; i--) { |
| | | String line = lines.get(i); |
| | | if (line == null || line.trim().isEmpty()) { |
| | | continue; |
| | | } |
| | | try { |
| | | DevicePingSample sample = JSON.parseObject(line, DevicePingSample.class); |
| | | if (sample != null && sample.getCreateTime() != null) { |
| | | return sample; |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Map<String, Object> buildOverviewSummary(List<Map<String, Object>> devices) { |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | long total = devices == null ? 0L : devices.size(); |
| | | long okCount = 0L; |
| | | long unstableCount = 0L; |
| | | long offlineCount = 0L; |
| | | long noDataCount = 0L; |
| | | long latencyCount = 0L; |
| | | long latencySum = 0L; |
| | | Long maxLatency = null; |
| | | if (devices != null) { |
| | | for (Map<String, Object> item : devices) { |
| | | String status = String.valueOf(item.get("status")); |
| | | if ("OK".equals(status)) { |
| | | okCount++; |
| | | } else if ("UNSTABLE".equals(status)) { |
| | | unstableCount++; |
| | | } else if ("NO_DATA".equals(status)) { |
| | | noDataCount++; |
| | | } else { |
| | | offlineCount++; |
| | | } |
| | | Object avgLatency = item.get("avgLatencyMs"); |
| | | if (avgLatency instanceof Number) { |
| | | latencySum += ((Number) avgLatency).longValue(); |
| | | latencyCount++; |
| | | } |
| | | Object peakLatency = item.get("maxLatencyMs"); |
| | | if (peakLatency instanceof Number) { |
| | | long candidate = ((Number) peakLatency).longValue(); |
| | | if (maxLatency == null || candidate > maxLatency) { |
| | | maxLatency = candidate; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | summary.put("totalDevices", total); |
| | | summary.put("okDevices", okCount); |
| | | summary.put("unstableDevices", unstableCount); |
| | | summary.put("offlineDevices", offlineCount); |
| | | summary.put("noDataDevices", noDataCount); |
| | | summary.put("avgLatencyMs", latencyCount <= 0 ? null : Math.round(latencySum * 100D / latencyCount) / 100D); |
| | | summary.put("maxLatencyMs", maxLatency); |
| | | return summary; |
| | | } |
| | | |
| | | private String safeText(String value, String defaultValue) { |
| | | if (value == null || value.trim().isEmpty()) { |
| | | return defaultValue; |
| | | } |
| | | return value.trim(); |
| | | } |
| | | |
| | | private Double round2(double value) { |
| | | return Math.round(value * 100D) / 100D; |
| | | } |
| | | |
| | | private Integer resolvePacketSize(Integer samplePacketSize) { |
| | | if (samplePacketSize != null) { |
| | | return Math.max(-1, samplePacketSize); |
| | | } |
| | | return packetSize == null ? -1 : Math.max(-1, packetSize); |
| | | } |
| | | |
| | | private double summaryRate(Integer successCount, Integer probeCount) { |
| | | int total = safeInt(probeCount); |
| | | if (total <= 0) { |
| | | return 0D; |
| | | } |
| | | return safeInt(successCount) * 100D / total; |
| | | } |
| | | |
| | | private int safeInt(Integer value) { |
| | | return value == null ? 0 : value; |
| | | } |
| | | |
| | | private int toInt(Object value) { |
| | | if (value == null) { |
| | | return 0; |
| | | } |
| | | if (value instanceof Number) { |
| | | return ((Number) value).intValue(); |
| | | } |
| | | try { |
| | | return Integer.parseInt(String.valueOf(value)); |
| | | } catch (Exception ignored) { |
| | | return 0; |
| | | } |
| | | } |
| | | |
| | | private String resolveStatusText(String status) { |
| | | if ("OK".equalsIgnoreCase(status)) { |
| | | return "正常"; |
| | | } |
| | | if ("UNSTABLE".equalsIgnoreCase(status)) { |
| | | return "波动"; |
| | | } |
| | | if ("TIMEOUT".equalsIgnoreCase(status)) { |
| | | return "超时"; |
| | | } |
| | | if ("ERROR".equalsIgnoreCase(status)) { |
| | | return "异常"; |
| | | } |
| | | if ("NO_DATA".equalsIgnoreCase(status)) { |
| | | return "暂无数据"; |
| | | } |
| | | return safeText(status, "未知"); |
| | | } |
| | | |
| | | private int resolveStatusLevel(String status) { |
| | | if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) { |
| | | return 0; |
| | | } |
| | | if ("UNSTABLE".equalsIgnoreCase(status)) { |
| | | return 1; |
| | | } |
| | | if ("OK".equalsIgnoreCase(status)) { |
| | | return 2; |
| | | } |
| | | return 3; |
| | | } |
| | | |
| | | private static class TrendAccumulator { |
| | | |
| | | private long totalCount; |
| | | private long successCount; |
| | | private long failCount; |
| | | private Long latestAvgLatency; |
| | | private Long latestMinLatency; |
| | | private Long latestMaxLatency; |
| | | private Integer latestPacketSize; |
| | | private String latestStatus; |
| | | private long latestTime; |
| | | |
| | | private void add(DevicePingSample sample) { |
| | | totalCount++; |
| | | if (Boolean.TRUE.equals(sample.getReachable())) { |
| | | successCount++; |
| | | } else { |
| | | failCount++; |
| | | } |
| | | if (sample.getCreateTime() != null) { |
| | | long currentTime = sample.getCreateTime().getTime(); |
| | | if (currentTime >= latestTime) { |
| | | latestTime = currentTime; |
| | | latestAvgLatency = sample.getAvgLatencyMs(); |
| | | latestMinLatency = sample.getMinLatencyMs(); |
| | | latestMaxLatency = sample.getMaxLatencyMs(); |
| | | latestPacketSize = sample.getPacketSize(); |
| | | latestStatus = sample.getStatus(); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private Double successRate() { |
| | | if (totalCount <= 0) { |
| | | return 0D; |
| | | } |
| | | return (successCount * 100D) / totalCount; |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.core.task; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; |
| | | import com.core.common.Cools; |
| | | import com.zy.asrs.domain.DevicePingSample; |
| | | import com.zy.asrs.entity.DeviceConfig; |
| | | import com.zy.asrs.service.DeviceConfigService; |
| | | import com.zy.asrs.service.DevicePingFileStorageService; |
| | | import jakarta.annotation.PreDestroy; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | |
| | | import java.io.ByteArrayOutputStream; |
| | | import java.io.InputStream; |
| | | import java.nio.charset.Charset; |
| | | import java.util.ArrayList; |
| | | import java.util.Arrays; |
| | | import java.util.Date; |
| | | import java.util.List; |
| | | import java.util.concurrent.ExecutorService; |
| | | import java.util.concurrent.Executors; |
| | | import java.util.concurrent.Future; |
| | | import java.util.concurrent.ThreadFactory; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | @Component |
| | | public class DevicePingScheduler { |
| | | |
| | | private static final Pattern LATENCY_PATTERN = Pattern.compile("(?:time|时间)\\s*[=<]?\\s*([0-9]+(?:[\\.,][0-9]+)?)\\s*(?:ms|毫秒)", Pattern.CASE_INSENSITIVE); |
| | | private static final int OUTPUT_MESSAGE_LIMIT = 120; |
| | | |
| | | @Value("${devicePingStorage.enabled:true}") |
| | | private boolean enabled; |
| | | |
| | | @Value("${devicePingStorage.timeoutMs:800}") |
| | | private int timeoutMs; |
| | | |
| | | @Value("${devicePingStorage.probeCount:3}") |
| | | private int probeCount; |
| | | |
| | | @Value("${devicePingStorage.maxParallel:8}") |
| | | private int maxParallel; |
| | | |
| | | @Value("${devicePingStorage.packetSize:-1}") |
| | | private int packetSize; |
| | | |
| | | @Autowired |
| | | private DeviceConfigService deviceConfigService; |
| | | |
| | | @Autowired |
| | | private DevicePingFileStorageService devicePingFileStorageService; |
| | | |
| | | private final AtomicBoolean running = new AtomicBoolean(false); |
| | | private volatile ExecutorService executorService; |
| | | private volatile ExecutorService probeExecutorService; |
| | | private volatile long lastCleanupAt = 0L; |
| | | |
| | | @Scheduled(fixedDelayString = "${devicePingStorage.intervalMs:1000}") |
| | | public void schedule() { |
| | | if (!enabled) { |
| | | return; |
| | | } |
| | | if (!running.compareAndSet(false, true)) { |
| | | return; |
| | | } |
| | | ensureExecutor().submit(() -> { |
| | | try { |
| | | collectOnce(); |
| | | } finally { |
| | | running.set(false); |
| | | } |
| | | }); |
| | | } |
| | | |
| | | private void collectOnce() { |
| | | List<DeviceConfig> configs = loadConfigs(); |
| | | if (configs.isEmpty()) { |
| | | maybeCleanup(); |
| | | return; |
| | | } |
| | | |
| | | List<Future<DevicePingSample>> futures = new ArrayList<>(); |
| | | for (DeviceConfig config : configs) { |
| | | futures.add(ensureProbeExecutor().submit(() -> probe(config))); |
| | | } |
| | | |
| | | List<DevicePingSample> samples = new ArrayList<>(); |
| | | for (Future<DevicePingSample> future : futures) { |
| | | try { |
| | | DevicePingSample sample = future.get(); |
| | | if (sample != null) { |
| | | samples.add(sample); |
| | | } |
| | | } catch (Exception ignored) { |
| | | } |
| | | } |
| | | devicePingFileStorageService.appendSamples(samples); |
| | | maybeCleanup(); |
| | | } |
| | | |
| | | private List<DeviceConfig> loadConfigs() { |
| | | try { |
| | | QueryWrapper<DeviceConfig> wrapper = new QueryWrapper<DeviceConfig>() |
| | | .isNotNull("ip") |
| | | .ne("ip", "") |
| | | .orderBy(true, true, "device_type", "device_no"); |
| | | return deviceConfigService.list(wrapper); |
| | | } catch (Exception ignored) { |
| | | return new ArrayList<>(); |
| | | } |
| | | } |
| | | |
| | | private DevicePingSample probe(DeviceConfig config) { |
| | | DevicePingSample sample = new DevicePingSample(); |
| | | sample.setCreateTime(new Date()); |
| | | sample.setDeviceType(config.getDeviceType()); |
| | | sample.setDeviceNo(config.getDeviceNo()); |
| | | sample.setIp(config.getIp()); |
| | | sample.setPort(config.getPort()); |
| | | sample.setPacketSize(packetSize); |
| | | int actualProbeCount = Math.max(1, probeCount); |
| | | sample.setProbeCount(actualProbeCount); |
| | | |
| | | List<Long> successLatencies = new ArrayList<>(); |
| | | String lastError = ""; |
| | | int successCount = 0; |
| | | int perProbeTimeoutMs = Math.max(100, timeoutMs / actualProbeCount); |
| | | |
| | | for (int i = 0; i < actualProbeCount; i++) { |
| | | try { |
| | | PingResult pingResult = systemPing(config.getIp(), perProbeTimeoutMs); |
| | | if (pingResult.success) { |
| | | successLatencies.add(pingResult.latencyMs); |
| | | successCount++; |
| | | } else { |
| | | lastError = "第" + (i + 1) + "次探测失败"; |
| | | if (!Cools.isEmpty(pingResult.message)) { |
| | | lastError = lastError + "," + pingResult.message; |
| | | } |
| | | } |
| | | } catch (Exception ex) { |
| | | lastError = Cools.isEmpty(ex.getMessage()) ? ex.getClass().getSimpleName() : ex.getMessage(); |
| | | } |
| | | } |
| | | |
| | | sample.setSuccessProbeCount(successCount); |
| | | sample.setReachable(successCount > 0); |
| | | if (!successLatencies.isEmpty()) { |
| | | long minLatency = Long.MAX_VALUE; |
| | | long maxLatency = Long.MIN_VALUE; |
| | | long latencySum = 0L; |
| | | for (Long latency : successLatencies) { |
| | | if (latency == null) { |
| | | continue; |
| | | } |
| | | latencySum += latency; |
| | | if (latency < minLatency) { |
| | | minLatency = latency; |
| | | } |
| | | if (latency > maxLatency) { |
| | | maxLatency = latency; |
| | | } |
| | | } |
| | | long avgLatency = Math.round(latencySum * 1.0D / successLatencies.size()); |
| | | sample.setLatencyMs(avgLatency); |
| | | sample.setAvgLatencyMs(avgLatency); |
| | | sample.setMinLatencyMs(minLatency); |
| | | sample.setMaxLatencyMs(maxLatency); |
| | | } else { |
| | | sample.setLatencyMs(null); |
| | | sample.setAvgLatencyMs(null); |
| | | sample.setMinLatencyMs(null); |
| | | sample.setMaxLatencyMs(null); |
| | | } |
| | | |
| | | if (successCount == actualProbeCount) { |
| | | sample.setStatus("OK"); |
| | | sample.setMessage(actualProbeCount + "/" + actualProbeCount + " 次探测成功"); |
| | | } else if (successCount > 0) { |
| | | sample.setStatus("UNSTABLE"); |
| | | sample.setMessage(successCount + "/" + actualProbeCount + " 次探测成功" + (Cools.isEmpty(lastError) ? "" : "," + lastError)); |
| | | } else { |
| | | sample.setStatus("TIMEOUT"); |
| | | sample.setMessage(Cools.isEmpty(lastError) ? "全部探测均超时" : lastError); |
| | | } |
| | | return sample; |
| | | } |
| | | |
| | | private PingResult systemPing(String ip, int timeoutMs) throws Exception { |
| | | List<String> command = buildPingCommand(ip, timeoutMs); |
| | | ProcessBuilder processBuilder = new ProcessBuilder(command); |
| | | processBuilder.redirectErrorStream(true); |
| | | if (!isWindows()) { |
| | | processBuilder.environment().put("LC_ALL", "C"); |
| | | processBuilder.environment().put("LANG", "C"); |
| | | } |
| | | |
| | | long start = System.nanoTime(); |
| | | Process process = processBuilder.start(); |
| | | String output; |
| | | try (InputStream inputStream = process.getInputStream()) { |
| | | boolean finished = process.waitFor(Math.max(1L, timeoutMs + 500L), TimeUnit.MILLISECONDS); |
| | | if (!finished) { |
| | | process.destroyForcibly(); |
| | | return new PingResult(false, null, "超时"); |
| | | } |
| | | output = readFully(inputStream); |
| | | } |
| | | |
| | | int exitCode = process.exitValue(); |
| | | Long latencyMs = parseLatencyMs(output); |
| | | if (exitCode == 0) { |
| | | if (latencyMs == null) { |
| | | latencyMs = Math.max(0L, (System.nanoTime() - start) / 1_000_000L); |
| | | } |
| | | return new PingResult(true, latencyMs, "成功"); |
| | | } |
| | | return new PingResult(false, null, extractFailureMessage(output, exitCode)); |
| | | } |
| | | |
| | | private List<String> buildPingCommand(String ip, int timeoutMs) { |
| | | int actualPacketSize = Math.max(-1, packetSize); |
| | | if (isWindows()) { |
| | | List<String> command = new ArrayList<>(Arrays.asList("ping", "-n", "1", "-w", String.valueOf(Math.max(1, timeoutMs)))); |
| | | if (actualPacketSize >= 0) { |
| | | command.add("-l"); |
| | | command.add(String.valueOf(actualPacketSize)); |
| | | } |
| | | command.add(ip); |
| | | return command; |
| | | } |
| | | if (isMac()) { |
| | | List<String> command = new ArrayList<>(Arrays.asList("ping", "-n", "-c", "1", "-W", String.valueOf(Math.max(1, timeoutMs)))); |
| | | if (actualPacketSize >= 0) { |
| | | command.add("-s"); |
| | | command.add(String.valueOf(actualPacketSize)); |
| | | } |
| | | command.add(ip); |
| | | return command; |
| | | } |
| | | int timeoutSec = Math.max(1, (int) Math.ceil(timeoutMs / 1000.0D)); |
| | | List<String> command = new ArrayList<>(Arrays.asList("ping", "-n", "-c", "1", "-W", String.valueOf(timeoutSec))); |
| | | if (actualPacketSize >= 0) { |
| | | command.add("-s"); |
| | | command.add(String.valueOf(actualPacketSize)); |
| | | } |
| | | command.add(ip); |
| | | return command; |
| | | } |
| | | |
| | | private boolean isWindows() { |
| | | return System.getProperty("os.name", "").toLowerCase().startsWith("windows"); |
| | | } |
| | | |
| | | private boolean isMac() { |
| | | String osName = System.getProperty("os.name", "").toLowerCase(); |
| | | return osName.startsWith("mac") || osName.startsWith("darwin"); |
| | | } |
| | | |
| | | private Long parseLatencyMs(String output) { |
| | | if (Cools.isEmpty(output)) { |
| | | return null; |
| | | } |
| | | Matcher matcher = LATENCY_PATTERN.matcher(output.replace(',', '.')); |
| | | if (matcher.find()) { |
| | | try { |
| | | return Math.round(Double.parseDouble(matcher.group(1))); |
| | | } catch (Exception ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String extractFailureMessage(String output, int exitCode) { |
| | | if (Cools.isEmpty(output)) { |
| | | return "exit=" + exitCode; |
| | | } |
| | | String[] lines = output.split("\\R"); |
| | | for (String line : lines) { |
| | | String value = line == null ? "" : line.trim(); |
| | | if (value.isEmpty()) { |
| | | continue; |
| | | } |
| | | String lower = value.toLowerCase(); |
| | | if (lower.contains("request timeout") |
| | | || lower.contains("100.0% packet loss") |
| | | || lower.contains("100% packet loss") |
| | | || lower.contains("destination host unreachable") |
| | | || lower.contains("could not find host") |
| | | || lower.contains("name or service not known") |
| | | || lower.contains("temporary failure in name resolution") |
| | | || value.contains("请求超时") |
| | | || value.contains("找不到主机") |
| | | || value.contains("无法访问目标主机") |
| | | || value.contains("100.0% 丢失")) { |
| | | return trimMessage(value); |
| | | } |
| | | } |
| | | for (String line : lines) { |
| | | String value = line == null ? "" : line.trim(); |
| | | if (!value.isEmpty()) { |
| | | return trimMessage(value); |
| | | } |
| | | } |
| | | return "exit=" + exitCode; |
| | | } |
| | | |
| | | private String trimMessage(String message) { |
| | | if (Cools.isEmpty(message)) { |
| | | return ""; |
| | | } |
| | | String value = message.trim(); |
| | | if (value.length() > OUTPUT_MESSAGE_LIMIT) { |
| | | return value.substring(0, OUTPUT_MESSAGE_LIMIT); |
| | | } |
| | | return value; |
| | | } |
| | | |
| | | private String readFully(InputStream inputStream) throws Exception { |
| | | ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); |
| | | byte[] buffer = new byte[1024]; |
| | | int length; |
| | | while ((length = inputStream.read(buffer)) != -1) { |
| | | outputStream.write(buffer, 0, length); |
| | | } |
| | | return outputStream.toString(Charset.defaultCharset()); |
| | | } |
| | | |
| | | private ExecutorService ensureExecutor() { |
| | | ExecutorService current = executorService; |
| | | if (current != null && !current.isShutdown()) { |
| | | return current; |
| | | } |
| | | synchronized (this) { |
| | | current = executorService; |
| | | if (current == null || current.isShutdown()) { |
| | | executorService = Executors.newSingleThreadExecutor(new NamedThreadFactory("device-ping-schedule-")); |
| | | } |
| | | return executorService; |
| | | } |
| | | } |
| | | |
| | | private ExecutorService ensureProbeExecutor() { |
| | | ExecutorService current = probeExecutorService; |
| | | if (current != null && !current.isShutdown()) { |
| | | return current; |
| | | } |
| | | synchronized (this) { |
| | | current = probeExecutorService; |
| | | if (current == null || current.isShutdown()) { |
| | | probeExecutorService = Executors.newFixedThreadPool(Math.max(1, maxParallel), new NamedThreadFactory("device-ping-probe-")); |
| | | } |
| | | return probeExecutorService; |
| | | } |
| | | } |
| | | |
| | | private void maybeCleanup() { |
| | | long now = System.currentTimeMillis(); |
| | | if (now - lastCleanupAt < 60L * 60L * 1000L) { |
| | | return; |
| | | } |
| | | lastCleanupAt = now; |
| | | devicePingFileStorageService.cleanupExpired(); |
| | | } |
| | | |
| | | @PreDestroy |
| | | public void shutdown() { |
| | | ExecutorService current = executorService; |
| | | if (current != null) { |
| | | current.shutdownNow(); |
| | | } |
| | | ExecutorService probeCurrent = probeExecutorService; |
| | | if (probeCurrent != null) { |
| | | probeCurrent.shutdownNow(); |
| | | } |
| | | } |
| | | |
| | | private static class NamedThreadFactory implements ThreadFactory { |
| | | |
| | | private final String prefix; |
| | | private final AtomicInteger index = new AtomicInteger(1); |
| | | |
| | | private NamedThreadFactory(String prefix) { |
| | | this.prefix = prefix; |
| | | } |
| | | |
| | | @Override |
| | | public Thread newThread(Runnable runnable) { |
| | | Thread thread = new Thread(runnable, prefix + index.getAndIncrement()); |
| | | thread.setDaemon(true); |
| | | return thread; |
| | | } |
| | | } |
| | | |
| | | private static class PingResult { |
| | | |
| | | private final boolean success; |
| | | private final Long latencyMs; |
| | | private final String message; |
| | | |
| | | private PingResult(boolean success, Long latencyMs, String message) { |
| | | this.success = success; |
| | | this.latencyMs = latencyMs; |
| | | this.message = message; |
| | | } |
| | | } |
| | | } |
| | |
| | | import com.zy.core.thread.StationThread; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | |
| | | private LlmCallLogService llmCallLogService; |
| | | @Autowired |
| | | private AiChatSessionMapper aiChatSessionMapper; |
| | | @Autowired |
| | | private DevicePingFileStorageService devicePingFileStorageService; |
| | | |
| | | @Value("${devicePingStorage.intervalMs:1000}") |
| | | private int devicePingIntervalMs; |
| | | @Value("${devicePingStorage.timeoutMs:800}") |
| | | private int devicePingTimeoutMs; |
| | | @Value("${devicePingStorage.probeCount:3}") |
| | | private int devicePingProbeCount; |
| | | @Value("${devicePingStorage.packetSize:-1}") |
| | | private int devicePingPacketSize; |
| | | |
| | | @GetMapping("/summary/auth") |
| | | @ManagerAuth(memo = "系统仪表盘统计") |
| | | public R summary() { |
| | | Map<String, Object> tasks = buildTaskStats(); |
| | | Map<String, Object> devices = buildDeviceStats(); |
| | | Map<String, Object> network = buildNetworkStats(); |
| | | Map<String, Object> ai = buildAiStats(); |
| | | |
| | | Map<String, Object> overview = new LinkedHashMap<>(); |
| | |
| | | result.put("overview", overview); |
| | | result.put("tasks", tasks); |
| | | result.put("devices", devices); |
| | | result.put("network", network); |
| | | result.put("ai", ai); |
| | | return R.ok(result); |
| | | } |
| | |
| | | return result; |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | private Map<String, Object> buildNetworkStats() { |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | Map<String, Object> overview = new LinkedHashMap<>(); |
| | | overview.put("totalDevices", 0L); |
| | | overview.put("okDevices", 0L); |
| | | overview.put("unstableDevices", 0L); |
| | | overview.put("offlineDevices", 0L); |
| | | overview.put("noDataDevices", 0L); |
| | | overview.put("attentionDevices", 0L); |
| | | overview.put("avgLatencyMs", null); |
| | | overview.put("maxLatencyMs", null); |
| | | |
| | | Map<String, Object> samplingConfig = new LinkedHashMap<>(); |
| | | samplingConfig.put("intervalMs", devicePingIntervalMs); |
| | | samplingConfig.put("timeoutMs", devicePingTimeoutMs); |
| | | samplingConfig.put("probeCount", devicePingProbeCount); |
| | | samplingConfig.put("packetSize", Math.max(devicePingPacketSize, -1)); |
| | | |
| | | List<Map<String, Object>> statusStats = new ArrayList<>(); |
| | | statusStats.add(metric("正常", 0L)); |
| | | statusStats.add(metric("波动", 0L)); |
| | | statusStats.add(metric("超时/异常", 0L)); |
| | | statusStats.add(metric("暂无数据", 0L)); |
| | | |
| | | List<Map<String, Object>> focusDevices = new ArrayList<>(); |
| | | |
| | | try { |
| | | Map<String, Object> overviewResult = devicePingFileStorageService.queryOverview(listPingConfigs()); |
| | | Map<String, Object> summary = overviewResult.get("summary") instanceof Map |
| | | ? (Map<String, Object>) overviewResult.get("summary") |
| | | : Collections.emptyMap(); |
| | | List<Map<String, Object>> devices = overviewResult.get("devices") instanceof List |
| | | ? (List<Map<String, Object>>) overviewResult.get("devices") |
| | | : Collections.emptyList(); |
| | | |
| | | long okDevices = toLong(summary.get("okDevices")); |
| | | long unstableDevices = toLong(summary.get("unstableDevices")); |
| | | long offlineDevices = toLong(summary.get("offlineDevices")); |
| | | long noDataDevices = toLong(summary.get("noDataDevices")); |
| | | |
| | | overview.put("totalDevices", toLong(summary.get("totalDevices"))); |
| | | overview.put("okDevices", okDevices); |
| | | overview.put("unstableDevices", unstableDevices); |
| | | overview.put("offlineDevices", offlineDevices); |
| | | overview.put("noDataDevices", noDataDevices); |
| | | overview.put("attentionDevices", unstableDevices + offlineDevices + noDataDevices); |
| | | overview.put("avgLatencyMs", summary.get("avgLatencyMs")); |
| | | overview.put("maxLatencyMs", summary.get("maxLatencyMs")); |
| | | |
| | | statusStats = new ArrayList<>(); |
| | | statusStats.add(metric("正常", okDevices)); |
| | | statusStats.add(metric("波动", unstableDevices)); |
| | | statusStats.add(metric("超时/异常", offlineDevices)); |
| | | statusStats.add(metric("暂无数据", noDataDevices)); |
| | | |
| | | focusDevices = buildNetworkFocusDevices(devices); |
| | | } catch (Exception e) { |
| | | log.warn("dashboard network stats load failed: {}", safeMessage(e)); |
| | | } |
| | | |
| | | result.put("overview", overview); |
| | | result.put("samplingConfig", samplingConfig); |
| | | result.put("statusStats", statusStats); |
| | | result.put("focusDevices", focusDevices); |
| | | return result; |
| | | } |
| | | |
| | | private List<DeviceConfig> listDeviceConfig(SlaveType type) { |
| | | try { |
| | | return deviceConfigService.list(new QueryWrapper<DeviceConfig>() |
| | |
| | | } |
| | | } |
| | | |
| | | private List<DeviceConfig> listPingConfigs() { |
| | | try { |
| | | return deviceConfigService.list(new QueryWrapper<DeviceConfig>() |
| | | .isNotNull("ip") |
| | | .ne("ip", "") |
| | | .orderBy(true, true, "device_type", "device_no")); |
| | | } catch (Exception e) { |
| | | log.warn("dashboard ping device config load failed: {}", safeMessage(e)); |
| | | return Collections.emptyList(); |
| | | } |
| | | } |
| | | |
| | | private List<Map<String, Object>> buildNetworkFocusDevices(List<Map<String, Object>> devices) { |
| | | if (devices == null || devices.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | List<Map<String, Object>> candidates = new ArrayList<>(); |
| | | for (Map<String, Object> row : devices) { |
| | | if (resolveNetworkFocusRank(row) < 3) { |
| | | candidates.add(row); |
| | | } |
| | | } |
| | | if (candidates.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | |
| | | candidates.sort((left, right) -> { |
| | | int rankDiff = Integer.compare(resolveNetworkFocusRank(left), resolveNetworkFocusRank(right)); |
| | | if (rankDiff != 0) { |
| | | return rankDiff; |
| | | } |
| | | int latencyDiff = Long.compare(toLong(right.get("avgLatencyMs")), toLong(left.get("avgLatencyMs"))); |
| | | if (latencyDiff != 0) { |
| | | return latencyDiff; |
| | | } |
| | | return Long.compare(toLong(right.get("latestTime")), toLong(left.get("latestTime"))); |
| | | }); |
| | | |
| | | List<Map<String, Object>> result = new ArrayList<>(); |
| | | for (Map<String, Object> item : candidates) { |
| | | if (item == null) { |
| | | continue; |
| | | } |
| | | Map<String, Object> focus = new LinkedHashMap<>(); |
| | | focus.put("name", toText(item.get("deviceType")) + "-" + toText(item.get("deviceNo"))); |
| | | focus.put("ip", toText(item.get("ip"))); |
| | | focus.put("statusText", defaultText(toText(item.get("statusText")), "未知")); |
| | | focus.put("statusType", resolveNetworkStatusTagType(toText(item.get("status")))); |
| | | focus.put("avgLatencyMs", item.get("avgLatencyMs")); |
| | | focus.put("latestTimeLabel", defaultText(toText(item.get("latestTimeLabel")), "--")); |
| | | focus.put("message", defaultText(toText(item.get("message")), "暂无额外说明")); |
| | | result.add(focus); |
| | | if (result.size() >= 4) { |
| | | break; |
| | | } |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private int resolveNetworkFocusRank(Map<String, Object> row) { |
| | | String status = row == null ? "" : toText(row.get("status")); |
| | | if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) { |
| | | return 0; |
| | | } |
| | | if ("UNSTABLE".equalsIgnoreCase(status)) { |
| | | return 1; |
| | | } |
| | | if ("NO_DATA".equalsIgnoreCase(status)) { |
| | | return 2; |
| | | } |
| | | return 3; |
| | | } |
| | | |
| | | private String resolveNetworkStatusTagType(String status) { |
| | | if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) { |
| | | return "danger"; |
| | | } |
| | | if ("UNSTABLE".equalsIgnoreCase(status)) { |
| | | return "warning"; |
| | | } |
| | | if ("OK".equalsIgnoreCase(status)) { |
| | | return "success"; |
| | | } |
| | | return "info"; |
| | | } |
| | | |
| | | private boolean isInboundTask(Long wrkSts) { |
| | | return wrkSts != null && wrkSts > 0 && wrkSts < 100; |
| | | } |
| | |
| | | # 日志过期时间 单位天 |
| | | expireDays: 7 |
| | | |
| | | devicePingStorage: |
| | | enabled: true |
| | | # 秒级设备网络探测日志存储地址 |
| | | loggingPath: ./stock/out/@pom.build.finalName@/devicePingLogs |
| | | # 日志过期时间 单位天 |
| | | expireDays: 7 |
| | | # 采样周期(毫秒) |
| | | intervalMs: 1000 |
| | | # 单次探测超时(毫秒) |
| | | timeoutMs: 800 |
| | | # 每个样本内连续探测次数,用于直接得到 min/avg/max 三个指标 |
| | | probeCount: 3 |
| | | # ping 数据包大小(字节),< 0 时沿用系统默认;Windows 对应 -l,macOS/Linux 对应 -s |
| | | packetSize: 1024 |
| | | # 并行探测线程数 |
| | | maxParallel: 8 |
| | | |
| | | llm: |
| | | # 现已迁移到数据库表 sys_llm_route 维护(支持多API/多模型/多Key自动切换) |
| | | # 以下仅作为数据库为空时的兼容回退配置 |
| New file |
| | |
| | | -- 将 设备网络分析 菜单挂载到:日志报表(优先)或开发专用 |
| | | -- 说明:执行本脚本后,请在“角色授权”里给对应角色勾选新菜单和“查看”权限。 |
| | | |
| | | SET @device_ping_parent_id := COALESCE( |
| | | ( |
| | | SELECT id |
| | | FROM sys_resource |
| | | WHERE code = 'logReport' AND level = 1 |
| | | ORDER BY id |
| | | LIMIT 1 |
| | | ), |
| | | ( |
| | | SELECT id |
| | | FROM sys_resource |
| | | WHERE code = 'develop' AND level = 1 |
| | | ORDER BY id |
| | | LIMIT 1 |
| | | ) |
| | | ); |
| | | |
| | | INSERT INTO sys_resource(code, name, resource_id, level, sort, status) |
| | | SELECT 'devicePingLog/devicePingLog.html', '设备网络分析', @device_ping_parent_id, 2, 997, 1 |
| | | FROM dual |
| | | WHERE @device_ping_parent_id IS NOT NULL |
| | | AND NOT EXISTS ( |
| | | SELECT 1 |
| | | FROM sys_resource |
| | | WHERE code = 'devicePingLog/devicePingLog.html' AND level = 2 |
| | | ); |
| | | |
| | | UPDATE sys_resource |
| | | SET name = '设备网络分析', |
| | | resource_id = @device_ping_parent_id, |
| | | level = 2, |
| | | sort = 997, |
| | | status = 1 |
| | | WHERE code = 'devicePingLog/devicePingLog.html' AND level = 2; |
| | | |
| | | SET @device_ping_id := ( |
| | | SELECT id |
| | | FROM sys_resource |
| | | WHERE code = 'devicePingLog/devicePingLog.html' AND level = 2 |
| | | ORDER BY id |
| | | LIMIT 1 |
| | | ); |
| | | |
| | | INSERT INTO sys_resource(code, name, resource_id, level, sort, status) |
| | | SELECT 'devicePingLog/devicePingLog.html#view', '查看', @device_ping_id, 3, 1, 1 |
| | | FROM dual |
| | | WHERE @device_ping_id IS NOT NULL |
| | | AND NOT EXISTS ( |
| | | SELECT 1 |
| | | FROM sys_resource |
| | | WHERE code = 'devicePingLog/devicePingLog.html#view' AND level = 3 |
| | | ); |
| | | |
| | | UPDATE sys_resource |
| | | SET name = '查看', |
| | | resource_id = @device_ping_id, |
| | | level = 3, |
| | | sort = 1, |
| | | status = 1 |
| | | WHERE code = 'devicePingLog/devicePingLog.html#view' AND level = 3; |
| | | |
| | | SELECT id, code, name, resource_id, level, sort, status |
| | | FROM sys_resource |
| | | WHERE code IN ( |
| | | 'devicePingLog/devicePingLog.html', |
| | | 'devicePingLog/devicePingLog.html#view' |
| | | ) |
| | | ORDER BY level, sort, id; |
| | |
| | | taskDirection: null, |
| | | taskStage: null, |
| | | deviceType: null, |
| | | networkStatus: null, |
| | | aiRoute: null |
| | | }, |
| | | overview: { |
| | |
| | | onlineRate: 0 |
| | | }, |
| | | typeStats: [] |
| | | }, |
| | | network: { |
| | | overview: { |
| | | totalDevices: 0, |
| | | okDevices: 0, |
| | | unstableDevices: 0, |
| | | offlineDevices: 0, |
| | | noDataDevices: 0, |
| | | attentionDevices: 0, |
| | | avgLatencyMs: null, |
| | | maxLatencyMs: null |
| | | }, |
| | | samplingConfig: { |
| | | intervalMs: 1000, |
| | | timeoutMs: 800, |
| | | probeCount: 3, |
| | | packetSize: -1 |
| | | }, |
| | | statusStats: [], |
| | | focusDevices: [] |
| | | }, |
| | | ai: { |
| | | overview: { |
| | |
| | | this.overview = payload.overview || this.overview; |
| | | this.tasks = payload.tasks || this.tasks; |
| | | this.devices = payload.devices || this.devices; |
| | | this.network = payload.network || this.network; |
| | | this.ai = payload.ai || this.ai; |
| | | this.updateCharts(); |
| | | }, |
| | |
| | | this.charts.taskDirection = echarts.init(this.$refs.taskDirectionChart); |
| | | this.charts.taskStage = echarts.init(this.$refs.taskStageChart); |
| | | this.charts.deviceType = echarts.init(this.$refs.deviceTypeChart); |
| | | this.charts.networkStatus = echarts.init(this.$refs.networkStatusChart); |
| | | this.charts.aiRoute = echarts.init(this.$refs.aiRouteChart); |
| | | }, |
| | | updateCharts: function () { |
| | | if (!this.charts.taskDirection || !this.charts.taskStage || !this.charts.deviceType || !this.charts.aiRoute) { |
| | | if (!this.charts.taskDirection || !this.charts.taskStage || !this.charts.deviceType || !this.charts.networkStatus || !this.charts.aiRoute) { |
| | | return; |
| | | } |
| | | |
| | | this.charts.taskDirection.setOption(this.buildTaskDirectionOption()); |
| | | this.charts.taskStage.setOption(this.buildTaskStageOption()); |
| | | this.charts.deviceType.setOption(this.buildDeviceTypeOption()); |
| | | this.charts.networkStatus.setOption(this.buildNetworkStatusOption()); |
| | | this.charts.aiRoute.setOption(this.buildAiRouteOption()); |
| | | this.resizeCharts(); |
| | | }, |
| | |
| | | }] |
| | | }; |
| | | }, |
| | | buildNetworkStatusOption: function () { |
| | | var data = cloneMetricList(this.network.statusStats); |
| | | return { |
| | | color: ["#2fa38e", "#f59a4a", "#de5c5c", "#c8d4e1"], |
| | | tooltip: { |
| | | trigger: "item", |
| | | formatter: "{b}<br/>{c} ({d}%)" |
| | | }, |
| | | graphic: [{ |
| | | type: "text", |
| | | left: "center", |
| | | top: "38%", |
| | | style: { |
| | | text: this.formatNumber(this.network.overview.attentionDevices || 0), |
| | | fill: "#1f3142", |
| | | fontSize: 26, |
| | | fontWeight: 700 |
| | | } |
| | | }, { |
| | | type: "text", |
| | | left: "center", |
| | | top: "54%", |
| | | style: { |
| | | text: "需关注设备", |
| | | fill: "#7c8fa4", |
| | | fontSize: 12 |
| | | } |
| | | }], |
| | | legend: { |
| | | bottom: 0, |
| | | itemWidth: 10, |
| | | itemHeight: 10, |
| | | textStyle: { color: "#60778d", fontSize: 12 } |
| | | }, |
| | | series: [{ |
| | | type: "pie", |
| | | radius: ["55%", "75%"], |
| | | center: ["50%", "42%"], |
| | | avoidLabelOverlap: false, |
| | | label: { show: false }, |
| | | labelLine: { show: false }, |
| | | data: data |
| | | }] |
| | | }; |
| | | }, |
| | | formatNumber: function (value) { |
| | | var num = Number(value || 0); |
| | | if (!isFinite(num)) { |
| | |
| | | } |
| | | return num.toLocaleString("zh-CN"); |
| | | }, |
| | | formatLatency: function (value) { |
| | | var num; |
| | | if (value == null || value === "") { |
| | | return "--"; |
| | | } |
| | | num = Number(value); |
| | | if (!isFinite(num)) { |
| | | return "--"; |
| | | } |
| | | return num.toLocaleString("zh-CN", { maximumFractionDigits: 2 }) + " ms"; |
| | | }, |
| | | formatPacketSize: function (value) { |
| | | var num = Number(value); |
| | | if (!isFinite(num) || num < 0) { |
| | | return "系统默认"; |
| | | } |
| | | return num + " B"; |
| | | }, |
| | | displayText: function (value, fallback) { |
| | | return value == null || value === "" ? (fallback || "") : value; |
| | | }, |
| | | networkSamplingText: function () { |
| | | var config = this.network && this.network.samplingConfig ? this.network.samplingConfig : {}; |
| | | return "采样 " + this.displayText(config.intervalMs, 0) + " ms / 超时 " + this.displayText(config.timeoutMs, 0) + |
| | | " ms / 每样本 " + this.displayText(config.probeCount, 0) + " 次 / 包大小 " + this.formatPacketSize(config.packetSize); |
| | | }, |
| | | startAutoRefresh: function () { |
| | | var self = this; |
| | |
| | | } |
| | | } |
| | | }, |
| | | openMonitor: function () { |
| | | resolveParentMenuApp: function () { |
| | | var parentDocument; |
| | | var parentRoot; |
| | | try { |
| | | if (!window.parent || window.parent === window || !window.parent.document) { |
| | | return null; |
| | | } |
| | | parentDocument = window.parent.document; |
| | | parentRoot = parentDocument.getElementById("app"); |
| | | return parentRoot && parentRoot.__vue__ ? parentRoot.__vue__ : null; |
| | | } catch (e) { |
| | | return null; |
| | | } |
| | | }, |
| | | resolveAbsoluteViewPath: function (path) { |
| | | if (!path) { |
| | | return ""; |
| | | } |
| | | if (/^https?:\/\//.test(path) || path.indexOf(baseUrl) === 0) { |
| | | return path; |
| | | } |
| | | if (path.indexOf("/views/") === 0) { |
| | | return baseUrl + path; |
| | | } |
| | | if (path.indexOf("views/") === 0) { |
| | | return baseUrl + "/" + path; |
| | | } |
| | | return baseUrl + "/views/" + path.replace(/^\/+/, ""); |
| | | }, |
| | | findParentMenuByPath: function (targetPath, preferredName, preferredGroup) { |
| | | var parentApp = this.resolveParentMenuApp(); |
| | | var targetBasePath = this.resolveAbsoluteViewPath(targetPath).split("#")[0].split("?")[0]; |
| | | var fallback = null; |
| | | var i; |
| | | var j; |
| | | var group; |
| | | var item; |
| | | var itemBasePath; |
| | | |
| | | if (!parentApp || !Array.isArray(parentApp.menus)) { |
| | | return null; |
| | | } |
| | | |
| | | for (i = 0; i < parentApp.menus.length; i++) { |
| | | group = parentApp.menus[i]; |
| | | for (j = 0; j < (group.subMenu || []).length; j++) { |
| | | item = group.subMenu[j]; |
| | | if (!item || !item.url) { |
| | | continue; |
| | | } |
| | | itemBasePath = item.url.split("#")[0].split("?")[0]; |
| | | if (!fallback && ((preferredName && item.name === preferredName) || itemBasePath === targetBasePath)) { |
| | | fallback = item; |
| | | } |
| | | if ((!preferredGroup || group.menu === preferredGroup) && itemBasePath === targetBasePath) { |
| | | return item; |
| | | } |
| | | } |
| | | } |
| | | |
| | | return fallback; |
| | | }, |
| | | openParentMenuView: function (targetPath, fallbackName, preferredName, preferredGroup) { |
| | | var targetMenu = this.findParentMenuByPath(targetPath, preferredName || fallbackName, preferredGroup); |
| | | var menuPath = targetMenu && targetMenu.url ? targetMenu.url : targetPath; |
| | | var menuName = targetMenu && targetMenu.name ? targetMenu.name : (fallbackName || preferredName || ""); |
| | | if (window.parent && window.parent.index && typeof window.parent.index.loadView === "function") { |
| | | window.parent.index.loadView({ |
| | | menuPath: "/views/watch/console.html", |
| | | menuName: "监控工作台" |
| | | menuPath: menuPath, |
| | | menuName: menuName |
| | | }); |
| | | return; |
| | | } |
| | | window.open(baseUrl + "/views/watch/console.html", "_blank"); |
| | | window.open(targetMenu && targetMenu.url ? targetMenu.url : this.resolveAbsoluteViewPath(targetPath), "_blank"); |
| | | }, |
| | | openMonitor: function () { |
| | | this.openParentMenuView("/views/watch/console.html", "监控画面", "监控画面", "监控系统"); |
| | | }, |
| | | openDevicePingAnalysis: function () { |
| | | this.openParentMenuView("/views/devicePingLog/devicePingLog.html", "设备网络分析", "设备网络分析"); |
| | | } |
| | | } |
| | | }); |
| New file |
| | |
| | | (function () { |
| | | "use strict"; |
| | | |
| | | function nowDate() { |
| | | return new Date(); |
| | | } |
| | | |
| | | function minutesAgo(minutes) { |
| | | return new Date(Date.now() - minutes * 60 * 1000); |
| | | } |
| | | |
| | | function createEmptyOverviewSummary() { |
| | | return { |
| | | totalDevices: 0, |
| | | okDevices: 0, |
| | | unstableDevices: 0, |
| | | offlineDevices: 0, |
| | | noDataDevices: 0, |
| | | avgLatencyMs: null, |
| | | maxLatencyMs: null |
| | | }; |
| | | } |
| | | |
| | | function createEmptyDetailSummary() { |
| | | return { |
| | | totalSamples: 0, |
| | | successSamples: 0, |
| | | failSamples: 0, |
| | | successRate: 0, |
| | | avgLatencyMs: null, |
| | | minLatencyMs: null, |
| | | maxLatencyMs: null, |
| | | packetSize: -1, |
| | | latestStatus: "", |
| | | latestTimeLabel: "" |
| | | }; |
| | | } |
| | | |
| | | function createEmptySamplingConfig() { |
| | | return { |
| | | intervalMs: 1000, |
| | | timeoutMs: 800, |
| | | probeCount: 3, |
| | | packetSize: -1 |
| | | }; |
| | | } |
| | | |
| | | new Vue({ |
| | | el: "#app", |
| | | data: function () { |
| | | return { |
| | | overviewLoading: false, |
| | | detailLoading: false, |
| | | devices: [], |
| | | availableDays: [], |
| | | samplingConfig: createEmptySamplingConfig(), |
| | | overviewSummary: createEmptyOverviewSummary(), |
| | | overviewRows: [], |
| | | overviewFilters: { |
| | | deviceType: "", |
| | | keyword: "" |
| | | }, |
| | | detailFilters: { |
| | | deviceKey: "", |
| | | range: [minutesAgo(30), nowDate()] |
| | | }, |
| | | detailSummary: createEmptyDetailSummary(), |
| | | series: [], |
| | | alerts: [], |
| | | charts: { |
| | | latency: null, |
| | | availability: null |
| | | }, |
| | | resizeHandler: null |
| | | }; |
| | | }, |
| | | computed: { |
| | | currentDevice: function () { |
| | | var key = this.detailFilters.deviceKey; |
| | | if (!key) { |
| | | return null; |
| | | } |
| | | for (var i = 0; i < this.devices.length; i++) { |
| | | var item = this.devices[i]; |
| | | if ((item.deviceType + "#" + item.deviceNo) === key) { |
| | | return item; |
| | | } |
| | | } |
| | | for (var j = 0; j < this.overviewRows.length; j++) { |
| | | var row = this.overviewRows[j]; |
| | | if ((row.deviceType + "#" + row.deviceNo) === key) { |
| | | return row; |
| | | } |
| | | } |
| | | return null; |
| | | }, |
| | | filteredOverviewRows: function () { |
| | | var type = this.overviewFilters.deviceType; |
| | | var keyword = (this.overviewFilters.keyword || "").toLowerCase(); |
| | | return this.overviewRows.filter(function (item) { |
| | | if (type && item.deviceType !== type) { |
| | | return false; |
| | | } |
| | | if (!keyword) { |
| | | return true; |
| | | } |
| | | var deviceText = (item.deviceType + "-" + item.deviceNo).toLowerCase(); |
| | | var ipText = (item.ip || "").toLowerCase(); |
| | | return deviceText.indexOf(keyword) >= 0 || ipText.indexOf(keyword) >= 0; |
| | | }); |
| | | }, |
| | | samplingConfigText: function () { |
| | | var config = this.samplingConfig || createEmptySamplingConfig(); |
| | | return "采样 " + config.intervalMs + " ms / 超时 " + config.timeoutMs + " ms / 每样本 " + config.probeCount + " 次"; |
| | | } |
| | | }, |
| | | mounted: function () { |
| | | var self = this; |
| | | this.$nextTick(function () { |
| | | self.loadOptions(); |
| | | self.loadOverview(); |
| | | self.resizeHandler = function () { |
| | | self.resizeCharts(); |
| | | }; |
| | | window.addEventListener("resize", self.resizeHandler); |
| | | }); |
| | | }, |
| | | beforeDestroy: function () { |
| | | if (this.resizeHandler) { |
| | | window.removeEventListener("resize", this.resizeHandler); |
| | | } |
| | | this.disposeCharts(); |
| | | }, |
| | | methods: { |
| | | loadOptions: function () { |
| | | var self = this; |
| | | $.ajax({ |
| | | url: baseUrl + "/devicePingLog/options/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | method: "GET", |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | var data = res.data || {}; |
| | | self.devices = data.devices || []; |
| | | self.availableDays = data.days || []; |
| | | self.samplingConfig = Object.assign(createEmptySamplingConfig(), data.samplingConfig || {}); |
| | | return; |
| | | } |
| | | self.$message.error((res && res.msg) || "设备配置加载失败"); |
| | | }, |
| | | error: function () { |
| | | self.$message.error("设备配置加载失败"); |
| | | } |
| | | }); |
| | | }, |
| | | loadOverview: function () { |
| | | var self = this; |
| | | this.overviewLoading = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/devicePingLog/overview/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | method: "GET", |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | var data = res.data || {}; |
| | | self.overviewSummary = Object.assign(createEmptyOverviewSummary(), data.summary || {}); |
| | | self.overviewRows = data.devices || []; |
| | | return; |
| | | } |
| | | self.overviewSummary = createEmptyOverviewSummary(); |
| | | self.overviewRows = []; |
| | | self.$message.error((res && res.msg) || "总览数据加载失败"); |
| | | }, |
| | | error: function () { |
| | | self.overviewSummary = createEmptyOverviewSummary(); |
| | | self.overviewRows = []; |
| | | self.$message.error("总览数据加载失败"); |
| | | }, |
| | | complete: function () { |
| | | self.overviewLoading = false; |
| | | } |
| | | }); |
| | | }, |
| | | openDetail: function (row) { |
| | | var self = this; |
| | | if (!row) { |
| | | return; |
| | | } |
| | | this.detailFilters.deviceKey = row.deviceType + "#" + row.deviceNo; |
| | | this.setQuickRange(30, false); |
| | | this.$nextTick(function () { |
| | | self.ensureCharts(); |
| | | self.queryTrend(); |
| | | }); |
| | | }, |
| | | setQuickRange: function (minutes, autoQuery) { |
| | | this.detailFilters.range = [minutesAgo(minutes), nowDate()]; |
| | | if (autoQuery !== false && this.currentDevice) { |
| | | this.queryTrend(); |
| | | } |
| | | }, |
| | | queryTrend: function () { |
| | | if (!this.currentDevice) { |
| | | return; |
| | | } |
| | | if (!this.detailFilters.range || this.detailFilters.range.length !== 2) { |
| | | this.$message.warning("请选择时间范围"); |
| | | return; |
| | | } |
| | | var parts = this.detailFilters.deviceKey.split("#"); |
| | | if (parts.length !== 2) { |
| | | this.$message.warning("设备信息无效"); |
| | | return; |
| | | } |
| | | var self = this; |
| | | this.detailLoading = true; |
| | | $.ajax({ |
| | | url: baseUrl + "/devicePingLog/trend/auth", |
| | | headers: { token: localStorage.getItem("token") }, |
| | | method: "GET", |
| | | data: { |
| | | deviceType: parts[0], |
| | | deviceNo: parts[1], |
| | | startTime: this.detailFilters.range[0].getTime(), |
| | | endTime: this.detailFilters.range[1].getTime() |
| | | }, |
| | | success: function (res) { |
| | | if (res && res.code === 200) { |
| | | var data = res.data || {}; |
| | | self.detailSummary = Object.assign(createEmptyDetailSummary(), data.summary || {}); |
| | | self.series = data.series || []; |
| | | self.alerts = data.alerts || []; |
| | | self.updateCharts(); |
| | | return; |
| | | } |
| | | self.detailSummary = createEmptyDetailSummary(); |
| | | self.series = []; |
| | | self.alerts = []; |
| | | self.updateCharts(); |
| | | self.$message.error((res && res.msg) || "设备详情加载失败"); |
| | | }, |
| | | error: function () { |
| | | self.detailSummary = createEmptyDetailSummary(); |
| | | self.series = []; |
| | | self.alerts = []; |
| | | self.updateCharts(); |
| | | self.$message.error("设备详情加载失败"); |
| | | }, |
| | | complete: function () { |
| | | self.detailLoading = false; |
| | | } |
| | | }); |
| | | }, |
| | | ensureCharts: function () { |
| | | if (this.$refs.latencyChart && !this.charts.latency) { |
| | | this.charts.latency = echarts.init(this.$refs.latencyChart); |
| | | } |
| | | if (this.$refs.availabilityChart && !this.charts.availability) { |
| | | this.charts.availability = echarts.init(this.$refs.availabilityChart); |
| | | } |
| | | }, |
| | | disposeCharts: function () { |
| | | if (this.charts.latency) { |
| | | this.charts.latency.dispose(); |
| | | this.charts.latency = null; |
| | | } |
| | | if (this.charts.availability) { |
| | | this.charts.availability.dispose(); |
| | | this.charts.availability = null; |
| | | } |
| | | }, |
| | | resizeCharts: function () { |
| | | if (this.charts.latency) { |
| | | this.charts.latency.resize(); |
| | | } |
| | | if (this.charts.availability) { |
| | | this.charts.availability.resize(); |
| | | } |
| | | }, |
| | | updateCharts: function () { |
| | | this.ensureCharts(); |
| | | if (!this.charts.latency || !this.charts.availability) { |
| | | return; |
| | | } |
| | | if (!this.series.length) { |
| | | this.charts.latency.clear(); |
| | | this.charts.availability.clear(); |
| | | return; |
| | | } |
| | | this.charts.latency.setOption(this.buildLatencyOption(), true); |
| | | this.charts.availability.setOption(this.buildAvailabilityOption(), true); |
| | | this.resizeCharts(); |
| | | }, |
| | | buildLatencyOption: function () { |
| | | var xAxisData = this.series.map(function (item) { return item.timeLabel; }); |
| | | return { |
| | | color: ["#1f6fb2", "#2fa38e", "#f59a4a"], |
| | | tooltip: { |
| | | trigger: "axis" |
| | | }, |
| | | legend: { |
| | | top: 0, |
| | | textStyle: { color: "#5e738a" } |
| | | }, |
| | | grid: { |
| | | left: 54, |
| | | right: 18, |
| | | top: 40, |
| | | bottom: 54 |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: xAxisData, |
| | | boundaryGap: false, |
| | | axisLine: { lineStyle: { color: "#d5dfeb" } }, |
| | | axisLabel: { |
| | | color: "#74879a", |
| | | formatter: function (value) { |
| | | return value.slice(11); |
| | | } |
| | | } |
| | | }, |
| | | yAxis: { |
| | | type: "value", |
| | | name: "ms", |
| | | splitLine: { lineStyle: { color: "#edf2f7" } }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: "#7f92a5" } |
| | | }, |
| | | dataZoom: [{ |
| | | type: "inside" |
| | | }, { |
| | | type: "slider", |
| | | height: 18, |
| | | bottom: 10 |
| | | }], |
| | | series: [{ |
| | | name: "平均", |
| | | type: "line", |
| | | showSymbol: false, |
| | | sampling: "lttb", |
| | | data: this.series.map(function (item) { return item.avgLatencyMs; }), |
| | | lineStyle: { width: 2 } |
| | | }, { |
| | | name: "最小", |
| | | type: "line", |
| | | showSymbol: false, |
| | | sampling: "lttb", |
| | | data: this.series.map(function (item) { return item.minLatencyMs; }), |
| | | lineStyle: { width: 1.5 } |
| | | }, { |
| | | name: "最大", |
| | | type: "line", |
| | | showSymbol: false, |
| | | sampling: "lttb", |
| | | data: this.series.map(function (item) { return item.maxLatencyMs; }), |
| | | lineStyle: { width: 1.5, type: "dashed" } |
| | | }] |
| | | }; |
| | | }, |
| | | buildAvailabilityOption: function () { |
| | | var xAxisData = this.series.map(function (item) { return item.timeLabel; }); |
| | | return { |
| | | color: ["#de5c5c", "#2fa38e"], |
| | | tooltip: { |
| | | trigger: "axis" |
| | | }, |
| | | legend: { |
| | | top: 0, |
| | | textStyle: { color: "#5e738a" } |
| | | }, |
| | | grid: { |
| | | left: 48, |
| | | right: 50, |
| | | top: 40, |
| | | bottom: 54 |
| | | }, |
| | | xAxis: { |
| | | type: "category", |
| | | data: xAxisData, |
| | | axisLine: { lineStyle: { color: "#d5dfeb" } }, |
| | | axisLabel: { |
| | | color: "#74879a", |
| | | formatter: function (value) { |
| | | return value.slice(11); |
| | | } |
| | | } |
| | | }, |
| | | yAxis: [{ |
| | | type: "value", |
| | | name: "失败", |
| | | splitLine: { lineStyle: { color: "#edf2f7" } }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: "#7f92a5" } |
| | | }, { |
| | | type: "value", |
| | | name: "成功率%", |
| | | min: 0, |
| | | max: 100, |
| | | splitLine: { show: false }, |
| | | axisLine: { show: false }, |
| | | axisTick: { show: false }, |
| | | axisLabel: { color: "#7f92a5" } |
| | | }], |
| | | dataZoom: [{ |
| | | type: "inside" |
| | | }, { |
| | | type: "slider", |
| | | height: 18, |
| | | bottom: 10 |
| | | }], |
| | | series: [{ |
| | | name: "失败次数", |
| | | type: "bar", |
| | | yAxisIndex: 0, |
| | | barMaxWidth: 18, |
| | | data: this.series.map(function (item) { return item.failProbeCount; }), |
| | | itemStyle: { |
| | | borderRadius: [6, 6, 0, 0] |
| | | } |
| | | }, { |
| | | name: "成功率", |
| | | type: "line", |
| | | yAxisIndex: 1, |
| | | showSymbol: false, |
| | | sampling: "lttb", |
| | | data: this.series.map(function (item) { return item.successRate; }), |
| | | lineStyle: { width: 2 } |
| | | }] |
| | | }; |
| | | }, |
| | | statusClass: function (status) { |
| | | if (status === "OK") { |
| | | return "ok"; |
| | | } |
| | | if (status === "UNSTABLE") { |
| | | return "unstable"; |
| | | } |
| | | if (status === "NO_DATA") { |
| | | return "nodata"; |
| | | } |
| | | return "offline"; |
| | | }, |
| | | formatLatency: function (value) { |
| | | if (value === null || value === undefined || value === "") { |
| | | return "--"; |
| | | } |
| | | return value + " ms"; |
| | | }, |
| | | formatPercent: function (value) { |
| | | if (value === null || value === undefined || value === "") { |
| | | return "--"; |
| | | } |
| | | return value + "%"; |
| | | }, |
| | | formatPacketSize: function (value) { |
| | | if (value === null || value === undefined || value === "" || value < 0) { |
| | | return "系统默认"; |
| | | } |
| | | return value + " B"; |
| | | } |
| | | } |
| | | }); |
| | | })(); |
| | |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .panel-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .mini-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(4, minmax(0, 1fr)); |
| | |
| | | border-color: rgba(151, 110, 204, 0.18); |
| | | } |
| | | |
| | | .network-mini-ok { |
| | | background: linear-gradient(180deg, rgba(47, 163, 142, 0.11) 0%, rgba(47, 163, 142, 0.03) 100%); |
| | | border-color: rgba(47, 163, 142, 0.18); |
| | | } |
| | | |
| | | .network-mini-warning { |
| | | background: linear-gradient(180deg, rgba(245, 154, 74, 0.12) 0%, rgba(245, 154, 74, 0.03) 100%); |
| | | border-color: rgba(245, 154, 74, 0.18); |
| | | } |
| | | |
| | | .network-mini-offline { |
| | | background: linear-gradient(180deg, rgba(222, 92, 92, 0.10) 0%, rgba(222, 92, 92, 0.03) 100%); |
| | | border-color: rgba(222, 92, 92, 0.18); |
| | | } |
| | | |
| | | .network-mini-latency { |
| | | background: linear-gradient(180deg, rgba(31, 111, 178, 0.09) 0%, rgba(31, 111, 178, 0.02) 100%); |
| | | border-color: rgba(31, 111, 178, 0.16); |
| | | } |
| | | |
| | | .chart-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | |
| | | margin-bottom: 8px; |
| | | } |
| | | |
| | | .chart-subtitle { |
| | | margin-bottom: 10px; |
| | | font-size: 12px; |
| | | color: #7d90a4; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .chart-box { |
| | | width: 100%; |
| | | height: 280px; |
| | | } |
| | | |
| | | .panel-device .mini-grid, |
| | | .panel-network .mini-grid, |
| | | .panel-ai .mini-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | |
| | | } |
| | | |
| | | .device-chart-box, |
| | | .network-chart-box, |
| | | .ai-chart-box { |
| | | width: 100%; |
| | | height: 250px; |
| | |
| | | padding: 8px 10px; |
| | | } |
| | | |
| | | .network-note { |
| | | margin-top: 6px; |
| | | font-size: 12px; |
| | | line-height: 1.5; |
| | | border-radius: 12px; |
| | | padding: 8px 10px; |
| | | } |
| | | |
| | | .network-note-danger { |
| | | color: #c15b5b; |
| | | background: rgba(222, 92, 92, 0.08); |
| | | } |
| | | |
| | | .network-note-warning { |
| | | color: #b67632; |
| | | background: rgba(245, 154, 74, 0.12); |
| | | } |
| | | |
| | | .network-note-info { |
| | | color: #657d95; |
| | | background: rgba(125, 144, 164, 0.10); |
| | | } |
| | | |
| | | .network-healthy-state { |
| | | margin-top: 14px; |
| | | padding: 14px 16px; |
| | | border-radius: 16px; |
| | | border: 1px solid rgba(87, 186, 128, 0.20); |
| | | background: linear-gradient(135deg, rgba(87, 186, 128, 0.10) 0%, rgba(87, 186, 128, 0.03) 100%); |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 14px; |
| | | } |
| | | |
| | | .network-healthy-main { |
| | | min-width: 0; |
| | | flex: 1; |
| | | } |
| | | |
| | | .network-healthy-title { |
| | | font-size: 14px; |
| | | font-weight: 700; |
| | | color: #2d7f56; |
| | | line-height: 1.5; |
| | | } |
| | | |
| | | .network-healthy-desc { |
| | | margin-top: 4px; |
| | | font-size: 12px; |
| | | color: #698399; |
| | | line-height: 1.6; |
| | | } |
| | | |
| | | .network-healthy-tags { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | flex-wrap: wrap; |
| | | gap: 8px; |
| | | } |
| | | |
| | | .network-healthy-tag { |
| | | padding: 6px 10px; |
| | | border-radius: 999px; |
| | | background: rgba(255, 255, 255, 0.72); |
| | | border: 1px solid rgba(87, 186, 128, 0.18); |
| | | font-size: 12px; |
| | | color: #557160; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .recent-panel { |
| | | min-height: 100%; |
| | | min-height: 0; |
| | | } |
| | | |
| | | .recent-table { |
| | |
| | | } |
| | | |
| | | .panel-device .mini-grid, |
| | | .panel-network .mini-grid, |
| | | .panel-ai .mini-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | |
| | | .route-row-side { |
| | | align-items: flex-start; |
| | | text-align: left; |
| | | } |
| | | |
| | | .network-healthy-state { |
| | | flex-direction: column; |
| | | align-items: flex-start; |
| | | } |
| | | |
| | | .network-healthy-tags { |
| | | justify-content: flex-start; |
| | | } |
| | | |
| | | .panel-actions { |
| | | width: 100%; |
| | | justify-content: flex-start; |
| | | } |
| | | } |
| | | </style> |
| | |
| | | </el-table> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | |
| | | <div class="dashboard-column"> |
| | | <section class="panel panel-device"> |
| | | <div class="panel-header"> |
| | | <div> |
| | | <div class="panel-kicker">Devices</div> |
| | | <h2 class="panel-title">设备态势</h2> |
| | | <div class="panel-desc">汇总输送站点、堆垛机、双工位堆垛机与 RGV 的在线、忙碌和告警情况。</div> |
| | | </div> |
| | | <el-tag size="small" type="info">在线率 {{ devices.overview.onlineRate || 0 }}%</el-tag> |
| | | </div> |
| | | |
| | | <div class="mini-grid"> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">设备总数</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.total) }}</div> |
| | | <div class="mini-hint">已启用配置设备</div> |
| | | </div> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">在线设备</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.online) }}</div> |
| | | <div class="mini-hint">实时连通设备数量</div> |
| | | </div> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">忙碌设备</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.busy) }}</div> |
| | | <div class="mini-hint">当前承载任务的设备</div> |
| | | </div> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">告警设备</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.alarm) }}</div> |
| | | <div class="mini-hint">含阻塞或报警状态</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-card"> |
| | | <div class="chart-title">设备在线分布</div> |
| | | <div ref="deviceTypeChart" class="device-chart-box"></div> |
| | | </div> |
| | | |
| | | <div class="type-list"> |
| | | <div v-for="item in devices.typeStats" :key="item.name" class="type-row"> |
| | | <div class="type-row-main"> |
| | | <div class="type-row-name">{{ item.name }}</div> |
| | | <div class="type-row-desc">在线 {{ formatNumber(item.online) }} / 总数 {{ formatNumber(item.total) }},离线 {{ formatNumber(item.offline) }}</div> |
| | | </div> |
| | | <div class="type-row-side"> |
| | | <el-tag size="mini" type="success">忙碌 {{ formatNumber(item.busy) }}</el-tag> |
| | | <el-tag size="mini" :type="item.alarm > 0 ? 'danger' : 'info'">告警 {{ formatNumber(item.alarm) }}</el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="panel panel-ai"> |
| | | <div class="panel-header"> |
| | |
| | | <el-empty v-else description="暂无 AI 路由数据"></el-empty> |
| | | </section> |
| | | </div> |
| | | |
| | | <div class="dashboard-column"> |
| | | <section class="panel panel-device"> |
| | | <div class="panel-header"> |
| | | <div> |
| | | <div class="panel-kicker">Devices</div> |
| | | <h2 class="panel-title">设备态势</h2> |
| | | <div class="panel-desc">汇总输送站点、堆垛机、双工位堆垛机与 RGV 的在线、忙碌和告警情况。</div> |
| | | </div> |
| | | <el-tag size="small" type="info">在线率 {{ devices.overview.onlineRate || 0 }}%</el-tag> |
| | | </div> |
| | | |
| | | <div class="mini-grid"> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">设备总数</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.total) }}</div> |
| | | <div class="mini-hint">已启用配置设备</div> |
| | | </div> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">在线设备</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.online) }}</div> |
| | | <div class="mini-hint">实时连通设备数量</div> |
| | | </div> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">忙碌设备</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.busy) }}</div> |
| | | <div class="mini-hint">当前承载任务的设备</div> |
| | | </div> |
| | | <div class="mini-card"> |
| | | <div class="mini-label">告警设备</div> |
| | | <div class="mini-value">{{ formatNumber(devices.overview.alarm) }}</div> |
| | | <div class="mini-hint">含阻塞或报警状态</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-card"> |
| | | <div class="chart-title">设备在线分布</div> |
| | | <div ref="deviceTypeChart" class="device-chart-box"></div> |
| | | </div> |
| | | |
| | | <div class="type-list"> |
| | | <div v-for="item in devices.typeStats" :key="item.name" class="type-row"> |
| | | <div class="type-row-main"> |
| | | <div class="type-row-name">{{ item.name }}</div> |
| | | <div class="type-row-desc">在线 {{ formatNumber(item.online) }} / 总数 {{ formatNumber(item.total) }},离线 {{ formatNumber(item.offline) }}</div> |
| | | </div> |
| | | <div class="type-row-side"> |
| | | <el-tag size="mini" type="success">忙碌 {{ formatNumber(item.busy) }}</el-tag> |
| | | <el-tag size="mini" :type="item.alarm > 0 ? 'danger' : 'info'">告警 {{ formatNumber(item.alarm) }}</el-tag> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="panel panel-network"> |
| | | <div class="panel-header"> |
| | | <div> |
| | | <div class="panel-kicker">Network</div> |
| | | <h2 class="panel-title">设备网络分析</h2> |
| | | <div class="panel-desc">汇总最新 Ping 样本的连通性、延迟与异常设备,帮助快速发现网络波动。</div> |
| | | </div> |
| | | <div class="panel-actions"> |
| | | <el-tag size="small" :type="network.overview.attentionDevices > 0 ? 'warning' : 'success'"> |
| | | 需关注 {{ formatNumber(network.overview.attentionDevices) }} |
| | | </el-tag> |
| | | <el-button size="mini" plain @click="openDevicePingAnalysis">查看明细</el-button> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="mini-grid"> |
| | | <div class="mini-card network-mini-ok"> |
| | | <div class="mini-label">正常</div> |
| | | <div class="mini-value">{{ formatNumber(network.overview.okDevices) }}</div> |
| | | <div class="mini-hint">最新样本状态 OK</div> |
| | | </div> |
| | | <div class="mini-card network-mini-warning"> |
| | | <div class="mini-label">波动</div> |
| | | <div class="mini-value">{{ formatNumber(network.overview.unstableDevices) }}</div> |
| | | <div class="mini-hint">部分探测成功</div> |
| | | </div> |
| | | <div class="mini-card network-mini-offline"> |
| | | <div class="mini-label">超时/异常</div> |
| | | <div class="mini-value">{{ formatNumber(network.overview.offlineDevices) }}</div> |
| | | <div class="mini-hint">暂无数据 {{ formatNumber(network.overview.noDataDevices) }}</div> |
| | | </div> |
| | | <div class="mini-card network-mini-latency"> |
| | | <div class="mini-label">平均延迟</div> |
| | | <div class="mini-value">{{ formatLatency(network.overview.avgLatencyMs) }}</div> |
| | | <div class="mini-hint">峰值 {{ formatLatency(network.overview.maxLatencyMs) }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-card"> |
| | | <div class="chart-title">连通状态分布</div> |
| | | <div class="chart-subtitle">{{ networkSamplingText() }}</div> |
| | | <div ref="networkStatusChart" class="network-chart-box"></div> |
| | | </div> |
| | | |
| | | <div class="route-list" v-if="network.focusDevices.length"> |
| | | <div v-for="item in network.focusDevices" :key="item.name + '-' + item.ip" class="route-row"> |
| | | <div class="route-row-main"> |
| | | <div class="route-row-name">{{ item.name }}</div> |
| | | <div class="route-row-desc">{{ displayText(item.ip, '-') }}</div> |
| | | <div v-if="item.message" :class="['network-note', 'network-note-' + (item.statusType || 'info')]">{{ item.message }}</div> |
| | | </div> |
| | | <div class="route-row-side"> |
| | | <el-tag size="mini" :type="item.statusType">{{ item.statusText }}</el-tag> |
| | | <div class="route-extra">平均 {{ formatLatency(item.avgLatencyMs) }}</div> |
| | | <div class="route-extra">最近样本 {{ displayText(item.latestTimeLabel, '-') }}</div> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <div v-else-if="network.overview.totalDevices > 0" class="network-healthy-state"> |
| | | <div class="network-healthy-main"> |
| | | <div class="network-healthy-title">当前网络探测稳定</div> |
| | | <div class="network-healthy-desc">已纳入 {{ formatNumber(network.overview.totalDevices) }} 台设备,最近一轮未发现超时或波动。</div> |
| | | </div> |
| | | <div class="network-healthy-tags"> |
| | | <div class="network-healthy-tag">正常 {{ formatNumber(network.overview.okDevices) }}</div> |
| | | <div class="network-healthy-tag">平均 {{ formatLatency(network.overview.avgLatencyMs) }}</div> |
| | | <div class="network-healthy-tag">峰值 {{ formatLatency(network.overview.maxLatencyMs) }}</div> |
| | | </div> |
| | | </div> |
| | | <el-empty v-else description="暂无设备网络样本"></el-empty> |
| | | </section> |
| | | </div> |
| | | </div> |
| | | |
| | | <div v-if="loading" class="loading-mask"> |
| | |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/echarts/echarts.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/dashboard/dashboard.js"></script> |
| | | <script type="text/javascript" src="../../static/js/dashboard/dashboard.js?v=20260317-dashboard-network-focus"></script> |
| | | </body> |
| | | </html> |
| New file |
| | |
| | | <!DOCTYPE html> |
| | | <html lang="zh-CN"> |
| | | <head> |
| | | <meta charset="UTF-8" /> |
| | | <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| | | <title>设备网络分析</title> |
| | | <link rel="stylesheet" href="../../static/vue/element/element.css" /> |
| | | <style> |
| | | [v-cloak] { |
| | | display: none; |
| | | } |
| | | |
| | | html, |
| | | body { |
| | | margin: 0; |
| | | min-height: 100%; |
| | | font-family: "Avenir Next", "PingFang SC", "Microsoft YaHei", sans-serif; |
| | | background: linear-gradient(180deg, #f4f7fb 0%, #edf2f7 100%); |
| | | color: #243447; |
| | | } |
| | | |
| | | * { |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .page-shell { |
| | | max-width: 1680px; |
| | | margin: 0 auto; |
| | | padding: 16px; |
| | | } |
| | | |
| | | .page-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | margin-bottom: 14px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .page-title { |
| | | font-size: 28px; |
| | | font-weight: 700; |
| | | color: #1f3142; |
| | | } |
| | | |
| | | .page-meta { |
| | | font-size: 12px; |
| | | color: #7d8ea2; |
| | | } |
| | | |
| | | .summary-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(6, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .summary-card { |
| | | padding: 14px 16px; |
| | | border-radius: 18px; |
| | | background: linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 251, 255, 0.92) 100%); |
| | | border: 1px solid rgba(216, 226, 235, 0.96); |
| | | box-shadow: 0 14px 28px rgba(39, 63, 92, 0.06); |
| | | min-height: 90px; |
| | | } |
| | | |
| | | .summary-label { |
| | | font-size: 12px; |
| | | color: #7d8ea2; |
| | | } |
| | | |
| | | .summary-value { |
| | | margin-top: 8px; |
| | | font-size: 28px; |
| | | line-height: 1.1; |
| | | font-weight: 700; |
| | | color: #22364a; |
| | | } |
| | | |
| | | .summary-sub { |
| | | margin-top: 8px; |
| | | font-size: 12px; |
| | | color: #8a99ab; |
| | | } |
| | | |
| | | .panel { |
| | | margin-top: 16px; |
| | | border-radius: 22px; |
| | | border: 1px solid rgba(216, 226, 235, 0.96); |
| | | background: rgba(255, 255, 255, 0.9); |
| | | box-shadow: 0 16px 32px rgba(39, 63, 92, 0.06); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .panel-head { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | padding: 18px 18px 0; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .panel-title { |
| | | font-size: 18px; |
| | | font-weight: 700; |
| | | color: #22364a; |
| | | } |
| | | |
| | | .panel-body { |
| | | padding: 16px 18px 18px; |
| | | } |
| | | |
| | | .toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | flex-wrap: wrap; |
| | | margin-bottom: 12px; |
| | | } |
| | | |
| | | .toolbar-right { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 10px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .detail-shell { |
| | | display: flex; |
| | | flex-direction: column; |
| | | gap: 16px; |
| | | } |
| | | |
| | | .detail-toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: space-between; |
| | | gap: 12px; |
| | | flex-wrap: wrap; |
| | | } |
| | | |
| | | .detail-selected { |
| | | font-size: 14px; |
| | | color: #50657b; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .detail-summary-grid { |
| | | display: grid; |
| | | grid-template-columns: repeat(6, minmax(0, 1fr)); |
| | | gap: 12px; |
| | | } |
| | | |
| | | .detail-card { |
| | | padding: 14px 16px; |
| | | border-radius: 16px; |
| | | background: #f7fafc; |
| | | border: 1px solid #e1e9f2; |
| | | min-height: 84px; |
| | | } |
| | | |
| | | .detail-card-label { |
| | | font-size: 12px; |
| | | color: #8091a4; |
| | | } |
| | | |
| | | .detail-card-value { |
| | | margin-top: 8px; |
| | | font-size: 24px; |
| | | line-height: 1.1; |
| | | font-weight: 700; |
| | | color: #23364a; |
| | | } |
| | | |
| | | .chart-grid { |
| | | display: grid; |
| | | grid-template-columns: minmax(0, 1.2fr) minmax(360px, 0.8fr); |
| | | gap: 16px; |
| | | } |
| | | |
| | | .chart-card { |
| | | padding: 16px; |
| | | border-radius: 18px; |
| | | border: 1px solid #e2eaf2; |
| | | background: #fbfdff; |
| | | } |
| | | |
| | | .chart-title { |
| | | margin-bottom: 12px; |
| | | font-size: 16px; |
| | | font-weight: 700; |
| | | color: #243447; |
| | | } |
| | | |
| | | .chart-box { |
| | | height: 340px; |
| | | width: 100%; |
| | | } |
| | | |
| | | .empty-shell { |
| | | padding: 48px 16px; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | color: #8a99ab; |
| | | border: 1px dashed #d8e2ec; |
| | | border-radius: 16px; |
| | | background: #fafcff; |
| | | } |
| | | |
| | | .status-chip { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | min-width: 74px; |
| | | height: 26px; |
| | | padding: 0 10px; |
| | | border-radius: 999px; |
| | | font-size: 12px; |
| | | font-weight: 700; |
| | | } |
| | | |
| | | .status-chip.ok { |
| | | color: #177d5a; |
| | | background: rgba(30, 170, 112, 0.14); |
| | | } |
| | | |
| | | .status-chip.unstable { |
| | | color: #b56d05; |
| | | background: rgba(245, 154, 74, 0.16); |
| | | } |
| | | |
| | | .status-chip.offline { |
| | | color: #bb3d3d; |
| | | background: rgba(222, 92, 92, 0.14); |
| | | } |
| | | |
| | | .status-chip.nodata { |
| | | color: #6f8194; |
| | | background: rgba(176, 190, 204, 0.2); |
| | | } |
| | | |
| | | @media (max-width: 1360px) { |
| | | .summary-grid { |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .detail-summary-grid { |
| | | grid-template-columns: repeat(3, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .chart-grid { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .page-shell { |
| | | padding: 12px; |
| | | } |
| | | |
| | | .summary-grid, |
| | | .detail-summary-grid { |
| | | grid-template-columns: repeat(2, minmax(0, 1fr)); |
| | | } |
| | | |
| | | .page-title { |
| | | font-size: 24px; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app" v-cloak class="page-shell"> |
| | | <div class="page-head"> |
| | | <div class="page-title">设备网络分析</div> |
| | | <div class="page-meta">包大小 {{ formatPacketSize(samplingConfig.packetSize) }},{{ samplingConfigText }}</div> |
| | | </div> |
| | | |
| | | <section class="summary-grid"> |
| | | <div class="summary-card"> |
| | | <div class="summary-label">设备总数</div> |
| | | <div class="summary-value">{{ overviewSummary.totalDevices }}</div> |
| | | <div class="summary-sub">已配置 IP 的设备</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="summary-label">正常</div> |
| | | <div class="summary-value">{{ overviewSummary.okDevices }}</div> |
| | | <div class="summary-sub">最近样本状态 OK</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="summary-label">波动</div> |
| | | <div class="summary-value">{{ overviewSummary.unstableDevices }}</div> |
| | | <div class="summary-sub">部分探测成功</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="summary-label">超时/异常</div> |
| | | <div class="summary-value">{{ overviewSummary.offlineDevices }}</div> |
| | | <div class="summary-sub">最近样本不可达</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="summary-label">暂无数据</div> |
| | | <div class="summary-value">{{ overviewSummary.noDataDevices }}</div> |
| | | <div class="summary-sub">还没有落盘样本</div> |
| | | </div> |
| | | <div class="summary-card"> |
| | | <div class="summary-label">整体平均延迟</div> |
| | | <div class="summary-value">{{ formatLatency(overviewSummary.avgLatencyMs) }}</div> |
| | | <div class="summary-sub">峰值 {{ formatLatency(overviewSummary.maxLatencyMs) }}</div> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="panel"> |
| | | <div class="panel-head"> |
| | | <div class="panel-title">设备总览</div> |
| | | <el-button size="small" :loading="overviewLoading" @click="loadOverview">刷新</el-button> |
| | | </div> |
| | | <div class="panel-body"> |
| | | <div class="toolbar"> |
| | | <el-form :inline="true" size="small" :model="overviewFilters" style="margin-bottom: -18px;"> |
| | | <el-form-item label="设备类型"> |
| | | <el-select v-model="overviewFilters.deviceType" clearable style="width: 120px;"> |
| | | <el-option label="全部" value=""></el-option> |
| | | <el-option label="Crn" value="Crn"></el-option> |
| | | <el-option label="DualCrn" value="DualCrn"></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.trim="overviewFilters.keyword" clearable style="width: 220px;" placeholder="设备号 / IP"></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | <div class="toolbar-right"> |
| | | <span class="page-meta">总览设备 {{ filteredOverviewRows.length }} 台</span> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-table :data="filteredOverviewRows" border stripe size="mini" v-loading="overviewLoading" style="width: 100%;"> |
| | | <el-table-column label="设备" min-width="170"> |
| | | <template slot-scope="scope"> |
| | | {{ scope.row.deviceType }}-{{ scope.row.deviceNo }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="ip" label="IP" min-width="150"></el-table-column> |
| | | <el-table-column label="状态" width="110" align="center"> |
| | | <template slot-scope="scope"> |
| | | <span class="status-chip" :class="statusClass(scope.row.status)">{{ scope.row.statusText }}</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="成功率" width="90" align="right"> |
| | | <template slot-scope="scope"> |
| | | {{ formatPercent(scope.row.successRate) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="平均" width="90" align="right"> |
| | | <template slot-scope="scope"> |
| | | {{ formatLatency(scope.row.avgLatencyMs) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="最小" width="90" align="right"> |
| | | <template slot-scope="scope"> |
| | | {{ formatLatency(scope.row.minLatencyMs) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="最大" width="90" align="right"> |
| | | <template slot-scope="scope"> |
| | | {{ formatLatency(scope.row.maxLatencyMs) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="latestTimeLabel" label="更新时间" width="170"></el-table-column> |
| | | <el-table-column prop="message" label="说明" min-width="200" show-overflow-tooltip></el-table-column> |
| | | <el-table-column label="操作" width="110" align="center" fixed="right"> |
| | | <template slot-scope="scope"> |
| | | <el-button size="mini" type="primary" plain @click="openDetail(scope.row)">查看详情</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </section> |
| | | |
| | | <section class="panel"> |
| | | <div class="panel-head"> |
| | | <div class="panel-title">设备详情</div> |
| | | </div> |
| | | <div class="panel-body"> |
| | | <div v-if="!currentDevice" class="empty-shell">从上方设备总览选择一台设备查看秒级明细</div> |
| | | <div v-else class="detail-shell"> |
| | | <div class="detail-toolbar"> |
| | | <div class="detail-selected">{{ currentDevice.label }}</div> |
| | | <div class="toolbar-right"> |
| | | <el-form :inline="true" size="small" :model="detailFilters" style="margin-bottom: -18px;"> |
| | | <el-form-item label="时间范围"> |
| | | <el-date-picker |
| | | v-model="detailFilters.range" |
| | | type="datetimerange" |
| | | unlink-panels |
| | | range-separator="至" |
| | | start-placeholder="开始" |
| | | end-placeholder="结束" |
| | | style="width: 360px;"> |
| | | </el-date-picker> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="setQuickRange(30)">30 分钟</el-button> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="setQuickRange(60)">1 小时</el-button> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button @click="setQuickRange(360)">6 小时</el-button> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" :loading="detailLoading" @click="queryTrend">查询</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="detail-summary-grid"> |
| | | <div class="detail-card"> |
| | | <div class="detail-card-label">状态</div> |
| | | <div class="detail-card-value">{{ detailSummary.latestStatus || '--' }}</div> |
| | | </div> |
| | | <div class="detail-card"> |
| | | <div class="detail-card-label">包大小</div> |
| | | <div class="detail-card-value">{{ formatPacketSize(detailSummary.packetSize) }}</div> |
| | | </div> |
| | | <div class="detail-card"> |
| | | <div class="detail-card-label">成功率</div> |
| | | <div class="detail-card-value">{{ formatPercent(detailSummary.successRate) }}</div> |
| | | </div> |
| | | <div class="detail-card"> |
| | | <div class="detail-card-label">平均</div> |
| | | <div class="detail-card-value">{{ formatLatency(detailSummary.avgLatencyMs) }}</div> |
| | | </div> |
| | | <div class="detail-card"> |
| | | <div class="detail-card-label">最小</div> |
| | | <div class="detail-card-value">{{ formatLatency(detailSummary.minLatencyMs) }}</div> |
| | | </div> |
| | | <div class="detail-card"> |
| | | <div class="detail-card-label">最大</div> |
| | | <div class="detail-card-value">{{ formatLatency(detailSummary.maxLatencyMs) }}</div> |
| | | </div> |
| | | </div> |
| | | |
| | | <div class="chart-grid"> |
| | | <div class="chart-card"> |
| | | <div class="chart-title">延迟</div> |
| | | <div v-if="!series.length && !detailLoading" class="empty-shell">当前范围暂无秒级样本</div> |
| | | <div v-show="series.length || detailLoading" ref="latencyChart" class="chart-box"></div> |
| | | </div> |
| | | <div class="chart-card"> |
| | | <div class="chart-title">成功率 / 失败次数</div> |
| | | <div v-if="!series.length && !detailLoading" class="empty-shell">当前范围暂无秒级样本</div> |
| | | <div v-show="series.length || detailLoading" ref="availabilityChart" class="chart-box"></div> |
| | | </div> |
| | | </div> |
| | | |
| | | <el-table :data="alerts" border stripe size="mini" style="width: 100%;"> |
| | | <el-table-column prop="timeLabel" label="时间" width="180"></el-table-column> |
| | | <el-table-column prop="status" label="状态" width="110"></el-table-column> |
| | | <el-table-column prop="ip" label="IP" width="150"></el-table-column> |
| | | <el-table-column prop="message" label="说明" min-width="240" show-overflow-tooltip></el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </section> |
| | | </div> |
| | | |
| | | <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> |
| | | <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../../static/vue/element/element.js"></script> |
| | | <script type="text/javascript" src="../../static/js/echarts/echarts.min.js"></script> |
| | | <script type="text/javascript" src="../../static/js/devicePingLog/devicePingLog.js?v=20260316_device_ping_packet_size_detail" charset="utf-8"></script> |
| | | </body> |
| | | </html> |
| | |
| | | <script type="text/javascript" src="../static/vue/js/vue.min.js"></script> |
| | | <script type="text/javascript" src="../static/vue/element/element.js"></script> |
| | | <script> |
| | | var DASHBOARD_VIEW_VERSION = "20260316-hero-two-rows-flat-compact"; |
| | | var DASHBOARD_VIEW_VERSION = "20260317-dashboard-network-balanced-columns"; |
| | | var HOME_TAB_CONFIG = { |
| | | title: "系统仪表盘", |
| | | url: baseUrl + "/views/dashboard/dashboard.html?layoutVersion=" + encodeURIComponent(DASHBOARD_VIEW_VERSION), |
| | |
| | | return this.tl(title); |
| | | }, |
| | | findMenuMeta: function (tab) { |
| | | var normalizedUrl; |
| | | var menuEntry; |
| | | var i; |
| | | var j; |
| | | var group; |
| | |
| | | if (!tab) { |
| | | return null; |
| | | } |
| | | normalizedUrl = this.resolveViewSrc(tab.url); |
| | | if (tab.menuKey) { |
| | | for (i = 0; i < this.menus.length; i++) { |
| | | group = this.menus[i]; |
| | | for (j = 0; j < group.subMenu.length; j++) { |
| | | item = group.subMenu[j]; |
| | | if (item.tabKey === tab.menuKey) { |
| | | return { |
| | | group: group, |
| | | item: item |
| | | }; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | menuEntry = this.findMenuEntryByUrl(tab.url); |
| | | if (menuEntry) { |
| | | return menuEntry; |
| | | } |
| | | return null; |
| | | }, |
| | | normalizeMenuMatchUrl: function (url, stripQuery) { |
| | | var normalized = this.resolveViewSrc(url || ""); |
| | | var hashIndex = normalized.indexOf("#"); |
| | | var queryIndex; |
| | | if (hashIndex > -1) { |
| | | normalized = normalized.substring(0, hashIndex); |
| | | } |
| | | if (stripQuery) { |
| | | queryIndex = normalized.indexOf("?"); |
| | | if (queryIndex > -1) { |
| | | normalized = normalized.substring(0, queryIndex); |
| | | } |
| | | } |
| | | return normalized; |
| | | }, |
| | | findMenuEntryByUrl: function (url) { |
| | | var normalized = this.normalizeMenuMatchUrl(url, false); |
| | | var normalizedBase = this.normalizeMenuMatchUrl(url, true); |
| | | var fallback = null; |
| | | var i; |
| | | var j; |
| | | var group; |
| | | var item; |
| | | |
| | | for (i = 0; i < this.menus.length; i++) { |
| | | group = this.menus[i]; |
| | | for (j = 0; j < group.subMenu.length; j++) { |
| | | item = group.subMenu[j]; |
| | | if ((tab.menuKey && item.tabKey === tab.menuKey) || item.url === normalizedUrl) { |
| | | if (item.url === normalized) { |
| | | return { |
| | | group: group, |
| | | item: item |
| | | }; |
| | | } |
| | | if (!fallback && this.normalizeMenuMatchUrl(item.url, true) === normalizedBase) { |
| | | fallback = { |
| | | group: group, |
| | | item: item |
| | | }; |
| | | } |
| | | } |
| | | } |
| | | return null; |
| | | return fallback; |
| | | }, |
| | | syncTabMeta: function (tab, homeConfig, profileConfig) { |
| | | var menuMeta; |
| | |
| | | }); |
| | | }, |
| | | findMenuKeyByUrl: function (url) { |
| | | var normalized = this.resolveViewSrc(url); |
| | | var i; |
| | | var j; |
| | | var group; |
| | | var item; |
| | | |
| | | for (i = 0; i < this.menus.length; i++) { |
| | | group = this.menus[i]; |
| | | for (j = 0; j < group.subMenu.length; j++) { |
| | | item = group.subMenu[j]; |
| | | if (item.url === normalized) { |
| | | return item.tabKey; |
| | | } |
| | | } |
| | | var entry = this.findMenuEntryByUrl(url); |
| | | if (entry && entry.item) { |
| | | return entry.item.tabKey || ""; |
| | | } |
| | | return ""; |
| | | }, |
| | | findMenuGroupIndexByUrl: function (url) { |
| | | var normalized = this.resolveViewSrc(url); |
| | | var i; |
| | | var j; |
| | | var group; |
| | | var item; |
| | | |
| | | for (i = 0; i < this.menus.length; i++) { |
| | | group = this.menus[i]; |
| | | for (j = 0; j < group.subMenu.length; j++) { |
| | | item = group.subMenu[j]; |
| | | if (item.url === normalized) { |
| | | return "group-" + group.menuId; |
| | | } |
| | | } |
| | | var entry = this.findMenuEntryByUrl(url); |
| | | if (entry && entry.group) { |
| | | return "group-" + entry.group.menuId; |
| | | } |
| | | return ""; |
| | | }, |
| | |
| | | window.index = window.index || {}; |
| | | window.index.loadView = function (param) { |
| | | var url; |
| | | var menuEntry; |
| | | if (!param || !param.menuPath) { |
| | | return; |
| | | } |
| | | url = that.resolveViewSrc(param.menuPath); |
| | | menuEntry = that.findMenuEntryByUrl(url); |
| | | that.addOrActivateTab({ |
| | | title: that.stripTags(param.menuName) || that.t("common.workPage"), |
| | | url: url, |
| | | title: that.stripTags(param.menuName) || (menuEntry && menuEntry.item ? menuEntry.item.name : that.t("common.workPage")), |
| | | url: menuEntry && menuEntry.item ? menuEntry.item.url : url, |
| | | home: false, |
| | | group: that.t("common.businessPage"), |
| | | menuKey: that.findMenuKeyByUrl(url) |
| | | group: menuEntry && menuEntry.group ? menuEntry.group.menu : that.t("common.businessPage"), |
| | | menuKey: menuEntry && menuEntry.item ? menuEntry.item.tabKey : that.findMenuKeyByUrl(url) |
| | | }); |
| | | }; |
| | | window.index.loadHome = function (param) { |
| | | var url; |
| | | var menuEntry; |
| | | if (!param || !param.menuPath) { |
| | | that.openHomeTab(); |
| | | return; |
| | | } |
| | | url = that.resolveViewSrc(param.menuPath); |
| | | menuEntry = that.findMenuEntryByUrl(url); |
| | | that.addOrActivateTab({ |
| | | title: that.stripTags(param.menuName) || that.resolveHomeConfig().title, |
| | | url: url, |
| | | title: that.stripTags(param.menuName) || (menuEntry && menuEntry.item ? menuEntry.item.name : that.resolveHomeConfig().title), |
| | | url: menuEntry && menuEntry.item ? menuEntry.item.url : url, |
| | | home: true, |
| | | group: that.resolveHomeConfig().group, |
| | | menuKey: that.findMenuKeyByUrl(url) |
| | | group: menuEntry && menuEntry.group ? menuEntry.group.menu : that.resolveHomeConfig().group, |
| | | menuKey: menuEntry && menuEntry.item ? menuEntry.item.tabKey : that.findMenuKeyByUrl(url) |
| | | }); |
| | | }; |
| | | |