更新
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
<!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: 20px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.refresh-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 {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.update-time {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
/* 主内容区 */
|
||||
.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="header">
|
||||
<h1>告警监控看板</h1>
|
||||
<div class="header-right">
|
||||
<span class="update-time">最后更新:<span id="updateTime">--</span></span>
|
||||
<button class="refresh-btn" onclick="loadAllData()">🔄 刷新数据</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();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
$(function() {
|
||||
loadAllData();
|
||||
// 每60秒自动刷新
|
||||
setInterval(loadAllData, 60000);
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user