<!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">{{ i18n('devicePingLog.title', '设备网络分析') }}</div>
|
<div class="page-meta">{{ i18n('devicePingLog.pageMeta', '包大小 {0},{1}', [formatPacketSize(samplingConfig.packetSize), samplingConfigText]) }}</div>
|
</div>
|
|
<section class="summary-grid">
|
<div class="summary-card">
|
<div class="summary-label">{{ i18n('devicePingLog.summary.totalDevices', '设备总数') }}</div>
|
<div class="summary-value">{{ formatNumber(overviewSummary.totalDevices) }}</div>
|
<div class="summary-sub">{{ i18n('devicePingLog.summary.totalDevicesHint', '已配置 IP 的设备') }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="summary-label">{{ i18n('devicePingLog.summary.ok', '正常') }}</div>
|
<div class="summary-value">{{ formatNumber(overviewSummary.okDevices) }}</div>
|
<div class="summary-sub">{{ i18n('devicePingLog.summary.okHint', '最近样本状态 OK') }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="summary-label">{{ i18n('devicePingLog.summary.unstable', '波动') }}</div>
|
<div class="summary-value">{{ formatNumber(overviewSummary.unstableDevices) }}</div>
|
<div class="summary-sub">{{ i18n('devicePingLog.summary.unstableHint', '部分探测成功') }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="summary-label">{{ i18n('devicePingLog.summary.offline', '超时/异常') }}</div>
|
<div class="summary-value">{{ formatNumber(overviewSummary.offlineDevices) }}</div>
|
<div class="summary-sub">{{ i18n('devicePingLog.summary.offlineHint', '最近样本不可达') }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="summary-label">{{ i18n('devicePingLog.summary.noData', '暂无数据') }}</div>
|
<div class="summary-value">{{ formatNumber(overviewSummary.noDataDevices) }}</div>
|
<div class="summary-sub">{{ i18n('devicePingLog.summary.noDataHint', '还没有落盘样本') }}</div>
|
</div>
|
<div class="summary-card">
|
<div class="summary-label">{{ i18n('devicePingLog.summary.avgLatency', '整体平均延迟') }}</div>
|
<div class="summary-value">{{ formatLatency(overviewSummary.avgLatencyMs) }}</div>
|
<div class="summary-sub">{{ i18n('devicePingLog.summary.peakLatency', '峰值 {0}', [formatLatency(overviewSummary.maxLatencyMs)]) }}</div>
|
</div>
|
</section>
|
|
<section class="panel">
|
<div class="panel-head">
|
<div class="panel-title">{{ i18n('devicePingLog.overviewTitle', '设备总览') }}</div>
|
<el-button size="small" :loading="overviewLoading" @click="loadOverview">{{ i18n('devicePingLog.refresh', '刷新') }}</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="i18n('devicePingLog.filter.deviceType', '设备类型')">
|
<el-select v-model="overviewFilters.deviceType" clearable style="width: 120px;">
|
<el-option :label="i18n('devicePingLog.filter.all', '全部')" 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="i18n('devicePingLog.filter.keyword', '关键字')">
|
<el-input v-model.trim="overviewFilters.keyword" clearable style="width: 220px;" :placeholder="i18n('devicePingLog.filter.keywordPlaceholder', '设备号 / IP')"></el-input>
|
</el-form-item>
|
</el-form>
|
<div class="toolbar-right">
|
<span class="page-meta">{{ i18n('devicePingLog.overviewCount', '总览设备 {0} 台', [formatNumber(filteredOverviewRows.length)]) }}</span>
|
</div>
|
</div>
|
|
<el-table :data="filteredOverviewRows" border stripe size="mini" v-loading="overviewLoading" style="width: 100%;">
|
<el-table-column :label="i18n('devicePingLog.column.device', '设备')" 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="i18n('devicePingLog.column.status', '状态')" 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="i18n('devicePingLog.column.successRate', '成功率')" width="90" align="right">
|
<template slot-scope="scope">
|
{{ formatPercent(scope.row.successRate) }}
|
</template>
|
</el-table-column>
|
<el-table-column :label="i18n('devicePingLog.column.avg', '平均')" width="90" align="right">
|
<template slot-scope="scope">
|
{{ formatLatency(scope.row.avgLatencyMs) }}
|
</template>
|
</el-table-column>
|
<el-table-column :label="i18n('devicePingLog.column.min', '最小')" width="90" align="right">
|
<template slot-scope="scope">
|
{{ formatLatency(scope.row.minLatencyMs) }}
|
</template>
|
</el-table-column>
|
<el-table-column :label="i18n('devicePingLog.column.max', '最大')" width="90" align="right">
|
<template slot-scope="scope">
|
{{ formatLatency(scope.row.maxLatencyMs) }}
|
</template>
|
</el-table-column>
|
<el-table-column prop="latestTimeLabel" :label="i18n('devicePingLog.column.updateTime', '更新时间')" width="170"></el-table-column>
|
<el-table-column prop="message" :label="i18n('devicePingLog.column.message', '说明')" min-width="200" show-overflow-tooltip></el-table-column>
|
<el-table-column :label="i18n('devicePingLog.column.action', '操作')" width="110" align="center" fixed="right">
|
<template slot-scope="scope">
|
<el-button size="mini" type="primary" plain @click="openDetail(scope.row)">{{ i18n('devicePingLog.viewDetail', '查看详情') }}</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</section>
|
|
<section class="panel">
|
<div class="panel-head">
|
<div class="panel-title">{{ i18n('devicePingLog.detailTitle', '设备详情') }}</div>
|
</div>
|
<div class="panel-body">
|
<div v-if="!currentDevice" class="empty-shell">{{ i18n('devicePingLog.detailEmpty', '从上方设备总览选择一台设备查看秒级明细') }}</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="i18n('devicePingLog.filter.timeRange', '时间范围')">
|
<el-date-picker
|
v-model="detailFilters.range"
|
type="datetimerange"
|
unlink-panels
|
:range-separator="i18n('devicePingLog.filter.rangeSeparator', '至')"
|
:start-placeholder="i18n('devicePingLog.filter.start', '开始')"
|
:end-placeholder="i18n('devicePingLog.filter.end', '结束')"
|
style="width: 360px;">
|
</el-date-picker>
|
</el-form-item>
|
<el-form-item>
|
<el-button @click="setQuickRange(30)">{{ i18n('devicePingLog.quickRange.30m', '30 分钟') }}</el-button>
|
</el-form-item>
|
<el-form-item>
|
<el-button @click="setQuickRange(60)">{{ i18n('devicePingLog.quickRange.1h', '1 小时') }}</el-button>
|
</el-form-item>
|
<el-form-item>
|
<el-button @click="setQuickRange(360)">{{ i18n('devicePingLog.quickRange.6h', '6 小时') }}</el-button>
|
</el-form-item>
|
<el-form-item>
|
<el-button type="primary" :loading="detailLoading" @click="queryTrend">{{ i18n('devicePingLog.query', '查询') }}</el-button>
|
</el-form-item>
|
</el-form>
|
</div>
|
</div>
|
|
<div class="detail-summary-grid">
|
<div class="detail-card">
|
<div class="detail-card-label">{{ i18n('devicePingLog.detail.status', '状态') }}</div>
|
<div class="detail-card-value">{{ detailSummary.latestStatus || '--' }}</div>
|
</div>
|
<div class="detail-card">
|
<div class="detail-card-label">{{ i18n('devicePingLog.detail.packetSize', '包大小') }}</div>
|
<div class="detail-card-value">{{ formatPacketSize(detailSummary.packetSize) }}</div>
|
</div>
|
<div class="detail-card">
|
<div class="detail-card-label">{{ i18n('devicePingLog.detail.successRate', '成功率') }}</div>
|
<div class="detail-card-value">{{ formatPercent(detailSummary.successRate) }}</div>
|
</div>
|
<div class="detail-card">
|
<div class="detail-card-label">{{ i18n('devicePingLog.detail.avg', '平均') }}</div>
|
<div class="detail-card-value">{{ formatLatency(detailSummary.avgLatencyMs) }}</div>
|
</div>
|
<div class="detail-card">
|
<div class="detail-card-label">{{ i18n('devicePingLog.detail.min', '最小') }}</div>
|
<div class="detail-card-value">{{ formatLatency(detailSummary.minLatencyMs) }}</div>
|
</div>
|
<div class="detail-card">
|
<div class="detail-card-label">{{ i18n('devicePingLog.detail.max', '最大') }}</div>
|
<div class="detail-card-value">{{ formatLatency(detailSummary.maxLatencyMs) }}</div>
|
</div>
|
</div>
|
|
<div class="chart-grid">
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('devicePingLog.chart.latencyTitle', '延迟') }}</div>
|
<div v-if="!series.length && !detailLoading" class="empty-shell">{{ i18n('devicePingLog.chart.empty', '当前范围暂无秒级样本') }}</div>
|
<div v-show="series.length || detailLoading" ref="latencyChart" class="chart-box"></div>
|
</div>
|
<div class="chart-card">
|
<div class="chart-title">{{ i18n('devicePingLog.chart.availabilityTitle', '成功率 / 失败次数') }}</div>
|
<div v-if="!series.length && !detailLoading" class="empty-shell">{{ i18n('devicePingLog.chart.empty', '当前范围暂无秒级样本') }}</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="i18n('devicePingLog.alertColumn.time', '时间')" width="180"></el-table-column>
|
<el-table-column prop="status" :label="i18n('devicePingLog.alertColumn.status', '状态')" width="110"></el-table-column>
|
<el-table-column prop="ip" label="IP" width="150"></el-table-column>
|
<el-table-column prop="message" :label="i18n('devicePingLog.alertColumn.message', '说明')" 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=20260317_device_ping_i18n" charset="utf-8"></script>
|
</body>
|
</html>
|