Files
aler_dashboard_git/alert_index1.php
T
2026-07-09 16:31:30 +08:00

341 lines
18 KiB
PHP

<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$userName = $_SESSION['user_name'] ?? '';
$today = date("Y-m-d");
$grafanaUrl = "http://10.150.117.190:3000";
?>
<!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/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/chart.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1400px;margin:0 auto;padding:20px;}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.nav-group{display:flex;gap:12px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.user-info{display:flex;gap:12px;align-items:center;}
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
.filter-full{width:100%;}
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
label{font-size:13px;color:#4E5969;}
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-primary:hover{background:#0E4BDB;}
.btn-warning{background:#FF7D00;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
.stat-card{padding:16px;border-radius:8px;text-align:center;}
.stat-1{background:#E8F3FF;}
.stat-2{background:#FFECE8;}
.stat-3{background:#E6FFEA;}
.stat-4{background:#FFF7E6;}
.stat-label{font-size:13px;color:#4E5969;}
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
.chart-box{width:100%;height:260px;position:relative;overflow:hidden;}
canvas{width:100% !important;height:100% !important;}
.table-wrap{overflow-x:auto;}
table{width:100%;border-collapse:collapse;font-size:14px;}
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
.row-firing{background:#FFF7F7;}
.row-resolved{background:#F7FFF9;}
.storm{color:#F53F3F;font-weight:500;}
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
.empty-box{padding:60px;text-align:center;color:#86909C;}
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<div class="nav-group">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
</div>
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
</div>
<!-- 筛选区域 -->
<div class="card">
<div class="filter-row filter-full">
<input type="text" id="globalSearch" placeholder="全局检索(告警/实例/级别)" style="width:100%;">
</div>
<div class="filter-row">
<div class="filter-item">
<label>起始日期</label>
<input type="date" id="startDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>结束日期</label>
<input type="date" id="endDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>实例模糊匹配</label>
<input type="text" id="filterInstance" placeholder="10.150.10.82">
</div>
<div class="filter-item">
<label>告警级别</label>
<div class="check-group">
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
<label><input type="checkbox" name="sev" value="info"> Info</label>
</div>
</div>
<div class="filter-item">
<label>告警状态</label>
<div class="check-group">
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
</div>
</div>
</div>
<div class="filter-row">
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> 查询</button>
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> 重置筛选</button>
<button class="btn btn-warning" id="btnToggleMode"><i class="fa fa-list"></i> 切换详细模式</button>
<span class="tip-text" id="modeTip">当前:去重汇总模式</span>
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> 导出CSV报表</button>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="autoRefresh"> 60秒自动刷新
</label>
</div>
</div>
<!-- 统计图表 -->
<div class="card" id="statCard" style="display:none;">
<div class="stat-row">
<div class="stat-card stat-1">
<div class="stat-label">告警种类</div>
<div class="stat-num" id="statTotal">0</div>
</div>
<div class="stat-card stat-2">
<div class="stat-label">触发中</div>
<div class="stat-num" id="statFiring">0</div>
</div>
<div class="stat-card stat-3">
<div class="stat-label">已恢复</div>
<div class="stat-num" id="statResolved">0</div>
</div>
<div class="stat-card stat-4">
<div class="stat-label">严重告警</div>
<div class="stat-num" id="statCritical">0</div>
</div>
</div>
<div class="chart-row">
<div>
<h4 class="card-title">24小时告警触发趋势</h4>
<div class="chart-box">
<canvas id="hourChart"></canvas>
</div>
</div>
<div>
<h4 class="card-title">告警级别分布占比</h4>
<div class="chart-box">
<canvas id="pieChart"></canvas>
</div>
</div>
</div>
</div>
<!-- 告警表格 -->
<div class="card">
<div class="table-wrap">
<table>
<thead id="tableHead"></thead>
<tbody id="tableBody">
<tr><td colspan="6" class="empty-box">数据加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pag-info">总告警条数:<span id="pageTotal">0</span></div>
<div style="display:flex;gap:8px;align-items:center;">
<span>每页展示</span>
<select id="pageSizeSel" class="pag-btn">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst">首页</button>
<button class="pag-btn" id="pagePrev">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext">下一页</button>
<button class="pag-btn" id="pageLast">末页</button>
</div>
</div>
</div>
<div class="shortcut-tip">快捷键:Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索</div>
</div>
<script>
// 原有告警查询、图表、分页、导出逻辑不变
const grafanaBase = "<?php echo $grafanaUrl; ?>";
let page = 1;
let pageSize = 20;
let total = 0;
let distinctMode = 1;
let autoRefreshTimer = null;
let hourChart = null;
let pieChart = null;
const stormThreshold = 1000;
const t = {
col_state:"状态",col_alert:"告警名称",col_instance:"实例",
col_severity:"级别",col_count:"触发次数",col_time:"最新时间",
empty_tip:"暂无匹配告警数据,请调整筛选条件",storm_tip:"风暴告警"
};
function saveFilter(){
const d={start:$("#startDate").val(),end:$("#endDate").val(),instance:$("#filterInstance").val(),global:$("#globalSearch").val(),sev:$("input[name=sev]:checked").map((i,e)=>e.value).get(),sta:$("input[name=sta]:checked").map((i,e)=>e.value).get(),distinct:distinctMode,pageSize:pageSize};
localStorage.setItem("alertFilter",JSON.stringify(d));
}
function loadFilter(){
const s=localStorage.getItem("alertFilter");if(!s)return;
const d=JSON.parse(s);
$("#startDate").val(d.start);$("#endDate").val(d.end);$("#filterInstance").val(d.instance);$("#globalSearch").val(d.global);
pageSize=d.pageSize;$("#pageSizeSel").val(pageSize);distinctMode=d.distinct;
$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");
$("input[name=sev]").prop("checked",false);d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
$("input[name=sta]").prop("checked",false);d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
}
function renderHead(){
let h="";
if(distinctMode===1){
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_count}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}else{
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}
$("#tableHead").html(h);
}
function drawChart(stat){
$("#statCard").show();
$("#statTotal").text(stat.total_all);$("#statFiring").text(stat.total_firing);
$("#statResolved").text(stat.total_resolved);$("#statCritical").text(stat.total_critical);
const hourData=stat.hour_data,hourLabels=[],hourVals=[];
for(let i=0;i<24;i++){hourLabels.push(i+":00");hourVals.push(hourData[i]);}
if(hourChart) hourChart.destroy();
const hCtx=document.getElementById("hourChart").getContext("2d");
hourChart=new Chart(hCtx,{type:"line",data:{labels:hourLabels,datasets:[{label:"触发量",data:hourVals,borderColor:"#165DFF",backgroundColor:"rgba(22,93,255,0.08)",fill:true,tension:0.2,pointRadius:2}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"top",labels:{boxWidth:12,font:{size:12}}}},scales:{x:{grid:{display:false}},y:{grid:{color:"#F2F3F5"}}}}});
if(pieChart) pieChart.destroy();
const pCtx=document.getElementById("pieChart").getContext("2d");
let c=0,w=0,i=0;$("#tableBody tr").each((idx,tr)=>{const s=$(tr).find("td:nth-child(4)").text();if(s.includes("critical"))c++;else if(s.includes("warning"))w++;else i++;});
pieChart=new Chart(pCtx,{type:"doughnut",data:{labels:["Critical","Warning","Info"],datasets:[{data:[c,w,i],backgroundColor:["#F53F3F","#FF7D00","#165DFF"],borderWidth:0}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"bottom"}},cutout:"60%"}});
}
function loadData(){
saveFilter();renderHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">数据加载中...</td></tr>`);
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]:checked").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("page",page);params.append("page_size",pageSize);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">${res.msg}</td></tr>`);$("#statCard").hide();return;}
total=res.total;$("#pageTotal").text(total);const tp=Math.ceil(total/pageSize)||1;$("#pageInfo").text(`${page} / ${tp}`);
if(res.stat.total_all>0) drawChart(res.stat);else $("#statCard").hide();
const list=res.list;if(list.length===0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);return;}
let html="";
list.forEach(row=>{
const cls=row.state==="firing"?"row-firing":"row-resolved";
const tagText=row.state==="firing"?"Firing":"Resolved";
let sevCls="";if(row.severity==="critical")sevCls="color:#F53F3F";else if(row.severity==="warning")sevCls="color:#FF7D00";else sevCls="color:#165DFF";
let storm="";if(distinctMode===1&&row.total_count>stormThreshold)storm=`<span class="storm">${t.storm_tip}</span>`;
if(distinctMode===1){
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${row.total_count} ${storm}</td><td>${row.time}</td>
<td><button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button></td>
</tr>`;
}else{
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${row.time}</td>
<td><button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button></td>
</tr>`;
}
});
$("#tableBody").html(html);
}).catch(()=>{$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">接口请求失败</td></tr>`);$("#statCard").hide();});
}
function exportExcel(){
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("export",1);params.append("page_size",9999);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0) return alert(res.msg);
let csv="";
if(distinctMode===1) csv="状态,告警名称,实例,级别,触发次数,最新时间\n";
else csv="状态,告警名称,实例,级别,触发时间\n";
res.list.forEach(r=>{
if(distinctMode===1) csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${r.time}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.time}\n`;
});
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`告警报表_${new Date().getTime()}.csv`;a.click();
});
}
function toggleAutoRefresh(){
if($("#autoRefresh").is(":checked")) autoRefreshTimer=setInterval(()=>loadData(),60000);
else clearInterval(autoRefreshTimer);
}
$(function(){
loadFilter();loadData();
$("#btnSearch").click(()=>{page=1;loadData();});
$("#btnReset").click(()=>{
const today=new Date().toISOString().split('T')[0];
$("#startDate").val(today);$("#endDate").val(today);$("#filterInstance").val("");$("#globalSearch").val("");
$("input[name=sev]").prop("checked",false);$("input[name=sta]").prop("checked",false);
distinctMode=1;$("#modeTip").text("当前:去重汇总模式");page=1;loadData();
});
$("#btnToggleMode").click(()=>{distinctMode=distinctMode===1?0:1;$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");page=1;loadData();});
$("#btnExport").click(exportExcel);
$("#autoRefresh").change(toggleAutoRefresh);
$("#pageSizeSel").change(function(){pageSize=parseInt($(this).val());page=1;loadData();});
$("#pageFirst").click(()=>{page=1;loadData();});
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
$(document).on("click",".copy-name",function(){
const name=$(this).data("name");
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
});
$(document).keydown(function(e){
if(e.key==="Enter"){e.preventDefault();page=1;loadData();}
if(e.ctrlKey&&e.key.toLowerCase()==="f"){e.preventDefault();$("#globalSearch").focus();}
});
})
</script>
</body>
</html>