712 lines
20 KiB
Plaintext
712 lines
20 KiB
Plaintext
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>告警监控看板 | Alert Dashboard</title>
|
|
<script src="/static/js/echarts.min.js"></script>
|
|
<script src="/static/js/jquery.min.js"></script>
|
|
<style>
|
|
* {
|
|
box-sizing: border-box;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
body {
|
|
background: #f0f2f5;
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
color: #1f2937;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* 顶部导航栏 */
|
|
.header {
|
|
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #2563eb 100%);
|
|
color: #fff;
|
|
padding: 16px 32px;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.15);
|
|
}
|
|
|
|
.header h1 {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.header h1::before {
|
|
content: "";
|
|
width: 8px;
|
|
height: 8px;
|
|
background: #10b981;
|
|
border-radius: 50%;
|
|
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
|
|
animation: pulse 2s infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2); }
|
|
50% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0.1); }
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.refresh-btn, .export-btn {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
|
color: #fff;
|
|
padding: 6px 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-size: 13px;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.refresh-btn:hover, .export-btn:hover {
|
|
background: rgba(255, 255, 255, 0.25);
|
|
}
|
|
|
|
.export-btn {
|
|
background: rgba(16, 185, 129, 0.2);
|
|
border-color: rgba(16, 185, 129, 0.4);
|
|
}
|
|
|
|
.update-time {
|
|
opacity: 0.85;
|
|
}
|
|
|
|
/* 导出加载遮罩 */
|
|
.mask {
|
|
position: fixed;
|
|
left: 0;
|
|
top: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0,0,0,0.4);
|
|
display: none;
|
|
z-index: 999;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.mask-box {
|
|
background: #fff;
|
|
padding: 24px 32px;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
display: flex;
|
|
gap:10px;
|
|
align-items: center;
|
|
}
|
|
|
|
/* 主内容区 */
|
|
.container {
|
|
padding: 24px 32px;
|
|
max-width: 1600px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
/* 统计卡片 */
|
|
.stat-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 24px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
position: relative;
|
|
overflow: hidden;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 4px;
|
|
height: 100%;
|
|
}
|
|
|
|
.stat-card.fire::before { background: linear-gradient(180deg, #ef4444, #dc2626); }
|
|
.stat-card.resolve::before { background: linear-gradient(180deg, #10b981, #059669); }
|
|
.stat-card.instance::before { background: linear-gradient(180deg, #3b82f6, #2563eb); }
|
|
.stat-card.rate::before { background: linear-gradient(180deg, #f59e0b, #d97706); }
|
|
|
|
.stat-card .label {
|
|
font-size: 14px;
|
|
color: #6b7280;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.stat-card .label .icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
font-weight: bold;
|
|
}
|
|
|
|
.stat-card.fire .icon { background: #fef2f2; color: #dc2626; }
|
|
.stat-card.resolve .icon { background: #ecfdf5; color: #059669; }
|
|
.stat-card.instance .icon { background: #eff6ff; color: #2563eb; }
|
|
.stat-card.rate .icon { background: #fffbeb; color: #d97706; }
|
|
|
|
.stat-card .value {
|
|
font-size: 36px;
|
|
font-weight: 700;
|
|
line-height: 1.2;
|
|
font-family: "DIN Alternate", -apple-system, sans-serif;
|
|
}
|
|
|
|
.stat-card.fire .value { color: #dc2626; }
|
|
.stat-card.resolve .value { color: #059669; }
|
|
.stat-card.instance .value { color: #2563eb; }
|
|
.stat-card.rate .value { color: #d97706; }
|
|
|
|
.stat-card .sub {
|
|
font-size: 12px;
|
|
color: #9ca3af;
|
|
margin-top: 8px;
|
|
}
|
|
|
|
/* 图表区 */
|
|
.chart-section {
|
|
display: grid;
|
|
grid-template-columns: 1fr;
|
|
gap: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.chart-card {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 20px 24px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.chart-card .chart-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #111827;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.chart-card .chart-title::before {
|
|
content: "";
|
|
width: 3px;
|
|
height: 16px;
|
|
background: #2563eb;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
#chart1 {
|
|
height: 380px;
|
|
width: 100%;
|
|
}
|
|
|
|
/* 表格区 */
|
|
.table-section {
|
|
background: #fff;
|
|
border-radius: 8px;
|
|
padding: 20px 24px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.table-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.table-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: #111827;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.table-title::before {
|
|
content: "";
|
|
width: 3px;
|
|
height: 16px;
|
|
background: #2563eb;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.table-count {
|
|
font-size: 13px;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.table-wrap {
|
|
overflow-x: auto;
|
|
}
|
|
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 13px;
|
|
}
|
|
|
|
th {
|
|
background: #f9fafb;
|
|
color: #374151;
|
|
font-weight: 600;
|
|
padding: 12px 16px;
|
|
text-align: left;
|
|
border-bottom: 2px solid #e5e7eb;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
td {
|
|
padding: 12px 16px;
|
|
border-bottom: 1px solid #f3f4f6;
|
|
color: #4b5563;
|
|
}
|
|
|
|
tbody tr {
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
tbody tr:hover {
|
|
background: #f9fafb;
|
|
}
|
|
|
|
tbody tr:nth-child(even) {
|
|
background: #fafbfc;
|
|
}
|
|
|
|
tbody tr:nth-child(even):hover {
|
|
background: #f3f4f6;
|
|
}
|
|
|
|
/* 状态标签 */
|
|
.status-tag {
|
|
display: inline-block;
|
|
padding: 4px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.status-tag.fire {
|
|
background: #fef2f2;
|
|
color: #dc2626;
|
|
border: 1px solid #fecaca;
|
|
}
|
|
|
|
.status-tag.resolve {
|
|
background: #ecfdf5;
|
|
color: #059669;
|
|
border: 1px solid #a7f3d0;
|
|
}
|
|
|
|
/* 级别标签 */
|
|
.severity-tag {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.severity-critical { background: #fef2f2; color: #dc2626; }
|
|
.severity-warning { background: #fffbeb; color: #d97706; }
|
|
.severity-info { background: #eff6ff; color: #2563eb; }
|
|
|
|
/* 空状态 */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: 60px 20px;
|
|
color: #9ca3af;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.empty-state .empty-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* 加载动画 */
|
|
.loading {
|
|
display: inline-block;
|
|
width: 16px;
|
|
height: 16px;
|
|
border: 2px solid #e5e7eb;
|
|
border-top-color: #2563eb;
|
|
border-radius: 50%;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
/* 响应式 */
|
|
@media (max-width: 1200px) {
|
|
.stat-cards {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.header {
|
|
padding: 12px 16px;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
align-items: flex-start;
|
|
}
|
|
.container {
|
|
padding: 16px;
|
|
}
|
|
.stat-cards {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- 导出加载遮罩 -->
|
|
<div class="mask" id="exportMask">
|
|
<div class="mask-box">
|
|
<span class="loading"></span>
|
|
<span>正在生成CSV报表,请稍候...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 顶部导航 -->
|
|
<div class="header">
|
|
<h1>告警监控看板</h1>
|
|
<div class="header-right">
|
|
<span class="update-time">最后更新:<span id="updateTime">--</span></span>
|
|
<button class="refresh-btn" onclick="loadAllData()">🔄 刷新数据</button>
|
|
<button class="export-btn" onclick="exportCsv()">📥 导出CSV报表</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<!-- 统计卡片 -->
|
|
<div class="stat-cards">
|
|
<div class="stat-card fire">
|
|
<div class="label">
|
|
<span class="icon">!</span>
|
|
<span>今日触发告警</span>
|
|
</div>
|
|
<div class="value" id="fire">0</div>
|
|
<div class="sub">Firing Alerts</div>
|
|
</div>
|
|
<div class="stat-card resolve">
|
|
<div class="label">
|
|
<span class="icon">✓</span>
|
|
<span>今日恢复告警</span>
|
|
</div>
|
|
<div class="value" id="resolve">0</div>
|
|
<div class="sub">Resolved Alerts</div>
|
|
</div>
|
|
<div class="stat-card instance">
|
|
<div class="label">
|
|
<span class="icon">#</span>
|
|
<span>涉及故障实例</span>
|
|
</div>
|
|
<div class="value" id="instance">0</div>
|
|
<div class="sub">Affected Instances</div>
|
|
</div>
|
|
<div class="stat-card rate">
|
|
<div class="label">
|
|
<span class="icon">%</span>
|
|
<span>恢复率</span>
|
|
</div>
|
|
<div class="value" id="rate">0%</div>
|
|
<div class="sub">Recovery Rate</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 图表区 -->
|
|
<div class="chart-section">
|
|
<div class="chart-card">
|
|
<div class="chart-title">今日告警次数 TOP 10</div>
|
|
<div id="chart1"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 表格区 -->
|
|
<div class="table-section">
|
|
<div class="table-header">
|
|
<div class="table-title">告警明细列表</div>
|
|
<div class="table-count">共 <span id="totalCount">0</span> 条记录</div>
|
|
</div>
|
|
<div class="table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th style="width:80px">状态</th>
|
|
<th>告警名称</th>
|
|
<th>实例地址</th>
|
|
<th style="width:100px">级别</th>
|
|
<th style="width:160px">故障开始时间</th>
|
|
<th style="width:160px">恢复时间</th>
|
|
<th style="width:160px">接收时间</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="table-body">
|
|
<tr>
|
|
<td colspan="7">
|
|
<div class="empty-state">
|
|
<div class="empty-icon">📊</div>
|
|
<div>数据加载中...</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<script>
|
|
const api = "http://10.150.117.190:8080/api";
|
|
let chartInstance = null;
|
|
|
|
// 格式化时间
|
|
function formatTime(dateStr) {
|
|
if (!dateStr || dateStr === '0000-00-00 00:00:00') return '-';
|
|
return dateStr;
|
|
}
|
|
|
|
// 级别标签
|
|
function getSeverityTag(severity) {
|
|
const s = (severity || '').toLowerCase();
|
|
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
|
|
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
|
|
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
|
|
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
|
|
}
|
|
|
|
// 1、顶部统计卡片
|
|
function loadStats() {
|
|
$.getJSON(api + "?act=day_total", function(res) {
|
|
const fire = parseInt(res.fire_count) || 0;
|
|
const resolve = parseInt(res.resolve_count) || 0;
|
|
const instance = parseInt(res.instance_num) || 0;
|
|
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
|
|
|
|
$("#fire").text(fire);
|
|
$("#resolve").text(resolve);
|
|
$("#instance").text(instance);
|
|
$("#rate").text(rate + "%");
|
|
}).fail(function() {
|
|
$("#fire").text("-");
|
|
$("#resolve").text("-");
|
|
$("#instance").text("-");
|
|
$("#rate").text("-");
|
|
});
|
|
}
|
|
|
|
// 2、TOP告警柱状图
|
|
function loadChart() {
|
|
$.getJSON(api + "?act=top_alert", function(list) {
|
|
if (!chartInstance) {
|
|
chartInstance = echarts.init(document.getElementById("chart1"));
|
|
window.addEventListener("resize", function() {
|
|
chartInstance.resize();
|
|
});
|
|
}
|
|
|
|
const names = list.map(item => item.alert_name);
|
|
const counts = list.map(item => item.cnt);
|
|
|
|
chartInstance.setOption({
|
|
tooltip: {
|
|
trigger: "axis",
|
|
axisPointer: { type: "shadow" }
|
|
},
|
|
grid: {
|
|
left: "3%",
|
|
right: "4%",
|
|
bottom: "15%",
|
|
top: "10%",
|
|
containLabel: true
|
|
},
|
|
xAxis: {
|
|
type: "category",
|
|
data: names,
|
|
axisLabel: {
|
|
rotate: 25,
|
|
fontSize: 12,
|
|
color: "#6b7280",
|
|
interval: 0
|
|
},
|
|
axisLine: { lineStyle: { color: "#e5e7eb" } },
|
|
axisTick: { show: false }
|
|
},
|
|
yAxis: {
|
|
type: "value",
|
|
name: "次数",
|
|
nameTextStyle: { color: "#9ca3af", fontSize: 12 },
|
|
axisLabel: { color: "#6b7280", fontSize: 12 },
|
|
axisLine: { show: false },
|
|
splitLine: { lineStyle: { color: "#f3f4f6", type: "dashed" } }
|
|
},
|
|
series: [{
|
|
type: "bar",
|
|
data: counts,
|
|
barWidth: "45%",
|
|
itemStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: "#3b82f6" },
|
|
{ offset: 1, color: "#2563eb" }
|
|
]),
|
|
borderRadius: [4, 4, 0, 0]
|
|
},
|
|
emphasis: {
|
|
itemStyle: {
|
|
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
|
{ offset: 0, color: "#60a5fa" },
|
|
{ offset: 1, color: "#3b82f6" }
|
|
])
|
|
}
|
|
},
|
|
label: {
|
|
show: true,
|
|
position: "top",
|
|
fontSize: 12,
|
|
color: "#374151"
|
|
}
|
|
}]
|
|
});
|
|
});
|
|
}
|
|
|
|
// 3、告警明细表格
|
|
function loadTable() {
|
|
$.getJSON(api + "?act=log_list", function(list) {
|
|
$("#totalCount").text(list.length);
|
|
|
|
if (list.length === 0) {
|
|
$("#table-body").html(`
|
|
<tr>
|
|
<td colspan="7">
|
|
<div class="empty-state">
|
|
<div class="empty-icon">📭</div>
|
|
<div>暂无告警记录</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
return;
|
|
}
|
|
|
|
let html = "";
|
|
list.forEach(function(row) {
|
|
const typeText = row.alert_type == 1
|
|
? '<span class="status-tag fire">触发</span>'
|
|
: '<span class="status-tag resolve">恢复</span>';
|
|
|
|
html += `<tr>
|
|
<td>${typeText}</td>
|
|
<td style="font-weight:500;color:#1f2937">${row.alert_name || '-'}</td>
|
|
<td style="font-family:monospace;font-size:12px">${row.instance || '-'}</td>
|
|
<td>${getSeverityTag(row.severity)}</td>
|
|
<td>${formatTime(row.starts_at)}</td>
|
|
<td>${formatTime(row.ends_at)}</td>
|
|
<td style="color:#6b7280">${formatTime(row.receive_time)}</td>
|
|
</tr>`;
|
|
});
|
|
$("#table-body").html(html);
|
|
}).fail(function() {
|
|
$("#table-body").html(`
|
|
<tr>
|
|
<td colspan="7">
|
|
<div class="empty-state" style="color:#ef4444">
|
|
<div class="empty-icon">⚠️</div>
|
|
<div>数据加载失败,请检查接口</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
}
|
|
|
|
// 更新时间
|
|
function updateTime() {
|
|
const now = new Date();
|
|
const str = now.getFullYear() + "-"
|
|
+ String(now.getMonth() + 1).padStart(2, "0") + "-"
|
|
+ String(now.getDate()).padStart(2, "0") + " "
|
|
+ String(now.getHours()).padStart(2, "0") + ":"
|
|
+ String(now.getMinutes()).padStart(2, "0") + ":"
|
|
+ String(now.getSeconds()).padStart(2, "0");
|
|
$("#updateTime").text(str);
|
|
}
|
|
|
|
// 加载全部数据
|
|
function loadAllData() {
|
|
loadStats();
|
|
loadChart();
|
|
loadTable();
|
|
updateTime();
|
|
}
|
|
|
|
// ====================== 导出CSV函数 ======================
|
|
function exportCsv(){
|
|
const mask = $("#exportMask");
|
|
mask.css("display","flex");
|
|
// 请求后端导出接口,后端调用 /root/alert_report.py 生成文件并返回下载流
|
|
window.location.href = api + "?act=export_csv";
|
|
// 3秒后自动关闭加载遮罩
|
|
setTimeout(()=>{
|
|
mask.hide();
|
|
},3000);
|
|
}
|
|
|
|
// 初始化
|
|
$(function() {
|
|
loadAllData();
|
|
// 每60秒自动刷新
|
|
setInterval(loadAllData, 60000);
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|