This commit is contained in:
2026-07-09 16:31:30 +08:00
commit 0bb2eb4c36
132 changed files with 61222 additions and 0 deletions
+619
View File
@@ -0,0 +1,619 @@
<?php
session_start();
// 新增:禁止页面缓存,解决刷新读旧数据
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
// 会话超时30分钟统一校验,和dashboard共用
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 数据库读取现有配置,页面打开自动回填
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$res = mysqli_query($conn, "SELECT k,v FROM sys_config");
$config = [];
while ($row = mysqli_fetch_assoc($res)) {
$config[$row['k']] = $row['v'];
}
mysqli_close($conn);
// 赋值表单默认值
$pageTitle = $config['page_title'] ?? "告警监控系统";
$themeColor = $config['theme_color'] ?? "#409eff";
$pageSize = $config['page_size'] ?? "20";
$footerText = $config['footer_text'] ?? "";
$sidebarLinks = $config['sidebar_links'] ?? "";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统后台管理 - Advantest 运维告警平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
/* 侧边栏 与dashboard完全同款深蓝色侧边 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 和dashboard完全一致 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
.btn-danger:hover{
background:#e53e3e;
}
/* 页面容器 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
.page-title{
font-size:24px;
font-weight:700;
color:#152c5b;
margin-bottom:26px;
display:flex;
align-items:center;
gap:12px;
}
.page-title i{
color:#2563eb;
font-size:26px;
}
/* Tab切换 */
.tab-box{
display:flex;
border-bottom:1px solid #eef2fb;
margin-bottom:36px;
}
.tab-item{
padding:14px 26px;
cursor:pointer;
font-size:15px;
color:#64748b;
border-bottom:3px solid transparent;
transition:0.2s;
}
.tab-item.active{
color:#2563eb;
border-bottom:3px solid #2563eb;
font-weight:600;
}
.tab-item:hover:not(.active){
color:#152c5b;
}
/* 统一企业卡片样式,和dashboard base-card完全一致 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.form-row{
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
flex-wrap:wrap;
}
.form-row label{
width:160px;
font-size:14px;
color:#475569;
font-weight:500;
}
.form-row input,.form-row textarea{
padding:10px 14px;
border:1px solid #dcdfe6;
border-radius:8px;
flex:1;
max-width:480px;
font-size:14px;
}
.form-row textarea{
min-height:120px;
resize:vertical;
}
.tip-text{
font-size:13px;
color:#94a3b8;
margin-top:-12px;
margin-bottom:16px;
padding-left:160px;
}
/* 提示消息框 */
#msg{
margin:20px 0;
padding:12px 16px;
border-radius:8px;
display:none;
font-size:14px;
}
.success{
background:#dcfce7;
color:#16a34a;
border:1px solid #bbf7d0;
}
.error{
background:#fee2e2;
color:#dc2626;
border:1px solid #fecdd3;
}
/* JSON返回结果框 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:160px;
font-family:Consolas,monospace;
font-size:13px;
}
.hide{display:none}
.link-group{
margin-top:30px;
padding-top:20px;
border-top:1px solid #eef2fb;
display:flex;
gap:20px;
}
.link-group a{
color:#2563eb;
text-decoration:none;
display:flex;
align-items:center;
gap:6px;
}
.link-group a:hover{
text-decoration:underline;
}
/* 响应式适配 和dashboard统一 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.card{padding:24px}
.form-row{flex-direction:column;align-items:flex-start}
.form-row label{width:auto}
.form-row input,.form-row textarea{width:100%;max-width:100%}
.tip-text{padding-left:0}
.tab-item{padding:12px 16px}
}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">系统管理</div>
<div class="menu-item active" data-tab="pwd">
<i class="fa fa-key"></i>
<span>修改登录密码</span>
</div>
<div class="menu-item" data-tab="theme">
<i class="fa fa-paint-brush"></i>
<span>界面外观配置</span>
</div>
<div class="menu-item" data-tab="link">
<i class="fa fa-link"></i>
<span>侧边栏外链管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">系统后台管理面板</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新配置</button>
<button class="btn-base btn-primary" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
<button class="btn-base btn-danger" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="page-title"><i class="fa fa-cog"></i>系统全局配置管理</div>
<!-- Tab切换 -->
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块 BCrypt兼容 -->
<div class="card tab-content" id="tab-pwd">
<h3><i class="fa fa-key"></i>账户密码修改(BCrypt哈希加密,与登录算法统一)</h3>
<div class="form-row">
<label>原登录密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前正在使用的密码">
</div>
<div class="tip-text">输入你现在登录的密码用于身份校验</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置全新登录密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码,保持一致">
</div>
<div class="form-row">
<button class="btn-base btn-primary" onclick="updatePwd()"><i class="fa fa-check"></i>提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置(保存后dashboard实时生效) -->
<div class="card tab-content hide" id="tab-theme">
<h3><i class="fa fa-paint-brush"></i>监控大屏界面外观全局配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" value="<?php echo htmlspecialchars($pageTitle); ?>" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题主色值:</label>
<input id="theme_color" value="<?php echo htmlspecialchars($themeColor); ?>" placeholder="#409eff">
</div>
<div class="form-row">
<label>表格每页展示条数:</label>
<input id="page_size" value="<?php echo htmlspecialchars($pageSize); ?>" placeholder="20">
</div>
<div class="form-row">
<label>页面底部自定义文案:</label>
<textarea id="footer_text"><?php echo htmlspecialchars($footerText); ?></textarea>
</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveTheme()"><i class="fa fa-save"></i>保存外观配置</button>
</div>
<div class="tip-text">保存后刷新告警监控大屏dashboard.php即可生效,前端标题、分页、底部文案同步更新</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理(保存后dashboard侧边同步更新) -->
<div class="card tab-content hide" id="tab-link">
<h3><i class="fa fa-link"></i>监控大屏侧边导航外链配置</h3>
<div class="form-row">
<label>外链列表(格式:名称|地址,一行一条):</label>
<textarea id="sidebar_links"><?php echo htmlspecialchars($sidebarLinks); ?></textarea>
</div>
<div class="tip-text">示例:Prometheus|http://10.150.117.190:9090<br>保存后刷新dashboard侧边栏立即加载新外链菜单</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveLink()"><i class="fa fa-save"></i>保存侧边外链配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 底部跳转链接 -->
<div class="link-group">
<a href="./api/panel.php" target="_blank"><i class="fa fa-code"></i>进入开发者调试面板(API可视化调用)</a>
<a href="dashboard.php"><i class="fa fa-line-chart"></i>返回告警监控总览大屏</a>
</div>
</div>
</div>
</div>
<script>
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 侧边菜单同步Tab切换
$(".menu-item").click(function(){
const tabId = $(this).data("tab");
$(".tab-item").removeClass("active");
$(`.tab-item[data-tab="${tabId}"]`).addClass("active");
$(".tab-content").addClass("hide");
$("#tab-"+tabId).removeClass("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
})
// 通用提示弹窗
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// 1 修改密码 POST 提交
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码输入项不能为空","error");
if(new1 !== new2) return showMsg("两次输入的新密码不一致,请核对","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,即将跳转登录页重新验证");
setTimeout(()=>location.href="login.php",1800);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 2 保存外观配置 POST 提交
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功!2秒后自动刷新大屏页面");
setTimeout(()=>{
window.location.href = "dashboard.php?_t="+new Date().getTime();
},2000);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 3 保存侧边外链 POST 提交
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功!刷新dashboard侧边栏立刻展示新增外链菜单");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
</script>
</body>
</html>
+203
View File
@@ -0,0 +1,203 @@
<?php
session_start();
// 校验登录,无session跳转登录页
if(empty($_SESSION['user_id'])){
header("Location: login.php");
exit;
}
// 简单权限控制,如需管理员账号可自行加判断
$isAdmin = true;
if(!$isAdmin){
echo "无后台管理权限";
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>系统后台管理 Admin</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:Microsoft Yahei}
body{background:#f0f2f5;padding:24px}
.wrap{max-width:1000px;margin:0 auto}
h1{margin-bottom:20px;color:#1f2937}
.tab-box{display:flex;border-bottom:1px solid #dcdfe6;margin-bottom:16px}
.tab-item{padding:10px 20px;cursor:pointer;border:1px solid transparent;border-bottom:none;background:#fff}
.tab-item.active{border:1px solid #409eff;border-bottom:1px solid #f0f2f5;color:#409eff}
.card{background:#fff;padding:20px;border-radius:6px;border:1px solid #e4e7ed;margin-bottom:16px}
.card h3{margin-bottom:14px;color:#303133}
.form-row{margin-bottom:12px;display:flex;align-items:center;gap:10px}
label{width:140px}
input,textarea{padding:8px 10px;border:1px solid #dcdfe6;border-radius:4px;flex:1;max-width:400px}
textarea{min-height:80px}
button{padding:8px 18px;background:#409eff;color:#fff;border:none;border-radius:4px;cursor:pointer}
#msg{margin-top:10px;padding:8px;border-radius:4px;display:none}
.success{background:#e1f3d8;color:#67c23a}
.error{background:#fef0f0;color:#f56c6c}
#result{margin-top:16px;padding:12px;background:#1e1e1e;color:#fff;white-space:pre-wrap;min-height:100px}
.hide{display:none}
</style>
</head>
<body>
<div class="wrap">
<h1>系统后台管理面板</h1>
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块(BCrypt哈希兼容原有密码) -->
<div class="card tab-content" id="tab-pwd">
<h3>账户密码修改(采用bcrypt哈希加密,和历史密码算法统一)</h3>
<div class="form-row">
<label>原密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前登录密码">
</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置新密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码">
</div>
<div class="form-row">
<button onclick="updatePwd()">提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置 -->
<div class="card tab-content hide" id="tab-theme">
<h3>大屏界面外观参数配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题色值:</label>
<input id="theme_color" placeholder="#409eff">
</div>
<div class="form-row">
<label>每页展示条数:</label>
<input id="page_size" placeholder="20">
</div>
<div class="form-row">
<label>自定义底部文案:</label>
<textarea id="footer_text"></textarea>
</div>
<div class="form-row">
<button onclick="saveTheme()">保存外观配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理 -->
<div class="card tab-content hide" id="tab-link">
<h3>侧边导航外链配置(一行一条,名称|链接)</h3>
<div class="form-row">
<textarea id="sidebar_links" placeholder="示例:
Prometheus|http://10.150.117.190:9090
Alertmanager|http://10.150.117.190:9093"></textarea>
</div>
<div class="form-row">
<button onclick="saveLink()">保存侧边外链</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee">
<a href="./api/dev_panel.php" target="_blank">👉 进入开发者调试面板(所有接口可视化调用)</a>
<br><br>
<a href="dashboard.php">返回监控告警大屏</a>
</div>
</div>
<script>
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 通用提示
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// 1 修改密码接口请求
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码项不能为空","error");
if(new1 !== new2) return showMsg("两次新密码输入不一致","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,请重新登录");
setTimeout(()=>location.href="login.php",1500);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("请求接口失败:"+e,"error");
})
}
// 2 保存外观配置
function saveTheme(){
const params = new URLSearchParams();
params.append("act","save_theme");
params.append("title",document.getElementById("page_title").value);
params.append("color",document.getElementById("theme_color").value);
params.append("pagesize",document.getElementById("page_size").value);
params.append("footer",document.getElementById("footer_text").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
json.code===0 ? showMsg("外观配置保存成功") : showMsg(json.msg,"error");
})
}
// 3 保存侧边外链
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
json.code===0 ? showMsg("侧边外链保存成功") : showMsg(json.msg,"error");
})
}
</script>
</body>
</html>
+605
View File
@@ -0,0 +1,605 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,和dashboard逻辑统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 预加载现有配置,页面打开自动回填表单
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($conn, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($conn);
// 读取已有配置默认值
$pageTitle = $sysConfig['page_title'] ?? "告警监控系统";
$themeColor = $sysConfig['theme_color'] ?? "#409eff";
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = $sysConfig['footer_text'] ?? "";
$sidebarLinks = $sysConfig['sidebar_links'] ?? "";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统后台管理 - Advantest 爱德万测试运维告警平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
/* 侧边栏 同dashboard深蓝色侧边 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 和dashboard完全一致 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
.btn-danger:hover{
background:#e53e3e;
}
/* 页面容器 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
.page-title{
font-size:24px;
font-weight:700;
color:#152c5b;
margin-bottom:26px;
display:flex;
align-items:center;
gap:12px;
}
.page-title i{
color:#2563eb;
font-size:26px;
}
/* Tab切换 */
.tab-box{
display:flex;
border-bottom:1px solid #eef2fb;
margin-bottom:36px;
}
.tab-item{
padding:14px 26px;
cursor:pointer;
font-size:15px;
color:#64748b;
border-bottom:3px solid transparent;
transition:0.2s;
}
.tab-item.active{
color:#2563eb;
border-bottom:3px solid #2563eb;
font-weight:600;
}
.tab-item:hover:not(.active){
color:#152c5b;
}
/* 统一企业卡片样式,和dashboard base-card完全一致 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.form-row{
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
flex-wrap:wrap;
}
.form-row label{
width:160px;
font-size:14px;
color:#475569;
font-weight:500;
}
.form-row input,.form-row textarea{
padding:10px 14px;
border:1px solid #dcdfe6;
border-radius:8px;
flex:1;
max-width:480px;
font-size:14px;
}
.form-row textarea{
min-height:120px;
resize:vertical;
}
.tip-text{
font-size:13px;
color:#94a3b8;
margin-top:-12px;
margin-bottom:16px;
padding-left:160px;
}
/* 提示消息框 */
#msg{
margin:20px 0;
padding:12px 16px;
border-radius:8px;
display:none;
font-size:14px;
}
.success{
background:#dcfce7;
color:#16a34a;
border:1px solid #bbf7d0;
}
.error{
background:#fee2e2;
color:#dc2626;
border:1px solid #fecdd3;
}
/* JSON返回结果框 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:160px;
font-family:Consolas,monospace;
font-size:13px;
}
.hide{display:none}
.link-group{
margin-top:30px;
padding-top:20px;
border-top:1px solid #eef2fb;
display:flex;
gap:20px;
}
.link-group a{
color:#2563eb;
text-decoration:none;
display:flex;
align-items:center;
gap:6px;
}
.link-group a:hover{
text-decoration:underline;
}
/* 响应式适配 和dashboard统一 */
@media (max-width:1440px){
}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.card{padding:24px}
.form-row{flex-direction:column;align-items:flex-start}
.form-row label{width:auto}
.form-row input,.form-row textarea{width:100%;max-width:100%}
.tip-text{padding-left:0}
.tab-item{padding:12px 16px}
}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">系统管理</div>
<div class="menu-item active" data-tab="pwd">
<i class="fa fa-key"></i>
<span>修改登录密码</span>
</div>
<div class="menu-item" data-tab="theme">
<i class="fa fa-paint-brush"></i>
<span>界面外观配置</span>
</div>
<div class="menu-item" data-tab="link">
<i class="fa fa-link"></i>
<span>侧边栏外链管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">系统后台管理面板</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新配置</button>
<button class="btn-base btn-primary" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
<button class="btn-base btn-danger" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="page-title"><i class="fa fa-cog"></i>系统全局配置管理</div>
<!-- Tab切换 -->
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块 BCrypt兼容 -->
<div class="card tab-content" id="tab-pwd">
<h3><i class="fa fa-key"></i>账户密码修改(BCrypt哈希加密,与登录算法统一)</h3>
<div class="form-row">
<label>原登录密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前正在使用的密码">
</div>
<div class="tip-text">输入你现在登录的密码用于身份校验</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置全新登录密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码,保持一致">
</div>
<div class="form-row">
<button class="btn-base btn-primary" onclick="updatePwd()"><i class="fa fa-check"></i>提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置(保存后dashboard实时生效) -->
<div class="card tab-content hide" id="tab-theme">
<h3><i class="fa fa-paint-brush"></i>监控大屏界面外观全局配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" value="<?php echo htmlspecialchars($pageTitle); ?>" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题主色值:</label>
<input id="theme_color" value="<?php echo htmlspecialchars($themeColor); ?>" placeholder="#409eff">
</div>
<div class="form-row">
<label>表格每页展示条数:</label>
<input id="page_size" value="<?php echo htmlspecialchars($pageSize); ?>" placeholder="20">
</div>
<div class="form-row">
<label>页面底部自定义文案:</label>
<textarea id="footer_text"><?php echo htmlspecialchars($footerText); ?></textarea>
</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveTheme()"><i class="fa fa-save"></i>保存外观配置</button>
</div>
<div class="tip-text">保存后刷新告警监控大屏dashboard.php即可生效</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理(保存后dashboard侧边同步更新) -->
<div class="card tab-content hide" id="tab-link">
<h3><i class="fa fa-link"></i>监控大屏侧边导航外链配置</h3>
<div class="form-row">
<label>外链列表(格式:名称|地址,一行一条):</label>
<textarea id="sidebar_links"><?php echo htmlspecialchars($sidebarLinks); ?></textarea>
</div>
<div class="tip-text">示例:Prometheus|http://10.150.117.190:9090<br>保存后刷新dashboard侧边栏立即加载</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveLink()"><i class="fa fa-save"></i>保存侧边外链配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 底部跳转链接 -->
<div class="link-group">
<a href="./api/dev_panel.php" target="_blank"><i class="fa fa-code"></i>进入开发者调试面板(API可视化调用)</a>
<a href="dashboard.php"><i class="fa fa-line-chart"></i>返回告警监控总览大屏</a>
</div>
</div>
</div>
</div>
<script>
// 相对路径接口,适配站点部署
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 侧边菜单同步Tab切换
$(".menu-item").click(function(){
const tabId = $(this).data("tab");
$(".tab-item").removeClass("active");
$(`.tab-item[data-tab="${tabId}"]`).addClass("active");
$(".tab-content").addClass("hide");
$("#tab-"+tabId).removeClass("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
})
// 通用提示弹窗
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// 1 修改密码接口请求
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码输入项不能为空","error");
if(new1 !== new2) return showMsg("两次输入的新密码不一致,请核对","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,即将跳转登录页重新验证");
setTimeout(()=>location.href="login.php",1800);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 2 保存外观配置(存入sys_configdashboard读取渲染)
function saveTheme(){
const params = new URLSearchParams();
params.append("act","save_theme");
params.append("title",document.getElementById("page_title").value);
params.append("color",document.getElementById("theme_color").value);
params.append("pagesize",document.getElementById("page_size").value);
params.append("footer",document.getElementById("footer_text").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("外观配置保存成功,刷新监控大屏即可生效");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 3 保存侧边外链(存入sys_configdashboard侧边读取渲染)
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功,刷新监控大屏侧边栏立即更新");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
</script>
</body>
</html>
+625
View File
@@ -0,0 +1,625 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,和dashboard逻辑统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 数据库连接读取全局配置,页面打开自动回填
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($conn, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($conn);
// 读取配置默认值
$pageTitle = $sysConfig['page_title'] ?? "告警监控系统";
$themeColor = $sysConfig['theme_color'] ?? "#409eff";
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = $sysConfig['footer_text'] ?? "";
$sidebarLinks = $sysConfig['sidebar_links'] ?? "";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统后台管理 - Advantest 爱德万测试运维告警平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
/* 侧边栏 与dashboard完全同款深蓝色侧边 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 和dashboard完全一致 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
.btn-danger:hover{
background:#e53e3e;
}
/* 页面容器 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
.page-title{
font-size:24px;
font-weight:700;
color:#152c5b;
margin-bottom:26px;
display:flex;
align-items:center;
gap:12px;
}
.page-title i{
color:#2563eb;
font-size:26px;
}
/* Tab切换 */
.tab-box{
display:flex;
border-bottom:1px solid #eef2fb;
margin-bottom:36px;
}
.tab-item{
padding:14px 26px;
cursor:pointer;
font-size:15px;
color:#64748b;
border-bottom:3px solid transparent;
transition:0.2s;
}
.tab-item.active{
color:#2563eb;
border-bottom:3px solid #2563eb;
font-weight:600;
}
.tab-item:hover:not(.active){
color:#152c5b;
}
/* 统一企业卡片样式,和dashboard base-card完全一致 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.form-row{
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
flex-wrap:wrap;
}
.form-row label{
width:160px;
font-size:14px;
color:#475569;
font-weight:500;
}
.form-row input,.form-row textarea{
padding:10px 14px;
border:1px solid #dcdfe6;
border-radius:8px;
flex:1;
max-width:480px;
font-size:14px;
}
.form-row textarea{
min-height:120px;
resize:vertical;
}
.tip-text{
font-size:13px;
color:#94a3b8;
margin-top:-12px;
margin-bottom:16px;
padding-left:160px;
}
/* 提示消息框 */
#msg{
margin:20px 0;
padding:12px 16px;
border-radius:8px;
display:none;
font-size:14px;
}
.success{
background:#dcfce7;
color:#16a34a;
border:1px solid #bbf7d0;
}
.error{
background:#fee2e2;
color:#dc2626;
border:1px solid #fecdd3;
}
/* JSON返回结果框 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:160px;
font-family:Consolas,monospace;
font-size:13px;
}
.hide{display:none}
.link-group{
margin-top:30px;
padding-top:20px;
border-top:1px solid #eef2fb;
display:flex;
gap:20px;
}
.link-group a{
color:#2563eb;
text-decoration:none;
display:flex;
align-items:center;
gap:6px;
}
.link-group a:hover{
text-decoration:underline;
}
/* 响应式适配 和dashboard统一 */
@media (max-width:1440px){
}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.card{padding:24px}
.form-row{flex-direction:column;align-items:flex-start}
.form-row label{width:auto}
.form-row input,.form-row textarea{width:100%;max-width:100%}
.tip-text{padding-left:0}
.tab-item{padding:12px 16px}
}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">系统管理</div>
<div class="menu-item active" data-tab="pwd">
<i class="fa fa-key"></i>
<span>修改登录密码</span>
</div>
<div class="menu-item" data-tab="theme">
<i class="fa fa-paint-brush"></i>
<span>界面外观配置</span>
</div>
<div class="menu-item" data-tab="link">
<i class="fa fa-link"></i>
<span>侧边栏外链管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">系统后台管理面板</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新配置</button>
<button class="btn-base btn-primary" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
<button class="btn-base btn-danger" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="page-title"><i class="fa fa-cog"></i>系统全局配置管理</div>
<!-- Tab切换 -->
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块 BCrypt兼容 -->
<div class="card tab-content" id="tab-pwd">
<h3><i class="fa fa-key"></i>账户密码修改(BCrypt哈希加密,与登录算法统一)</h3>
<div class="form-row">
<label>原登录密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前正在使用的密码">
</div>
<div class="tip-text">输入你现在登录的密码用于身份校验</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置全新登录密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码,保持一致">
</div>
<div class="form-row">
<button class="btn-base btn-primary" onclick="updatePwd()"><i class="fa fa-check"></i>提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置(保存后dashboard实时生效) -->
<div class="card tab-content hide" id="tab-theme">
<h3><i class="fa fa-paint-brush"></i>监控大屏界面外观全局配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" value="<?php echo htmlspecialchars($pageTitle); ?>" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题主色值:</label>
<input id="theme_color" value="<?php echo htmlspecialchars($themeColor); ?>" placeholder="#409eff">
</div>
<div class="form-row">
<label>表格每页展示条数:</label>
<input id="page_size" value="<?php echo htmlspecialchars($pageSize); ?>" placeholder="20">
</div>
<div class="form-row">
<label>页面底部自定义文案:</label>
<textarea id="footer_text"><?php echo htmlspecialchars($footerText); ?></textarea>
</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveTheme()"><i class="fa fa-save"></i>保存外观配置</button>
</div>
<div class="tip-text">保存后刷新告警监控大屏dashboard.php即可生效</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理(保存后dashboard侧边同步更新) -->
<div class="card tab-content hide" id="tab-link">
<h3><i class="fa fa-link"></i>监控大屏侧边导航外链配置</h3>
<div class="form-row">
<label>外链列表(格式:名称|地址,一行一条):</label>
<textarea id="sidebar_links"><?php echo htmlspecialchars($sidebarLinks); ?></textarea>
</div>
<div class="tip-text">示例:Prometheus|http://10.150.117.190:9090<br>保存后刷新dashboard侧边栏立即加载</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveLink()"><i class="fa fa-save"></i>保存侧边外链配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 底部跳转链接 -->
<div class="link-group">
<a href="./api/dev_panel.php" target="_blank"><i class="fa fa-code"></i>进入开发者调试面板(API可视化调用)</a>
<a href="dashboard.php"><i class="fa fa-line-chart"></i>返回告警监控总览大屏</a>
</div>
</div>
</div>
</div>
<script>
// 相对路径接口,适配站点部署
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 侧边菜单同步Tab切换
$(".menu-item").click(function(){
const tabId = $(this).data("tab");
$(".tab-item").removeClass("active");
$(`.tab-item[data-tab="${tabId}"]`).addClass("active");
$(".tab-content").addClass("hide");
$("#tab-"+tabId).removeClass("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
})
// 通用提示弹窗
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// ========== 核心修复:全部改用POST标准表单提交,补全编码头,解决参数丢失/乱码 ==========
// 1 修改密码接口请求
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码输入项不能为空","error");
if(new1 !== new2) return showMsg("两次输入的新密码不一致,请核对","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,即将跳转登录页重新验证");
setTimeout(()=>location.href="login.php",1800);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 2 保存外观配置(存入sys_configdashboard读取渲染)
function saveTheme(){
const params = new URLSearchParams();
params.append("act","save_theme");
params.append("title",document.getElementById("page_title").value);
params.append("color",document.getElementById("theme_color").value);
params.append("pagesize",document.getElementById("page_size").value);
params.append("footer",document.getElementById("footer_text").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("外观配置保存成功,刷新监控大屏即可生效");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 3 保存侧边外链(存入sys_configdashboard侧边读取渲染)
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功,刷新监控大屏侧边栏立即更新");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
</script>
</body>
</html>
+375
View File
@@ -0,0 +1,375 @@
<?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");
// Grafana固定看板地址
$grafanaBase = "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s";
?>
<!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-left{display:flex;gap:12px;align-items:center;}
.nav-right{display:flex;gap:10px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.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-danger{background:#F53F3F;color:#fff;}
.btn-danger:hover{background:#D83636;}
.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-left">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
</div>
<!-- 右侧:订阅管理 + 用户名 + 退出登录 -->
<div class="nav-right">
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
</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 grafanaFullBase = "<?php echo $grafanaBase; ?>";
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:"风暴告警"
};
// UTC秒级时间戳转北京时间
function utcTsToCst(ts) {
const date = new Date(ts * 1000);
const cstTime = date.getTime() + 8 * 3600 * 1000;
const cstDate = new Date(cstTime);
const Y = cstDate.getFullYear();
const m = String(cstDate.getMonth() + 1).padStart(2, '0');
const d = String(cstDate.getDate()).padStart(2, '0');
const H = String(cstDate.getHours()).padStart(2, '0');
const i = String(cstDate.getMinutes()).padStart(2, '0');
const s = String(cstDate.getSeconds()).padStart(2, '0');
return `${Y}-${m}-${d} ${H}:${i}:${s}`;
}
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]").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>`;
const cstTimeStr = utcTsToCst(row.timestamp);
const grafanaUrl = `${grafanaFullBase}&var-instance=${encodeURIComponent(row.instance)}&var-alertname=${encodeURIComponent(row.metric)}`;
const btnHtml = `
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button>
<a class="btn btn-sm btn-primary" target="_blank" href="${grafanaUrl}">查看图表</a>
`;
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>${cstTimeStr}</td>
<td>${btnHtml}</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>${cstTimeStr}</td>
<td>${btnHtml}</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},${utcTsToCst(r.timestamp)}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${utcTsToCst(r.timestamp)}\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();
// 退出登录按钮
$("#logoutBtn").click(function(){
if(confirm("确定退出当前账号?")){
location.href="login.php";
}
});
$("#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>
+340
View File
@@ -0,0 +1,340 @@
<?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>
+369
View File
@@ -0,0 +1,369 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
require_once 'auth.php';
$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");
$grafanaBase = "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s";
?>
<!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-left{display:flex;gap:12px;align-items:center;}
.nav-right{display:flex;gap:10px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.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-danger{background:#F53F3F;color:#fff;}
.btn-danger:hover{background:#D83636;}
.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-left">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
</div>
<div class="nav-right">
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?><?php echo $roleRealName; ?></span>
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
</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 grafanaFullBase = "<?php echo $grafanaBase; ?>";
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:"风暴告警"
};
// 时区回滚:原生浏览器本地时区,删除手动+8偏移
function utcTsToCst(ts) {
const date = new Date(ts * 1000);
const Y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const H = String(date.getHours()).padStart(2, '0');
const i = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${Y}-${m}-${d} ${H}:${i}:${s}`;
}
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]").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>`;
const cstTimeStr = utcTsToCst(row.timestamp);
const grafanaUrl = `${grafanaFullBase}&var-instance=${encodeURIComponent(row.instance)}&var-alertname=${encodeURIComponent(row.metric)}`;
// 统一btn-sm btn-outline,无蓝色主按钮
const btnHtml = `
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button>
<a class="btn btn-sm btn-outline" target="_blank" href="${grafanaUrl}">查看图表</a>
`;
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>${cstTimeStr}</td>
<td>${btnHtml}</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>${cstTimeStr}</td>
<td>${btnHtml}</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},${utcTsToCst(r.timestamp)}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${utcTsToCst(r.timestamp)}\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();
$("#logoutBtn").click(function(){
if(confirm("确定退出当前账号?")){
location.href="login.php";
}
});
$("#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>
+650
View File
@@ -0,0 +1,650 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username'] ?? "");
$userRole = $_SESSION['role'] ?? "user";
// 固定配置
$grafanaUrl = "http://10.150.117.190:3000";
$currentLang = "zh";
$langMap = [
'zh' => [
'page_title' => '告警时序监控面板',
'global_search' => '全局检索(告警/实例/级别)',
'filter_instance' => '实例IP模糊匹配',
'start_date' => '起始日期',
'end_date' => '结束日期',
'severity_title' => '告警级别',
'status_title' => '告警状态',
'btn_query' => '查询',
'btn_reset' => '重置筛选',
'btn_detail_mode' => '切换详细模式',
'tip_distinct' => '当前:去重汇总模式',
'tip_raw' => '当前:逐条时序模式',
'stat_total' => '总告警种类',
'stat_firing' => '触发中',
'stat_resolved' => '已恢复',
'stat_critical' => '严重告警',
'hour_chart_title' => '24小时告警触发趋势',
'pie_title' => '告警级别分布占比',
'col_state' => '状态',
'col_alert' => '告警名称',
'col_instance' => '实例地址',
'col_severity' => '级别',
'col_count' => '当日触发次数',
'col_time' => '最新触发时间',
'btn_copy_name' => '复制告警名',
'btn_copy_link' => '复制筛选链接',
'btn_export' => '导出CSV报表',
'btn_refresh_auto' => '60秒自动刷新',
'modal_title' => '告警完整详情',
'modal_grafana' => '跳转Grafana大盘',
'modal_labels' => '全量标签信息',
'empty_tip' => '暂无匹配告警数据,请调整筛选条件',
'storm_tip' => '风暴告警',
'shortcut_tip' => 'Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索'
]
];
$t = $langMap[$currentLang];
$today = date("Y-m-d");
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $t['page_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;}
.back-link{color:#165DFF;text-decoration:none;font-size:14px;}
.user-info{font-size:14px;color:#4E5969;}
/* 卡片通用 */
.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;}
.check-group label{display:flex;align-items:center;gap:4px;cursor:pointer;}
/* 按钮规范 */
.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;}
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;}
.pag-info{color:#4E5969;}
/* 弹窗 */
.modal-mask{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);display:none;align-items:center;justify-content:center;z-index:999;}
.modal-box{width:760px;max-height:80vh;background:#fff;border-radius:8px;padding:24px;overflow-y:auto;}
.modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.modal-close{cursor:pointer;font-size:20px;color:#4E5969;}
.labels-block{background:#F5F7FA;padding:12px;border-radius:6px;margin:12px 0;white-space:pre-wrap;font-family:monospace;font-size:13px;}
/* 空数据、提示文字 */
.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">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
<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="<?php echo $t['global_search']; ?>" style="width:100%;">
</div>
<div class="filter-row">
<div class="filter-item">
<label><?php echo $t['start_date']; ?></label>
<input type="date" id="startDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label><?php echo $t['end_date']; ?></label>
<input type="date" id="endDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label><?php echo $t['filter_instance']; ?></label>
<input type="text" id="filterInstance" placeholder="10.150.10.82">
</div>
<div class="filter-item">
<label><?php echo $t['severity_title']; ?></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><?php echo $t['status_title']; ?></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> <?php echo $t['btn_query']; ?></button>
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> <?php echo $t['btn_reset']; ?></button>
<button class="btn btn-warning" id="btnToggleDistinct"><i class="fa fa-list"></i> <?php echo $t['btn_detail_mode']; ?></button>
<span class="tip-text" id="modeTip"><?php echo $t['tip_distinct']; ?></span>
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> <?php echo $t['btn_export']; ?></button>
<button class="btn btn-outline" id="btnCopyLink"><i class="fa fa-link"></i> <?php echo $t['btn_copy_link']; ?></button>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="autoRefresh"> <?php echo $t['btn_refresh_auto']; ?>
</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"><?php echo $t['stat_total']; ?></div>
<div class="stat-num" id="statTotal">0</div>
</div>
<div class="stat-card stat-2">
<div class="stat-label"><?php echo $t['stat_firing']; ?></div>
<div class="stat-num" id="statFiring">0</div>
</div>
<div class="stat-card stat-3">
<div class="stat-label"><?php echo $t['stat_resolved']; ?></div>
<div class="stat-num" id="statResolved">0</div>
</div>
<div class="stat-card stat-4">
<div class="stat-label"><?php echo $t['stat_critical']; ?></div>
<div class="stat-num" id="statCritical">0</div>
</div>
</div>
<div class="chart-row">
<div>
<h4 class="card-title"><?php echo $t['hour_chart_title']; ?></h4>
<div class="chart-box">
<canvas id="hourChart"></canvas>
</div>
</div>
<div>
<h4 class="card-title"><?php echo $t['pie_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">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pag-info"><?php echo $t['stat_total']; ?><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"><?php echo $t['shortcut_tip']; ?></div>
</div>
<!-- 详情弹窗 -->
<div class="modal-mask" id="modalMask">
<div class="modal-box">
<div class="modal-head">
<h3 id="modalTitle"><?php echo $t['modal_title']; ?></h3>
<span class="modal-close" id="modalClose">×</span>
</div>
<div id="modalContent"></div>
<div style="margin-top:16px;">
<a target="_blank" id="grafanaLink" class="btn btn-primary"><i class="fa fa-chart-line"></i> <?php echo $t['modal_grafana']; ?></a>
</div>
</div>
</div>
<script>
const apiUrl = "./api/api_alert_list.php";
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 = <?php echo json_encode($t); ?>;
// 本地筛选记忆
function saveFilter() {
const data = {
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]").map((i,e)=>e.value).get(),
distinct: distinctMode,
pageSize: pageSize
};
localStorage.setItem("alertFilter", JSON.stringify(data));
}
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 ? t.tip_distinct : t.tip_raw);
$("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;
const hourLabels = [];
const 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: {display:true,position:"top",labels:{boxWidth:12,font:{size:12}}},
tooltip: {mode:"index",intersect:false}
},
scales: {
x: {grid:{display:false},ticks:{font:{size:11}}},
y: {grid:{color:"#F2F3F5"},ticks:{font:{size:11}}}
}
}
});
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",labels:{boxWidth:12,font:{size:12}}}},
cutout:"60%" // 缩小饼图半径,不会超大
}
});
}
// 加载数据
function loadData() {
saveFilter();
renderHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">Loading...</td></tr>`);
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("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(`${apiUrl}?${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;"><i class="fa fa-exclamation-circle"></i> ${res.msg}</td></tr>`);
$("#statCard").hide();
return;
}
total = res.total;
$("#pageTotal").text(total);
const totalPage = Math.ceil(total / pageSize) || 1;
$("#pageInfo").text(`${page} / ${totalPage}`);
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 tagClass = row.state === "firing" ? "tag-firing" : "tag-resolved";
const tagText = row.state === "firing" ? "Firing" : "Resolved";
let sevCls = "";
if (row.severity === "critical") sevCls = "severity-critical";
else if (row.severity === "warning") sevCls = "severity-warning";
else sevCls = "severity-info";
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><span class="${tagClass}">${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevCls}">${row.severity}</span></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}">${t.btn_copy_name}</button>
<button class="btn btn-sm btn-outline detail-open">详情</button>
</td>
</tr>`;
} else {
html += `<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td><span class="${tagClass}">${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevCls}">${row.severity}</span></td>
<td>${row.time}</td>
<td>
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">${t.btn_copy_name}</button>
<button class="btn btn-sm btn-outline detail-open">详情</button>
</td>
</tr>`;
}
})
$("#tableBody").html(html);
})
.catch(()=>{
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;"><i class="fa fa-exclamation-circle"></i> 接口请求失败</td></tr>`);
$("#statCard").hide();
})
}
// 导出CSV
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(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(res=>{
if (res.code !== 0) return alert(res.msg);
let csv = "";
if (distinctMode === 1) {
csv = "状态,告警名称,实例,级别,触发次数,最新时间\n";
res.list.forEach(r=>csv += `${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${r.time}\n`);
} else {
csv = "状态,告警名称,实例,级别,触发时间\n";
res.list.forEach(r=>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 = "alert_export_"+new Date().getTime()+".csv";
a.click();
})
}
// 复制筛选链接
function copyLink() {
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("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));
const link = location.origin + location.pathname + "?" + params.toString();
navigator.clipboard.writeText(link).then(()=>alert("筛选链接已复制"));
}
// 弹窗详情
function openModal(row) {
const metric = row.metric;
const inst = row.instance;
const grafanaLink = `${grafanaBase}/d/alert-overview?var-alert=${encodeURIComponent(metric)}&var-instance=${encodeURIComponent(inst)}`;
$("#grafanaLink").attr("href", grafanaLink);
let html = `<h4>${metric}</h4>
<p>实例:${inst}|级别:${row.severity}|状态:${row.state}</p>
<div class="labels-block">${JSON.stringify(row.labels, null, 2)}</div>`;
$("#modalContent").html(html);
$("#modalMask").css("display","flex");
}
// 自动刷新开关
function toggleAutoRefresh() {
if ($("#autoRefresh").is(":checked")) {
autoRefreshTimer = setInterval(()=>loadData(), 60000);
} else {
clearInterval(autoRefreshTimer);
}
}
$(function(){
loadFilter();
loadData();
// 切换汇总/详细模式
$("#btnToggleDistinct").click(()=>{
distinctMode = distinctMode === 1 ? 0 : 1;
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_raw);
page = 1;
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(t.tip_distinct);
page = 1;
loadData();
})
// 导出、复制链接、自动刷新
$("#btnExport").click(exportExcel);
$("#btnCopyLink").click(copyLink);
$("#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).on("click",".detail-open",function(){
const rowJson = $(this).closest("tr").attr("data-row");
const row = JSON.parse(rowJson);
openModal(row);
})
// 关闭弹窗
$("#modalClose, #modalMask").click(function(e){
if (e.target === this) $("#modalMask").hide();
})
// 全局快捷键
$(document).keydown(function(e){
if (e.key === "Enter") {
e.preventDefault();
page = 1;
loadData();
}
if (e.key === "Escape") {
$("#modalMask").hide();
}
if (e.ctrlKey && e.key.toLowerCase() === "f") {
e.preventDefault();
$("#globalSearch").focus();
}
})
})
</script>
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($connConfig, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) $sysConfig[$row['k']] = $row['v'];
mysqli_close($connConfig);
$currentLang = $sysConfig['lang'] ?? "zh";
$langMap = [
'zh' => [
'html_lang' => 'zh-CN',
'page_title_list' => '告警分页列表',
'filter_date' => '日期筛选',
'filter_alert_name' => '告警名称',
'filter_severity' => '告警级别',
'filter_status' => '告警状态',
'btn_search' => '查询',
'btn_reset' => '重置',
'page_total' => '共',
'page_record' => '条记录',
'page_size' => '每页',
'page_prev' => '上一页',
'page_next' => '下一页',
'page_first' => '首页',
'page_last' => '末页',
'empty_no_data' => '暂无告警时序数据',
'server_fail' => 'Prometheus 接口请求失败',
'tag' => '状态',
'alert_name' => '告警指标',
'instance_addr' => '实例',
'severity' => '级别',
'occur_time_group' => '最新触发时间',
'occur_time_detail' => '触发时间',
'firing' => '触发',
'resolved' => '恢复',
'critical' => '严重',
'warning' => '警告',
'info' => '信息',
'btn_detail' => '详细模式',
'tip_distinct' => '当前:合并去重(汇总次数)',
'tip_all' => '当前:详细时序(逐条展示触发时间)',
'col_count' => '总触发次数'
],
'en' => [
'html_lang' => 'en',
'page_title_list' => 'Alert Pagination List',
'filter_date' => 'Date',
'filter_alert_name' => 'Metric',
'filter_severity' => 'Severity',
'filter_status' => 'Status',
'btn_search' => 'Search',
'btn_reset' => 'Reset',
'page_total' => 'Total ',
'page_record' => ' records',
'page_size' => 'Per page',
'page_prev' => 'Prev',
'page_next' => 'Next',
'page_first' => 'First',
'page_last' => 'Last',
'empty_no_data' => 'No alert time series data',
'server_fail' => 'Prometheus API request failed',
'tag' => 'Status',
'alert_name' => 'Metric',
'instance_addr' => 'Instance',
'severity' => 'Severity',
'occur_time_group' => 'Last Trigger Time',
'occur_time_detail' => 'Trigger Time',
'firing' => 'Firing',
'resolved' => 'Resolved',
'critical' => 'Critical',
'warning' => 'Warning',
'info' => 'Info',
'btn_detail' => 'Detail Mode',
'tip_distinct' => 'Mode: Deduplicated (Total Count)',
'tip_all' => 'Mode: Raw Time Series (Each Trigger)',
'col_count' => 'Total Triggers'
]
];
$t = $langMap[$currentLang];
?>
<!DOCTYPE html>
<html lang="<?php echo $t['html_lang']; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $t['page_title_list']; ?></title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f9fafc;font-family:"Inter","PingFang SC","Microsoft YaHei";color:#1d2939;line-height:1.6;height:100vh;overflow:auto;padding:24px;}
.base-card{background:#fff;border-radius:16px;padding:32px;box-shadow:0 3px 18px rgba(21,44,91,0.06);border:1px solid #eef2fb;max-width:1600px;margin:0 auto;}
.title-wrap{display:flex;justify-content:space-between;align-items:center;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid #eef2fb;}
.title-left{display:flex;align-items:center;gap:12px;font-size:19px;font-weight:600;color:#152c5b;}
.title-left i{color:#2563eb;font-size:21px}
.filter-bar{display:flex;flex-wrap:wrap;gap:16px;margin-bottom:24px;align-items:center;}
.filter-bar label{font-size:14px;color:#475569;display:flex;align-items:center;gap:8px;}
.filter-bar input,.filter-bar select{padding:6px 10px;border:1px solid #cbd5e1;border-radius:6px;}
.btn-base{border:none;padding:9px 16px;border-radius:8px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;}
.btn-success{background:#10b981;color:#fff;}
.btn-outline{background:#fff;color:#152c5b;border:1px solid #cbd5e1;}
.btn-warning{background:#f59e0b;color:#fff;}
.table-wrap{overflow-x:auto;margin-bottom:24px;}
table{width:100%;border-collapse:collapse;font-size:14px}
th{background:#f9fafc;color:#475569;font-weight:600;padding:16px 18px;text-align:left;border-bottom:2px solid #e2e8f0;white-space:nowrap;}
td{padding:18px;border-bottom:1px solid #f1f5f9;color:#334155;}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
.status-tag{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:24px;font-size:13px;font-weight:500;}
.tag-firing{background:#fee2e2;color:#dc2626}
.tag-resolved{background:#dcfce7;color:#16a34a}
.severity-critical{background:#fee2e2;color:#dc2626;padding:5px 12px;border-radius:8px;font-size:13px;}
.severity-warning{background:#ffedd5;color:#ea580c;padding:5px 12px;border-radius:8px;font-size:13px;}
.severity-info{background:#dbeafe;color:#2563eb;padding:5px 12px;border-radius:8px;font-size:13px;}
.empty-state{text-align:center;padding:80px 20px;color:#94a3b8;font-size:15px;}
.empty-icon{font-size:56px;margin-bottom:18px;opacity:0.35;}
.pagination{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;}
.pag-btn{padding:4px 10px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;cursor:pointer;}
.pag-info{margin:0 12px;}
.header-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;max-width:1600px;margin-left:auto;margin-right:auto;}
.user-bar{display:flex;gap:16px;align-items:center;font-size:14px;color:#475569;}
.back-btn{color:#2563eb;text-decoration:none;}
.mode-tip{color:#64748b;font-size:13px;margin-left:10px;}
.count-badge{font-weight:bold;color:#dc2626;}
</style>
</head>
<body>
<div class="header-top">
<a href="dashboard.php" class="back-btn"><i class="fa fa-arrow-left"></i> 返回监控大屏</a>
<div class="user-bar">
<span><i class="fa fa-user-circle"></i> <?php echo $userName; ?> (<?php echo $roleText; ?>)</span>
<a href="logout.php" class="back-btn">退出登录</a>
</div>
</div>
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list"></i><?php echo $t['page_title_list']; ?></div>
<div style="display:flex;align-items:center;">
<button class="btn-base btn-warning" id="btnToggleDetail"><?php echo $t['btn_detail']; ?></button>
<span class="mode-tip" id="modeTip"><?php echo $t['tip_distinct']; ?></span>
</div>
</div>
<div class="filter-bar">
<label><?php echo $t['filter_date']; ?>
<input type="date" id="filterDate">
</label>
<label><?php echo $t['filter_alert_name']; ?>
<input type="text" id="filterMetric" placeholder="metric name">
</label>
<label><?php echo $t['filter_severity']; ?>
<select id="filterSeverity">
<option value="">--</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</label>
<label><?php echo $t['filter_status']; ?>
<select id="filterStatus">
<option value="">--</option>
<option value="firing"><?php echo $t['firing']; ?></option>
<option value="resolved"><?php echo $t['resolved']; ?></option>
</select>
</label>
<button class="btn-base btn-success" id="btnSearch"><?php echo $t['btn_search']; ?></button>
<button class="btn-base btn-outline" id="btnReset"><?php echo $t['btn_reset']; ?></button>
</div>
<div class="table-wrap">
<table id="dataTable">
<thead id="tableHead">
<!-- JS动态渲染表头 -->
</thead>
<tbody id="tableBody">
<tr>
<td colspan="6" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>Loading...</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<div>
<?php echo $t['page_total']; ?><span id="totalCnt">0</span><?php echo $t['page_record']; ?>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<span><?php echo $t['page_size']; ?></span>
<select id="pageSize">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst"><?php echo $t['page_first']; ?></button>
<button class="pag-btn" id="pagePrev"><?php echo $t['page_prev']; ?></button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext"><?php echo $t['page_next']; ?></button>
<button class="pag-btn" id="pageLast"><?php echo $t['page_last']; ?></button>
</div>
</div>
</div>
<script>
const apiUrl = "./api/api_alert_list.php";
let page = 1;
let pageSize = 20;
let total = 0;
const defaultDate = new Date().toISOString().split('T')[0];
let filter = {
date: defaultDate,
metric: "",
severity: "",
status: ""
};
let distinctMode = 1; // 1汇总去重,0详细逐条
const t = <?php echo json_encode($t); ?>;
$(function(){
$("#filterDate").val(defaultDate);
loadData();
})
// 切换模式按钮
$("#btnToggleDetail").click(function(){
distinctMode = distinctMode === 1 ? 0 : 1;
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_all);
page = 1;
loadData();
})
// 渲染表头
function renderTableHead(){
let headHtml = "";
if(distinctMode === 1){
// 汇总模式:带总次数列
headHtml = `<tr>
<th>${t.tag}</th>
<th>${t.alert_name}</th>
<th>${t.instance_addr}</th>
<th>${t.severity}</th>
<th>${t.col_count}</th>
<th>${t.occur_time_group}</th>
</tr>`;
}else{
// 详细模式:无次数列,纯触发时间
headHtml = `<tr>
<th>${t.tag}</th>
<th>${t.alert_name}</th>
<th>${t.instance_addr}</th>
<th>${t.severity}</th>
<th>${t.occur_time_detail}</th>
</tr>`;
}
$("#tableHead").html(headHtml);
}
function loadData(){
renderTableHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>Loading...</div></td></tr>`);
const params = new URLSearchParams();
params.append("page", page);
params.append("page_size", pageSize);
params.append("date", filter.date);
params.append("metric", filter.metric);
params.append("severity", filter.severity);
params.append("status", filter.status);
params.append("distinct", distinctMode);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(res=>{
if(res.code !== 0){
$("#tableBody").html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>${res.msg}</div></td></tr>`);
return;
}
const rawList = res.list;
total = res.total;
$("#totalCnt").text(total);
const totalPage = Math.ceil(total / pageSize) || 1;
$("#pageInfo").text(`${page} / ${totalPage}`);
const tbody = $("#tableBody");
if(rawList.length === 0){
tbody.html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>${t.empty_no_data}</div></td></tr>`);
return;
}
let html = "";
rawList.forEach(row=>{
const tagClass = row.state === "firing" ? "tag-firing" : "tag-resolved";
const tagText = row.state === "firing" ? t.firing : t.resolved;
let sevClass = "severity-info";
if(row.severity === "critical") sevClass = "severity-critical";
if(row.severity === "warning") sevClass = "severity-warning";
if(distinctMode === 1){
// 汇总行,带计数
html += `<tr>
<td><span class="status-tag ${tagClass}"><i class="fa fa-bolt"></i>${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevClass}">${row.severity}</span></td>
<td><span class="count-badge">${row.total_count}</span></td>
<td>${row.time}</td>
</tr>`;
}else{
// 详细模式,无计数
html += `<tr>
<td><span class="status-tag ${tagClass}"><i class="fa fa-bolt"></i>${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevClass}">${row.severity}</span></td>
<td>${row.time}</td>
</tr>`;
}
})
tbody.html(html);
})
.catch(()=>{
$("#tableBody").html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>${t.server_fail}</div></td></tr>`);
})
}
// 查询按钮
$("#btnSearch").click(function(){
filter.date = $("#filterDate").val();
filter.metric = $("#filterMetric").val().trim();
filter.severity = $("#filterSeverity").val();
filter.status = $("#filterStatus").val();
page = 1;
loadData();
})
// 重置:恢复默认去重汇总模式
$("#btnReset").click(function(){
$("#filterDate").val(defaultDate);
$("#filterMetric").val("");
$("#filterSeverity").val("");
$("#filterStatus").val("");
filter = {date: defaultDate, metric: "", severity: "", status: ""};
distinctMode = 1;
$("#modeTip").text(t.tip_distinct);
page = 1;
loadData();
})
$("#pageSize").change(function(){
pageSize = parseInt($(this).val());
page = 1;
loadData();
})
$("#pageFirst").click(()=>{if(page!==1){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(()=>{const tp=Math.ceil(total/pageSize);if(page!==tp){page=tp;loadData();}})
</script>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
<?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'] ?? '';
?>
<!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>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1000px;margin:0 auto;padding:20px;}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.back-link{color:#165DFF;text-decoration:none;}
.user-info{display:flex;gap:12px;align-items:center;}
.card{background:#fff;border-radius:8px;padding:24px;box-shadow:0 1px 2px rgba(0,0,0,0.06);}
.card-title{font-size:16px;font-weight:500;margin-bottom:20px;}
.form-row{margin-bottom:18px;}
.form-row label{display:block;margin-bottom:6px;color:#4E5969;}
.form-row input{width:100%;height:38px;padding:0 14px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;}
.form-desc{font-size:12px;color:#86909C;margin-top:4px;}
.switch-row{display:flex;align-items:center;gap:8px;margin-bottom:24px;font-size:14px;}
.btn{height:38px;padding:0 20px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-group{display:flex;gap:12px;}
.tip{color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<a href="alert_index.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回告警监控面板</a>
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
</div>
<div class="card">
<h3 class="card-title">告警推送订阅配置</h3>
<div class="switch-row">
<input type="checkbox" id="subEnable">
<label for="subEnable">全局启用告警推送订阅</label>
</div>
<div class="form-row">
<label>订阅告警级别(多值逗号分隔,为空匹配全部)</label>
<input type="text" id="subSeverity" placeholder="critical,warning">
<div class="form-desc">可选:critical / warning / info</div>
</div>
<div class="form-row">
<label>订阅实例模糊匹配(多实例逗号分隔)</label>
<input type="text" id="subInstance" placeholder="10.150,192.168">
<div class="form-desc">填写实例片段,为空订阅所有实例告警</div>
</div>
<div class="form-row">
<label>订阅告警名称模糊匹配(多告警逗号分隔)</label>
<input type="text" id="subAlertname" placeholder="CPU使用率过高,磁盘满">
<div class="form-desc">只推送包含该名称的告警,为空订阅全部告警</div>
</div>
<div class="form-row">
<label>钉钉机器人 Webhook</label>
<input type="text" id="subDingtalk" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxx">
</div>
<div class="form-row">
<label>企业微信机器人 Webhook</label>
<input type="text" id="subWecom" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx">
</div>
<div class="form-row">
<label>接收邮箱(多邮箱逗号分隔)</label>
<input type="text" id="subMail" placeholder="ops@company.com,admin@company.com">
</div>
<div class="btn-group">
<button class="btn btn-primary" id="saveSubscribe"><i class="fa fa-save"></i> 保存订阅配置</button>
<button class="btn btn-outline" id="resetForm"><i class="fa fa-refresh"></i> 重置表单</button>
</div>
<div class="tip">说明:配置保存后,符合筛选条件的新增告警将通过配置渠道推送;关闭全局开关则暂停所有推送。</div>
</div>
</div>
<script>
const apiSub = "./api/subscribe_api.php";
// 页面加载读取当前订阅
$(function(){
$.getJSON(apiSub,{action:"get_subscribe"},function(res){
if(res.code===0){
const d=res.data;
$("#subEnable").prop("checked",d.enable==1);
$("#subSeverity").val(d.severity);
$("#subInstance").val(d.instance);
$("#subAlertname").val(d.alertname);
$("#subDingtalk").val(d.dingtalk_webhook);
$("#subWecom").val(d.wecom_webhook);
$("#subMail").val(d.mail_receiver);
}
});
});
// 保存配置
$("#saveSubscribe").click(function(){
const data={
severity:$("#subSeverity").val().trim(),
instance:$("#subInstance").val().trim(),
alertname:$("#subAlertname").val().trim(),
dingtalk_webhook:$("#subDingtalk").val().trim(),
wecom_webhook:$("#subWecom").val().trim(),
mail_receiver:$("#subMail").val().trim(),
enable:$("#subEnable").prop("checked")?1:0
};
$.ajax({
url:apiSub+"?action=save_subscribe",
method:"POST",
contentType:"application/json",
data:JSON.stringify(data),
dataType:"json",
success:function(res){alert(res.msg);}
});
});
// 重置表单
$("#resetForm").click(function(){
$("#subSeverity,#subInstance,#subAlertname,#subDingtalk,#subWecom,#subMail").val("");
$("#subEnable").prop("checked",false);
});
</script>
</body>
</html>
Binary file not shown.
+198
View File
@@ -0,0 +1,198 @@
<?php
session_start();
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,OPTIONS");
header("Access-Control-Allow-Headers: *");
if($_SERVER['REQUEST_METHOD'] === 'OPTIONS'){
echo json_encode(array('code'=>0));
exit;
}
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(array('code'=>401,'msg'=>'登录失效','list'=>array(),'total'=>0,'stat'=>array()));
exit;
}
$_SESSION['login_time'] = time();
$promHost = "10.150.117.190";
$promPort = "9090";
$promApi = "http://".$promHost.":".$promPort."/api/v1";
// 修复低版本PHP不支持 ?? 运算符,改用三元判断
$page = isset($_REQUEST['page']) ? (int)$_REQUEST['page'] : 1;
$pageSize = isset($_REQUEST['page_size']) ? (int)$_REQUEST['page_size'] : 20;
$startDate = isset($_REQUEST['start_date']) ? $_REQUEST['start_date'] : "";
$endDate = isset($_REQUEST['end_date']) ? $_REQUEST['end_date'] : "";
$filterMetric = isset($_REQUEST['metric']) ? trim($_REQUEST['metric']) : "";
$filterInstance = isset($_REQUEST['instance']) ? trim($_REQUEST['instance']) : "";
$filterGlobal = isset($_REQUEST['global']) ? trim($_REQUEST['global']) : "";
$distinct = isset($_REQUEST['distinct']) ? (int)$_REQUEST['distinct'] : 1;
$filterSeverity = isset($_REQUEST['severity']) ? $_REQUEST['severity'] : array();
$filterStatus = isset($_REQUEST['status']) ? $_REQUEST['status'] : array();
$isExport = (isset($_REQUEST['export']) && $_REQUEST['export'] == 1) ? 1 : 0;
$startTs = 0;
$endTs = 0;
if (!empty($startDate) && !empty($endDate)) {
$startTs = strtotime($startDate);
$endTs = strtotime($endDate . " 23:59:59");
} else {
$today = date("Y-m-d");
$startTs = strtotime($today);
$endTs = strtotime($today . " 23:59:59");
}
$promQL = 'sort_desc(ALERTS)';
$params = array('query' => $promQL);
$params['start'] = $startTs;
$params['end'] = $endTs;
$params['step'] = 60;
$queryUrl = $promApi . "/query_range";
$url = $queryUrl . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!empty($err) || empty($resp)) {
echo json_encode(array(
'code' => 500,
'msg' => 'Prometheus连接失败:' . $err,
'list' => array(),
'total' => 0,
'stat' => array()
));
exit;
}
$promData = json_decode($resp, true);
if (!isset($promData['status']) || $promData['status'] !== 'success') {
echo json_encode(array(
'code' => 500,
'msg' => 'Prometheus查询异常:'.$resp,
'list' => array(),
'total' => 0,
'stat' => array()
));
exit;
}
$rawAll = array();
$hourCount = array();
for ($h = 0; $h < 24; $h++){
$hourCount[$h] = 0;
}
if (!empty($promData['data']['result'])) {
foreach ($promData['data']['result'] as $item) {
$metric = $item['metric'];
$state = isset($metric['alertstate']) ? $metric['alertstate'] : "";
$sev = isset($metric['severity']) ? $metric['severity'] : "info";
$alertName = isset($metric['alertname']) ? $metric['alertname'] : "";
$inst = isset($metric['instance']) ? $metric['instance'] : "";
$values = isset($item['values']) ? $item['values'] : array();
foreach ($values as $v) {
$ts = (int)$v[0];
$dt = date("Y-m-d H:i:s", $ts);
$hour = date("G", $ts);
$hourCount[$hour]++;
$rawAll[] = array(
'state' => $state,
'severity' => $sev,
'metric' => $alertName,
'instance' => $inst,
'time' => $dt,
'timestamp' => $ts,
'labels' => $metric
);
}
}
}
$filterFunc = function ($row) use ($filterMetric, $filterInstance, $filterGlobal, $filterSeverity, $filterStatus) {
if (!empty($filterGlobal)) {
$g = strtolower($filterGlobal);
$match = (stripos(strtolower($row['metric']), $g) !== false)
|| (stripos(strtolower($row['instance']), $g) !== false)
|| (stripos(strtolower($row['severity']), $g) !== false);
if (!$match) return false;
}
if (!empty($filterMetric) && stripos($row['metric'], $filterMetric) === false) return false;
if (!empty($filterInstance) && stripos($row['instance'], $filterInstance) === false) return false;
if (!empty($filterSeverity) && !in_array($row['severity'], $filterSeverity)) return false;
if (!empty($filterStatus) && !in_array($row['state'], $filterStatus)) return false;
return true;
};
$rawAll = array_filter($rawAll, $filterFunc);
usort($rawAll, function($a, $b){
return $b['timestamp'] - $a['timestamp'];
});
$listRaw = array();
$stat = array(
'total_all' => count($rawAll),
'total_firing' => 0,
'total_resolved' => 0,
'total_critical' => 0,
'unique_count' => 0,
'hour_data' => $hourCount
);
if ($distinct !== 1) {
$listRaw = $rawAll;
foreach ($rawAll as $r) {
if ($r['state'] == 'firing') $stat['total_firing']++;
else $stat['total_resolved']++;
if ($r['severity'] == 'critical') $stat['total_critical']++;
}
} else {
$group = array();
foreach ($rawAll as $row) {
$key = md5($row['metric'] . "|" . $row['instance'] . "|" . $row['severity'] . "|" . $row['state']);
if (!isset($group[$key])) {
$group[$key] = array(
'state' => $row['state'],
'severity' => $row['severity'],
'metric' => $row['metric'],
'instance' => $row['instance'],
'time' => $row['time'],
'timestamp' => $row['timestamp'],
'total_count' => 1,
'labels' => $row['labels']
);
} else {
$group[$key]['total_count']++;
if ($row['timestamp'] > $group[$key]['timestamp']) {
$group[$key]['time'] = $row['time'];
$group[$key]['timestamp'] = $row['timestamp'];
}
}
}
$listRaw = array_values($group);
$stat['unique_count'] = count($listRaw);
foreach ($listRaw as $r) {
if ($r['state'] == 'firing') $stat['total_firing']++;
else $stat['total_resolved']++;
if ($r['severity'] == 'critical') $stat['total_critical']++;
}
}
$total = count($listRaw);
if ($isExport) {
$pageData = $listRaw;
} else {
$offset = ($page - 1) * $pageSize;
$pageData = array_slice($listRaw, $offset, $pageSize);
}
echo json_encode(array(
'code' => 0,
'msg' => 'ok',
'total' => $total,
'stat' => $stat,
'list' => $pageData
), JSON_UNESCAPED_UNICODE);
exit;
?>
+143
View File
@@ -0,0 +1,143 @@
<?php
session_start();
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(json_encode(['code'=>0]));
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
exit(json_encode(['code'=>401,'msg'=>'登录失效','list'=>[],'total'=>0]));
}
$_SESSION['login_time'] = time();
$promHost = "10.150.117.190";
$promPort = "9090";
$promApi = "http://{$promHost}:{$promPort}/api/v1";
$page = (int)($_REQUEST['page'] ?? 1);
$pageSize = (int)($_REQUEST['page_size'] ?? 20);
$filterDate = $_REQUEST['date'] ?? "";
$filterMetric = trim($_REQUEST['metric'] ?? "");
$filterSeverity = $_REQUEST['severity'] ?? "";
$filterStatus = $_REQUEST['status'] ?? "";
$distinct = (int)($_REQUEST['distinct'] ?? 1);
$promQL = 'sort_desc(ALERTS)';
$params = ['query' => $promQL];
if (!empty($filterDate)) {
$end = strtotime($filterDate . " 23:59:59");
$start = $end - 86400;
$params['start'] = $start;
$params['end'] = $end;
$params['step'] = 60;
$queryUrl = $promApi . "/query_range";
} else {
$queryUrl = $promApi . "/query";
}
$url = $queryUrl . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if (!empty($err) || empty($resp)) {
exit(json_encode([
'code' => 500,
'msg' => 'Prometheus连接失败:'.$err,
'list' => [],
'total' => 0
]));
}
$promData = json_decode($resp, true);
if ($promData['status'] !== 'success') {
exit(json_encode([
'code' => 500,
'msg' => 'Prometheus查询异常',
'list' => [],
'total' => 0
]));
}
$rawAll = [];
if (!empty($promData['data']['result'])) {
foreach ($promData['data']['result'] as $item) {
$metric = $item['metric'];
$state = $metric['alertstate'] ?? "";
$sev = $metric['severity'] ?? "info";
$alertName = $metric['alertname'] ?? "";
$inst = $metric['instance'] ?? "";
$values = $item['values'] ?? [];
foreach ($values as $v) {
$ts = (int)$v[0];
$dt = date("Y-m-d H:i:s", $ts);
$rawAll[] = [
'state' => $state,
'severity' => $sev,
'metric' => $alertName,
'instance' => $inst,
'time' => $dt,
'timestamp' => $ts
];
}
}
}
// ==========修复过滤逻辑==========
if (!empty($filterMetric)) {
$rawAll = array_filter($rawAll, function($r) use ($filterMetric){
return stripos($r['metric'], $filterMetric) !== false;
});
}
if (!empty($filterSeverity)) {
$rawAll = array_filter($rawAll, fn($r) => $r['severity'] === $filterSeverity);
}
if (!empty($filterStatus)) {
$rawAll = array_filter($rawAll, fn($r) => $r['state'] === $filterStatus);
}
usort($rawAll, fn($a,$b) => $b['timestamp'] - $a['timestamp']);
if ($distinct !== 1) {
$list = $rawAll;
} else {
$group = [];
foreach ($rawAll as $row) {
$key = md5($row['metric'] . "|" . $row['instance'] . "|" . $row['severity'] . "|" . $row['state']);
if (!isset($group[$key])) {
$group[$key] = [
'state' => $row['state'],
'severity' => $row['severity'],
'metric' => $row['metric'],
'instance' => $row['instance'],
'time' => $row['time'],
'timestamp' => $row['timestamp'],
'total_count' => 1
];
} else {
$group[$key]['total_count']++;
if ($row['timestamp'] > $group[$key]['timestamp']) {
$group[$key]['time'] = $row['time'];
$group[$key]['timestamp'] = $row['timestamp'];
}
}
}
$list = array_values($group);
}
$total = count($list);
$offset = ($page - 1) * $pageSize;
$pageData = array_slice($list, $offset, $pageSize);
echo json_encode([
'code' => 0,
'msg' => 'ok',
'total' => $total,
'list' => $pageData
], JSON_UNESCAPED_UNICODE);
exit;
?>
+40
View File
@@ -0,0 +1,40 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(["code"=>401,"msg"=>"未登录"]);
exit;
}
header("Content-Type:application/json;charset=utf-8");
$configFile = "./system_config.json";
if(!file_exists($configFile)){
echo json_encode(["code"=>500,"msg"=>"配置文件缺失"]);
exit;
}
$config = json_decode(file_get_contents($configFile),true);
$act = $_POST['act'] ?? '';
if($act == "save_style"){
$config['theme_color'] = $_POST['theme_color'];
$config['card_bg'] = $_POST['card_bg'];
$config['body_bg'] = $_POST['body_bg'];
$config['font_family'] = $_POST['font_family'];
file_put_contents($configFile,json_encode($config,JSON_UNESCAPED_UNICODE));
echo json_encode(["code"=>0,"msg"=>"样式保存成功,刷新页面生效"]);
exit;
}
if($act == "save_sidebar_link"){
$list = json_decode($_POST['link_list'],true);
$config['sidebar_links'] = $list;
file_put_contents($configFile,json_encode($config,JSON_UNESCAPED_UNICODE));
echo json_encode(["code"=>0,"msg"=>"侧边栏链接更新成功"]);
exit;
}
if($act == "get_config"){
echo json_encode(["code"=>0,"data"=>$config]);
exit;
}
echo json_encode(["code"=>400,"msg"=>"非法操作"]);
?>
+3
View File
@@ -0,0 +1,3 @@
<?php
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>200,"msg"=>"PHP正常,mysqli已加载"]);
+205
View File
@@ -0,0 +1,205 @@
<?php
session_start();
if(empty($_SESSION['user_id'])){
header("Location: ../login.php");
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>开发者模式 - 接口可视化调试面板</title>
<style>
*{box-sizing: border-box;margin:0;padding:0;font-family:Microsoft Yahei}
body{background:#f5f7fa;padding:20px}
.container{max-width:1400px;margin:0 auto}
h1{color:#333;margin-bottom:24px;font-size:22px}
.card{background:#fff;border-radius:8px;padding:16px;margin-bottom:16px;border:1px solid #e4e7ed}
.card h3{margin-bottom:12px;color:#409eff;font-size:16px}
.row{display:flex;gap:12px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
input{padding:8px 10px;border:1px solid #dcdfe6;border-radius:4px;width:260px}
button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;color:#fff;background:#409eff}
button.warn{background:#e6a23c}
button.danger{background:#f56c6c}
button.success{background:#67c23a}
#result{margin-top:20px;padding:16px;background:#1e1e1e;color:#fff;border-radius:6px;white-space:pre-wrap;min-height:200px;font-family:Consolas}
.tip{font-size:12px;color:#999}
</style>
</head>
<body>
<div class="container">
<h1>开发者调试面板 | 全部告警接口可视化调用</h1>
<a href="../admin.php">← 返回系统后台管理页</a>
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button onclick="req('clear_single_instance','c_rule','c_ins')">执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;冒号自动编码无需手动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="danger" onclick="req('offline_single_instance','off_rule','off_ins')">永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不恢复永远不告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="success" onclick="req('restore_single_instance','res_rule','res_ins')">恢复实例告警</button>
</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="danger" onclick="reqSingle('clear_offline_rule','cr_rule')">下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="success" onclick="reqSingle('restore_rule','rr_rule')">恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="warn" onclick="req('delete_single_instance_log','del_rule','del_ins')">清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button onclick="simpleReq('sync_prom_alerts')">立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端不显示告警bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3>8. 数据查询接口(查看面板原始数据)</h3>
<div class="row">
<button onclick="simpleReq('day_total')">今日统计 day_total</button>
<button onclick="simpleReq('top_alert')">告警TOP10 top_alert</button>
<button onclick="simpleReq('log_list')">今日明细 log_list</button>
<button onclick="simpleReq('active_firing')">当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表 -->
<div class="card">
<h3>9. 导出今日告警CSV报表</h3>
<div class="row">
<button onclick="exportCsv()">下载报表 export_csv</button>
</div>
</div>
<!-- 结果输出区域 -->
<div class="card">
<h3>接口返回结果(JSON)</h3>
<div id="result">等待操作...</div>
</div>
</div>
<script>
// 当前文件在/api/下,接口同目录index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV新窗口
function exportCsv(){
window.open(`${apiUrl}?act=export_csv`,"_blank");
}
</script>
</body>
</html>
+411
View File
@@ -0,0 +1,411 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: ../login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者调试面板 - Advantest 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-title{
font-size:24px;
font-weight:700;
letter-spacing:1px;
color:#152c5b;
}
.header-sub{
font-size:14px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 18px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-warning{
background:#e6a23c;
color:#fff;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1600px;
margin:0 auto;
}
h1{
display:none;
}
/* 卡片样式 统一企业面板风格 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.row{
display:flex;
gap:12px;
align-items:center;
margin-bottom:10px;
flex-wrap:wrap;
}
label{
font-size:14px;
color:#475569;
width:110px;
}
input{
padding:8px 12px;
border:1px solid #dcdfe6;
border-radius:8px;
width:280px;
font-size:14px;
}
input[type="date"]{
width:200px;
}
button{
padding:9px 18px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
}
button.normal{
background:#409eff;
color:#fff;
}
button.normal:hover{
background:#2979e0;
}
.tip{
font-size:13px;
color:#94a3b8;
margin-top:8px;
padding-left:110px;
}
/* 返回结果区块 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:260px;
font-family:Consolas,monospace;
font-size:13px;
}
/* 响应式适配 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.container{padding:24px}
.card{padding:24px}
.row{flex-direction:column;align-items:flex-start}
label{width:auto}
input{width:100% !important;max-width:320px}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-title"><i class="fa fa-code"></i> 开发者调试面板</div>
<div class="header-sub">全部告警接口可视化调用 · 后端API调试工具</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.href='../admin.php'"><i class="fa fa-arrow-left"></i>返回系统后台</button>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新页面</button>
<button class="btn-base btn-primary" onclick="location.href='../dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
</div>
</div>
<div class="container">
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3><i class="fa fa-eraser"></i>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button class="normal" onclick="req('clear_single_instance','c_rule','c_ins')"><i class="fa fa-play"></i>执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号无需手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3><i class="fa fa-ban"></i>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="btn-danger" onclick="req('offline_single_instance','off_rule','off_ins')"><i class="fa fa-ban"></i>永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不执行恢复接口则永久不展示该实例告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3><i class="fa fa-check-circle"></i>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="btn-success" onclick="req('restore_single_instance','res_rule','res_ins')"><i class="fa fa-unlock"></i>恢复实例告警</button>
</div>
<div class="tip">解除单实例永久屏蔽状态,新故障会正常展示</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-danger" onclick="reqSingle('clear_offline_rule','cr_rule')"><i class="fa fa-power-off"></i>下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-success" onclick="reqSingle('restore_rule','rr_rule')"><i class="fa fa-refresh"></i>恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3><i class="fa fa-trash"></i>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="btn-warning" onclick="req('delete_single_instance_log','del_rule','del_ins')"><i class="fa fa-trash"></i>清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3><i class="fa fa-refresh"></i>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('sync_prom_alerts')"><i class="fa fa-sync"></i>立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端“故障存在但页面不显示”bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3><i class="fa fa-database"></i>8. 数据查询接口(获取面板原始数据)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('day_total')"><i class="fa fa-bar-chart"></i>今日统计 day_total</button>
<button class="normal" onclick="simpleReq('top_alert')"><i class="fa fa-pie-chart"></i>告警TOP10 top_alert</button>
<button class="normal" onclick="simpleReq('log_list')"><i class="fa fa-list"></i>今日明细 log_list</button>
<button class="normal" onclick="simpleReq('active_firing')"><i class="fa fa-exclamation-circle"></i>当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表(带日历选择) -->
<div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
<div class="row">
<label>选择日期:</label>
<input type="date" id="csv_date" placeholder="留空导出今日">
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
</div>
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
</div>
<!-- 返回结果输出 -->
<div class="card">
<h3><i class="fa fa-terminal"></i>接口返回结果(JSON)</h3>
<div id="result">等待执行接口操作...</div>
</div>
</div>
<script>
// 接口路径 同目录 index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV 支持日历选择日期
function exportCsv(){
const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`;
if(dateVal){
url += `&date=${encodeURIComponent(dateVal)}`;
}
window.open(url,"_blank");
}
</script>
</body>
</html>
+60
View File
@@ -0,0 +1,60 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"],JSON_UNESCAPED_UNICODE);
exit;
}
header("Content-Type:application/json; charset=utf-8");
$promAddr = "http://10.150.117.190:9090";
$gbUnit = 1073741824;
$promQuery = urlencode('node_directory_size_bytes');
$apiUrl = "{$promAddr}/api/v1/query?query={$promQuery}";
// 改用curl,不依赖allow_url_fopen
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch);
$curlErr = curl_error($ch);
curl_close($ch);
if(!$resp || !empty($curlErr)){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus".$curlErr],JSON_UNESCAPED_UNICODE);
exit;
}
$raw = json_decode($resp, true);
$allData = [];
if ($raw['status'] === 'success' && !empty($raw['data']['result'])) {
foreach ($raw['data']['result'] as $item) {
$dir = $item['metric']['directory'];
$instanceIp = $item['metric']['instance'];
$instanceIp = explode(':', $instanceIp)[0];
$userName = basename($dir);
$byteNum = floatval($item['value'][1]);
$gbNum = round($byteNum / $gbUnit, 2);
$allData[] = [
"instance_ip" => $instanceIp,
"username" => $userName,
"directory" => $dir,
"size_gb" => $gbNum
];
}
usort($allData, function ($a, $b) {
return $b['size_gb'] <=> $a['size_gb'];
});
}
echo json_encode([
"code" => 0,
"msg" => "查询成功",
"list" => $allData
], JSON_UNESCAPED_UNICODE);
exit;
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"],JSON_UNESCAPED_UNICODE);
exit;
}
header("Content-Type:application/json; charset=utf-8");
$promAddr = "http://10.150.117.190:9090";
$gbUnit = 1073741824;
$promQuery = urlencode('node_directory_size_bytes');
$apiUrl = "{$promAddr}/api/v1/query?query={$promQuery}";
$resp = @file_get_contents($apiUrl);
if(!$resp){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus"],JSON_UNESCAPED_UNICODE);
exit;
}
$raw = json_decode($resp, true);
$allData = [];
if ($raw['status'] === 'success' && !empty($raw['data']['result'])) {
foreach ($raw['data']['result'] as $item) {
$dir = $item['metric']['directory'];
$instanceIp = $item['metric']['instance'];
$instanceIp = explode(':', $instanceIp)[0];
$userName = basename($dir);
$byteNum = floatval($item['value'][1]);
$gbNum = round($byteNum / $gbUnit, 2);
$allData[] = [
"instance_ip" => $instanceIp,
"username" => $userName,
"directory" => $dir,
"size_gb" => $gbNum
];
}
usort($allData, function ($a, $b) {
return $b['size_gb'] <=> $a['size_gb'];
});
}
echo json_encode([
"code" => 0,
"msg" => "查询成功",
"list" => $allData
], JSON_UNESCAPED_UNICODE);
exit;
?>
+165
View File
@@ -0,0 +1,165 @@
<?php
session_start();
header("Access-Control-Allow-Credentials: true");
header("Content-Type:application/json;charset=utf-8");
$SECRET_RAW = "DockerAdminPlatform2026SecretKey123456789";
$uidGet = $_GET['uid'] ?? '';
if (!empty($uidGet) && empty($_SESSION['user_id'])) {
$_SESSION['user_id'] = $uidGet;
$_SESSION['login_time'] = time();
}
$debug = [
"sid" => session_id(),
"session_data" => $_SESSION
];
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
echo json_encode([
"code"=>401,
"msg"=>"未登录",
"debug"=>$debug
],JSON_UNESCAPED_UNICODE);
exit;
}
$user = $_SESSION['user_id'];
$role = $_SESSION['role'];
$ts = time();
$rawSignStr = $user . "|" . $role . "|" . $ts;
$sign = hash_hmac("sha256", $rawSignStr, $SECRET_RAW);
$clientIp = $_SERVER['REMOTE_ADDR'];
$act = $_POST['act'] ?? $_GET['act'] ?? 'list';
$cid = $_POST['container_id'] ?? $_GET['container_id'] ?? '';
$imageId = $_POST['image_id'] ?? $_GET['image_id'] ?? '';
$lines = $_POST['lines'] ?? $_GET['lines'] ?? 200;
$postRaw = file_get_contents("php://input");
$apiBase = "http://10.150.117.190:8090";
$authHeaders = [
"X-User: {$user}",
"X-Role: {$role}",
"X-Time: {$ts}",
"X-Sign: {$sign}",
"X-Client-Ip: {$clientIp}",
"Content-Type: application/json; charset=utf-8"
];
$headerStr = implode("\r\n", $authHeaders);
$getMap = [
'list' => '/list',
'image_list' => '/image_list',
'restart' => '/restart/%s',
'stop' => '/stop/%s',
'rm' => '/rm/%s',
'remove_image' => '/remove_image/%s',
'logs' => '/logs/%s/%d',
'network' => '/network/%s',
'stats' => '/stats/%s',
'detail' => '/detail/%s'
];
$postMap = [
'exec' => '/exec/%s',
'create' => '/create',
'bind_repo' => '/bind_repo',
];
if (isset($getMap[$act])) {
$path = $getMap[$act];
$url = '';
switch ($act) {
case 'restart':
case 'stop':
case 'rm':
case 'network':
case 'stats':
case 'detail':
if (empty($cid)) {
echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
exit;
}
$url = $apiBase . sprintf($path, urlencode($cid));
break;
case 'remove_image':
if (empty($imageId)) {
echo json_encode(["code"=>400,"msg"=>"缺少镜像ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
exit;
}
$url = $apiBase . sprintf($path, urlencode($imageId));
break;
case 'logs':
if (empty($cid)) {
echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
exit;
}
$url = $apiBase . sprintf($path, urlencode($cid), (int)$lines);
break;
default:
$url = $apiBase . $path;
break;
}
$ctx = stream_context_create([
'http' => [
'method' => 'GET',
'header' => $headerStr,
'timeout' => 30
]
]);
$resp = @file_get_contents($url, false, $ctx);
if ($resp === false) {
echo json_encode([
"code" => 503,
"msg" => "后端Docker接口连接失败,请检查服务是否启动",
"debug" => $debug
], JSON_UNESCAPED_UNICODE);
exit;
}
echo $resp;
exit;
}
if (isset($postMap[$act])) {
$path = $postMap[$act];
$url = '';
if ($act === 'exec') {
if (empty($cid)) {
echo json_encode(["code"=>400,"msg"=>"缺少容器ID","debug"=>$debug],JSON_UNESCAPED_UNICODE);
exit;
}
$url = $apiBase . sprintf($path, urlencode($cid));
} else {
$url = $apiBase . $path;
}
$ctx = stream_context_create([
'http' => [
'method' => 'POST',
'header' => $headerStr,
'content' => $postRaw,
'timeout' => 30
]
]);
$resp = @file_get_contents($url, false, $ctx);
if ($resp === false) {
echo json_encode([
"code" => 503,
"msg" => "后端Docker接口连接失败,请检查服务是否启动",
"debug" => $debug
], JSON_UNESCAPED_UNICODE);
exit;
}
echo $resp;
exit;
}
echo json_encode([
"code"=>400,
"msg"=>"无效操作act参数",
"debug"=>$debug
],JSON_UNESCAPED_UNICODE);
exit;
+752
View File
@@ -0,0 +1,752 @@
import json
import shlex
import time
import hashlib
import hmac
import docker
from flask import Flask, jsonify, request
from flask_socketio import SocketIO
# 全局签名密钥,前后端统一,生产环境使用环境变量注入
SIGN_SECRET = b"DockerAdminPlatform2026SecretKey123456789"
# 签名有效期 300秒(5分钟)
SIGN_EXPIRE = 300
# 兼容新旧docker-py客户端
try:
cli = docker.DockerClient(base_url='unix:///var/run/docker.sock')
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
except AttributeError:
cli = docker.Client(base_url='unix:///var/run/docker.sock')
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
app = Flask(__name__)
app.config['SECRET_KEY'] = 'docker-terminal-secret-2026'
# WebSocket心跳保活,防止Nginx/反向代理断连
socketio = SocketIO(
app,
cors_allowed_origins="*",
async_mode="threading",
ping_timeout=60,
ping_interval=25
)
# 全局会话存储
session_exec_map = {} # sid => TTY终端流信息
log_task_map = {} # sid => {"cid": "", "running": True}
ws_user_map = {} # 新增:sid 映射 WebSocket鉴权用户信息
registry_config = {
"repo_name": "",
"repo_addr": "",
"repo_user": "",
"repo_pwd": ""
}
# 多系统shell兼容列表(Ubuntu/Alpine/标准Linux
SHELL_CANDIDATES = [
"/bin/bash",
"/usr/bin/bash",
"/bin/sh",
"/bin/ash"
]
# ===================== 工具函数1docker socket兼容recv =====================
def docker_recv(sock, size=1024):
try:
return sock.recv(size)
except Exception:
try:
return sock._sock.recv(size)
except Exception:
return b""
# ===================== 工具函数2:操作审计日志(终端输入脱敏,不存明文密码) =====================
def write_audit_log(user, role, action, target_name, target_id, result, client_ip):
"""企业审计记录,可对接数据库docker_audit_log表"""
# 终端输入仅标记事件,不记录明文命令,规避密码泄露风险
if action == "terminal_input":
result = "command_entered"
log_item = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"user": user,
"role": role,
"action": action,
"container_name": target_name,
"container_id": target_id,
"result": result,
"client_ip": client_ip
}
# 生产环境替换为mysql insert语句
print(json.dumps(log_item, ensure_ascii=False))
# ===================== 工具函数3HTTP Header HMAC签名鉴权(原有,HTTP接口专用) =====================
def verify_auth_sign():
"""
校验前端PHP生成的HMAC签名,防止伪造X-Role:admin
请求Header要求:
X-User: 用户名
X-Role: admin/user/guest
X-Time: 当前时间戳(秒)
X-Sign: hmac-sha256签名
返回 (user, role, client_ip, True/False)
"""
user = request.headers.get("X-User", "anonymous")
role = request.headers.get("X-Role", "guest")
ts_str = request.headers.get("X-Time", "0")
sign = request.headers.get("X-Sign", "")
client_ip = request.headers.get("X-Client-Ip", request.remote_addr)
try:
ts = int(ts_str)
except ValueError:
return user, role, client_ip, False
# 校验签名有效期
now = int(time.time())
if abs(now - ts) > SIGN_EXPIRE:
return user, role, client_ip, False
# 校验HMAC签名
raw_data = f"{user}|{role}|{ts}".encode("utf-8")
calc_sign = hmac.new(SIGN_SECRET, raw_data, hashlib.sha256).hexdigest()
if calc_sign != sign:
return user, role, client_ip, False
return user, role, client_ip, True
# ===================== 新增工具函数4WebSocket连接专用鉴权函数 =====================
def verify_socket_auth(auth):
user = auth.get("user", "")
role = auth.get("role", "")
ts_str = auth.get("ts", "0")
sign = auth.get("sign", "")
try:
ts = int(ts_str)
except:
return user, role, "", False
if abs(int(time.time()) - ts) > SIGN_EXPIRE:
return user, role, "", False
raw_data = f"{user}|{role}|{ts}".encode("utf-8")
calc_sign = hmac.new(
SIGN_SECRET,
raw_data,
hashlib.sha256
).hexdigest()
if calc_sign != sign:
return user, role, "", False
return user, role, "", True
# ===================== 工具函数5:环境变量脱敏,屏蔽密码/密钥 =====================
def safe_env_filter(env_list):
safe_env = []
sensitive_keywords = ["PASS", "PASSWORD", "TOKEN", "SECRET", "KEY", "CREDENTIAL"]
for item in env_list:
equal_pos = item.find("=")
if equal_pos == -1:
safe_env.append(item)
continue
key = item[:equal_pos]
val = item[equal_pos+1:]
if any(word in key.upper() for word in sensitive_keywords):
safe_env.append(f"{key}=******")
else:
safe_env.append(item)
return safe_env
# ===================== 全部HTTP业务接口 =====================
# 1. 容器列表(端口预拼接字符串,前端直接渲染,无需formatPorts
@app.route("/list")
def api_list():
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "list_container", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
arr = []
for c in cli.containers.list(all=True):
img_name = c.image.tags[0] if c.image.tags else ""
port_str_arr = []
ports_raw = c.ports
for c_port, host_list in ports_raw.items():
if not host_list:
continue
for host_item in host_list:
host_port = host_item.get("HostPort", "")
port_str_arr.append(f"{host_port}:{c_port.split('/')[0]}")
port_display = ",".join(port_str_arr) if port_str_arr else "-"
arr.append({
"id": c.short_id,
"full_id": c.id,
"name": c.name,
"image": img_name,
"status": c.status,
"ports": port_display
})
write_audit_log(user, role, "list_container", "", "", "success", ip)
return jsonify({"code":0,"msg":"success","list":arr})
# 2. 镜像列表
@app.route("/image_list")
def api_image_list():
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "list_image", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
arr = []
for img in cli.images.list():
if not img.tags:
continue
full_tag = img.tags[0]
repo, tag = full_tag.split(":", 1)
size_mb = round(img.attrs["Size"] / 1024 / 1024, 1)
arr.append({
"repo": repo,
"tag": tag,
"id": img.short_id,
"full_id": img.id,
"size": f"{size_mb}MB",
"created": img.attrs["Created"]
})
write_audit_log(user, role, "list_image", "", "", "success", ip)
return jsonify({"code":0,"msg":"success","list":arr})
# 3. 删除镜像
@app.route("/remove_image/<image_id>")
def api_remove_image(image_id):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "remove_image", "", image_id, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "remove_image", "", image_id, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除镜像"})
try:
img = cli.images.get(image_id)
cli.images.remove(image_id, force=True)
write_audit_log(user, role, "remove_image", img.tags[0] if img.tags else "", image_id, "success", ip)
return jsonify({"code":0,"msg":"镜像已强制删除"})
except Exception as e:
write_audit_log(user, role, "remove_image", "", image_id, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 4. 重启容器
@app.route("/restart/<cid>")
def api_restart(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "restart", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "restart", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
try:
container = cli.containers.get(cid)
container.restart()
write_audit_log(user, role, "restart", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"容器已重启"})
except Exception as e:
write_audit_log(user, role, "restart", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 5. 停止容器
@app.route("/stop/<cid>")
def api_stop(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "stop", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "stop", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
try:
container = cli.containers.get(cid)
container.stop()
write_audit_log(user, role, "stop", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"容器已停止"})
except Exception as e:
write_audit_log(user, role, "stop", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 6. 删除容器
@app.route("/rm/<cid>")
def api_rm(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "rm", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "rm", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除容器"})
try:
container = cli.containers.get(cid)
container.remove(force=True)
write_audit_log(user, role, "rm", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"容器已强制删除"})
except Exception as e:
write_audit_log(user, role, "rm", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 7. 一次性拉取日志(兼容旧页面单次查看)
@app.route("/logs/<cid>/<int:tail_lines>")
def api_logs(cid, tail_lines):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_log", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
log_bytes = container.logs(tail=tail_lines)
log_str = log_bytes.decode("utf-8", errors="replace")
write_audit_log(user, role, "read_log", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":log_str})
except Exception as e:
write_audit_log(user, role, "read_log", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":""})
# 8. 容器网络详情
@app.route("/network/<cid>")
def api_network(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_network", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
info = container.attrs["NetworkSettings"]["Networks"]
write_audit_log(user, role, "read_network", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":json.dumps(info,indent=2)})
except Exception as e:
write_audit_log(user, role, "read_network", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":""})
# 9. 短连接单次执行命令(旧弹窗兼容,非实时终端)
@app.route("/exec/<cid>", methods=["POST"])
def api_exec(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "single_exec", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "single_exec", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可执行命令"})
try:
data = request.get_json()
cmd = data.get("cmd", "")
if not cmd:
return jsonify({"code":400,"msg":"命令不能为空","data":""})
cmd_list = shlex.split(cmd)
container = cli.containers.get(cid)
exec_obj = container.exec_run(cmd_list, stdout=True, stderr=True)
output = exec_obj.output.decode("utf-8", errors="replace")
write_audit_log(user, role, "single_exec", container.name, cid, "command_executed", ip)
return jsonify({"code":0,"msg":"success","data":output})
except Exception as e:
write_audit_log(user, role, "single_exec", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":""})
# 10. 创建容器(端口标准格式 + 前端可配置CPU/内存)
@app.route("/create", methods=["POST"])
def api_create():
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "create_container", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "create_container", "", "", "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可创建容器"})
try:
data = request.get_json()
name = data.get("name", "")
image = data.get("image")
cmd = data.get("cmd")
ports_input = data.get("ports", {})
# 修复:docker标准端口字典格式 {内部端口:宿主机端口}
port_bind = {}
for inner_port, host_port in ports_input.items():
port_bind[f"{inner_port}/tcp"] = int(host_port)
# 前端可配置资源限制,默认1核512M
cpu_core = int(data.get("cpu", 1))
memory_limit = data.get("memory", "512m")
nano_cpus = cpu_core * 1000000000
container = cli.containers.run(
image=image,
name=name if name else None,
command=cmd if cmd else None,
ports=port_bind,
detach=True,
restart_policy={"Name": "always"},
mem_limit=memory_limit,
nano_cpus=nano_cpus
)
write_audit_log(user, role, "create_container", container.name, container.id, "success", ip)
return jsonify({"code":0,"msg":f"容器创建成功,ID:{container.short_id}"})
except Exception as e:
write_audit_log(user, role, "create_container", "", "", f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 11. 登录私有镜像仓库
@app.route("/bind_repo", methods=["POST"])
def api_bind_repo():
global registry_config
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "repo_login", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "repo_login", "", "", "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可登录镜像仓库"})
try:
data = request.get_json()
repo_name = data.get("repo_name","")
repo_addr = data.get("repo_addr")
repo_user = data.get("repo_user","")
repo_pwd = data.get("repo_pwd","")
api_cli.login(username=repo_user, password=repo_pwd, registry=repo_addr)
registry_config["repo_name"] = repo_name
registry_config["repo_addr"] = repo_addr
registry_config["repo_user"] = repo_user
registry_config["repo_pwd"] = repo_pwd
write_audit_log(user, role, "repo_login", repo_addr, "", "success", ip)
return jsonify({"code":0,"msg":"镜像仓库登录绑定成功"})
except Exception as e:
write_audit_log(user, role, "repo_login", repo_addr, "", f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 修复:stats接口online_cpus兜底兼容,无KeyError
@app.route("/stats/<cid>")
def api_stats(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_stats", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
raw = container.stats(stream=False)
# CPU兼容兜底,不存在online_cpus自动计算
cpu_stats = raw["cpu_stats"]
online_cpus = cpu_stats.get("online_cpus", len(cpu_stats["cpu_usage"].get("percpu_usage", [])) or 1)
cpu_delta = cpu_stats["cpu_usage"]["total_usage"] - raw["precpu_stats"]["cpu_usage"]["total_usage"]
system_delta = cpu_stats["system_cpu_usage"] - raw["precpu_stats"]["system_cpu_usage"]
cpu_percent = 0.0
if system_delta > 0 and cpu_delta > 0:
cpu_percent = round((cpu_delta / system_delta) * online_cpus * 100, 1)
# 内存计算
mem_usage = raw["memory_stats"]["usage"] / 1024 / 1024
mem_limit = raw["memory_stats"]["limit"] / 1024 / 1024
mem_percent = round((mem_usage / mem_limit) * 100, 1) if mem_limit > 0 else 0
# 网络流量
net_rx = 0
net_tx = 0
for net in raw["networks"].values():
net_rx += net["rx_bytes"] / 1024 / 1024
net_tx += net["tx_bytes"] / 1024 / 1024
res = {
"cpu": cpu_percent,
"mem_mb": round(mem_usage, 1),
"mem_limit_mb": round(mem_limit, 1),
"mem_percent": mem_percent,
"net_rx_mb": round(net_rx, 1),
"net_tx_mb": round(net_tx, 1)
}
write_audit_log(user, role, "read_stats", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":res})
except Exception as e:
write_audit_log(user, role, "read_stats", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":{}})
# 修复:detail接口ENV脱敏,多网卡IP兼容
@app.route("/detail/<cid>")
def api_detail(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_detail", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
attrs = container.attrs
nets = attrs["NetworkSettings"]["Networks"]
ip_map = {}
for net_name, net_info in nets.items():
ip_map[net_name] = net_info.get("IPAddress", "")
# 敏感环境变量脱敏
raw_env = attrs["Config"].get("Env", [])
safe_env = safe_env_filter(raw_env)
res = {
"network_ips": ip_map,
"mounts": attrs["Mounts"],
"env": safe_env,
"created": attrs["Created"],
"entrypoint": attrs["Config"]["Entrypoint"],
"cmd": attrs["Config"]["Cmd"],
"restart_policy": attrs["HostConfig"]["RestartPolicy"]
}
write_audit_log(user, role, "read_detail", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":res})
except Exception as e:
write_audit_log(user, role, "read_detail", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":{}})
# 根路由
@app.route("/")
def index():
return jsonify({"code":400,"msg":"无效操作","list":[],"data":""})
# ===================== WebSocket 连接鉴权事件(新增) =====================
@socketio.on("connect")
def handle_connect(auth):
user, role, _, auth_ok = verify_socket_auth(auth)
if not auth_ok:
return False
ws_user_map[request.sid] = {
"user": user,
"role": role
}
print("WS Connected:", user, role)
# ===================== WebSocket 交互式TTY实时终端 =====================
def stream_terminal_output(sid, sock):
"""后台异步读取docker终端输出,兼容两种socket接收方式"""
try:
while True:
chunk = docker_recv(sock, 1024)
if not chunk:
break
text = chunk.decode("utf-8", errors="replace")
socketio.emit("terminal_output", {"data": text}, room=sid)
except Exception:
pass
# 流结束自动清理会话
if sid in session_exec_map:
try:
session_exec_map[sid]["socket"].close()
except Exception:
pass
del session_exec_map[sid]
@socketio.on("terminal_start")
def handle_terminal_start(json_data):
sid = request.sid
# 新增调试输出
socketio.emit(
"terminal_output",
{
"data":"[DEBUG] terminal_start reached\r\n"
},
room=sid
)
# 替换原verify_auth_sign(),从ws_user_map读取身份
info = ws_user_map.get(request.sid)
if not info:
socketio.emit(
"terminal_output",
{"data":"身份认证失效\n"},
room=request.sid
)
return
user = info["user"]
role = info["role"]
client_ip = request.remote_addr
# 权限拦截:非管理员禁止打开交互式终端
if role != "admin":
socketio.emit("terminal_output", {"data": "Permission denied: 仅管理员可访问交互式终端\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "denied", client_ip)
return
cid = json_data.get("cid")
if not cid:
socketio.emit("terminal_output", {"data": "错误:容器ID不能为空\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", "", "empty_cid", client_ip)
return
# 自动探测容器可用shell,兼容全版本docker-pyexec_create返回dict取Id
target_shell = None
for shell in SHELL_CANDIDATES:
try:
ret = api_cli.exec_create(
container=cid,
cmd=[shell, "-c", "echo ok"]
)
test_exec_id = ret["Id"]
test_stream = api_cli.exec_start(test_exec_id)
test_stream.read()
target_shell = shell
break
except Exception:
continue
if not target_shell:
socketio.emit("terminal_output", {"data": "错误:容器内无可用shell(bash/sh/ash)\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", cid, "no_shell", client_ip)
return
try:
# 创建交互式TTY会话,统一提取Id兼容所有SDK版本
exec_ret = api_cli.exec_create(
container=cid,
cmd=target_shell,
stdin=True,
stdout=True,
stderr=True,
tty=True
)
exec_id = exec_ret["Id"]
sock = api_cli.exec_start(exec_id=exec_id, tty=True, socket=True, detach=False)
session_exec_map[sid] = {
"cid": cid,
"exec_id": exec_id,
"socket": sock
}
socketio.start_background_task(stream_terminal_output, sid, sock)
container_name = cli.containers.get(cid).name
write_audit_log(user, role, "terminal_start", container_name, cid, "success", client_ip)
except Exception as e:
socketio.emit("terminal_output", {"data": f"启动终端失败:{str(e)}\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", cid, f"fail:{str(e)}", client_ip)
@socketio.on("terminal_input")
def handle_terminal_input(json_data):
"""前端输入字符写入容器标准输入,审计脱敏不记录明文"""
sid = request.sid
# 替换原verify_auth_sign()
info = ws_user_map.get(request.sid)
if not info:
return
user = info["user"]
role = info["role"]
client_ip = request.remote_addr
if sid not in session_exec_map:
socketio.emit("terminal_output", {"data": "终端会话已断开,请重新打开\n"}, room=sid)
return
input_text = json_data.get("data", "")
sock = session_exec_map[sid]["socket"]
sock.send(input_text.encode("utf-8"))
cid = session_exec_map[sid]["cid"]
# 审计仅标记事件,不再记录明文命令,规避密码泄露
write_audit_log(user, role, "terminal_input", "", cid, "command_entered", client_ip)
@socketio.on("terminal_resize")
def handle_terminal_resize(json_data):
"""前端xterm窗口大小自适应"""
sid = request.sid
if sid not in session_exec_map:
return
exec_id = session_exec_map[sid]["exec_id"]
height = int(json_data.get("height", 24))
width = int(json_data.get("width", 80))
try:
api_cli.exec_resize(exec_id=exec_id, height=height, width=width)
except Exception:
pass
# 新增主动关闭终端事件(解决切换容器残留TTY)
@socketio.on("terminal_close")
def handle_terminal_close():
sid = request.sid
info = ws_user_map.get(request.sid)
if not info:
return
user = info["user"]
role = info["role"]
client_ip = request.remote_addr
if sid in session_exec_map:
cid = session_exec_map[sid]["cid"]
try:
session_exec_map[sid]["socket"].close()
except Exception:
pass
del session_exec_map[sid]
write_audit_log(user, role, "terminal_close", "", cid, "manual_close", client_ip)
@socketio.on("disconnect")
def handle_disconnect():
"""前端断开连接,释放docker socket、清理会话,防止句柄泄漏"""
sid = request.sid
# 清理ws_user_map会话
if sid in ws_user_map:
del ws_user_map[sid]
info = ws_user_map.get(sid)
if not info:
user = ""
role = ""
client_ip = request.remote_addr
else:
user = info["user"]
role = info["role"]
client_ip = request.remote_addr
# 清理TTY终端
if sid in session_exec_map:
cid = session_exec_map[sid]["cid"]
try:
session_exec_map[sid]["socket"].close()
except Exception:
pass
del session_exec_map[sid]
write_audit_log(user, role, "terminal_close", "", cid, "disconnect", client_ip)
# 清理实时日志标记,终止日志循环
if sid in log_task_map:
del log_task_map[sid]
# ===================== WebSocket 实时流式日志 docker logs -f(生成器主动释放) =====================
def stream_follow_log(sid, cid):
generator = None
try:
container = cli.containers.get(cid)
generator = container.logs(stream=True, follow=True)
log_task_map[sid] = {"cid": cid, "running": True}
for line in generator:
task_info = log_task_map.get(sid, {})
if not task_info.get("running"):
break
txt = line.decode("utf-8", errors="replace")
socketio.emit("log_output", {"data": txt}, room=sid)
except Exception:
pass
finally:
# 主动关闭日志生成器,释放docker daemon长连接
if generator is not None:
generator.close()
if sid in log_task_map:
del log_task_map[sid]
@socketio.on("log_start")
def handle_log_start(json_data):
sid = request.sid
# 替换verify_auth_sign,读取ws_user_map
info = ws_user_map.get(request.sid)
if not info:
socketio.emit("log_output", {"data": "身份校验失败"}, room=sid)
write_audit_log("", "", "log_start", "", "", "auth_failed", request.remote_addr)
return
user = info["user"]
role = info["role"]
client_ip = request.remote_addr
cid = json_data.get("cid")
if not cid:
socketio.emit("log_output", {"data": "容器ID不能为空"}, room=sid)
write_audit_log(user, role, "log_start", "", "", "empty_cid", client_ip)
return
if role != "admin":
socketio.emit("log_output", {"data": "权限不足,仅管理员可查看实时日志"}, room=sid)
write_audit_log(user, role, "log_start", "", cid, "denied", client_ip)
return
socketio.start_background_task(stream_follow_log, sid, cid)
container_name = cli.containers.get(cid).name
write_audit_log(user, role, "log_start", container_name, cid, "success", client_ip)
@socketio.on("log_stop")
def handle_log_stop():
sid = request.sid
info = ws_user_map.get(request.sid)
if not info:
return
user = info["user"]
role = info["role"]
client_ip = request.remote_addr
if sid in log_task_map:
log_task_map[sid]["running"] = False
cid = log_task_map[sid]["cid"]
write_audit_log(user, role, "log_stop", "", cid, "manual_close", client_ip)
# 启动入口(eventlet异步,必须socketio.run
if __name__ == "__main__":
socketio.run(app, host="0.0.0.0", port=8090, debug=False, allow_unsafe_werkzeug=True)
+100
View File
@@ -0,0 +1,100 @@
import json
import docker
from flask import Flask, jsonify
# 兼容新旧版本
try:
cli = docker.DockerClient(base_url='unix:///var/run/docker.sock')
except AttributeError:
cli = docker.Client(base_url='unix:///var/run/docker.sock')
app = Flask(__name__)
# 容器列表
@app.route("/list")
def api_list():
arr = []
for c in cli.containers.list(all=True):
img_name = c.image.tags[0] if c.image.tags else ""
arr.append({
"id": c.short_id,
"full_id": c.id,
"name": c.name,
"image": img_name,
"status": c.status,
"ports": str(c.ports)
})
return jsonify({"code":0,"msg":"success","list":arr})
# 镜像列表
@app.route("/image_list")
def api_image_list():
arr = []
for img in cli.images.list():
if not img.tags:
continue
full_tag = img.tags[0]
repo, tag = full_tag.split(":", 1)
size_mb = round(img.attrs["Size"] / 1024 / 1024, 1)
arr.append({
"repo": repo,
"tag": tag,
"id": img.short_id,
"full_id": img.id,
"size": f"{size_mb}MB",
"created": img.attrs["Created"]
})
return jsonify({"code":0,"msg":"success","list":arr})
# 重启容器
@app.route("/restart/<cid>")
def api_restart(cid):
try:
cli.containers.get(cid).restart()
return jsonify({"code":0,"msg":"容器已重启"})
except Exception as e:
return jsonify({"code":500,"msg":str(e)})
# 停止容器
@app.route("/stop/<cid>")
def api_stop(cid):
try:
cli.containers.get(cid).stop()
return jsonify({"code":0,"msg":"容器已停止"})
except Exception as e:
return jsonify({"code":500,"msg":str(e)})
# 删除容器
@app.route("/rm/<cid>")
def api_rm(cid):
try:
cli.containers.get(cid).remove(force=True)
return jsonify({"code":0,"msg":"容器已强制删除"})
except Exception as e:
return jsonify({"code":500,"msg":str(e)})
# 容器日志
@app.route("/logs/<cid>/<int:tail_lines>")
def api_logs(cid, tail_lines):
try:
log_bytes = cli.containers.get(cid).logs(tail=tail_lines)
log_str = log_bytes.decode("utf-8", errors="ignore")
return jsonify({"code":0,"msg":"success","data":log_str})
except Exception as e:
return jsonify({"code":500,"msg":str(e),"data":""})
# 网络信息
@app.route("/network/<cid>")
def api_network(cid):
try:
info = cli.containers.get(cid).attrs["NetworkSettings"]["Networks"]
return jsonify({"code":0,"msg":"success","data":info})
except Exception as e:
return jsonify({"code":500,"msg":str(e),"data":""})
@app.route("/")
def index():
return jsonify({"code":400,"msg":"无效操作","list":[],"data":""})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8090, debug=False)
+669
View File
@@ -0,0 +1,669 @@
# 【强制置顶】eventlet猴子补丁,必须放在所有导入最前面
import json
import shlex
import time
import hashlib
import hmac
import docker
from flask import Flask, jsonify, request
from flask_socketio import SocketIO
# 全局签名密钥,前后端统一,生产环境使用环境变量注入
SIGN_SECRET = b"DockerAdminPlatform2026SecretKey123456789"
# 签名有效期 300秒(5分钟)
SIGN_EXPIRE = 300
# 兼容新旧docker-py客户端
try:
cli = docker.DockerClient(base_url='unix:///var/run/docker.sock')
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
except AttributeError:
cli = docker.Client(base_url='unix:///var/run/docker.sock')
api_cli = docker.APIClient(base_url='unix:///var/run/docker.sock')
app = Flask(__name__)
app.config['SECRET_KEY'] = 'docker-terminal-secret-2026'
# WebSocket心跳保活,防止Nginx/反向代理断连
socketio = SocketIO(
app,
cors_allowed_origins="*",
async_mode="threading",
ping_timeout=60,
ping_interval=25
)
# 全局会话存储
session_exec_map = {} # sid => TTY终端流信息
log_task_map = {} # sid => {"cid": "", "running": True}
registry_config = {
"repo_name": "",
"repo_addr": "",
"repo_user": "",
"repo_pwd": ""
}
# 多系统shell兼容列表(Ubuntu/Alpine/标准Linux
SHELL_CANDIDATES = [
"/bin/bash",
"/usr/bin/bash",
"/bin/sh",
"/bin/ash"
]
# ===================== 工具函数1docker socket兼容recv =====================
def docker_recv(sock, size=1024):
try:
return sock.recv(size)
except Exception:
try:
return sock._sock.recv(size)
except Exception:
return b""
# ===================== 工具函数2:操作审计日志(终端输入脱敏,不存明文密码) =====================
def write_audit_log(user, role, action, target_name, target_id, result, client_ip):
"""企业审计记录,可对接数据库docker_audit_log表"""
# 终端输入仅标记事件,不记录明文命令,规避密码泄露风险
if action == "terminal_input":
result = "command_entered"
log_item = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"user": user,
"role": role,
"action": action,
"container_name": target_name,
"container_id": target_id,
"result": result,
"client_ip": client_ip
}
# 生产环境替换为mysql insert语句
print(json.dumps(log_item, ensure_ascii=False))
# ===================== 工具函数3HMAC签名鉴权,防Header伪造 =====================
def verify_auth_sign():
"""
校验前端PHP生成的HMAC签名,防止伪造X-Role:admin
请求Header要求:
X-User: 用户名
X-Role: admin/user/guest
X-Time: 当前时间戳(秒)
X-Sign: hmac-sha256签名
返回 (user, role, client_ip, True/False)
"""
user = request.headers.get("X-User", "anonymous")
role = request.headers.get("X-Role", "guest")
ts_str = request.headers.get("X-Time", "0")
sign = request.headers.get("X-Sign", "")
client_ip = request.headers.get("X-Client-Ip", request.remote_addr)
try:
ts = int(ts_str)
except ValueError:
return user, role, client_ip, False
# 校验签名有效期
now = int(time.time())
if abs(now - ts) > SIGN_EXPIRE:
return user, role, client_ip, False
# 校验HMAC签名
raw_data = f"{user}|{role}|{ts}".encode("utf-8")
calc_sign = hmac.new(SIGN_SECRET, raw_data, hashlib.sha256).hexdigest()
if calc_sign != sign:
return user, role, client_ip, False
return user, role, client_ip, True
# ===================== 工具函数4:环境变量脱敏,屏蔽密码/密钥 =====================
def safe_env_filter(env_list):
safe_env = []
sensitive_keywords = ["PASS", "PASSWORD", "TOKEN", "SECRET", "KEY", "CREDENTIAL"]
for item in env_list:
equal_pos = item.find("=")
if equal_pos == -1:
safe_env.append(item)
continue
key = item[:equal_pos]
val = item[equal_pos+1:]
if any(word in key.upper() for word in sensitive_keywords):
safe_env.append(f"{key}=******")
else:
safe_env.append(item)
return safe_env
# ===================== 全部HTTP业务接口 =====================
# 1. 容器列表(端口预拼接字符串,前端直接渲染,无需formatPorts
@app.route("/list")
def api_list():
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "list_container", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
arr = []
for c in cli.containers.list(all=True):
img_name = c.image.tags[0] if c.image.tags else ""
port_str_arr = []
ports_raw = c.ports
for c_port, host_list in ports_raw.items():
if not host_list:
continue
for host_item in host_list:
host_port = host_item.get("HostPort", "")
port_str_arr.append(f"{host_port}:{c_port.split('/')[0]}")
port_display = ",".join(port_str_arr) if port_str_arr else "-"
arr.append({
"id": c.short_id,
"full_id": c.id,
"name": c.name,
"image": img_name,
"status": c.status,
"ports": port_display
})
write_audit_log(user, role, "list_container", "", "", "success", ip)
return jsonify({"code":0,"msg":"success","list":arr})
# 2. 镜像列表
@app.route("/image_list")
def api_image_list():
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "list_image", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
arr = []
for img in cli.images.list():
if not img.tags:
continue
full_tag = img.tags[0]
repo, tag = full_tag.split(":", 1)
size_mb = round(img.attrs["Size"] / 1024 / 1024, 1)
arr.append({
"repo": repo,
"tag": tag,
"id": img.short_id,
"full_id": img.id,
"size": f"{size_mb}MB",
"created": img.attrs["Created"]
})
write_audit_log(user, role, "list_image", "", "", "success", ip)
return jsonify({"code":0,"msg":"success","list":arr})
# 3. 删除镜像
@app.route("/remove_image/<image_id>")
def api_remove_image(image_id):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "remove_image", "", image_id, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "remove_image", "", image_id, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除镜像"})
try:
img = cli.images.get(image_id)
cli.images.remove(image_id, force=True)
write_audit_log(user, role, "remove_image", img.tags[0] if img.tags else "", image_id, "success", ip)
return jsonify({"code":0,"msg":"镜像已强制删除"})
except Exception as e:
write_audit_log(user, role, "remove_image", "", image_id, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 4. 重启容器
@app.route("/restart/<cid>")
def api_restart(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "restart", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "restart", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
try:
container = cli.containers.get(cid)
container.restart()
write_audit_log(user, role, "restart", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"容器已重启"})
except Exception as e:
write_audit_log(user, role, "restart", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 5. 停止容器
@app.route("/stop/<cid>")
def api_stop(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "stop", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "stop", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可操作容器"})
try:
container = cli.containers.get(cid)
container.stop()
write_audit_log(user, role, "stop", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"容器已停止"})
except Exception as e:
write_audit_log(user, role, "stop", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 6. 删除容器
@app.route("/rm/<cid>")
def api_rm(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "rm", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "rm", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可删除容器"})
try:
container = cli.containers.get(cid)
container.remove(force=True)
write_audit_log(user, role, "rm", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"容器已强制删除"})
except Exception as e:
write_audit_log(user, role, "rm", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 7. 一次性拉取日志(兼容旧页面单次查看)
@app.route("/logs/<cid>/<int:tail_lines>")
def api_logs(cid, tail_lines):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_log", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
log_bytes = container.logs(tail=tail_lines)
log_str = log_bytes.decode("utf-8", errors="replace")
write_audit_log(user, role, "read_log", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":log_str})
except Exception as e:
write_audit_log(user, role, "read_log", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":""})
# 8. 容器网络详情
@app.route("/network/<cid>")
def api_network(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_network", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
info = container.attrs["NetworkSettings"]["Networks"]
write_audit_log(user, role, "read_network", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":json.dumps(info,indent=2)})
except Exception as e:
write_audit_log(user, role, "read_network", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":""})
# 9. 短连接单次执行命令(旧弹窗兼容,非实时终端)
@app.route("/exec/<cid>", methods=["POST"])
def api_exec(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "single_exec", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "single_exec", "", cid, "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可执行命令"})
try:
data = request.get_json()
cmd = data.get("cmd", "")
if not cmd:
return jsonify({"code":400,"msg":"命令不能为空","data":""})
cmd_list = shlex.split(cmd)
container = cli.containers.get(cid)
exec_obj = container.exec_run(cmd_list, stdout=True, stderr=True)
output = exec_obj.output.decode("utf-8", errors="replace")
write_audit_log(user, role, "single_exec", container.name, cid, "command_executed", ip)
return jsonify({"code":0,"msg":"success","data":output})
except Exception as e:
write_audit_log(user, role, "single_exec", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":""})
# 10. 创建容器(端口标准格式 + 前端可配置CPU/内存)
@app.route("/create", methods=["POST"])
def api_create():
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "create_container", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "create_container", "", "", "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可创建容器"})
try:
data = request.get_json()
name = data.get("name", "")
image = data.get("image")
cmd = data.get("cmd")
ports_input = data.get("ports", {})
# 修复:docker标准端口字典格式 {内部端口:宿主机端口}
port_bind = {}
for inner_port, host_port in ports_input.items():
port_bind[f"{inner_port}/tcp"] = int(host_port)
# 前端可配置资源限制,默认1核512M
cpu_core = int(data.get("cpu", 1))
memory_limit = data.get("memory", "512m")
nano_cpus = cpu_core * 1000000000
container = cli.containers.run(
image=image,
name=name if name else None,
command=cmd if cmd else None,
ports=port_bind,
detach=True,
restart_policy={"Name": "always"},
mem_limit=memory_limit,
nano_cpus=nano_cpus
)
write_audit_log(user, role, "create_container", container.name, container.id, "success", ip)
return jsonify({"code":0,"msg":f"容器创建成功,ID:{container.short_id}"})
except Exception as e:
write_audit_log(user, role, "create_container", "", "", f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 11. 登录私有镜像仓库
@app.route("/bind_repo", methods=["POST"])
def api_bind_repo():
global registry_config
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "repo_login", "", "", "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
if role != "admin":
write_audit_log(user, role, "repo_login", "", "", "denied", ip)
return jsonify({"code":403,"msg":"权限不足,仅管理员可登录镜像仓库"})
try:
data = request.get_json()
repo_name = data.get("repo_name","")
repo_addr = data.get("repo_addr")
repo_user = data.get("repo_user","")
repo_pwd = data.get("repo_pwd","")
api_cli.login(username=repo_user, password=repo_pwd, registry=repo_addr)
registry_config["repo_name"] = repo_name
registry_config["repo_addr"] = repo_addr
registry_config["repo_user"] = repo_user
registry_config["repo_pwd"] = repo_pwd
write_audit_log(user, role, "repo_login", repo_addr, "", "success", ip)
return jsonify({"code":0,"msg":"镜像仓库登录绑定成功"})
except Exception as e:
write_audit_log(user, role, "repo_login", repo_addr, "", f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e)})
# 修复:stats接口online_cpus兜底兼容,无KeyError
@app.route("/stats/<cid>")
def api_stats(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_stats", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
raw = container.stats(stream=False)
# CPU兼容兜底,不存在online_cpus自动计算
cpu_stats = raw["cpu_stats"]
online_cpus = cpu_stats.get("online_cpus", len(cpu_stats["cpu_usage"].get("percpu_usage", [])) or 1)
cpu_delta = cpu_stats["cpu_usage"]["total_usage"] - raw["precpu_stats"]["cpu_usage"]["total_usage"]
system_delta = cpu_stats["system_cpu_usage"] - raw["precpu_stats"]["system_cpu_usage"]
cpu_percent = 0.0
if system_delta > 0 and cpu_delta > 0:
cpu_percent = round((cpu_delta / system_delta) * online_cpus * 100, 1)
# 内存计算
mem_usage = raw["memory_stats"]["usage"] / 1024 / 1024
mem_limit = raw["memory_stats"]["limit"] / 1024 / 1024
mem_percent = round((mem_usage / mem_limit) * 100, 1) if mem_limit > 0 else 0
# 网络流量
net_rx = 0
net_tx = 0
for net in raw["networks"].values():
net_rx += net["rx_bytes"] / 1024 / 1024
net_tx += net["tx_bytes"] / 1024 / 1024
res = {
"cpu": cpu_percent,
"mem_mb": round(mem_usage, 1),
"mem_limit_mb": round(mem_limit, 1),
"mem_percent": mem_percent,
"net_rx_mb": round(net_rx, 1),
"net_tx_mb": round(net_tx, 1)
}
write_audit_log(user, role, "read_stats", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":res})
except Exception as e:
write_audit_log(user, role, "read_stats", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":{}})
# 修复:detail接口ENV脱敏,多网卡IP兼容
@app.route("/detail/<cid>")
def api_detail(cid):
user, role, ip, auth_ok = verify_auth_sign()
if not auth_ok:
write_audit_log(user, role, "read_detail", "", cid, "auth_failed", ip)
return jsonify({"code":403,"msg":"身份校验失败,非法访问"})
try:
container = cli.containers.get(cid)
attrs = container.attrs
nets = attrs["NetworkSettings"]["Networks"]
ip_map = {}
for net_name, net_info in nets.items():
ip_map[net_name] = net_info.get("IPAddress", "")
# 敏感环境变量脱敏
raw_env = attrs["Config"].get("Env", [])
safe_env = safe_env_filter(raw_env)
res = {
"network_ips": ip_map,
"mounts": attrs["Mounts"],
"env": safe_env,
"created": attrs["Created"],
"entrypoint": attrs["Config"]["Entrypoint"],
"cmd": attrs["Config"]["Cmd"],
"restart_policy": attrs["HostConfig"]["RestartPolicy"]
}
write_audit_log(user, role, "read_detail", container.name, cid, "success", ip)
return jsonify({"code":0,"msg":"success","data":res})
except Exception as e:
write_audit_log(user, role, "read_detail", "", cid, f"fail:{str(e)}", ip)
return jsonify({"code":500,"msg":str(e),"data":{}})
# 根路由
@app.route("/")
def index():
return jsonify({"code":400,"msg":"无效操作","list":[],"data":""})
# ===================== WebSocket 交互式TTY实时终端 =====================
def stream_terminal_output(sid, sock):
"""后台异步读取docker终端输出,兼容两种socket接收方式"""
try:
while True:
chunk = docker_recv(sock, 1024)
if not chunk:
break
text = chunk.decode("utf-8", errors="replace")
socketio.emit("terminal_output", {"data": text}, room=sid)
except Exception:
pass
# 流结束自动清理会话
if sid in session_exec_map:
try:
session_exec_map[sid]["socket"].close()
except Exception:
pass
del session_exec_map[sid]
@socketio.on("terminal_start")
def handle_terminal_start(json_data):
sid = request.sid
user, role, client_ip, auth_ok = verify_auth_sign()
if not auth_ok:
socketio.emit("terminal_output", {"data": "Permission denied: 身份校验失败\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "auth_failed", client_ip)
return
# 权限拦截:非管理员禁止打开交互式终端
if role != "admin":
socketio.emit("terminal_output", {"data": "Permission denied: 仅管理员可访问交互式终端\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", json_data.get("cid"), "denied", client_ip)
return
cid = json_data.get("cid")
if not cid:
socketio.emit("terminal_output", {"data": "错误:容器ID不能为空\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", "", "empty_cid", client_ip)
return
# 自动探测容器可用shell,兼容全版本docker-pyexec_create返回dict取Id
target_shell = None
for shell in SHELL_CANDIDATES:
try:
ret = api_cli.exec_create(
container=cid,
cmd=[shell, "-c", "echo ok"]
)
test_exec_id = ret["Id"]
test_stream = api_cli.exec_start(test_exec_id)
test_stream.read()
target_shell = shell
break
except Exception:
continue
if not target_shell:
socketio.emit("terminal_output", {"data": "错误:容器内无可用shell(bash/sh/ash)\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", cid, "no_shell", client_ip)
return
try:
# 创建交互式TTY会话,统一提取Id兼容所有SDK版本
exec_ret = api_cli.exec_create(
container=cid,
cmd=target_shell,
stdin=True,
stdout=True,
stderr=True,
tty=True
)
exec_id = exec_ret["Id"]
sock = api_cli.exec_start(exec_id=exec_id, tty=True, socket=True, detach=False)
session_exec_map[sid] = {
"cid": cid,
"exec_id": exec_id,
"socket": sock
}
socketio.start_background_task(stream_terminal_output, sid, sock)
container_name = cli.containers.get(cid).name
write_audit_log(user, role, "terminal_start", container_name, cid, "success", client_ip)
except Exception as e:
socketio.emit("terminal_output", {"data": f"启动终端失败:{str(e)}\n"}, room=sid)
write_audit_log(user, role, "terminal_start", "", cid, f"fail:{str(e)}", client_ip)
@socketio.on("terminal_input")
def handle_terminal_input(json_data):
"""前端输入字符写入容器标准输入,审计脱敏不记录明文"""
sid = request.sid
user, role, client_ip, auth_ok = verify_auth_sign()
if not auth_ok:
socketio.emit("terminal_output", {"data": "身份校验失败\n"}, room=sid)
return
if sid not in session_exec_map:
socketio.emit("terminal_output", {"data": "终端会话已断开,请重新打开\n"}, room=sid)
return
input_text = json_data.get("data", "")
sock = session_exec_map[sid]["socket"]
sock.send(input_text.encode("utf-8"))
cid = session_exec_map[sid]["cid"]
# 审计仅标记事件,不再记录明文命令,规避密码泄露
write_audit_log(user, role, "terminal_input", "", cid, "command_entered", client_ip)
@socketio.on("terminal_resize")
def handle_terminal_resize(json_data):
"""前端xterm窗口大小自适应"""
sid = request.sid
if sid not in session_exec_map:
return
exec_id = session_exec_map[sid]["exec_id"]
height = int(json_data.get("height", 24))
width = int(json_data.get("width", 80))
try:
api_cli.exec_resize(exec_id=exec_id, height=height, width=width)
except Exception:
pass
# 新增主动关闭终端事件(解决切换容器残留TTY)
@socketio.on("terminal_close")
def handle_terminal_close():
sid = request.sid
user, role, client_ip, auth_ok = verify_auth_sign()
if sid in session_exec_map:
cid = session_exec_map[sid]["cid"]
try:
session_exec_map[sid]["socket"].close()
except Exception:
pass
del session_exec_map[sid]
write_audit_log(user, role, "terminal_close", "", cid, "manual_close", client_ip)
@socketio.on("disconnect")
def handle_disconnect():
"""前端断开连接,释放docker socket、清理会话,防止句柄泄漏"""
sid = request.sid
user, role, client_ip, auth_ok = verify_auth_sign()
# 清理TTY终端
if sid in session_exec_map:
cid = session_exec_map[sid]["cid"]
try:
session_exec_map[sid]["socket"].close()
except Exception:
pass
del session_exec_map[sid]
write_audit_log(user, role, "terminal_close", "", cid, "disconnect", client_ip)
# 清理实时日志标记,终止日志循环
if sid in log_task_map:
del log_task_map[sid]
# ===================== WebSocket 实时流式日志 docker logs -f(生成器主动释放) =====================
def stream_follow_log(sid, cid):
generator = None
try:
container = cli.containers.get(cid)
generator = container.logs(stream=True, follow=True)
log_task_map[sid] = {"cid": cid, "running": True}
for line in generator:
task_info = log_task_map.get(sid, {})
if not task_info.get("running"):
break
txt = line.decode("utf-8", errors="replace")
socketio.emit("log_output", {"data": txt}, room=sid)
except Exception:
pass
finally:
# 主动关闭日志生成器,释放docker daemon长连接
if generator is not None:
generator.close()
if sid in log_task_map:
del log_task_map[sid]
@socketio.on("log_start")
def handle_log_start(json_data):
sid = request.sid
user, role, client_ip, auth_ok = verify_auth_sign()
if not auth_ok:
socketio.emit("log_output", {"data": "身份校验失败"}, room=sid)
write_audit_log(user, role, "log_start", "", "", "auth_failed", client_ip)
return
cid = json_data.get("cid")
if not cid:
socketio.emit("log_output", {"data": "容器ID不能为空"}, room=sid)
write_audit_log(user, role, "log_start", "", "", "empty_cid", client_ip)
return
if role != "admin":
socketio.emit("log_output", {"data": "权限不足,仅管理员可查看实时日志"}, room=sid)
write_audit_log(user, role, "log_start", "", cid, "denied", client_ip)
return
socketio.start_background_task(stream_follow_log, sid, cid)
container_name = cli.containers.get(cid).name
write_audit_log(user, role, "log_start", container_name, cid, "success", client_ip)
@socketio.on("log_stop")
def handle_log_stop():
sid = request.sid
user, role, client_ip, auth_ok = verify_auth_sign()
if sid in log_task_map:
log_task_map[sid]["running"] = False
cid = log_task_map[sid]["cid"]
write_audit_log(user, role, "log_stop", "", cid, "manual_close", client_ip)
# 启动入口(eventlet异步,必须socketio.run
if __name__ == "__main__":
socketio.run(app, host="0.0.0.0", port=8090, debug=False, allow_unsafe_werkzeug=True)
+9
View File
@@ -0,0 +1,9 @@
#!/bin/bash
while true; do
nc -l 127.0.0.1 9999 <<EOF
HTTP/1.1 200 OK
Content-Type: application/json
$(docker ps -a --format '{{.Names}}|{{.ID}}|{{.Image}}|{{.Status}}|{{.Ports}}')
EOF
done
+436
View File
@@ -0,0 +1,436 @@
<?php
session_start();
$act = $_GET['act'] ?? '';
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"], JSON_UNESCAPED_UNICODE);
exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
// CSV导出接口(支持指定历史日期下载,不输出JSON头)
if ($act === "export_csv") {
date_default_timezone_set('Asia/Shanghai');
$conn = getDbConn();
mysqli_query($conn, "SET time_zone = '+08:00'");
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$singleDate = $_GET['date'] ?? '';
$startDate = $_GET['start'] ?? '';
$endDate = $_GET['end'] ?? '';
$where = "";
$fileName = "";
if (!empty($startDate) && !empty($endDate)) {
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) || !preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>400,"msg"=>"日期格式错误"], JSON_UNESCAPED_UNICODE);
exit;
}
if ($startDate > $endDate) {
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>400,"msg"=>"开始日期不能大于结束日期"], JSON_UNESCAPED_UNICODE);
exit;
}
$where = "receive_time >= '$startDate 00:00:00' AND receive_time <= '$endDate 23:59:59'";
$fileName = "告警报表_{$startDate}_至_{$endDate}.csv";
} elseif (!empty($singleDate)) {
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $singleDate)) {
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>400,"msg"=>"日期格式错误"], JSON_UNESCAPED_UNICODE);
exit;
}
$where = "DATE(receive_time) = '$singleDate'";
$fileName = "告警报表_{$singleDate}.csv";
} else {
$today = date("Y-m-d");
$where = "DATE(receive_time) = '$today'";
$fileName = "告警报表_今日_{$today}.csv";
}
// 调试:打印where条件,访问接口先看这个
// echo "WHERE条件:".$where;exit;
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE {$where} ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename={$fileName}");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 调试代码:打印GET入参,测试完成后删除此段
// $getAct = $_GET['act'] ?? '';
//echo json_encode([
// "debug_act" => $getAct,
// "debug_rule" => $_GET['rule_name'] ?? '',
// "debug_instance" => $_GET['instance'] ?? ''
//], JSON_UNESCAPED_UNICODE);
//exit;
// 接口1:下线整条监控规则(该规则下所有实例一并清理)
if($act === "clear_offline_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
// 标记该规则下全部实例为下线
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
// 批量插入恢复记录,消除页面残留告警
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc'
AND (alert_name,instance) NOT IN (
SELECT alert_name,instance FROM alert_log WHERE alert_type=2
)
";
mysqli_query($conn, $insertSql);
echo json_encode(["code"=>0,"msg"=>"整条规则下线完成,所有实例告警已清除"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口2:单独下线单台实例(仅隐藏指定机器,同规则其他主机不受影响)
if($act === "clear_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 第一步:删除当前实例旧的恢复记录,避免重复拦截
$delSql = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
// 第二步:插入最新恢复记录,移除NOT IN限制,强制生成
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
$affected = mysqli_affected_rows($conn);
echo json_encode([
"code"=>0,
"msg"=>"单实例告警清理完成",
"insert_rows" => $affected
],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口3:恢复整条已下线规则(status改为1,重新展示所有实例告警)
if($act === "restore_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"监控规则已恢复,新故障会正常展示"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口4:恢复单台实例(仅把指定rule+instance置为启用)
if($act === "restore_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"单实例已恢复"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口5:删除单实例全部告警日志
if($act === "delete_single_instance_log"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$delSql = "DELETE FROM alert_log WHERE alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
$aff = mysqli_affected_rows($conn);
echo json_encode(["code"=>0,"msg"=>"单实例告警日志已删除","affected_rows"=>$aff],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口:永久屏蔽单台实例(monitor_rule 该条实例status=0,不手动恢复永远不告警)
if($act === "offline_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 把这条【规则+实例】状态改为0,永久屏蔽
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
$aff = mysqli_affected_rows($conn);
// 同步生成恢复日志,界面立刻消失旧告警
$delOld = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delOld);
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
echo json_encode([
"code"=>0,
"msg"=>"单实例已永久屏蔽,需调用restore_single_instance恢复",
"update_rows"=>$aff
],JSON_UNESCAPED_UNICODE);
exit;
}
// 新增:同步Prometheus实时告警,自动清理残留恢复记录
if($act === "sync_prom_alerts"){
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
$firingMap = [];
foreach($promData['data']['alerts'] as $item){
$an = $item['labels']['alertname'];
$ins = $item['labels']['instance'];
$key = "$an||$ins";
$firingMap[$key] = 1;
}
$res = mysqli_query($conn,"SELECT alert_name,instance FROM alert_log WHERE alert_type=2");
$delCnt = 0;
while($row = mysqli_fetch_assoc($res)){
$k = $row['alert_name']."||".$row['instance'];
if(isset($firingMap[$k])){
$n = mysqli_real_escape_string($conn,$row['alert_name']);
$i = mysqli_real_escape_string($conn,$row['instance']);
mysqli_query($conn,"DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$n' AND instance='$i'");
$delCnt++;
}
}
echo json_encode(["code"=>0,"msg"=>"同步完成","delete_clear_log"=>$delCnt],JSON_UNESCAPED_UNICODE);
exit;
}
// ============后台管理专用接口 start============
// 修改密码 BCrypt哈希加密,匹配历史密码算法
if($act === "change_pwd"){
$uid = $_SESSION['user_id'];
$oldPwdRaw = $_GET['old_pwd'] ?? '';
$newPwdRaw = $_GET['new_pwd'] ?? '';
if(empty($oldPwdRaw) || empty($newPwdRaw)){
echo json_encode(["code"=>400,"msg"=>"密码不能为空"],JSON_UNESCAPED_UNICODE);
exit;
}
// 查询原密码哈希
$userSql = "SELECT password FROM admin_user WHERE user_id='$uid' LIMIT 1";
$userRes = mysqli_query($conn,$userSql);
$userRow = mysqli_fetch_assoc($userRes);
if(!$userRow){
echo json_encode(["code"=>401,"msg"=>"账户不存在"],JSON_UNESCAPED_UNICODE);
exit;
}
$oldHash = $userRow['password'];
// 校验旧密码(和原有登录逻辑一致)
if(!password_verify($oldPwdRaw, $oldHash)){
echo json_encode(["code"=>400,"msg"=>"原密码输入错误"],JSON_UNESCAPED_UNICODE);
exit;
}
// 生成新bcrypt哈希
$newHash = password_hash($newPwdRaw, PASSWORD_DEFAULT);
$updateSql = "UPDATE admin_user SET password='$newHash', update_at=NOW() WHERE user_id='$uid'";
mysqli_query($conn,$updateSql);
echo json_encode(["code"=>0,"msg"=>"密码修改成功"],JSON_UNESCAPED_UNICODE);
exit;
}
// 保存界面外观配置
if($act === "save_theme"){
// 兼容POST/GET
$title = mysqli_real_escape_string($conn,$_POST['title'] ?? $_GET['title'] ?? '');
$color = mysqli_real_escape_string($conn,$_POST['color'] ?? $_GET['color'] ?? '');
$pagesize = mysqli_real_escape_string($conn,$_POST['pagesize'] ?? $_GET['pagesize'] ?? '20');
$footer = mysqli_real_escape_string($conn,$_POST['footer'] ?? $_GET['footer'] ?? '');
$lang = mysqli_real_escape_string($conn,$_POST['lang'] ?? $_GET['lang'] ?? 'zh');
$sql = "REPLACE INTO sys_config (k,v) VALUES
('page_title','$title'),
('theme_color','$color'),
('page_size','$pagesize'),
('footer_text','$footer'),
('lang','$lang')";
mysqli_query($conn,$sql);
echo json_encode(["code"=>0,"msg"=>"外观配置已保存"],JSON_UNESCAPED_UNICODE);
exit;
}
// 保存侧边栏外链
if($act === "save_sidebar_link"){
$linkText = mysqli_real_escape_string($conn,$_GET['link_text'] ?? '');
$sql = "REPLACE INTO sys_config (k,v) VALUES ('sidebar_links','$linkText')";
mysqli_query($conn,$sql);
echo json_encode(["code"=>0,"msg"=>"侧边外链保存成功"],JSON_UNESCAPED_UNICODE);
exit;
}
// 单独切换语言接口
if ($_POST['act'] === 'save_lang') {
$lang = mysqli_real_escape_string($conn, $_POST['lang']);
$sql = "REPLACE INTO sys_config(k, v) VALUES ('lang', '$lang')";
mysqli_query($conn, $sql);
echo json_encode(["code" => 0]);
exit;
}
// ============后台管理专用接口 end============
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 4. 全局跨天未恢复告警(过滤整条下线规则,单实例仅靠恢复日志隐藏)
if ($act === "active_firing") {
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($maxRow = mysqli_fetch_assoc($maxRes)) {
$an = $conn->real_escape_string($maxRow['alert_name']);
$ins = $conn->real_escape_string($maxRow['instance']);
$mrt = $conn->real_escape_string($maxRow['max_rt']);
// 判断该规则是否整条下线,下线直接跳过
$ruleCheck = mysqli_query($conn, "SELECT status FROM monitor_rule WHERE rule_name='$an' LIMIT 1");
$ruleStatus = 1;
if($ruleCheck && $ruleRow = mysqli_fetch_assoc($ruleCheck)){
$ruleStatus = intval($ruleRow['status']);
}
if($ruleStatus === 0){
continue;
}
$rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log
WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
$rowRes = mysqli_query($conn, $rowSql);
$data = mysqli_fetch_assoc($rowRes);
if ($data && (int)$data['alert_type'] === 1) {
$activeList[] = $data;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+45
View File
@@ -0,0 +1,45 @@
<?php
header("Content-Type:application/json;charset=utf-8");
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
// 1. 获取今日总统计:触发次数、恢复次数、故障实例数
if($act == "day_total"){
$today = date("Y-m-d");
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 2. 今日告警TOP排行(按发生次数)
if($act == "top_alert"){
$today = date("Y-m-d");
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 3. 今日明细列表(前端表格)
if($act == "log_list"){
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
+49
View File
@@ -0,0 +1,49 @@
<?php
header("Content-Type:application/json;charset=utf-8");
// 宿主机内网IP,容器访问MySQL
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
echo json_encode(["code"=>500,"msg"=>"数据库连接失败:".mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
$today = date("Y-m-d");
// 当日总统计
if($act == "day_total"){
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 告警TOP10
if($act == "top_alert"){
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 明细列表
if($act == "log_list"){
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
+49
View File
@@ -0,0 +1,49 @@
<?php
header("Content-Type:application/json;charset=utf-8");
// 宿主机内网IP,容器访问MySQL
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
echo json_encode(["code"=>500,"msg"=>"数据库连接失败:".mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
$today = date("Y-m-d");
// 当日总统计
if($act == "day_total"){
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 告警TOP10
if($act == "top_alert"){
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 明细列表
if($act == "log_list"){
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
+106
View File
@@ -0,0 +1,106 @@
<?php
$act = $_GET['act'] ?? '';
if($act == "export_csv"){
// 先不输出JSON头,单独处理下载
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:".mysqli_connect_error();
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$today = date("Y-m-d");
// 查询今日告警,去掉ends_at恢复时间字段
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn,$sql);
// 下载头
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_".$today.".csv");
// UTF8 BOMWindows Excel不乱码
echo "\xEF\xBB\xBF";
// CSV表头
$header = ["告警状态","告警名称","实例","级别","故障开始时间","接收时间"];
echo implode(",",$header)."\r\n";
// 逐行输出数据
while($row = mysqli_fetch_assoc($res)){
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
// 字段逗号转义,防止内容逗号破坏CSV格式
foreach($line as &$v){
$v = '"'.str_replace('"','""',$v).'"';
}
echo implode(",",$line)."\r\n";
}
mysqli_close($conn);
exit;
}
// 以下普通JSON接口
header("Content-Type:application/json;charset=utf-8");
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
echo json_encode(["code"=>500,"msg"=>"数据库连接失败:".mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$today = date("Y-m-d");
// 当日总统计
if($act == "day_total"){
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 告警TOP10
if($act == "top_alert"){
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 明细列表
if($act == "log_list"){
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 无效参数兜底
echo json_encode(["code"=>400,"msg"=>"无效请求参数act"],JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+140
View File
@@ -0,0 +1,140 @@
<?php
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
$act = $_GET['act'] ?? '';
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 4. 全局跨天未恢复告警(解决6.30残留故障不显示)
if ($act === "active_firing") {
// 查询全部触发告警
$fireSql = "SELECT DISTINCT alert_name,instance,severity,starts_at,receive_time FROM alert_log WHERE alert_type=1";
$fireRes = mysqli_query($conn, $fireSql);
$fireMap = [];
while ($row = mysqli_fetch_assoc($fireRes)) {
$key = $row['alert_name'] . "|" . $row['instance'];
$fireMap[$key] = $row;
}
if (empty($fireMap)) {
echo json_encode([], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 查询全部恢复告警唯一标识
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'|',instance) as k FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($conn, $resolveSql);
$resolveKeys = [];
while ($r = mysqli_fetch_assoc($resolveRes)) {
$resolveKeys[] = $r['k'];
}
// 过滤:存在触发、无对应恢复 = 当前活跃故障
$activeList = [];
foreach ($fireMap as $key => $row) {
if (!in_array($key, $resolveKeys)) {
$row['alert_type'] = 1;
$activeList[] = $row;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"]);
exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
$act = $_GET['act'] ?? '';
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 4. 全局跨天未恢复告警【修复版:只看每条告警最新状态,兼容低版本MariaDB】
if ($act === "active_firing") {
// 第一步:分组获取每个告警+实例的最新入库时间
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($maxRow = mysqli_fetch_assoc($maxRes)) {
// 转义防止SQL注入
$an = $conn->real_escape_string($maxRow['alert_name']);
$ins = $conn->real_escape_string($maxRow['instance']);
$mrt = $conn->real_escape_string($maxRow['max_rt']);
// 根据告警名、实例、最新时间取出最后一条记录
$rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log
WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
$rowRes = mysqli_query($conn, $rowSql);
$data = mysqli_fetch_assoc($rowRes);
// 仅最后一条状态为触发,才判定为当前活跃未恢复告警
if ($data && (int)$data['alert_type'] === 1) {
$activeList[] = $data;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+330
View File
@@ -0,0 +1,330 @@
<?php
session_start();
$act = $_GET['act'] ?? '';
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"], JSON_UNESCAPED_UNICODE);
exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 调试代码:打印GET入参,测试完成后删除此段
// $getAct = $_GET['act'] ?? '';
//echo json_encode([
// "debug_act" => $getAct,
// "debug_rule" => $_GET['rule_name'] ?? '',
// "debug_instance" => $_GET['instance'] ?? ''
//], JSON_UNESCAPED_UNICODE);
//exit;
// 接口1:下线整条监控规则(该规则下所有实例一并清理)
if($act === "clear_offline_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
// 标记该规则下全部实例为下线
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
// 批量插入恢复记录,消除页面残留告警
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc'
AND (alert_name,instance) NOT IN (
SELECT alert_name,instance FROM alert_log WHERE alert_type=2
)
";
mysqli_query($conn, $insertSql);
echo json_encode(["code"=>0,"msg"=>"整条规则下线完成,所有实例告警已清除"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口2:单独下线单台实例(仅隐藏指定机器,同规则其他主机不受影响)
if($act === "clear_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 第一步:删除当前实例旧的恢复记录,避免重复拦截
$delSql = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
// 第二步:插入最新恢复记录,移除NOT IN限制,强制生成
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
$affected = mysqli_affected_rows($conn);
echo json_encode([
"code"=>0,
"msg"=>"单实例告警清理完成",
"insert_rows" => $affected
],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口3:恢复整条已下线规则(status改为1,重新展示所有实例告警)
if($act === "restore_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"监控规则已恢复,新故障会正常展示"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口4:恢复单台实例(仅把指定rule+instance置为启用)
if($act === "restore_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"单实例已恢复"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口5:删除单实例全部告警日志
if($act === "delete_single_instance_log"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$delSql = "DELETE FROM alert_log WHERE alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
$aff = mysqli_affected_rows($conn);
echo json_encode(["code"=>0,"msg"=>"单实例告警日志已删除","affected_rows"=>$aff],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口:永久屏蔽单台实例(monitor_rule 该条实例status=0,不手动恢复永远不告警)
if($act === "offline_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 把这条【规则+实例】状态改为0,永久屏蔽
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
$aff = mysqli_affected_rows($conn);
// 同步生成恢复日志,界面立刻消失旧告警
$delOld = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delOld);
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
echo json_encode([
"code"=>0,
"msg"=>"单实例已永久屏蔽,需调用restore_single_instance恢复",
"update_rows"=>$aff
],JSON_UNESCAPED_UNICODE);
exit;
}
// 新增:同步Prometheus实时告警,自动清理残留恢复记录
if($act === "sync_prom_alerts"){
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
$firingMap = [];
foreach($promData['data']['alerts'] as $item){
$an = $item['labels']['alertname'];
$ins = $item['labels']['instance'];
$key = "$an||$ins";
$firingMap[$key] = 1;
}
$res = mysqli_query($conn,"SELECT alert_name,instance FROM alert_log WHERE alert_type=2");
$delCnt = 0;
while($row = mysqli_fetch_assoc($res)){
$k = $row['alert_name']."||".$row['instance'];
if(isset($firingMap[$k])){
$n = mysqli_real_escape_string($conn,$row['alert_name']);
$i = mysqli_real_escape_string($conn,$row['instance']);
mysqli_query($conn,"DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$n' AND instance='$i'");
$delCnt++;
}
}
echo json_encode(["code"=>0,"msg"=>"同步完成","delete_clear_log"=>$delCnt],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 4. 全局跨天未恢复告警(过滤整条下线规则,单实例仅靠恢复日志隐藏)
if ($act === "active_firing") {
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($maxRow = mysqli_fetch_assoc($maxRes)) {
$an = $conn->real_escape_string($maxRow['alert_name']);
$ins = $conn->real_escape_string($maxRow['instance']);
$mrt = $conn->real_escape_string($maxRow['max_rt']);
// 判断该规则是否整条下线,下线直接跳过
$ruleCheck = mysqli_query($conn, "SELECT status FROM monitor_rule WHERE rule_name='$an' LIMIT 1");
$ruleStatus = 1;
if($ruleCheck && $ruleRow = mysqli_fetch_assoc($ruleCheck)){
$ruleStatus = intval($ruleRow['status']);
}
if($ruleStatus === 0){
continue;
}
$rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log
WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
$rowRes = mysqli_query($conn, $rowSql);
$data = mysqli_fetch_assoc($rowRes);
if ($data && (int)$data['alert_type'] === 1) {
$activeList[] = $data;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+46
View File
@@ -0,0 +1,46 @@
<?php
header("Content-Type:application/json;charset=utf-8");
// 改为宿主机真实内网IP,不要写localhost/mysql容器名
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
// 1. 获取今日总统计:触发次数、恢复次数、故障实例数
if($act == "day_total"){
$today = date("Y-m-d");
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 2. 今日告警TOP排行(按发生次数)
if($act == "top_alert"){
$today = date("Y-m-d");
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 3. 今日明细列表(前端表格)
if($act == "log_list"){
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
// 后台存放真实密码,前端完全看不到
$realPwd = "hp93000";
$inputPwd = $_POST['pwd'] ?? '';
header("Content-Type:application/json;charset=utf-8");
if($inputPwd === $realPwd){
// 生成临时token返回前端
echo json_encode(["code"=>200,"token"=>"alert_".md5(time().uniqid())]);
}else{
echo json_encode(["code"=>400,"msg"=>"密码错误"]);
}
+126
View File
@@ -0,0 +1,126 @@
<?php
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
$act = $_GET['act'] ?? '';
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 1.今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 2.今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 3.今日全量告警明细
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 4.全局跨天未恢复告警(修复版,取每组最新一条判断状态)
if ($act === "active_firing") {
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($row = mysqli_fetch_assoc($maxRes)) {
$an = mysqli_real_escape_string($conn, $row['alert_name']);
$ins = mysqli_real_escape_string($conn, $row['instance']);
$rt = mysqli_real_escape_string($conn, $row['max_rt']);
$sql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log WHERE alert_name='$an' AND instance='$ins' AND receive_time='$rt' LIMIT 1";
$one = mysqli_fetch_assoc(mysqli_query($conn, $sql));
if ($one && (int)$one['alert_type'] === 1) {
$activeList[] = $one;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 非法act
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+440
View File
@@ -0,0 +1,440 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: ../login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者调试面板 - Advantest 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-title{
font-size:24px;
font-weight:700;
letter-spacing:1px;
color:#152c5b;
}
.header-sub{
font-size:14px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 18px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-warning{
background:#e6a23c;
color:#fff;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1600px;
margin:0 auto;
}
h1{
display:none;
}
/* 卡片样式 统一企业面板风格 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.row{
display:flex;
gap:12px;
align-items:center;
margin-bottom:10px;
flex-wrap:wrap;
}
label{
font-size:14px;
color:#475569;
width:110px;
}
input{
padding:8px 12px;
border:1px solid #dcdfe6;
border-radius:8px;
width:280px;
font-size:14px;
}
input[type="date"]{
width:200px;
}
button{
padding:9px 18px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
}
button.normal{
background:#409eff;
color:#fff;
}
button.normal:hover{
background:#2979e0;
}
.tip{
font-size:13px;
color:#94a3b8;
margin-top:8px;
padding-left:110px;
}
/* 返回结果区块 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:260px;
font-family:Consolas,monospace;
font-size:13px;
}
/* 响应式适配 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.container{padding:24px}
.card{padding:24px}
.row{flex-direction:column;align-items:flex-start}
label{width:auto}
input{width:100% !important;max-width:320px}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-title"><i class="fa fa-code"></i> 开发者调试面板</div>
<div class="header-sub">全部告警接口可视化调用 · 后端API调试工具</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.href='../admin.php'"><i class="fa fa-arrow-left"></i>返回系统后台</button>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新页面</button>
<button class="btn-base btn-primary" onclick="location.href='../dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
</div>
</div>
<div class="container">
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3><i class="fa fa-eraser"></i>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button class="normal" onclick="req('clear_single_instance','c_rule','c_ins')"><i class="fa fa-play"></i>执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号无需手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3><i class="fa fa-ban"></i>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="btn-danger" onclick="req('offline_single_instance','off_rule','off_ins')"><i class="fa fa-ban"></i>永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不执行恢复接口则永久不展示该实例告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3><i class="fa fa-check-circle"></i>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="btn-success" onclick="req('restore_single_instance','res_rule','res_ins')"><i class="fa fa-unlock"></i>恢复实例告警</button>
</div>
<div class="tip">解除单实例永久屏蔽状态,新故障会正常展示</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-danger" onclick="reqSingle('clear_offline_rule','cr_rule')"><i class="fa fa-power-off"></i>下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-success" onclick="reqSingle('restore_rule','rr_rule')"><i class="fa fa-refresh"></i>恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3><i class="fa fa-trash"></i>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="btn-warning" onclick="req('delete_single_instance_log','del_rule','del_ins')"><i class="fa fa-trash"></i>清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3><i class="fa fa-refresh"></i>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('sync_prom_alerts')"><i class="fa fa-sync"></i>立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端“故障存在但页面不显示”bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3><i class="fa fa-database"></i>8. 数据查询接口(获取面板原始数据)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('day_total')"><i class="fa fa-bar-chart"></i>今日统计 day_total</button>
<button class="normal" onclick="simpleReq('top_alert')"><i class="fa fa-pie-chart"></i>告警TOP10 top_alert</button>
<button class="normal" onclick="simpleReq('log_list')"><i class="fa fa-list"></i>今日明细 log_list</button>
<button class="normal" onclick="simpleReq('active_firing')"><i class="fa fa-exclamation-circle"></i>当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表(带日历选择) -->
<div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表</h3>
<!-- 第一组:单日导出(原有功能保留) -->
<div class="row" style="margin-bottom:12px;gap:10px;align-items:center;flex-wrap:wrap;">
<label>单日导出:</label>
<input type="date" id="csv_date" placeholder="留空导出今日">
<button class="btn-success" onclick="exportCsvSingle()"><i class="fa fa-download"></i>下载单日报表</button>
</div>
<div class="tip" style="margin-bottom:16px;">不选择日期默认导出今日告警;选择历史日期下载单日报表</div>
<!-- 第二组:区间导出(新增) -->
<div class="row" style="gap:10px;align-items:center;flex-wrap:wrap;">
<label>区间导出:</label>
<input type="date" id="csv_start">
<span></span>
<input type="date" id="csv_end">
<button class="btn-primary" onclick="exportCsvRange()"><i class="fa fa-download"></i>下载区间报表</button>
</div>
<div class="tip">填写开始、结束日期,导出该时间段全部告警数据</div>
</div>
<!-- 返回结果输出 -->
<div class="card">
<h3><i class="fa fa-terminal"></i>接口返回结果(JSON)</h3>
<div id="result">等待执行接口操作...</div>
</div>
</div>
<script>
// 接口路径 同目录 index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单日导出
function exportCsvSingle(){
const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`;
if(dateVal){
url += `&date=${encodeURIComponent(dateVal)}`;
}
window.open(url,"_blank");
}
// 区间导出
function exportCsvRange(){
const startDate = document.getElementById("csv_start").value.trim();
const endDate = document.getElementById("csv_end").value.trim();
if(!startDate || !endDate){
alert("请同时选择开始日期和结束日期");
return;
}
if(startDate > endDate){
alert("开始日期不能晚于结束日期");
return;
}
let url = `${apiUrl}?act=export_csv`;
url += `&start=${encodeURIComponent(startDate)}&end=${encodeURIComponent(endDate)}`;
window.open(url,"_blank");
}
</script>
</body>
</html>
+411
View File
@@ -0,0 +1,411 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: ../login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者调试面板 - Advantest 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-title{
font-size:24px;
font-weight:700;
letter-spacing:1px;
color:#152c5b;
}
.header-sub{
font-size:14px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 18px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-warning{
background:#e6a23c;
color:#fff;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1600px;
margin:0 auto;
}
h1{
display:none;
}
/* 卡片样式 统一企业面板风格 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.row{
display:flex;
gap:12px;
align-items:center;
margin-bottom:10px;
flex-wrap:wrap;
}
label{
font-size:14px;
color:#475569;
width:110px;
}
input{
padding:8px 12px;
border:1px solid #dcdfe6;
border-radius:8px;
width:280px;
font-size:14px;
}
input[type="date"]{
width:200px;
}
button{
padding:9px 18px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
}
button.normal{
background:#409eff;
color:#fff;
}
button.normal:hover{
background:#2979e0;
}
.tip{
font-size:13px;
color:#94a3b8;
margin-top:8px;
padding-left:110px;
}
/* 返回结果区块 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:260px;
font-family:Consolas,monospace;
font-size:13px;
}
/* 响应式适配 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.container{padding:24px}
.card{padding:24px}
.row{flex-direction:column;align-items:flex-start}
label{width:auto}
input{width:100% !important;max-width:320px}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-title"><i class="fa fa-code"></i> 开发者调试面板</div>
<div class="header-sub">全部告警接口可视化调用 · 后端API调试工具</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.href='../admin.php'"><i class="fa fa-arrow-left"></i>返回系统后台</button>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新页面</button>
<button class="btn-base btn-primary" onclick="location.href='../dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
</div>
</div>
<div class="container">
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3><i class="fa fa-eraser"></i>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button class="normal" onclick="req('clear_single_instance','c_rule','c_ins')"><i class="fa fa-play"></i>执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号无需手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3><i class="fa fa-ban"></i>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="btn-danger" onclick="req('offline_single_instance','off_rule','off_ins')"><i class="fa fa-ban"></i>永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不执行恢复接口则永久不展示该实例告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3><i class="fa fa-check-circle"></i>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="btn-success" onclick="req('restore_single_instance','res_rule','res_ins')"><i class="fa fa-unlock"></i>恢复实例告警</button>
</div>
<div class="tip">解除单实例永久屏蔽状态,新故障会正常展示</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-danger" onclick="reqSingle('clear_offline_rule','cr_rule')"><i class="fa fa-power-off"></i>下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-success" onclick="reqSingle('restore_rule','rr_rule')"><i class="fa fa-refresh"></i>恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3><i class="fa fa-trash"></i>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="btn-warning" onclick="req('delete_single_instance_log','del_rule','del_ins')"><i class="fa fa-trash"></i>清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3><i class="fa fa-refresh"></i>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('sync_prom_alerts')"><i class="fa fa-sync"></i>立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端“故障存在但页面不显示”bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3><i class="fa fa-database"></i>8. 数据查询接口(获取面板原始数据)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('day_total')"><i class="fa fa-bar-chart"></i>今日统计 day_total</button>
<button class="normal" onclick="simpleReq('top_alert')"><i class="fa fa-pie-chart"></i>告警TOP10 top_alert</button>
<button class="normal" onclick="simpleReq('log_list')"><i class="fa fa-list"></i>今日明细 log_list</button>
<button class="normal" onclick="simpleReq('active_firing')"><i class="fa fa-exclamation-circle"></i>当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表(带日历选择) -->
<div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
<div class="row">
<label>选择日期:</label>
<input type="date" id="csv_date" placeholder="留空导出今日">
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
</div>
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
</div>
<!-- 返回结果输出 -->
<div class="card">
<h3><i class="fa fa-terminal"></i>接口返回结果(JSON)</h3>
<div id="result">等待执行接口操作...</div>
</div>
</div>
<script>
// 接口路径 同目录 index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV 支持日历选择日期
function exportCsv(){
const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`;
if(dateVal){
url += `&date=${encodeURIComponent(dateVal)}`;
}
window.open(url,"_blank");
}
</script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(json_encode(['code' => 0, 'msg' => 'ok']));
}
// 数据库配置
$dbHost = '127.0.0.1';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效,请重新登录']);
exit;
}
$uid = $_SESSION['user_id'];
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
if (!$conn) {
echo json_encode(['code' => 500, 'msg' => '数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
// 获取当前用户订阅配置
if ($action === 'get_subscribe') {
$sql = "SELECT * FROM alert_subscribe WHERE uid = ? LIMIT 1";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $uid);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$data = mysqli_fetch_assoc($res);
if (empty($data)) {
$data = [
'severity' => '',
'instance' => '',
'alertname' => '',
'dingtalk_webhook' => '',
'wecom_webhook' => '',
'mail_receiver' => '',
'enable' => 0
];
}
echo json_encode(['code' => 0, 'msg' => 'ok', 'data' => $data], JSON_UNESCAPED_UNICODE);
exit;
}
// 保存/更新订阅配置
if ($action === 'save_subscribe') {
$post = json_decode(file_get_contents('php://input'), true);
$severity = $post['severity'] ?? '';
$instance = $post['instance'] ?? '';
$alertname = $post['alertname'] ?? '';
$dingtalk = $post['dingtalk_webhook'] ?? '';
$wecom = $post['wecom_webhook'] ?? '';
$mail = $post['mail_receiver'] ?? '';
$enable = intval($post['enable'] ?? 0);
$check = mysqli_query($conn, "SELECT id FROM alert_subscribe WHERE uid = '$uid' LIMIT 1");
if (mysqli_num_rows($check) > 0) {
$sql = "UPDATE alert_subscribe SET severity=?,instance=?,alertname=?,dingtalk_webhook=?,wecom_webhook=?,mail_receiver=?,enable=? WHERE uid=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssis', $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable, $uid);
} else {
$sql = "INSERT INTO alert_subscribe(uid,severity,instance,alertname,dingtalk_webhook,wecom_webhook,mail_receiver,enable) VALUES (?,?,?,?,?,?,?,?)";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $uid, $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable);
}
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '订阅配置保存成功'], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode(['code' => 500, 'msg' => '保存失败:' . mysqli_error($conn)], JSON_UNESCAPED_UNICODE);
}
exit;
}
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
?>
+130
View File
@@ -0,0 +1,130 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(json_encode(['code' => 0, 'msg' => 'ok']));
}
// 数据库配置
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
// 登录会话校验(和你原有登录体系一致)
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效,请重新登录']);
exit;
}
$uid = $_SESSION['user_id'];
// 连接数据库
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
if (!$conn) {
echo json_encode(['code' => 500, 'msg' => '数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
// 1、获取当前用户订阅配置
if ($action === 'get_subscribe') {
$sql = "SELECT * FROM alert_subscribe WHERE uid = ? LIMIT 1";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $uid);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$data = mysqli_fetch_assoc($res);
if (empty($data)) {
$data = [
'severity' => '',
'instance' => '',
'alertname' => '',
'dingtalk_webhook' => '',
'wecom_webhook' => '',
'mail_receiver' => '',
'enable' => 0
];
}
echo json_encode(['code' => 0, 'msg' => 'ok', 'data' => $data], JSON_UNESCAPED_UNICODE);
exit;
}
// 2、保存/更新订阅配置
if ($action === 'save_subscribe') {
$post = json_decode(file_get_contents('php://input'), true);
$severity = $post['severity'] ?? '';
$instance = $post['instance'] ?? '';
$alertname = $post['alertname'] ?? '';
$dingtalk = $post['dingtalk_webhook'] ?? '';
$wecom = $post['wecom_webhook'] ?? '';
$mail = $post['mail_receiver'] ?? '';
$enable = intval($post['enable'] ?? 0);
// 判断是否已有记录
$check = mysqli_query($conn, "SELECT id FROM alert_subscribe WHERE uid = '$uid' LIMIT 1");
if (mysqli_num_rows($check) > 0) {
$sql = "UPDATE alert_subscribe SET severity=?,instance=?,alertname=?,dingtalk_webhook=?,wecom_webhook=?,mail_receiver=?,enable=? WHERE uid=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssis', $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable, $uid);
} else {
$sql = "INSERT INTO alert_subscribe(uid,severity,instance,alertname,dingtalk_webhook,wecom_webhook,mail_receiver,enable) VALUES (?,?,?,?,?,?,?,?)";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $uid, $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable);
}
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '订阅配置保存成功'], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode(['code' => 500, 'msg' => '保存失败:' . mysqli_error($conn)], JSON_UNESCAPED_UNICODE);
}
exit;
}
// 3、分页查询推送日志
if ($action === 'list_push_log') {
$page = isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
$pageSize = isset($_REQUEST['page_size']) ? intval($_REQUEST['page_size']) : 10;
$offset = ($page - 1) * $pageSize;
// 总条数
$cntSql = "SELECT COUNT(*) as total FROM alert_push_log WHERE uid = ?";
$stmtCnt = mysqli_prepare($conn, $cntSql);
mysqli_stmt_bind_param($stmtCnt, 's', $uid);
mysqli_stmt_execute($stmtCnt);
$cntRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCnt));
$total = $cntRow['total'];
// 分页数据
$sql = "SELECT * FROM alert_push_log WHERE uid = ? ORDER BY push_time DESC LIMIT ?,?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sii', $uid, $offset, $pageSize);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode([
'code' => 0,
'msg' => 'ok',
'data' => [
'list' => $list,
'page' => $page,
'page_size' => $pageSize,
'total' => $total,
'pages' => ceil($total / $pageSize)
]
], JSON_UNESCAPED_UNICODE);
exit;
}
// 无匹配操作
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
+14
View File
@@ -0,0 +1,14 @@
{
"theme_color": "#152c5b",
"card_bg": "#ffffff",
"body_bg": "#f9fafc",
"font_family": "Inter,PingFang SC,Microsoft YaHei",
"sidebar_links": [
{
"icon": "fa-github",
"title": "运维监控中心",
"url": "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s",
"target": "_blank"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"user_id": "admin001",
"username": "admin",
"role": "admin",
"password": "e10adc3949ba59abbe56e057f20f883e"
}
+35
View File
@@ -0,0 +1,35 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(["code"=>401,"msg"=>"未登录"]);
exit;
}
header("Content-Type:application/json;charset=utf-8");
$userFile = "./user_info.json";
if(!file_exists($userFile)){
echo json_encode(["code"=>500,"msg"=>"账号配置文件缺失"]);
exit;
}
$userData = json_decode(file_get_contents($userFile),true);
$act = $_POST['act'] ?? '';
if($act == "change_pwd"){
$old = $_POST['old_pwd'];
$new = $_POST['new_pwd'];
$confirm = $_POST['confirm_pwd'];
if($userData['password'] != md5($old)){
echo json_encode(["code"=>400,"msg"=>"原密码错误"]);
exit;
}
if($new != $confirm){
echo json_encode(["code"=>400,"msg"=>"两次新密码不一致"]);
exit;
}
$userData['password'] = md5($new);
file_put_contents($userFile,json_encode($userData,JSON_UNESCAPED_UNICODE));
echo json_encode(["code"=>0,"msg"=>"密码修改成功,请重新登录"]);
exit;
}
echo json_encode(["code"=>400,"msg"=>"非法操作"]);
?>
+64
View File
@@ -0,0 +1,64 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
header("Content-Type:application/json;charset=utf-8");
// 改成宿主机IP
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if (!$conn) {
echo json_encode([
"code" => 500,
"msg" => "数据库连接失败:" . mysqli_connect_error()
], JSON_UNESCAPED_UNICODE);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$rawBody = file_get_contents('php://input');
$alertData = json_decode($rawBody, true);
if (!is_array($alertData) || empty($alertData['alerts'])) {
echo json_encode([
"code" => 400,
"msg" => "未获取到有效告警数据"
], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
$insertCount = 0;
foreach ($alertData['alerts'] as $alert) {
$alertName = mysqli_real_escape_string($conn, $alert['labels']['alertname'] ?? "未知告警");
$instance = mysqli_real_escape_string($conn, $alert['labels']['instance'] ?? "");
$severity = mysqli_real_escape_string($conn, $alert['labels']['severity'] ?? "warning");
$status = $alert['status'] ?? "firing";
$startsAt = mysqli_real_escape_string($conn, $alert['startsAt'] ?? date("Y-m-d H:i:s"));
$endsAt = isset($alert['endsAt']) ? "'" . mysqli_real_escape_string($conn, $alert['endsAt']) . "'" : "NULL";
$fingerprint= mysqli_real_escape_string($conn, $alert['fingerprint'] ?? md5($alertName.$instance.$startsAt));
$content = mysqli_real_escape_string($conn, json_encode($alert, JSON_UNESCAPED_UNICODE));
$alertType = $status === "resolved" ? 2 : 1;
$checkSql = "SELECT id FROM alert_log WHERE mail_uid = '$fingerprint'";
$checkRes = mysqli_query($conn, $checkSql);
if (mysqli_num_rows($checkRes) > 0) continue;
$insertSql = "INSERT INTO alert_log
(mail_uid, alert_type, alert_name, instance, severity, starts_at, ends_at, content, receive_time)
VALUES ('$fingerprint', $alertType, '$alertName', '$instance', '$severity', '$startsAt', $endsAt, '$content', NOW())";
if (mysqli_query($conn, $insertSql)) $insertCount++;
}
echo json_encode([
"code" => 200,
"msg" => "告警处理完成",
"insert_num" => $insertCount
], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+528
View File
@@ -0,0 +1,528 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time']) > $expire) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleId = 0;
$roleSql = "SELECT r.is_admin, r.perm_list, u.role_id FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
$roleId = intval($roleRow['role_id'] ?? 0);
}
// 读取当前角色页面权限列表(用于分发工单权限判断)
$allowPages = [];
if ($roleId > 0) {
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = ?";
$stmtPerm = mysqli_prepare($connWork, $permSql);
mysqli_stmt_bind_param($stmtPerm, 'i', $roleId);
mysqli_stmt_execute($stmtPerm);
$pRes = mysqli_stmt_get_result($stmtPerm);
while ($p = mysqli_fetch_assoc($pRes)) {
$allowPages[] = $p['page_key'];
}
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 工单列表查询 =====================
if ($action === "get_list") {
$page = intval($_GET['page'] ?? 1);
$size = intval($_GET['size'] ?? 20);
$offset = ($page - 1) * $size;
$status = trim($_GET['status'] ?? '');
$keyword = trim($_GET['keyword'] ?? '');
$where = [];
$param = [];
$paramType = '';
// 全部用户查看全部工单,不再按uid过滤
$whereSql = "1=1";
if ($status !== '') {
$where[] = "status = ?";
$param[] = $status;
$paramType .= 'i';
}
if ($keyword !== '') {
$where[] = "(title LIKE ? OR content LIKE ?)";
$param[] = "%$keyword%";
$param[] = "%$keyword%";
$paramType .= 'ss';
}
if (!empty($where)) {
$whereSql = "WHERE " . implode(" AND ", $where);
}
// 分页列表,LEFT JOIN 认领人姓名
$listSql = "SELECT wo.*, uu.real_name claim_name
FROM work_order wo
LEFT JOIN sys_user uu ON wo.claim_uid = uu.uid
$whereSql ORDER BY create_time DESC LIMIT ?,?";
$param[] = $offset;
$param[] = $size;
$paramType .= 'ii';
$stmtList = mysqli_prepare($connWork, $listSql);
mysqli_stmt_bind_param($stmtList, $paramType, ...$param);
mysqli_stmt_execute($stmtList);
$res = mysqli_stmt_get_result($stmtList);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
// 总数统计
$countSql = "SELECT COUNT(id) total FROM work_order $whereSql";
$stmtCount = mysqli_prepare($connWork, $countSql);
array_pop($param);
array_pop($param);
$paramTypeCount = rtrim($paramType, 'ii');
if ($paramTypeCount) {
mysqli_stmt_bind_param($stmtCount, $paramTypeCount, ...$param);
}
mysqli_stmt_execute($stmtCount);
$countRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCount));
$total = intval($countRow['total']);
echo json_encode([
'code' => 0,
'list' => $list,
'total' => $total,
'page' => $page,
'size' => $size
], JSON_UNESCAPED_UNICODE);
exit;
}
// ===================== 根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
// 新建工单默认无认领人 claim_uid=''
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,claim_uid,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,?, '',NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// ========== 新增:工单认领接口 claim_work ==========
if ($action === "claim_work") {
$id = intval($post['id'] ?? 0);
$uid = trim($post['uid'] ?? '');
if ($id <= 0 || empty($uid)) {
echo json_encode(["code" => 400, "msg" => "参数错误"]);
exit;
}
// 校验工单未被认领
$chkSql = "SELECT id FROM work_order WHERE id=? AND (claim_uid IS NULL OR claim_uid = '')";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'i', $id);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if(mysqli_num_rows($chkRes) === 0){
echo json_encode(["code" => 400, "msg" => "该工单已被他人认领,无法重复认领"]);
exit;
}
$updateSql = "UPDATE work_order SET claim_uid=?, claim_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $updateSql);
mysqli_stmt_bind_param($stmt, 'si', $uid, $id);
$ok = mysqli_stmt_execute($stmt);
if($ok){
echo json_encode(["code" => 0, "msg" => "工单认领成功,现在你可以编辑处理"]);
}else{
echo json_encode(["code" => 500, "msg" => "认领失败:".mysqli_error($connWork)]);
}
exit;
}
// 3、编辑工单(权限:管理员 或 当前用户是认领人)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限重写
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND claim_uid=?";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'is', $id, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '仅工单认领人可编辑']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态(权限:管理员 或 当前用户是认领人)
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND claim_uid=?";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'is', $id, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '仅工单认领人可修改状态']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员/拥有system_manage权限角色 分发工单
if ($action === "assign") {
if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) {
echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 一键标记当前用户全部待处理工单为已读
if ($action === 'mark_all_read') {
$uid = $post['uid'] ?? '';
$sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, "s", $uid);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']);
exit;
}
// 语言切换
if ($action === 'save_lang') {
$lang = $post['lang'] ?? 'zh';
$sql = "UPDATE sys_config SET v=? WHERE k='lang'";
$stmt = mysqli_prepare($connConf, $sql);
mysqli_stmt_bind_param($stmt, "s", $lang);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '成功']);
exit;
}
// 同步Prometheus实时告警,自动清理恢复告警
if ($action === "sync_prom_alerts") {
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
$syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($action === "sync_alert_auto_close"){
$alertApi = "./index.php?act=active_firing";
$raw = @file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "_" . $item['instance'];
}
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($connWork, $sql);
while($row = mysqli_fetch_assoc($res)){
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($connWork, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+117
View File
@@ -0,0 +1,117 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 数据库本地连接,避免远程拦截
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error()
]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// 获取当前用户是否管理员
$isAdmin = 0;
$roleSql = "SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($conn, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) $isAdmin = intval($roleRow['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、新建工单(修复第50行引用报错)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$status = 1; // 单独定义变量,不能直接写1传入bind_param
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,status,create_time,update_time) VALUES (?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn, $sql);
// 全部使用变量传参,禁止直接写数字/字符串
mysqli_stmt_bind_param($stmt, 'ssssi', $title, $content, $loginUid, $assign, $status);
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '工单提交成功']);
} else {
echo json_encode(['code' => 500, 'msg' => '提交失败:' . mysqli_error($conn)]);
}
exit;
}
// 2、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
// 普通用户权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 3、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 无效操作
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
?>
+144
View File
@@ -0,0 +1,144 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 数据库本地连接
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error()
]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// 获取当前用户是否管理员
$isAdmin = 0;
$roleSql = "SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($conn, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) $isAdmin = intval($roleRow['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、新建工单
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,status,create_time,update_time) VALUES (?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssi', $title, $content, $loginUid, $assign, $status);
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '工单提交成功']);
} else {
echo json_encode(['code' => 500, 'msg' => '提交失败:' . mysqli_error($conn)]);
}
exit;
}
// 2、编辑工单(新增,支持修改标题、内容备注、分配人)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sssi', $title, $content, $assign, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单修改备注保存成功']);
exit;
}
// 3、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 4、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
?>
+146
View File
@@ -0,0 +1,146 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单数据库本地连接
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error()
]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// 判断管理员
$isAdmin = 0;
$roleSql = "SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($conn, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) $isAdmin = intval($roleRow['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、新建工单(新增relate_alert_id字段)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,status,create_time,update_time) VALUES (?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $loginUid, $assign, $relateAlertId, $status);
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '工单提交成功']);
} else {
echo json_encode(['code' => 500, 'msg' => '提交失败:' . mysqli_error($conn)]);
}
exit;
}
// 2、编辑工单(不修改关联告警ID)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sssi', $title, $content, $assign, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单修改备注保存成功']);
exit;
}
// 3、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 4、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 无匹配操作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($conn);
exit;
?>
+247
View File
@@ -0,0 +1,247 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = 'localhost';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、获取当前活跃告警(解决前端下拉空白)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(新增通知用户、通知渠道、关联告警)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
// 发送通知
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发']);
exit;
}
// 3、编辑工单(更新通知、不修改关联告警)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 通知发送统一函数
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 获取SMTP配置
$smtpRow = mysqli_fetch_assoc(mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1"));
$smtpConfig = $smtpRow ?? [];
// 获取工单详情
$orderRow = mysqli_fetch_assoc(mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId"));
$title = $orderRow['title'];
$content = $orderRow['content'];
// 遍历渠道发送
foreach ($channelArr as $ch) {
switch ($ch) {
case "mail":
sendMailNotify($smtpConfig, $userArr, $title, $content);
break;
case "dingtalk":
// 此处填入钉钉机器人发送逻辑,按需补充
break;
case "wecom":
// 此处填入企业微信机器人发送逻辑,按需补充
break;
}
}
}
// 邮件发送函数
function sendMailNotify($smtp, $uidList, $title, $content)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_user']) || empty($smtp['smtp_pass'])) return;
// 查询用户邮箱
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", $emailList);
$subject = "【工单通知】" . $title;
$body = "<h3>工单内容</h3><pre>".$content."</pre>";
// PHPMailer 发送逻辑,环境需安装phpmailer
/*
require_once 'PHPMailer.php';
require_once 'SMTP.php';
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $smtp['smtp_host'];
$mail->SMTPAuth = true;
$mail->Username = $smtp['smtp_user'];
$mail->Password = $smtp['smtp_pass'];
$mail->SMTPSecure = $smtp['smtp_ssl'] ? PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS : '';
$mail->Port = $smtp['smtp_port'];
$mail->setFrom($smtp['send_from'], $smtp['send_from']);
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->send();
} catch (Exception $e) {}
*/
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+267
View File
@@ -0,0 +1,267 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发']);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+378
View File
@@ -0,0 +1,378 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
header("Content-Type: application/json;charset=utf-8");
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新,无需额外调用
$syncWorkUrl = apiOrder . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+407
View File
@@ -0,0 +1,407 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
header("Content-Type: application/json;charset=utf-8");
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新,无需额外调用
$syncWorkUrl = apiOrder . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($_POST['action'] === "sync_alert_auto_close"){
// 1、拉取仪表盘实时未恢复告警
$alertApi = "./index.php?act=active_firing";
$raw = file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "|" . $item['instance'];
}
// 2、查询所有未完成、绑定告警的工单
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($conn, $sql);
while($row = mysqli_fetch_assoc($res)){
// 工单关联告警不在当前活跃列表=已恢复,自动设为完成
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($conn, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// 读取通知配置
$notifyRow = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_workorder_notify LIMIT 1"));
if(!$notifyRow || $notifyRow['notify_open'] != 1) return;
// 组装钉钉、企业微信请求,curl发送webhook
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+427
View File
@@ -0,0 +1,427 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
// 新增工单默认未读 is_read=0
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,0,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
// 修改状态自动标记已读
$sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
// 修复BUG:仅按工单唯一ID更新,不再使用标题模糊匹配,选中哪一行只修改该行工单
$sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 一键标记当前用户全部待处理工单为已读
if ($action === 'mark_all_read') {
$uid = $post['uid'] ?? '';
$sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, "s", $uid);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']);
exit;
}
// 语言切换
if ($action === 'save_lang') {
$lang = $post['lang'] ?? 'zh';
$sql = "UPDATE sys_config SET v=? WHERE k='lang'";
$stmt = mysqli_prepare($connConf, $sql);
mysqli_stmt_bind_param($stmt, "s", $lang);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新
$syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($action === "sync_alert_auto_close"){
// 1、拉取仪表盘实时未恢复告警
$alertApi = "./index.php?act=active_firing";
$raw = @file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "|" . $item['instance'];
}
// 2、查询所有未完成、绑定告警的工单
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($connWork, $sql);
while($row = mysqli_fetch_assoc($res)){
// 工单关联告警不在当前活跃列表=已恢复,自动设为完成
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($connWork, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+515
View File
@@ -0,0 +1,515 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time']) > $expire) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleId = 0;
$roleSql = "SELECT r.is_admin, r.perm_list, u.role_id FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
$roleId = intval($roleRow['role_id'] ?? 0);
}
// 读取当前角色页面权限列表(用于分发工单权限判断)
$allowPages = [];
if ($roleId > 0) {
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = ?";
$stmtPerm = mysqli_prepare($connWork, $permSql);
mysqli_stmt_bind_param($stmtPerm, 'i', $roleId);
mysqli_stmt_execute($stmtPerm);
$pRes = mysqli_stmt_get_result($stmtPerm);
while ($p = mysqli_fetch_assoc($pRes)) {
$allowPages[] = $p['page_key'];
}
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 工单列表查询(新增,解决数据可见权限隔离) =====================
if ($action === "get_list") {
$page = intval($_GET['page'] ?? 1);
$size = intval($_GET['size'] ?? 20);
$offset = ($page - 1) * $size;
$status = trim($_GET['status'] ?? '');
$keyword = trim($_GET['keyword'] ?? '');
$where = [];
$param = [];
$paramType = '';
// 普通用户:仅能查看自己创建 或 分配给自己的工单;管理员无限制查看全部
if ($isAdmin !== 1) {
$where[] = "(create_uid = ? OR assign_uid = ?)";
$param[] = $loginUid;
$param[] = $loginUid;
$paramType .= 'ss';
}
if ($status !== '') {
$where[] = "status = ?";
$param[] = $status;
$paramType .= 'i';
}
if ($keyword !== '') {
$where[] = "(title LIKE ? OR content LIKE ?)";
$param[] = "%$keyword%";
$param[] = "%$keyword%";
$paramType .= 'ss';
}
$whereSql = $where ? "WHERE " . implode(" AND ", $where) : "";
// 分页列表
$listSql = "SELECT * FROM work_order $whereSql ORDER BY create_time DESC LIMIT ?,?";
$param[] = $offset;
$param[] = $size;
$paramType .= 'ii';
$stmtList = mysqli_prepare($connWork, $listSql);
mysqli_stmt_bind_param($stmtList, $paramType, ...$param);
mysqli_stmt_execute($stmtList);
$res = mysqli_stmt_get_result($stmtList);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
// 总数统计
$countSql = "SELECT COUNT(id) total FROM work_order $whereSql";
$stmtCount = mysqli_prepare($connWork, $countSql);
array_pop($param);
array_pop($param);
$paramTypeCount = rtrim($paramType, 'ii');
if ($paramTypeCount) {
mysqli_stmt_bind_param($stmtCount, $paramTypeCount, ...$param);
}
mysqli_stmt_execute($stmtCount);
$countRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCount));
$total = intval($countRow['total']);
echo json_encode([
'code' => 0,
'list' => $list,
'total' => $total,
'page' => $page,
'size' => $size
], JSON_UNESCAPED_UNICODE);
exit;
}
// ===================== 根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
// 新增工单默认未读 is_read=0
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,0,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
// 修改状态自动标记已读
$sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员/拥有system_manage权限角色 分发工单
if ($action === "assign") {
// 修复:超级管理员 或 拥有system_manage页面权限均可分发工单
if ($isAdmin !== 1 && !in_array("system_manage", $allowPages)) {
echo json_encode(['code' => 403, 'msg' => '无工单分发权限,请联系管理员']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
// 修复BUG:仅按工单唯一ID更新,不再使用标题模糊匹配,选中哪一行只修改该行工单
$sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 一键标记当前用户全部待处理工单为已读
if ($action === 'mark_all_read') {
$uid = $post['uid'] ?? '';
$sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, "s", $uid);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']);
exit;
}
// 语言切换
if ($action === 'save_lang') {
$lang = $post['lang'] ?? 'zh';
$sql = "UPDATE sys_config SET v=? WHERE k='lang'";
$stmt = mysqli_prepare($connConf, $sql);
mysqli_stmt_bind_param($stmt, "s", $lang);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '成功']);
exit;
}
// 同步Prometheus实时告警,自动清理恢复告警
if ($action === "sync_prom_alerts") {
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新
$syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($action === "sync_alert_auto_close"){
// 1、拉取仪表盘实时未恢复告警
$alertApi = "./index.php?act=active_firing";
$raw = @file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "|" . $item['instance'];
}
// 2、查询所有未完成、绑定告警的工单
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($connWork, $sql);
while($row = mysqli_fetch_assoc($res)){
// 工单关联告警不在当前活跃列表=已恢复,自动设为完成
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($connWork, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
+104
View File
@@ -0,0 +1,104 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(json_encode(['code' => 0]));
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 数据库配置,优先本地localhost
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
// 关键:连接后立刻判断是否失败
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error() . ' 错误码:' . mysqli_connect_errno()
]);
exit;
}
// 只有连接成功才执行字符集
mysqli_set_charset($conn, 'utf8mb4');
// 下面原有业务代码不变 ...
// 获取角色
$isAdmin = 0;
$r = mysqli_query($conn,"SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid='$loginUid'");
if($rr = mysqli_fetch_assoc($r)) $isAdmin = intval($rr['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"),true) ?: [];
// 1、新建工单
if($action === "create"){
$title = $post['title'] ?? '';
$content = $post['content'] ?? '';
$assign = $post['assign_uid'] ?? '';
if(!$title || !$content){
echo json_encode(['code'=>400,'msg'=>'标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,status,create_time,update_time) VALUES (?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn,$sql);
mysqli_stmt_bind_param($stmt,'ssssi',$title,$content,$loginUid,$assign,1);
$ok = mysqli_stmt_execute($stmt);
if($ok) echo json_encode(['code'=>0,'msg'=>'工单提交成功']);
else echo json_encode(['code'=>500,'msg'=>'提交失败:'.mysqli_error($conn)]);
exit;
}
// 2、修改工单状态
if($action === "update_status"){
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if($id <=0){
echo json_encode(['code'=>400,'msg'=>'工单ID错误']);
exit;
}
// 权限校验:普通用户只能改自己创建/分配给自己的工单
if($isAdmin !== 1){
$chk = mysqli_query($conn,"SELECT id FROM work_order WHERE id=$id AND (create_uid='$loginUid' OR assign_uid='$loginUid')");
if(mysqli_num_rows($chk) === 0){
echo json_encode(['code'=>403,'msg'=>'无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn,$sql);
mysqli_stmt_bind_param($stmt,'ii',$status,$id);
mysqli_stmt_execute($stmt);
echo json_encode(['code'=>0,'msg'=>'状态更新成功']);
exit;
}
// 3、管理员分发工单
if($action === "assign_order"){
if($isAdmin !== 1){
echo json_encode(['code'=>403,'msg'=>'仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = $post['assign_uid'] ?? '';
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn,$sql);
mysqli_stmt_bind_param($stmt,'si',$assignUid,$id);
mysqli_stmt_execute($stmt);
echo json_encode(['code'=>0,'msg'=>'工单分发成功']);
exit;
}
echo json_encode(['code'=>400,'msg'=>'无效操作']);
mysqli_close($conn);
exit;
?>
+163
View File
@@ -0,0 +1,163 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
// 兼容低版本str_contains
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return $needle === '' || strpos($haystack, $needle) !== false;
}
}
$expireTime = 1800;
// 未登录直接跳转登录页
if (empty($_SESSION['user_id']) || empty($_SESSION['login_time']) || (time() - $_SESSION['login_time']) > $expireTime) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
// 登录用户基础信息
$loginUserId = $_SESSION['user_id'];
$userName = htmlspecialchars($_SESSION['username'] ?? '');
$isAdmin = intval($_SESSION['is_admin'] ?? 0);
$userRoleId = intval($_SESSION['role_id'] ?? 0);
// ====================== 双库固定连接 ======================
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
// 业务库 alert_mail_stat(工单、SMTP、大盘)
$dbAlertName = "alert_mail_stat";
$connAlert = mysqli_connect($dbHost, $dbUser, $dbPass, $dbAlertName);
if (!$connAlert) {
die("业务库alert_mail_stat连接失败:" . mysqli_connect_error());
}
mysqli_set_charset($connAlert, "utf8mb4");
// 权限库 monitor(用户、角色、权限、消息)
$dbMonitorName = "monitor";
$connMonitor = mysqli_connect($dbHost, $dbUser, $dbPass, $dbMonitorName);
if (!$connMonitor) {
die("权限库monitor连接失败:" . mysqli_connect_error());
}
mysqli_set_charset($connMonitor, "utf8mb4");
// ========================================================================
// ========== 新增:读取角色真实名称 ==========
$roleRealName = "未知角色";
if ($userRoleId > 0) {
$getRoleSql = "SELECT role_name FROM sys_role WHERE id = $userRoleId";
$roleResult = mysqli_query($connMonitor, $getRoleSql);
if ($roleResult && $roleRow = mysqli_fetch_assoc($roleResult)) {
$roleRealName = $roleRow['role_name'];
}
}
// 超级管理员强制覆盖文字
if ($isAdmin === 1) {
$roleRealName = "超级管理员";
}
// ===========================================
// 1. 登录时一次性加载当前角色全部权限,存入全局变量(侧边栏共用)
$allowPages = [];
if ($userRoleId > 0) {
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId";
$permRes = mysqli_query($connMonitor, $permSql);
if ($permRes) {
while ($row = mysqli_fetch_assoc($permRes)) {
$allowPages[] = $row['page_key'];
}
}
}
// 2. 获取当前访问页面文件名(去掉.php后缀)
$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php');
// 3. 非管理员自动拦截无权限页面(全局统一校验,无需每个页面重复写)
if ($isAdmin !== 1 && !in_array($currentPage, $allowPages)) {
echo "<script>alert('无访问权限!');history.back();</script>";
exit;
}
// 读取右上角未读消息数量
$unreadMsgCount = 0;
$msgSql = "SELECT COUNT(id) cnt FROM sys_user_msg WHERE user_id = $loginUserId AND is_read = 0";
$msgRes = mysqli_query($connMonitor, $msgSql);
if ($msgRes) {
$msgRow = mysqli_fetch_assoc($msgRes);
$unreadMsgCount = intval($msgRow['cnt']);
}
// 读取系统基础配置
$sysConfig = [];
$configSql = "SELECT k, v FROM sys_config";
$configRes = mysqli_query($connAlert, $configSql);
if ($configRes) {
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
}
$currentLang = $sysConfig['lang'] ?? "zh";
$sidebarRawLinks = $sysConfig['quick_link'] ?? "";
// 多语言字典
$langMap = [
'zh' => [
'html_lang' => 'zh-CN',
'page_title' => '运维监控平台',
'monitor_panel' => '监控面板',
'alert_overview' => '告警总览',
'quick_link' => '快捷外链',
'system_manage' => '系统管理',
'notify_config' => '消息通知配置',
'role_manage' => '角色管理',
'user_manage' => '用户管理',
'current_user' => '当前用户:',
'sys_time' => '系统时间:',
'refresh' => '刷新页面',
'logout' => '退出登录',
'unread_msg' => '未读消息'
],
'en' => [
'html_lang' => 'en',
'page_title' => "Monitor Platform",
'monitor_panel' => "Monitor Panel",
'alert_overview' => "Alert Dashboard",
'quick_link' => "Quick Link",
'system_manage' => "System Manage",
'notify_config' => "Notify Config",
'role_manage' => "Role Manage",
'user_manage' => "User Manage",
'current_user' => "User: ",
'sys_time' => "Time: ",
'refresh' => "Refresh",
'logout' => "Logout",
'unread_msg' => "Unread Msg"
]
];
$t = $langMap[$currentLang];
// 解析侧边栏外链
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line);
if (count($item) === 2) {
$sidebarLinkList[] = [
'name' => trim($item[0]),
'url' => trim($item[1])
];
}
}
}
// 【已删除冲突无用函数 hasPagePerm】
?>
+1094
View File
File diff suppressed because it is too large Load Diff
+954
View File
@@ -0,0 +1,954 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,与admin后台统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 读取全局系统配置(admin后台保存后同步生效)
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($connConfig, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($connConfig);
// 赋值全局配置
$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统");
$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff");
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = htmlspecialchars($sysConfig['footer_text'] ?? "");
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
// 解析侧边外链列表
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line, 2);
if (count($item) === 2) {
$sidebarLinkList[] = [
"name" => trim($item[0]),
"url" => trim($item[1])
];
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle; ?> - Advantest 爱德万测试 - 运维告警监控平台</title>
<!-- 本地静态资源,已移除CDN外网依赖 -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/echarts.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 日期选择框样式 */
#csvDatePicker{
padding:8px 10px;
border:1px solid #cbd5e1;
border-radius:8px;
font-size:14px;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后台配置快捷外链自动渲染 -->
<?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title">快捷外链</div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>')">
<i class="fa fa-external-link"></i>
<span><?php echo htmlspecialchars($link['name']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
<div class="menu-title">系统管理</div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i>
<span>后台配置管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="csvDatePicker">
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
</div>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 修复:使用相对路径,去除硬编码IP
const api = "./api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
if (!pageId) return;
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
const selectDate = $("#csvDatePicker").val().trim();
let exportUrl = api + "?act=export_csv";
if(selectDate){
exportUrl += "&date=" + encodeURIComponent(selectDate);
}
window.open(exportUrl, "_blank");
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
$.getJSON("./api/df.php").done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+707
View File
@@ -0,0 +1,707 @@
<!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>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="6">
<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/oldapi.php";
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、告警明细表格(已删除恢复时间列,不再渲染ends_at)
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="6">
<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 style="color:#6b7280">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(function() {
$("#table-body").html(`
<tr>
<td colspan="6">
<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");
window.location.href = api + "?act=export_csv";
setTimeout(()=>{
mask.hide();
},3000);
}
// 初始化
$(function() {
loadAllData();
// 每60秒自动刷新
setInterval(loadAllData, 60000);
});
</script>
</body>
</html>
+711
View File
@@ -0,0 +1,711 @@
<!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>
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>告警邮件统计看板</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:#f3f4f6;padding:20px;font-family:Microsoft Yahei}
.head{display:flex;gap:20px;margin-bottom:20px}
.card{background:#fff;padding:20px;border-radius:8px;width:24%;box-shadow:0 1px 3px #ddd}
.chart-box{display:flex;gap:20px;margin-bottom:20px}
.chart{width:50%;height:360px;background:#fff;padding:15px;border-radius:8px}
table{width:100%;background:#fff;border-radius:8px;padding:15px;border-collapse:collapse}
th,td{border:1px solid #eee;padding:10px;text-align:center}
th{background:#2f3542;color:#fff}
.type1{color:#e74c3c;font-weight:bold}
.type2{color:#27ae60;font-weight:bold}
</style>
</head>
<body>
<h1>当日告警邮件统计看板</h1>
<div class="head">
<div class="card">
<h3>今日触发告警总数</h3>
<p id="fire" style="font-size:32px;color:red">0</p>
</div>
<div class="card">
<h3>今日恢复告警总数</h3>
<p id="resolve" style="font-size:32px;color:green">0</p>
</div>
<div class="card">
<h3>涉及故障实例数</h3>
<p id="instance" style="font-size:32px;color:#2980b9">0</p>
</div>
</div>
<div class="chart-box">
<div class="chart" id="chart1"></div>
</div>
<table>
<thead>
<tr>
<th>类型</th>
<th>告警名称</th>
<th>实例</th>
<th>级别</th>
<th>故障开始</th>
<th>恢复时间</th>
<th>邮件接收时间</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<script>
// 改成你自己的服务器IP端口
const api = "http://10.150.117.190:8080/api";
// 1、顶部统计卡片
$.getJSON(api+"?act=day_total",res=>{
$("#fire").text(res.fire_count);
$("#resolve").text(res.resolve_count);
$("#instance").text(res.instance_num);
})
// 2、TOP告警柱状图
$.getJSON(api+"?act=top_alert",list=>{
let chart = echarts.init(document.getElementById("chart1"));
let name = [],cnt=[];
list.forEach(item=>{name.push(item.alert_name);cnt.push(item.cnt)})
chart.setOption({
title:{text:"今日告警次数TOP10"},
xAxis:{type:"category",data:name,axisLabel:{rotate:30}},
yAxis:{type:"value"},
series:[{type:"bar",data:cnt,color:"#3498db"}]
})
})
// 3、告警明细表格
$.getJSON(api+"?act=log_list",list=>{
let html = "";
list.forEach(row=>{
let typeText = row.alert_type == 1 ? "<span class='type1'>触发</span>" : "<span class='type2'>恢复</span>";
html += `<tr>
<td>${typeText}</td>
<td>${row.alert_name}</td>
<td>${row.instance}</td>
<td>${row.severity}</td>
<td>${row.starts_at || '-'}</td>
<td>${row.ends_at || '-'}</td>
<td>${row.receive_time}</td>
</tr>`
})
$("#table-body").html(html);
})
</script>
</body>
</html>
+663
View File
@@ -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>
+1193
View File
File diff suppressed because it is too large Load Diff
+1175
View File
File diff suppressed because it is too large Load Diff
+1193
View File
File diff suppressed because it is too large Load Diff
+601
View File
@@ -0,0 +1,601 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<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:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1800px;
margin:0 auto;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+899
View File
@@ -0,0 +1,899 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<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:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后续新增页面直接在这里复制模板
<div class="menu-title">系统管理</div>
<div class="menu-item" data-page="userPage">
<i class="fa fa-users"></i>
<span>用户权限管理</span>
</div>
<div class="menu-item" data-page="rulePage">
<i class="fa fa-cog"></i>
<span>监控规则管理</span>
</div>
-->
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
// 如果当前在磁盘页面同步刷新磁盘数据
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
$.getJSON("/api/df.php",{
headers:{"Accept":"application/json;charset=utf-8"}
}).done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
// 填充IP下拉框
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
// 渲染磁盘表格
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
// 磁盘导出CSV
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
// IP筛选切换
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+1066
View File
File diff suppressed because it is too large Load Diff
+916
View File
@@ -0,0 +1,916 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<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:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 日期选择框样式 */
#csvDatePicker{
padding:8px 10px;
border:1px solid #cbd5e1;
border-radius:8px;
font-size:14px;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后续新增页面直接在这里复制模板
<div class="menu-title">系统管理</div>
<div class="menu-item" data-page="userPage">
<i class="fa fa-users"></i>
<span>用户权限管理</span>
</div>
<div class="menu-item" data-page="rulePage">
<i class="fa fa-cog"></i>
<span>监控规则管理</span>
</div>
-->
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<!-- 新增日历日期选择框 + 导出按钮 -->
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="csvDatePicker">
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
</div>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
// 如果当前在磁盘页面同步刷新磁盘数据
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
// 已更新:支持日历选择日期导出
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
const selectDate = $("#csvDatePicker").val().trim();
let exportUrl = api + "?act=export_csv";
if(selectDate){
exportUrl += "&date=" + encodeURIComponent(selectDate);
}
window.open(exportUrl, "_blank");
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
$.getJSON("/api/df.php",{
headers:{"Accept":"application/json;charset=utf-8"}
}).done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
// 填充IP下拉框
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
// 渲染磁盘表格
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
// 磁盘导出CSV
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
// IP筛选切换
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+970
View File
@@ -0,0 +1,970 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,与admin后台统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 读取全局系统配置(admin后台保存后同步生效)
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($connConfig, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($connConfig);
// 赋值全局配置
$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统");
$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff");
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = htmlspecialchars($sysConfig['footer_text'] ?? "");
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
// 解析侧边外链列表
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line, 2);
if (count($item) === 2) {
$sidebarLinkList[] = [
"name" => trim($item[0]),
"url" => trim($item[1])
];
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle; ?> - Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<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:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 日期选择框样式 */
#csvDatePicker{
padding:8px 10px;
border:1px solid #cbd5e1;
border-radius:8px;
font-size:14px;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后台配置快捷外链自动渲染 -->
<?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title">快捷外链</div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>')">
<i class="fa fa-external-link"></i>
<span><?php echo htmlspecialchars($link['name']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 后续新增页面直接在这里复制模板
<div class="menu-title">系统管理</div>
<div class="menu-item" data-page="userPage">
<i class="fa fa-users"></i>
<span>用户权限管理</span>
</div>
<div class="menu-item" data-page="rulePage">
<i class="fa fa-cog"></i>
<span>监控规则管理</span>
</div>
-->
<div class="menu-title">系统管理</div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i>
<span>后台配置管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<!-- 新增日历日期选择框 + 导出按钮 -->
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="csvDatePicker">
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
</div>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 修复:使用相对路径,去除硬编码IP
const api = "./api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
if (!pageId) return;
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
// 如果当前在磁盘页面同步刷新磁盘数据
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
// 修复导出:相对路径、GET参数稳定兼容后端date接收
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
const selectDate = $("#csvDatePicker").val().trim();
let exportUrl = api + "?act=export_csv";
if(selectDate){
exportUrl += "&date=" + encodeURIComponent(selectDate);
}
window.open(exportUrl, "_blank");
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
// 修复:本地相对路径,移除headers跨域干扰
$.getJSON("./api/df.php").done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
// 填充IP下拉框
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
// 渲染磁盘表格
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
// 磁盘导出CSV
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
// IP筛选切换
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+1087
View File
File diff suppressed because it is too large Load Diff
+1126
View File
File diff suppressed because it is too large Load Diff
+1161
View File
File diff suppressed because it is too large Load Diff
+365
View File
@@ -0,0 +1,365 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = $_SESSION['username'];
$userRole = $_SESSION['role'];
?>
<!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/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f7f8fc;font-family:"Inter","PingFang SC","Microsoft YaHei";color:#1d2939;line-height:1.6}
.header{background:#152c5b;color:#fff;padding:18px 36px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 12px rgba(21,44,91,0.12)}
.header h1{font-size:22px;font-weight:600;display:flex;align-items:center;gap:12px}
.header h1 i{font-size:24px;color:#36d399}
.header-right{display:flex;align-items:center;gap:16px;font-size:14px}
.update-time{color:#cbd5e1;display:flex;align-items:center;gap:6px}
.refresh-btn,.export-btn{border:none;padding:8px 18px;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px}
.refresh-btn{background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2)}
.refresh-btn:hover{background:rgba(255,255,255,0.18)}
.export-btn{background:#10b981;color:#fff}
.export-btn:hover{background:#059669}
.mask{position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.45);display:none;z-index:999;align-items:center;justify-content:center}
.mask-box{background:#fff;padding:28px 36px;border-radius:10px;font-size:15px;display:flex;gap:12px;align-items:center;box-shadow:0 8px 30px rgba(0,0,0,0.15)}
.loading{display:inline-block;width:18px;height:18px;border:2px solid #e2e8f0;border-top-color:#152c5b;border-radius:50%;animation:spin 0.8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.container{padding:32px 36px;max-width:1700px;margin:0 auto}
.stat-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:24px;margin-bottom:32px}
.stat-card{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #eef2fb;transition:transform 0.24s,box-shadow 0.24s}
.stat-card:hover{transform:translateY(-4px);box-shadow:0 8px 24px rgba(21,44,91,0.1)}
.stat-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}
.stat-title{font-size:14px;color:#64748b}
.stat-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{font-size:40px;font-weight:700;color:#152c5b;letter-spacing:1px}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{font-size:12px;color:#94a3b8;margin-top:6px}
.chart-section{margin-bottom:32px}
.chart-card{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #eef2fb}
.chart-title-wrap{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;color:#152c5b;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid #eef2fb}
.chart-title-wrap i{color:#2563eb;font-size:20px}
#chart1{height:400px;width:100%}
.active-alert-section{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #fecdd3;margin-bottom:32px}
.table-header-wrap{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid #eef2fb}
.table-title-wrap{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;color:#152c5b}
.table-title-wrap i{color:#2563eb;font-size:20px}
.table-count{font-size:14px;color:#64748b}
.table-section{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #eef2fb;margin-bottom:32px}
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{background:#f8fafc;color:#475569;font-weight:600;padding:14px 16px;text-align:left;border-bottom:2px solid #e2e8f0;white-space:nowrap}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{padding:16px;border-bottom:1px solid #f1f5f9;color:#334155}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f8fafc}
tbody tr:nth-child(even){background:#fbfcfe}
.status-tag{display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border-radius:20px;font-size:13px;font-weight:500}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{display:inline-block;padding:4px 10px;border-radius:6px;font-size:13px;font-weight:500}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.empty-state{text-align:center;padding:70px 20px;color:#94a3b8;font-size:15px}
.empty-icon{font-size:52px;margin-bottom:16px;opacity:0.4}
@media (max-width:1400px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:768px){.header{padding:16px 20px;flex-direction:column;gap:14px;align-items:flex-start}.container{padding:20px}.stat-cards{grid-template-columns:1fr;gap:16px}}
</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><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-user-circle"></i>当前用户:<?php echo htmlspecialchars($userName);?><?php echo $userRole==='admin'?'管理员':'只读用户';?></span>
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
<button class="refresh-btn" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<div class="active-alert-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警
</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>今日全量告警明细列表
</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr><td>${t}</td><td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td><td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td><td>${getSeverityTag(row.severity)}</td><td style="color:#475569">${formatTime(row.receive_time)}</td></tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"15%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:28,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"46%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[5,5,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
// 退出登录跳转登录页销毁会话
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+638
View File
@@ -0,0 +1,638 @@
<!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/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 四类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 表格面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</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><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-clock-o"></i>最后更新:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Active Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Alerts Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 告警明细表格 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
// 顶部统计卡片
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("-");
});
}
// 柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", () => 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: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 告警表格(已调整字段:只展示接收时间=故障发生时间,删除原startsAt列)
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
});
}
// 顶部时间更新
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth()+1).padStart(2,'0');
const D = String(now.getDate()).padStart(2,'0');
const h = String(now.getHours()).padStart(2,'0');
const m = String(now.getMinutes()).padStart(2,'0');
const s = String(now.getSeconds()).padStart(2,'0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量加载
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// 导出CSV
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
});
</script>
</body>
</html>
+644
View File
@@ -0,0 +1,644 @@
<!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/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 四类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 表格面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</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><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<!-- 修改文字为系统实时时间 -->
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Active Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Alerts Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 告警明细表格 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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", () => 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: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 3、告警明细表格
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
});
}
// 实时更新时钟(每秒执行)
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量加载数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// ====================== 导出CSV函数 ======================
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// 初始化
$(function() {
loadAllData();
// 页面数据60秒刷新一次
setInterval(loadAllData, 60000);
// 实时时钟每秒刷新
setInterval(updateTime, 1000);
});
</script>
</body>
</html>
+776
View File
@@ -0,0 +1,776 @@
<!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/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 改为5列布局 */
.stat-cards {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 五类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.active .stat-icon {
background-color: #fef3c7;
color: #d97706;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-card.active .stat-value {
color: #d97706;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 活跃告警面板(新增) */
.active-alert-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #fecdd3;
margin-bottom: 32px;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
/* 历史全量告警面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
margin-bottom: 32px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1400px) {
.stat-cards {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</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><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 新增【当前活跃告警】 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<!-- 新增:当前未恢复活跃告警 -->
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 新增:当前未恢复活跃告警列表 -->
<div class="active-alert-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警
</div>
<div class="table-count"><span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 原有:今日全量告警明细列表 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>今日全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
let allAlertList = [];
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
// 筛选未恢复告警逻辑:同一条告警有触发无恢复则判定为活跃故障
function getUnresolvedAlert(list) {
// 建立指纹映射
let fireMap = new Map();
let resolveSet = new Set();
list.forEach(item => {
const key = `${item.alert_name}|${item.instance}`;
if(item.alert_type === 1){
fireMap.set(key, item);
}else{
resolveSet.add(key);
}
});
// 过滤只存在触发、无恢复记录的告警
let activeList = [];
fireMap.forEach((val, key) => {
if(!resolveSet.has(key)){
activeList.push(val);
}
});
return activeList;
}
// 渲染活跃未恢复告警表格
function renderActiveTable(activeList) {
$("#activeCount").text(activeList.length);
if (activeList.length === 0) {
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
activeList.forEach(row => {
const typeText = `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
$("#activeAlert").text(activeList.length);
}
// 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", () => 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: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 3、告警明细表格 + 同步计算活跃未恢复告警
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
allAlertList = list;
$("#totalCount").text(list.length);
// 计算并渲染当前未恢复告警
const activeList = getUnresolvedAlert(list);
renderActiveTable(activeList);
// 渲染今日全量明细
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
// 活跃表格同步清空
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>数据加载失败</div>
</div>
</td>
</tr>
`);
$("#activeAlert").text("-");
});
}
// 实时更新时钟(每秒执行)
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量加载数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// ====================== 导出CSV函数 ======================
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// 初始化
$(function() {
loadAllData();
// 页面数据60秒刷新一次
setInterval(loadAllData, 60000);
// 实时时钟每秒刷新
setInterval(updateTime, 1000);
});
</script>
</body>
</html>
+749
View File
@@ -0,0 +1,749 @@
<!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/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 改为5列布局 */
.stat-cards {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 五类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.active .stat-icon {
background-color: #fef3c7;
color: #d97706;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-card.active .stat-value {
color: #d97706;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 活跃告警面板(新增) */
.active-alert-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #fecdd3;
margin-bottom: 32px;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
/* 历史全量告警面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
margin-bottom: 32px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1400px) {
.stat-cards {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</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><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 新增【当前活跃告警】 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<!-- 新增:当前未恢复活跃告警 -->
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 新增:当前未恢复活跃告警列表 -->
<div class="active-alert-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警
</div>
<div class="table-count"><span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 原有:今日全量告警明细列表 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>今日全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
let activeAlertList = [];
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) 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>`;
}
// 加载全局未恢复活跃告警(跨天数据)
function loadActiveAlert() {
$.getJSON(api + "?act=active_firing", function(list) {
activeAlertList = list;
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if (list.length === 0) {
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(() => {
$("#activeAlert").text("-");
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>活跃告警数据加载失败</div>
</div>
</td>
</tr>
`);
});
}
// 顶部今日统计卡片
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("-");
});
}
// TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", () => 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: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 今日全量明细表格
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
});
}
// 实时更新时钟
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量刷新所有模块
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
}
// 导出CSV
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// 初始化
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
});
</script>
</body>
</html>
+246
View File
@@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务器磁盘目录容量监控 | Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", system-ui, -apple-system, sans-serif;
}
body {
background-color: #f4f7fa;
padding: 24px;
color: #2d3748;
}
.dashboard-card {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 16px rgba(0, 30, 80, 0.08);
padding: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 16px;
flex-wrap: wrap;
gap:16px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #1a202c;
}
.card-desc {
font-size: 13px;
color: #718096;
margin-top: 4px;
}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#exportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#exportBtn:hover{
background:#2c5282;
}
table {
width: 100%;
border-collapse: collapse;
margin-top:10px;
}
thead tr {
background-color: #2b6cb0;
color: #fff;
}
th, td {
padding: 14px 16px;
text-align: left;
font-size: 14px;
}
th {
font-weight: 500;
}
tbody tr {
border-bottom: 1px solid #edf2f7;
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f7fafc;
}
.num-big {
color: #c53030;
font-weight: 500;
}
.loading {
text-align: center;
padding: 40px;
color: #718096;
}
</style>
</head>
<body>
<div class="dashboard-card">
<div class="card-header">
<div>
<h2 class="card-title">用户目录磁盘占用统计</h2>
<div class="card-desc">多服务器 /serverhome 目录容量实时采集,支持按实例IP筛选、导出表格</div>
</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="exportBtn">导出当前表格CSV</button>
</div>
</div>
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="loading">数据加载中...</td>
</tr>
</tbody>
</table>
</div>
<script>
let sourceData = [];
let currentFilterIp = "all";
const ipSelect = document.getElementById('ipSelect');
const tbody = document.getElementById('diskTableBody');
const exportBtn = document.getElementById('exportBtn');
// 拉取全量数据
fetch('/api/df.php', {
headers: {
"Accept": "application/json;charset=utf-8"
}
})
.then(res => res.json())
.then(ret => {
if(ret.code !== 0){
tbody.innerHTML = `<tr><td colspan="4" class="loading">接口异常:${ret.msg}</td></tr>`;
return;
}
sourceData = ret.list;
if(sourceData.length === 0){
tbody.innerHTML = `<tr><td colspan="4" class="loading">暂无磁盘指标数据</td></tr>`;
return;
}
// 提取所有不重复IP写入下拉框
const ipSet = new Set();
sourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
let opt = document.createElement('option');
opt.value = ip;
opt.innerText = ip;
ipSelect.appendChild(opt);
})
// 初始渲染全部
renderTable("all");
})
.catch(err => {
tbody.innerHTML = `<tr><td colspan="4" class="loading">接口请求失败:${err}</td></tr>`;
})
// 筛选切换事件
ipSelect.addEventListener('change', function(){
currentFilterIp = this.value;
renderTable(currentFilterIp);
})
// 渲染表格
function renderTable(filterIp){
tbody.innerHTML = "";
let showList = [];
if(filterIp === "all"){
showList = sourceData;
}else{
showList = sourceData.filter(item=>item.instance_ip === filterIp);
}
if(showList.length === 0){
tbody.innerHTML = `<tr><td colspan="4" class="loading">该实例下无目录数据</td></tr>`;
return;
}
showList.forEach(row => {
let tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
`;
tbody.appendChild(tr);
})
}
// 导出CSV
exportBtn.addEventListener('click', function(){
let showList = [];
if(currentFilterIp === "all"){
showList = sourceData;
}else{
showList = sourceData.filter(item=>item.instance_ip === currentFilterIp);
}
if(showList.length === 0){
alert("无数据可导出");
return;
}
// CSV表头
let csvContent = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csvContent += line.join(",") + "\n";
});
// 下载文件
const blob = new Blob([csvContent], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "磁盘目录容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
</script>
</body>
</html>
+1469
View File
File diff suppressed because it is too large Load Diff
+1483
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
<?php
$raw = "Admin@2026";
$hash = password_hash($raw, PASSWORD_DEFAULT);
echo "哈希:" . $hash . "<br>";
var_dump(password_verify($raw, $hash));
?>
+3
View File
@@ -0,0 +1,3 @@
<?php
$rawPwd = "自定义密码";
echo password_hash($rawPwd, PASSWORD_DEFAULT);
+163
View File
@@ -0,0 +1,163 @@
<!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>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #152c5b;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-box {
background: #fff;
width: 420px;
padding: 40px 32px;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
}
.login-title {
text-align: center;
font-size: 24px;
color: #152c5b;
margin-bottom: 30px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.login-title i {
color: #36d399;
font-size: 26px;
}
.input-item {
margin-bottom: 22px;
}
.input-item label {
display: block;
color: #475569;
margin-bottom: 8px;
font-size: 14px;
}
.input-wrap {
position: relative;
}
.input-wrap i {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.input-wrap input {
width: 100%;
padding: 14px 14px 14px 42px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 15px;
transition: border 0.2s;
}
.input-wrap input:focus {
outline: none;
border-color: #152c5b;
}
.login-btn {
width: 100%;
padding: 14px;
background-color: #152c5b;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.24s;
margin-top: 8px;
}
.login-btn:hover {
background-color: #0f1e42;
}
.msg-tip {
text-align: center;
margin-top: 16px;
font-size: 14px;
display: none;
}
.error {
color: #dc2626;
}
.success {
color: #16a34a;
}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<div class="input-item">
<label>访问密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" id="pwd" placeholder="请输入访问密码" autocomplete="off">
</div>
</div>
<button class="login-btn" id="loginBtn">登录进入平台</button>
<div class="msg-tip" id="tip"></div>
</div>
<script>
$(function(){
// 自定义登录密码,自行修改
const PASSWORD = "hp93000";
const TOKEN_KEY = "alert_admin_token";
// 已有登录凭证直接跳转首页
if(localStorage.getItem(TOKEN_KEY)){
window.location.href = "dashboard6.0.html";
}
function showTip(text, type="error"){
const $tip = $("#tip");
$tip.text(text).removeClass("error success").addClass(type).show();
setTimeout(()=>$tip.hide(),2500);
}
$("#loginBtn").click(function(){
const inputPwd = $("#pwd").val().trim();
if(!inputPwd){
showTip("请输入访问密码");
return;
}
if(inputPwd !== PASSWORD){
showTip("密码错误,请重新输入");
$("#pwd").val("");
return;
}
// 登录成功
localStorage.setItem(TOKEN_KEY, "login_success");
showTip("登录成功,正在跳转...", "success");
setTimeout(()=>{
window.location.href = "dashboard5.0.html";
},800);
});
// 回车登录
$("#pwd").keydown(function(e){
if(e.keyCode === 13) $("#loginBtn").click();
})
})
</script>
</body>
</html>
+275
View File
@@ -0,0 +1,275 @@
<?php
session_start();
// 清除当前IP登录锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳转监控面板
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
// 数据库区分双库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
// 权限用户库 monitor(存放sys_user、sys_role、sys_role_permission
$dbMonitorName = "monitor";
// 业务库 alert_mail_stat(工单、SMTP配置、大盘数据)
$dbAlertName = "alert_mail_stat";
$errMsg = "";
// 5分钟5次错误锁定逻辑
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
}
// 登录提交处理
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请等待5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号与密码不能为空";
} else {
// 连接monitor权限库查询用户
$connMonitor = mysqli_connect($dbHost, $dbUser, $dbPass, $dbMonitorName);
if (!$connMonitor) {
$errMsg = "数据库连接失败:" . mysqli_connect_error();
} else {
mysqli_set_charset($connMonitor, "utf8mb4");
$u = mysqli_real_escape_string($connMonitor, $username);
// 适配真实表字段,新增is_admin查询
$sql = "SELECT id,username,password,role_id,is_admin,enable FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($connMonitor, $sql);
// 新增判断:查询成功才读取数据
if ($res) {
$user = mysqli_fetch_assoc($res);
// 先判断账号是否启用
if (!$user) {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
} elseif ($user['enable'] != 1) {
$errMsg = "该账号已被禁用,无法登录";
} elseif (password_verify($pwd, $user['password'])) {
// 登录成功逻辑
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['is_admin'] = $user['is_admin'];
$_SESSION['login_time'] = time();
mysqli_query($connMonitor, "UPDATE sys_user SET last_login=NOW() WHERE id=" . $user['id']);
mysqli_close($connMonitor);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
}
} else {
// 捕获SQL执行失败原因
$errMsg = "查询用户失败:" . mysqli_error($connMonitor);
}
mysqli_close($connMonitor);
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维监控登录</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
position: relative;
background: url("https://www.advantest.com/img/common/img-ogp.png") center center / cover no-repeat fixed;
display: flex;
align-items: center;
justify-content: center;
}
body::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.86);
z-index: 1;
}
.login-card {
position: relative;
z-index: 2;
width: 480px;
background: #ffffff;
padding: 60px 48px;
border-radius: 24px;
box-shadow: 0 12px 60px rgba(21, 44, 91, 0.08);
border: 1px solid rgba(21, 44, 91, 0.06);
backdrop-filter: blur(6px);
}
.brand-box {
text-align: center;
margin-bottom: 48px;
}
.brand-en {
font-size: 36px;
font-weight: 700;
letter-spacing: 4px;
color: #152c5b;
margin-bottom: 8px;
}
.brand-cn {
font-size: 16px;
color: #64748b;
letter-spacing: 1px;
}
.error-tip {
text-align: center;
padding: 10px;
background: #fef2f2;
color: #dc2626;
border-radius: 10px;
font-size: 14px;
margin-bottom: 24px;
}
.form-item {
margin-bottom: 26px;
}
.form-label {
display: block;
font-size: 14px;
color: #334155;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrap {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 16px;
}
.form-input {
width: 100%;
padding: 18px 18px 18px 52px;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
background: #f9fafc;
transition: all 0.25s ease;
}
.form-input:focus {
outline: none;
border-color: #152c5b;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(21, 44, 91, 0.08);
}
.login-submit {
width: 100%;
padding: 18px;
margin-top: 10px;
background: #152c5b;
color: #ffffff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.login-submit:hover {
background: #0f1e42;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.16);
}
.region-footer {
position: absolute;
z-index: 2;
right: 48px;
bottom: 36px;
font-size: 14px;
color: #152c5b;
font-weight: 500;
letter-spacing: 1px;
}
@media screen and (max-width: 540px) {
.login-card {
width: 92%;
padding: 48px 32px;
}
.brand-en {
font-size: 30px;
}
.region-footer {
right: 24px;
bottom: 20px;
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-box">
<div class="brand-en">Advantest</div>
<div class="brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
<?php if (!empty($errMsg)): ?>
<div class="error-tip"><?php echo $errMsg; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-item">
<label class="form-label">登录账号</label>
<div class="input-wrap">
<i class="fa fa-user input-icon"></i>
<input class="form-input" type="text" name="username" placeholder="请输入登录账号" autocomplete="off" required>
</div>
</div>
<div class="form-item">
<label class="form-label">登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock input-icon"></i>
<input class="form-input" type="password" name="pwd" placeholder="请输入登录密码" autocomplete="off" required>
</div>
</div>
<button class="login-submit" type="submit">登录进入系统</button>
</form>
</div>
<div class="region-footer">中国上海</div>
</body>
</html>
+278
View File
@@ -0,0 +1,278 @@
<?php
session_start();
// 清除当前IP登录锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳转监控面板
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
// 数据库区分双库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
// 权限用户库 monitor(存放sys_user、sys_role、sys_role_permission
$dbMonitorName = "monitor";
// 业务库 alert_mail_stat(工单、SMTP配置、大盘数据)
$dbAlertName = "alert_mail_stat";
$errMsg = "";
// 5分钟5次错误锁定逻辑
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
}
// 登录提交处理
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请等待5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号与密码不能为空";
} else {
// 连接monitor权限库查询用户
$connMonitor = mysqli_connect($dbHost, $dbUser, $dbPass, $dbMonitorName);
if (!$connMonitor) {
$errMsg = "数据库连接失败";
} else {
mysqli_set_charset($connMonitor, "utf8mb4");
$u = mysqli_real_escape_string($connMonitor, $username);
// 查询完整用户字段:id、账号、密码、角色ID、是否超级管理员
$sql = "SELECT id,username,password,role_id,is_admin FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($connMonitor, $sql);
$user = mysqli_fetch_assoc($res);
// 密码校验(password_hash加密存储)
if ($user && password_verify($pwd, $user['password'])) {
// 清空错误计数
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
// 写入鉴权必需session参数(适配auth.php
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['is_admin'] = $user['is_admin'];
$_SESSION['login_time'] = time();
// 更新最后登录时间(monitor库sys_user表)
mysqli_query($connMonitor, "UPDATE sys_user SET last_login=NOW() WHERE id=" . $user['id']);
mysqli_close($connMonitor);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
}
mysqli_close($connMonitor);
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维监控登录</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
position: relative;
/* 官方品牌大图全屏背景 */
background: url("https://www.advantest.com/img/common/img-ogp.png") center center / cover no-repeat fixed;
display: flex;
align-items: center;
justify-content: center;
}
/* 高透纯白遮罩,保留完整背景图,高级柔和不压抑 */
body::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.86);
z-index: 1;
}
/* 登录主卡片 高端质感 */
.login-card {
position: relative;
z-index: 2;
width: 480px;
background: #ffffff;
padding: 60px 48px;
border-radius: 24px;
box-shadow: 0 12px 60px rgba(21, 44, 91, 0.08);
border: 1px solid rgba(21, 44, 91, 0.06);
backdrop-filter: blur(6px);
}
/* 品牌标题区域 */
.brand-box {
text-align: center;
margin-bottom: 48px;
}
.brand-en {
font-size: 36px;
font-weight: 700;
letter-spacing: 4px;
color: #152c5b;
margin-bottom: 8px;
}
.brand-cn {
font-size: 16px;
color: #64748b;
letter-spacing: 1px;
}
/* 错误提示 */
.error-tip {
text-align: center;
padding: 10px;
background: #fef2f2;
color: #dc2626;
border-radius: 10px;
font-size: 14px;
margin-bottom: 24px;
}
/* 输入项通用 */
.form-item {
margin-bottom: 26px;
}
.form-label {
display: block;
font-size: 14px;
color: #334155;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrap {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 16px;
}
.form-input {
width: 100%;
padding: 18px 18px 18px 52px;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
background: #f9fafc;
transition: all 0.25s ease;
}
.form-input:focus {
outline: none;
border-color: #152c5b;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(21, 44, 91, 0.08);
}
/* 登录按钮 */
.login-submit {
width: 100%;
padding: 18px;
margin-top: 10px;
background: #152c5b;
color: #ffffff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.login-submit:hover {
background: #0f1e42;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.16);
}
/* 底部地区文字 */
.region-footer {
position: absolute;
z-index: 2;
right: 48px;
bottom: 36px;
font-size: 14px;
color: #152c5b;
font-weight: 500;
letter-spacing: 1px;
}
/* 移动端自适应 */
@media screen and (max-width: 540px) {
.login-card {
width: 92%;
padding: 48px 32px;
}
.brand-en {
font-size: 30px;
}
.region-footer {
right: 24px;
bottom: 20px;
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-box">
<div class="brand-en">Advantest</div>
<div class="brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
<?php if (!empty($errMsg)): ?>
<div class="error-tip"><?php echo $errMsg; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-item">
<label class="form-label">登录账号</label>
<div class="input-wrap">
<i class="fa fa-user input-icon"></i>
<input class="form-input" type="text" name="username" placeholder="请输入登录账号" autocomplete="off" required>
</div>
</div>
<div class="form-item">
<label class="form-label">登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock input-icon"></i>
<input class="form-input" type="password" name="pwd" placeholder="请输入登录密码" autocomplete="off" required>
</div>
</div>
<button class="login-submit" type="submit">登录进入系统</button>
</form>
</div>
<div class="region-footer">中国上海</div>
</body>
</html>
+259
View File
@@ -0,0 +1,259 @@
<?php
session_start();
// 清除当前IP登录锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳转监控面板
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
// 数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 5分钟5次错误锁定逻辑
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
}
// 登录提交处理
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请等待5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号与密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=" . $user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
}
mysqli_close($conn);
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维监控登录</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
position: relative;
/* 官方品牌大图全屏背景 */
background: url("https://www.advantest.com/img/common/img-ogp.png") center center / cover no-repeat fixed;
display: flex;
align-items: center;
justify-content: center;
}
/* 高透纯白遮罩,保留完整背景图,高级柔和不压抑 */
body::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.86);
z-index: 1;
}
/* 登录主卡片 高端质感 */
.login-card {
position: relative;
z-index: 2;
width: 480px;
background: #ffffff;
padding: 60px 48px;
border-radius: 24px;
box-shadow: 0 12px 60px rgba(21, 44, 91, 0.08);
border: 1px solid rgba(21, 44, 91, 0.06);
backdrop-filter: blur(6px);
}
/* 品牌标题区域 */
.brand-box {
text-align: center;
margin-bottom: 48px;
}
.brand-en {
font-size: 36px;
font-weight: 700;
letter-spacing: 4px;
color: #152c5b;
margin-bottom: 8px;
}
.brand-cn {
font-size: 16px;
color: #64748b;
letter-spacing: 1px;
}
/* 错误提示 */
.error-tip {
text-align: center;
padding: 10px;
background: #fef2f2;
color: #dc2626;
border-radius: 10px;
font-size: 14px;
margin-bottom: 24px;
}
/* 输入项通用 */
.form-item {
margin-bottom: 26px;
}
.form-label {
display: block;
font-size: 14px;
color: #334155;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrap {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 16px;
}
.form-input {
width: 100%;
padding: 18px 18px 18px 52px;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
background: #f9fafc;
transition: all 0.25s ease;
}
.form-input:focus {
outline: none;
border-color: #152c5b;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(21, 44, 91, 0.08);
}
/* 登录按钮 */
.login-submit {
width: 100%;
padding: 18px;
margin-top: 10px;
background: #152c5b;
color: #ffffff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.login-submit:hover {
background: #0f1e42;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.16);
}
/* 底部地区文字 */
.region-footer {
position: absolute;
z-index: 2;
right: 48px;
bottom: 36px;
font-size: 14px;
color: #152c5b;
font-weight: 500;
letter-spacing: 1px;
}
/* 移动端自适应 */
@media screen and (max-width: 540px) {
.login-card {
width: 92%;
padding: 48px 32px;
}
.brand-en {
font-size: 30px;
}
.region-footer {
right: 24px;
bottom: 20px;
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-box">
<div class="brand-en">Advantest</div>
<div class="brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
<?php if (!empty($errMsg)): ?>
<div class="error-tip"><?php echo $errMsg; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-item">
<label class="form-label">登录账号</label>
<div class="input-wrap">
<i class="fa fa-user input-icon"></i>
<input class="form-input" type="text" name="username" placeholder="请输入登录账号" autocomplete="off" required>
</div>
</div>
<div class="form-item">
<label class="form-label">登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock input-icon"></i>
<input class="form-input" type="password" name="pwd" placeholder="请输入登录密码" autocomplete="off" required>
</div>
</div>
<button class="login-submit" type="submit">登录进入系统</button>
</form>
</div>
<div class="region-footer">中国上海</div>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<?php
session_start();
// 清空本次IP登录错误锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳监控首页
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 限制5分钟最多5次错误密码,防暴力破解
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
// 超过5分钟重置计数
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
}
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
// 登录成功,清空错误计数,写入会话
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
// 更新最后登录时间
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=".$user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码错误";
}
mysqli_close($conn);
}
}
?>
<!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>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#152c5b;font-family:"PingFang SC","Microsoft YaHei";display:flex;align-items:center;justify-content:center;min-height:100vh}
.login-box{background:#fff;width:420px;padding:40px 32px;border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.2)}
.login-title{text-align:center;font-size:24px;color:#152c5b;margin-bottom:30px;display:flex;align-items:center;justify-content:center;gap:10px}
.login-title i{color:#36d399;font-size:26px}
.input-item{margin-bottom:22px}
.input-item label{display:block;color:#475569;margin-bottom:8px;font-size:14px}
.input-wrap{position:relative}
.input-wrap i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#94a3b8}
.input-wrap input{width:100%;padding:14px 14px 14px 42px;border:1px solid #e2e8f0;border-radius:8px;font-size:15px}
.input-wrap input:focus{outline:none;border-color:#152c5b}
.login-btn{width:100%;padding:14px;background:#152c5b;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-top:8px}
.login-btn:hover{background:#0f1e42}
.tip{text-align:center;margin-top:16px;font-size:14px}
.err{color:#dc2626}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<?php if($errMsg):?>
<div class="tip err"><?php echo $errMsg;?></div>
<?php endif;?>
<form method="post">
<div class="input-item">
<label>登录账号</label>
<div class="input-wrap">
<i class="fa fa-user"></i>
<input type="text" name="username" placeholder="请输入账号" autocomplete="off" required>
</div>
</div>
<div class="input-item">
<label>登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" name="pwd" placeholder="请输入密码" autocomplete="off" required>
</div>
</div>
<button class="login-btn" type="submit">登录进入平台</button>
</form>
</div>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<?php
session_start();
// 清空本次IP登录错误锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳监控首页
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 限制5分钟最多5次错误密码,防暴力破解
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
// 超过5分钟重置计数
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
}
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
// 登录成功,清空错误计数,写入会话
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
// 更新最后登录时间
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=".$user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码错误";
}
mysqli_close($conn);
}
}
?>
<!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>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#152c5b;font-family:"PingFang SC","Microsoft YaHei";display:flex;align-items:center;justify-content:center;min-height:100vh}
.login-box{background:#fff;width:420px;padding:40px 32px;border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.2)}
.login-title{text-align:center;font-size:24px;color:#152c5b;margin-bottom:30px;display:flex;align-items:center;justify-content:center;gap:10px}
.login-title i{color:#36d399;font-size:26px}
.input-item{margin-bottom:22px}
.input-item label{display:block;color:#475569;margin-bottom:8px;font-size:14px}
.input-wrap{position:relative}
.input-wrap i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#94a3b8}
.input-wrap input{width:100%;padding:14px 14px 14px 42px;border:1px solid #e2e8f0;border-radius:8px;font-size:15px}
.input-wrap input:focus{outline:none;border-color:#152c5b}
.login-btn{width:100%;padding:14px;background:#152c5b;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-top:8px}
.login-btn:hover{background:#0f1e42}
.tip{text-align:center;margin-top:16px;font-size:14px}
.err{color:#dc2626}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<?php if($errMsg):?>
<div class="tip err"><?php echo $errMsg;?></div>
<?php endif;?>
<form method="post">
<div class="input-item">
<label>登录账号</label>
<div class="input-wrap">
<i class="fa fa-user"></i>
<input type="text" name="username" placeholder="请输入账号" autocomplete="off" required>
</div>
</div>
<div class="input-item">
<label>登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" name="pwd" placeholder="请输入密码" autocomplete="off" required>
</div>
</div>
<button class="login-btn" type="submit">登录进入平台</button>
</form>
</div>
</body>
</html>
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
// 强制清空登录错误锁定(永远不会被锁)
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳监控首页
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 限制5分钟最多5次错误密码,防暴力破解
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
// 修复BUG:超过5分钟重置
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
}
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
// ===================== 调试输出(帮你定位问题) =====================
echo "<div style='background:#fff;padding:20px;'>";
echo "你输入的账号:" . $username . "<br>";
echo "你输入的密码:" . $pwd . "<br>";
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
if (!$conn) {
die("数据库连接失败:" . mysqli_connect_error());
}
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
echo "数据库查到的用户:" . ($user ? $user['username'] : "无此用户") . "<br>";
echo "数据库密码哈希:" . ($user ? $user['password'] : "") . "<br>";
$checkResult = $user ? password_verify($pwd, $user['password']) : false;
echo "密码校验结果:" . var_export($checkResult, true) . "<br>";
echo "</div>";
exit;
// ===================== 调试结束 =====================
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=".$user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码错误";
}
mysqli_close($conn);
}
}
?>
<!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>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#152c5b;font-family:"PingFang SC","Microsoft YaHei";display:flex;align-items:center;justify-content:center;min-height:100vh}
.login-box{background:#fff;width:420px;padding:40px 32px;border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.2)}
.login-title{text-align:center;font-size:24px;color:#152c5b;margin-bottom:30px;display:flex;align-items:center;justify-content:center;gap:10px}
.login-title i{color:#36d399;font-size:26px}
.input-item{margin-bottom:22px}
.input-item label{display:block;color:#475569;margin-bottom:8px;font-size:14px}
.input-wrap{position:relative}
.input-wrap i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#94a3b8}
.input-wrap input{width:100%;padding:14px 14px 14px 42px;border:1px solid #e2e8f0;border-radius:8px;font-size:15px}
.input-wrap input:focus{outline:none;border-color:#152c5b}
.login-btn{width:100%;padding:14px;background:#152c5b;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-top:8px}
.login-btn:hover{background:#0f1e42}
.tip{text-align:center;margin-top:16px;font-size:14px}
.err{color:#dc2626}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<?php if($errMsg):?>
<div class="tip err"><?php echo $errMsg;?></div>
<?php endif;?>
<form method="post">
<div class="input-item">
<label>登录账号</label>
<div class="input-wrap">
<i class="fa fa-user"></i>
<input type="text" name="username" placeholder="请输入账号" autocomplete="off" required>
</div>
</div>
<div class="input-item">
<label>登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" name="pwd" placeholder="请输入密码" autocomplete="off" required>
</div>
</div>
<button class="login-btn" type="submit">登录进入平台</button>
</form>
</div>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
<?php
session_start();
session_destroy();
header("Location: login.php");
exit;
+206
View File
@@ -0,0 +1,206 @@
<?php
session_start();
// 复用登录校验,和api统一
if(empty($_SESSION['user_id'])){
header("Location: login.php");
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>告警接口可视化控制面板</title>
<style>
*{box-sizing: border-box;margin:0;padding:0;font-family:Microsoft Yahei}
body{background:#f5f7fa;padding:20px}
.container{max-width:1400px;margin:0 auto}
h1{color:#333;margin-bottom:24px;font-size:22px}
.card{background:#fff;border-radius:8px;padding:16px;margin-bottom:16px;border:1px solid #e4e7ed}
.card h3{margin-bottom:12px;color:#409eff;font-size:16px}
.row{display:flex;gap:12px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
input{padding:8px 10px;border:1px solid #dcdfe6;border-radius:4px;width:260px}
button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;color:#fff;background:#409eff}
button.warn{background:#e6a23c}
button.danger{background:#f56c6c}
button.success{background:#67c23a}
#result{margin-top:20px;padding:16px;background:#1e1e1e;color:#fff;border-radius:6px;white-space:pre-wrap;min-height:200px;font-family:Consolas}
.tip{font-size:12px;color:#999}
</style>
</head>
<body>
<div class="container">
<h1>告警系统 - 接口可视化操作面板</h1>
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button onclick="req('clear_single_instance','c_rule','c_ins')">执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号不用手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="danger" onclick="req('offline_single_instance','off_rule','off_ins')">永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不恢复永远不告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="success" onclick="req('restore_single_instance','res_rule','res_ins')">恢复实例告警</button>
</div>
<div class="tip">放开单实例永久屏蔽状态</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="danger" onclick="reqSingle('clear_offline_rule','cr_rule')">下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="success" onclick="reqSingle('restore_rule','rr_rule')">恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="warn" onclick="req('delete_single_instance_log','del_rule','del_ins')">清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button onclick="simpleReq('sync_prom_alerts')">立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端不显示告警bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3>8. 数据查询接口(查看面板原始数据)</h3>
<div class="row">
<button onclick="simpleReq('day_total')">今日统计 day_total</button>
<button onclick="simpleReq('top_alert')">告警TOP10 top_alert</button>
<button onclick="simpleReq('log_list')">今日明细 log_list</button>
<button onclick="simpleReq('active_firing')">当前未恢复 active_firing</button>
</div>
</div>
<!-- 导出CSV报表 -->
<div class="card">
<h3>9. 导出今日告警CSV报表</h3>
<div class="row">
<button onclick="exportCsv()">下载报表 export_csv</button>
</div>
</div>
<!-- 结果输出区域 -->
<div class="card">
<h3>接口返回结果(JSON)</h3>
<div id="result">等待操作...</div>
</div>
</div>
<script>
// 关键修改:接口路径固定为 /api/index.php
const apiUrl = "/api/index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口(sync、day_total、active_firing等)
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV(新窗口直接下载)
function exportCsv(){
window.open(`${apiUrl}?act=export_csv`,"_blank");
}
</script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
[www]
user = www-data
group = www-data
listen = 0.0.0.0:9000
listen.mode = 0660
pm = dynamic
pm.max_children = 8
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 4
pm.max_requests = 1000
+20
View File
@@ -0,0 +1,20 @@
# 1. 系统编译工具
yum install gcc gcc-c++ python36-devel -y
# 2. 清理冲突socketio包
pip3 uninstall flask-socketio python-socketio eventlet greenlet -y
# 3. 安装基础依赖
pip3 install flask docker --user
# 4. docker权限
usermod -aG docker root
newgrp docker
# 5. 验证docker连通
python3 -c "import docker;cli=docker.DockerClient();print('Docker连接正常')"
# 6. 后台启动服务
nohup python3 /opt/alert_dashboard/www/api/docker_api.py > /opt/alert_dashboard/www/api/docker_api.log 2>&1 &
# 7. 防火墙放行端口
firewall-cmd --add-port=8090/tcp --permanent
firewall-cmd --reload
# 8. 查看运行状态
sleep 2
ps aux | grep docker_api.py
tail -n 20 /opt/alert_dashboard/www/api/docker_api.log
+218
View File
@@ -0,0 +1,218 @@
<?php require_once 'auth.php';
$pageTitle = $t['role_manage'];
// 新增/编辑角色提交
if ($_POST['act'] == "save_role") {
$roleName = trim($_POST['role_name']);
$roleId = intval($_POST['role_id'] ?? 0);
$pages = $_POST['page_perm'] ?? [];
if (empty($roleName)) {
echo "<script>alert('角色名称不能为空');history.back();</script>";
exit;
}
// 新增角色
if ($roleId == 0) {
mysqli_query($connConfig, "INSERT INTO sys_role(role_name) VALUES('$roleName')");
$roleId = mysqli_insert_id($connConfig);
} else {
// 更新角色名
mysqli_query($connConfig, "UPDATE sys_role SET role_name='$roleName' WHERE id=$roleId");
// 清空旧权限
mysqli_query($connConfig, "DELETE FROM sys_role_permission WHERE role_id=$roleId");
}
// 写入新权限
foreach ($pages as $page) {
$page = mysqli_real_escape_string($connConfig, $page);
mysqli_query($connConfig, "INSERT INTO sys_role_permission(role_id,page_key) VALUES($roleId,'$page')");
}
echo "<script>alert('保存成功');location.href='role_manage.php';</script>";
exit;
}
// 删除角色
if ($_GET['del']) {
$delId = intval($_GET['del']);
mysqli_query($connConfig, "DELETE FROM sys_role_permission WHERE role_id=$delId");
mysqli_query($connConfig, "DELETE FROM sys_role WHERE id=$delId");
header("Location: role_manage.php");
exit;
}
// 获取所有角色
$roleList = [];
$roleRes = mysqli_query($connConfig, "SELECT * FROM sys_role ORDER BY id DESC");
while ($r = mysqli_fetch_assoc($roleRes)) $roleList[] = $r;
// 全部页面权限列表
$allPages = [
['key'=>'dashboard','name'=>'告警总览'],
['key'=>'work_order','name'=>'工单系统'],
['key'=>'disk','name'=>'磁盘容量'],
['key'=>'history_query','name'=>'历史查询'],
['key'=>'smtp_config','name'=>'消息通知配置'],
['key'=>'role_manage','name'=>'角色管理'],
['key'=>'user_manage','name'=>'用户管理'],
];
// 编辑角色时读取已有权限
$editRole = ['id'=>0,'role_name'=>'','perms'=>[]];
if (!empty($_GET['edit'])) {
$eid = intval($_GET['edit']);
$er = mysqli_fetch_assoc(mysqli_query($connConfig, "SELECT * FROM sys_role WHERE id=$eid"));
if ($er) {
$editRole = $er;
$permRes = mysqli_query($connConfig, "SELECT page_key FROM sys_role_permission WHERE role_id=$eid");
while ($p = mysqli_fetch_assoc($permRes)) $editRole['perms'][] = $p['page_key'];
}
}
mysqli_close($connConfig);
?>
<!DOCTYPE html>
<html lang="<?php echo $t['html_lang']; ?>">
<head>
<meta charset="utf-8">
<title><?php echo $pageTitle; ?></title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{ background:#f8fafc; font-family:"Inter","PingFang SC","Microsoft YaHei",system-ui,sans-serif; color:#1e293b; line-height:1.55; display:flex; flex-direction:column; height:100vh; overflow:hidden; }
.wrap-main{display:flex;flex:1;overflow:hidden;}
.sidebar{ width:240px; background:#152c5b; color:#e2e8f0; flex-shrink:0; overflow-y:auto; }
.sidebar-brand{ padding:30px 24px; border-bottom:1px solid rgba(255,255,255,0.06); }
.sidebar-brand-en{ font-size:20px; font-weight:600; color:#fff; }
.sidebar-brand-cn{ font-size:12px; color:#a5b4fc; margin-top:6px; }
.sidebar-menu{padding:20px 0;}
.menu-title{ padding:10px 24px 6px; font-size:11px; color:#94a3b8; text-transform:uppercase; }
.menu-item{ display:flex; align-items:center; gap:12px; padding:13px 24px; cursor:pointer; font-size:14px; color:#cbd5e1; }
.menu-item i{ width:20px; text-align:center; }
.menu-item.active{ background:rgba(59,130,246,0.15); color:#fff; border-left:4px solid #3b82f6; }
.menu-item:hover:not(.active){ background:rgba(255,255,255,0.06); color:#fff; }
.main-content{ flex:1; overflow-y:auto; display:flex; flex-direction:column; }
.header{ background:#fff; border-bottom:1px solid #e2e8f0; padding:20px 40px; display:flex; justify-content:space-between; align-items:center; }
.header-left{display:flex;align-items:center;gap:14px;}
.header-brand-en{ font-size:24px; font-weight:700; color:#152c5b; }
.header-brand-cn{font-size:13px;color:#64748b;}
.header-right{display:flex;align-items:center;gap:20px;font-size:14px;}
.user-info,.time-info,.lang-box{display:flex;align-items:center;gap:6px;color:#475569;}
.lang-box select{padding:4px 8px;border:1px solid #cbd5e1;border-radius:6px;}
.msg-badge{ position:relative; cursor:pointer; }
.msg-badge span{ position:absolute; top:-6px; right:-6px; background:#ef4444; color:#fff; font-size:10px; border-radius:99px; min-width:16px; height:16px; text-align:center; line-height:16px; padding:0 4px; }
.btn-base{ border:none; padding:10px 24px; border-radius:999px; cursor:pointer; font-size:14px; font-weight:500; display:flex; align-items:center; gap:8px; transition:all 0.2s ease; }
.btn-outline{background:#fff;color:#152c5b;border:1px solid #cbd5e1;}
.btn-outline:hover{background:#f7f8fc;}
.btn-primary{background:linear-gradient(135deg,#1d4ed8,#2563eb);color:#fff;box-shadow:0 6px 18px rgba(37,99,235,.25);}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 10px 24px rgba(37,99,235,.32);}
.container{padding:36px 40px;flex:1;}
.base-card{background:#fff;border-radius:18px;padding:36px;border:1px solid #e5e7eb;box-shadow:0 1px 3px rgba(0,0,0,.04),0 8px 24px rgba(15,23,42,.04);max-width:900px;margin-bottom:30px;}
.title-wrap{display:flex;align-items:center;gap:12px;font-size:17px;font-weight:600;color:#152c5b;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid #f1f5f9;}
.title-wrap i{color:#2563eb;}
.form-row{margin-bottom:22px;}
.form-row label{display:block;margin-bottom:8px;font-size:13px;font-weight:600;color:#334155;letter-spacing:.2px;}
.form-row input{width:360px;height:46px;padding:0 16px;border:1px solid #d6dce5;border-radius:10px;font-size:14px;}
.check-group{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:10px;}
.check-item{display:flex;align-items:center;gap:8px;padding:10px 12px;border:1px solid #e2e8f0;border-radius:10px;}
table{width:100%;border-collapse:collapse;margin-top:20px;}
th,td{border:1px solid #e2e8f0;padding:14px;text-align:left;}
th{background:#f8fafc;font-weight:600;}
.operate a{margin-right:12px;color:#2563eb;}
.operate a.del{color:#ef4444;}
</style>
</head>
<body>
<div class="wrap-main">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn"><?php echo $pageTitle; ?></div>
</div>
<div class="sidebar-menu">
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
<div class="menu-item" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i><span><?php echo $t['alert_overview']; ?></span></div>
<div class="menu-item" onclick="location.href='work_order.php'"><i class="fa fa-ticket"></i><span><?php echo $t['work_order_mgr']; ?></span></div>
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>','_blank')"><i class="fa fa-external-link"></i><span><?php echo htmlspecialchars($link['name']); ?></span></div>
<?php endforeach; ?>
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
<div class="menu-item" onclick="location.href='smtp_config.php'"><i class="fa fa-bell"></i><span><?php echo $t['notify_config']; ?></span></div>
<div class="menu-item active"><i class="fa fa-user-secret"></i><span><?php echo $t['role_manage']; ?></span></div>
<div class="menu-item" onclick="location.href='user_manage.php'"><i class="fa fa-users"></i><span><?php echo $t['user_manage']; ?></span></div>
</div>
</div>
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn"><?php echo $pageTitle; ?></div>
</div>
</div>
<div class="header-right">
<span class="msg-badge" onclick="location.href='work_order.php'">
<i class="fa fa-bell-o"></i>
<?php if($unreadMsgCount>0):?><span><?php echo $unreadMsgCount; ?></span><?php endif;?>
</span>
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
<div class="lang-box">
<span><?php echo $t['lang_text']; ?></span>
<select id="langSwitch">
<option value="zh" <?php echo $currentLang=='zh'?'selected':''; ?>>中文</option>
<option value="en" <?php echo $currentLang=='en'?'selected':''; ?>>EN</option>
</select>
</div>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i><?php echo $t['refresh_all']; ?></button>
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i><?php echo $t['logout']; ?></button>
</div>
</div>
<div class="container">
<div class="base-card">
<div class="title-wrap"><i class="fa fa-user-secret"></i>新增/编辑角色</div>
<form method="post">
<input type="hidden" name="act" value="save_role">
<input type="hidden" name="role_id" value="<?php echo $editRole['id']; ?>">
<div class="form-row">
<label>角色名称</label>
<input name="role_name" value="<?php echo htmlspecialchars($editRole['role_name']); ?>" placeholder="如:运维人员">
</div>
<div class="form-row">
<label>页面权限(勾选可访问页面)</label>
<div class="check-group">
<?php foreach ($allPages as $p): ?>
<div class="check-item">
<input type="checkbox" name="page_perm[]" value="<?php echo $p['key']; ?>" id="p_<?php echo $p['key']; ?>"
<?php echo in_array($p['key'],$editRole['perms'])?'checked':''; ?>>
<label for="p_<?php echo $p['key']; ?>"><?php echo $p['name']; ?></label>
</div>
<?php endforeach; ?>
</div>
</div>
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i>保存角色</button>
</form>
</div>
<div class="base-card">
<div class="title-wrap"><i class="fa fa-list"></i>角色列表</div>
<table>
<tr>
<th>ID</th>
<th>角色名称</th>
<th>操作</th>
</tr>
<?php foreach ($roleList as $r): ?>
<tr>
<td><?php echo $r['id']; ?></td>
<td><?php echo htmlspecialchars($r['role_name']); ?></td>
<td class="operate">
<a href="?edit=<?php echo $r['id']; ?>">编辑</a>
<a href="?del=<?php echo $r['id']; ?>" class="del" onclick="return confirm('确认删除?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</div>
</div>
<script>
function updateNowTime(){const d=new Date();let Y=d.getFullYear(),M=String(d.getMonth()+1).padStart(2,'0'),D=String(d.getDate()).padStart(2,'0'),h=String(d.getHours()).padStart(2,'0'),m=String(d.getMinutes()).padStart(2,'0'),s=String(d.getSeconds()).padStart(2,'0');$("#nowTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);}
setInterval(updateNowTime,1000);updateNowTime();
$("#langSwitch").change(function(){let l=$(this).val();$.post("role_manage.php",{action:"save_lang",lang:l},()=>location.reload());})
</script>
</body>
</html>
+536
View File
@@ -0,0 +1,536 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
require_once 'auth.php';
// 兼容低PHP版本 str_contains
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return $needle === '' || strpos($haystack, $needle) !== false;
}
}
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRoleId = $_SESSION['role_id'];
// 全局配置库连接
$dbConfigHost = "10.150.117.190";
$dbConfigUser = "root";
$dbConfigPwd = "hp93000";
$dbConfigName = "alert_mail_stat";
$connConfig = mysqli_connect($dbConfigHost, $dbConfigUser, $dbConfigPwd, $dbConfigName);
if(!$connConfig){
die("配置库连接失败");
}
mysqli_set_charset($connConfig, "utf8mb4");
// 自动创建SMTP配置表,不存在则新建
mysqli_query($connConfig, "CREATE TABLE IF NOT EXISTS sys_smtp_config (
id INT PRIMARY KEY AUTO_INCREMENT,
smtp_host VARCHAR(200) NOT NULL DEFAULT '',
smtp_port INT NOT NULL DEFAULT 25,
smtp_ssl TINYINT NOT NULL DEFAULT 0,
smtp_user VARCHAR(200) NOT NULL DEFAULT '',
smtp_pass VARCHAR(200) NOT NULL DEFAULT '',
send_from VARCHAR(200) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$checkRow = mysqli_fetch_assoc(mysqli_query($connConfig, "SELECT id FROM sys_smtp_config LIMIT 1"));
if (empty($checkRow)) {
mysqli_query($connConfig, "INSERT INTO sys_smtp_config (id) VALUES (1)");
}
// 自动创建工单推送通知表
mysqli_query($connConfig, "CREATE TABLE IF NOT EXISTS sys_workorder_notify (
id INT PRIMARY KEY AUTO_INCREMENT,
dingtalk_webhook VARCHAR(500) NOT NULL DEFAULT '',
dingtalk_secret VARCHAR(200) NOT NULL DEFAULT '',
wecom_webhook VARCHAR(500) NOT NULL DEFAULT '',
wecom_mention VARCHAR(300) NOT NULL DEFAULT '',
notify_open TINYINT NOT NULL DEFAULT 0,
update_time DATETIME NOT NULL DEFAULT NOW()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
// 安全读取SMTP配置,杜绝mysqli警告
$smtpRow = [];
$sqlSmtp = "SELECT * FROM sys_smtp_config WHERE id=1";
if (!empty(trim($sqlSmtp))) {
$smtpQuery = mysqli_query($connConfig, $sqlSmtp);
if ($smtpQuery !== false && $smtpQuery instanceof mysqli_result) {
$smtpRow = mysqli_fetch_assoc($smtpQuery) ?: [];
}
}
$smtpConfig = $smtpRow ?? [];
// 安全读取推送通知配置
$notifyConfig = [];
$notifyQuery = mysqli_query($connConfig, "SELECT * FROM sys_workorder_notify LIMIT 1");
if ($notifyQuery instanceof mysqli_result) {
$notifyConfig = mysqli_fetch_assoc($notifyQuery) ?: [];
}
$pageTitle = htmlspecialchars($sysConfig['page_subtitle'] ?? "工单消息通知配置");
$currentLang = $sysConfig['lang'] ?? "zh";
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
mysqli_close($connConfig);
// 多语言词典(菜单文字全部替换:SMTP配置 → 工单消息通知配置)
$langMap = [
'zh' => [
'html_lang' => 'zh-CN',
'page_subtitle' => '工单消息通知配置',
'monitor_panel' => '监控面板',
'alert_overview' => '告警总览',
'quick_link' => '快捷外链',
'system_manage' => '系统管理',
'notify_config' => '工单消息通知配置',
'current_user' => '当前用户:',
'sys_time' => '系统时间:',
'refresh_all' => '刷新页面',
'export_csv' => '导出CSV',
'logout' => '退出登录',
'lang_text' => '语言:',
'work_order_mgr' => '工单系统',
'save_all' => '保存全部通知配置',
'card_title' => '工单消息推送 & 发件邮箱统一配置',
'group_mail' => '邮箱SMTP发件设置',
'smtp_host' => 'SMTP服务器',
'smtp_port' => '端口',
'smtp_ssl' => '启用SSL/TLS加密',
'smtp_account' => '登录账号',
'smtp_pwd' => '授权密码',
'smtp_from' => '发件人邮箱',
'group_push' => '工单变更推送配置',
'notify_switch' => '开启工单变更自动推送',
'dingtalk_webhook' => '钉钉Webhook地址',
'dingtalk_secret' => '钉钉安全密钥(可选)',
'wecom_webhook' => '企业微信Webhook地址',
'wecom_mention' => '@提醒人员(多个逗号分隔,all=全员)',
'placeholder_webhook' => '输入完整Webhook链接',
'placeholder_secret' => '无安全校验则留空',
'placeholder_mention' => '填写手机号,all代表所有人',
'save_success' => '配置保存成功'
],
'en' => [
'html_lang' => 'en',
'page_subtitle' => 'Work Order Notification Config',
'monitor_panel' => 'Monitor Panel',
'alert_overview' => 'Alert Overview',
'quick_link' => 'Quick Links',
'system_manage' => 'System Manage',
'notify_config' => 'Work Order Notification Config',
'current_user' => 'User: ',
'sys_time' => 'System Time: ',
'refresh_all' => 'Refresh',
'export_csv' => 'Export CSV',
'logout' => 'Logout',
'lang_text' => 'Lang: ',
'work_order_mgr' => 'Work Order System',
'save_all' => 'Save All Notification Config',
'card_title' => 'Work Order Push & Mail Sender Config',
'group_mail' => 'SMTP Mail Sender',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'Port',
'smtp_ssl' => 'Enable SSL/TLS',
'smtp_account' => 'Account',
'smtp_pwd' => 'Auth Password',
'smtp_from' => 'Sender Email',
'group_push' => 'Work Order Push Setting',
'notify_switch' => 'Auto push on order change',
'dingtalk_webhook' => 'DingTalk Webhook',
'dingtalk_secret' => 'DingTalk Secret (Optional)',
'wecom_webhook' => 'WeCom Webhook',
'wecom_mention' => 'Mention user (split by comma, all=everyone)',
'placeholder_webhook' => 'Full webhook url',
'placeholder_secret' => 'Leave empty if no secret',
'placeholder_mention' => 'Mobile number or all',
'save_success' => 'Config saved successfully'
]
];
$t = $langMap[$currentLang];
// 解析外链菜单
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line, 2);
if (count($item) === 2) {
$sidebarLinkList[] = ["name" => trim($item[0]), "url" => trim($item[1])];
}
}
}
// 统一保存表单提交逻辑(SMTP+推送一次保存)
if ($_POST['act'] === "save_all") {
// 更新SMTP邮箱配置
$host = trim($_POST['smtp_host'] ?? '');
$port = intval($_POST['smtp_port'] ?? 25);
$ssl = intval($_POST['smtp_ssl'] ?? 0);
$user = trim($_POST['smtp_user'] ?? '');
$pwd = trim($_POST['smtp_pass'] ?? '');
$from = trim($_POST['smtp_from'] ?? '');
mysqli_query($connConfig, "UPDATE sys_smtp_config SET smtp_host='$host',smtp_port=$port,smtp_ssl=$ssl,smtp_user='$user',smtp_pass='$pwd',send_from='$from' WHERE id=1");
// 更新工单推送配置
$ding_webhook = trim($_POST['dingtalk_webhook'] ?? '');
$ding_secret = trim($_POST['dingtalk_secret'] ?? '');
$wecom_webhook = trim($_POST['wecom_webhook'] ?? '');
$wecom_mention = trim($_POST['wecom_mention'] ?? '');
$notify_open = intval($_POST['notify_open'] ?? 0);
$existNotify = mysqli_fetch_assoc(mysqli_query($connConfig, "SELECT id FROM sys_workorder_notify LIMIT 1"));
if ($existNotify) {
mysqli_query($connConfig, "UPDATE sys_workorder_notify SET dingtalk_webhook='$ding_webhook', dingtalk_secret='$ding_secret', wecom_webhook='$wecom_webhook', wecom_mention='$wecom_mention', notify_open=$notify_open, update_time=NOW() WHERE id = {$existNotify['id']}");
} else {
mysqli_query($connConfig, "INSERT INTO sys_workorder_notify (dingtalk_webhook,dingtalk_secret,wecom_webhook,wecom_mention,notify_open) VALUES ('$ding_webhook','$ding_secret','$wecom_webhook','$wecom_mention',$notify_open)");
}
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
?>
<!DOCTYPE html>
<html lang="<?php echo $t['html_lang']; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle; ?></title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{ background:#f8fafc; font-family:"Inter","PingFang SC","Microsoft YaHei",system-ui,sans-serif; color:#1e293b; line-height:1.55; display:flex; flex-direction:column; height:100vh; overflow:hidden; }
.wrap-main{display:flex;flex:1;overflow:hidden;}
/* 侧边栏 深蓝导航 */
.sidebar{ width:240px; background:#152c5b; color:#e2e8f0; flex-shrink:0; overflow-y:auto; }
.sidebar-brand{ padding:30px 24px; border-bottom:1px solid rgba(255,255,255,0.06); }
.sidebar-brand-en{ font-size:20px; font-weight:600; letter-spacing:0.5px; color:#ffffff; }
.sidebar-brand-cn{ font-size:12px; color:#a5b4fc; margin-top:6px; opacity:0.9; }
.sidebar-menu{padding:20px 0;}
.menu-title{ padding:10px 24px 6px; font-size:11px; color:#94a3b8; letter-spacing:0.5px; text-transform:uppercase; }
.menu-item{ display:flex; align-items:center; gap:12px; padding:13px 24px; cursor:pointer; font-size:14px; transition:background 0.2s ease; color:#cbd5e1; }
.menu-item i{ font-size:16px; width:20px; text-align:center; }
.menu-item.active{ background:rgba(59,130,246,0.15); color:#ffffff; border-left:4px solid #3b82f6; }
.menu-item:hover:not(.active){ background:rgba(255,255,255,0.06); color:#ffffff; }
/* 顶部Header */
.main-content{ flex:1; overflow-y:auto; display:flex; flex-direction:column; }
.header{ background:#ffffff; border-bottom:1px solid #e2e8f0; padding:20px 40px; display:flex; justify-content:space-between; align-items:center; flex-shrink:0; }
.header-left{display:flex;align-items:center;gap:14px;}
.header-brand-en{ font-size:24px; font-weight:700; color:#152c5b; letter-spacing:1px; }
.header-brand-cn{font-size:13px;color:#64748b;}
.header-right{display:flex;align-items:center;gap:20px;font-size:14px;}
.user-info,.time-info,.lang-box{color:#475569;display:flex;align-items:center;gap:6px;}
.lang-box select{padding:4px 8px;border:1px solid #cbd5e1;border-radius:6px;}
/* 通用按钮基础 */
.btn-base{ border:none; padding:10px 24px; border-radius:999px; cursor:pointer; font-size:14px; font-weight:500; display:flex; align-items:center; gap:8px; transition:all 0.2s ease; }
.btn-outline{
background:#fff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
}
.container{padding:36px 40px;flex:1;}
/* ====================== 四、升级卡片样式 ====================== */
.base-card{
background:#fff;
border-radius:18px;
padding:36px;
border:1px solid #e5e7eb;
box-shadow:
0 1px 3px rgba(0,0,0,.04),
0 8px 24px rgba(15,23,42,.04);
max-width:900px;
}
.title-wrap{ display:flex; align-items:center; gap:12px; font-size:17px; font-weight:600; color:#152c5b; margin-bottom:26px; padding-bottom:16px; border-bottom:1px solid #f1f5f9; }
.title-wrap i{font-size:18px;color:#2563eb;}
.group-title{ font-size:15px; font-weight:600; color:#334155; margin:32px 0 18px; padding-bottom:10px; border-bottom:1px solid #f1f5f9; }
.group-title:first-of-type{margin-top:0;}
/* ====================== 一、企业级输入框统一样式 ====================== */
.form-row{
margin-bottom:22px;
}
.form-row label{
display:block;
margin-bottom:8px;
font-size:13px;
font-weight:600;
color:#334155;
letter-spacing:.2px;
}
/* 全部文本输入框统一短款360px宽度 */
.form-row input.short-width{
width:360px;
height:46px;
padding:0 16px;
border:1px solid #d6dce5;
border-radius:10px;
background:#ffffff;
font-size:14px;
color:#1e293b;
transition:all .2s ease;
}
.form-row input[type="text"]:hover,
.form-row input[type="password"]:hover,
.form-row input[type="number"]:hover{
border-color:#94a3b8;
}
.form-row input[type="text"]:focus,
.form-row input[type="password"]:focus,
.form-row input[type="number"]:focus{
outline:none;
border-color:#2563eb;
box-shadow:0 0 0 3px rgba(37,99,235,.12);
background:#fff;
}
.form-row input::placeholder{
color:#94a3b8;
font-size:13px;
}
.form-row input:disabled{
background:#f8fafc;
color:#94a3b8;
cursor:not-allowed;
}
/* 数字框去除上下箭头 */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button{
-webkit-appearance:none;
margin:0;
}
input[type="number"]{
-moz-appearance:textfield;
}
/* 勾选框容器,通栏100%宽度 */
.checkbox-input-box{
display:flex;
align-items:center;
gap:10px;
width:100%;
height:46px;
padding:0 16px;
border:1px solid #d6dce5;
border-radius:10px;
background:#fff;
transition:all .2s ease;
}
.checkbox-input-box:hover{
border-color:#94a3b8;
}
.checkbox-input-box input[type="checkbox"]{
width:18px;
height:18px;
accent-color:#2563eb;
cursor:pointer;
}
.checkbox-input-box label{
margin:0;
cursor:pointer;
font-size:14px;
color:#334155;
}
/* ====================== 三、主保存渐变按钮升级 ====================== */
.btn-primary{
background:linear-gradient(135deg,#1d4ed8,#2563eb);
color:#fff;
min-width:220px;
justify-content:center;
box-shadow:0 6px 18px rgba(37,99,235,.25);
}
.btn-primary:hover{
transform:translateY(-1px);
box-shadow:0 10px 24px rgba(37,99,235,.32);
background:linear-gradient(135deg,#1d4ed8,#2563eb);
}
.form-submit{margin-top:16px;}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 侧边栏菜单:工单消息通知配置放入系统管理分组内 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
<div class="sidebar-menu">
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
<!-- 1.告警总览 -->
<div class="menu-item" onclick="location.href='dashboard.php'">
<i class="fa fa-line-chart"></i><span><?php echo $t['alert_overview']; ?></span>
</div>
<!-- 2.工单系统 -->
<div class="menu-item" onclick="location.href='work_order.php'">
<i class="fa fa-ticket"></i><span><?php echo $t['work_order_mgr']; ?></span>
</div>
<!-- 快捷外链 -->
<?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>','_blank')">
<i class="fa fa-external-link"></i><span><?php echo htmlspecialchars($link['name']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 系统管理分组,工单通知配置放这里 -->
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
<div class="menu-item active">
<i class="fa fa-bell"></i><span><?php echo $t['notify_config']; ?></span>
</div>
</div>
</div>
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?><?php echo $roleRealName; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
<div class="lang-box">
<span><?php echo $t['lang_text']; ?></span>
<select id="langSwitch">
<option value="zh" <?php echo $currentLang==='zh'?'selected':''; ?>>中文</option>
<option value="en" <?php echo $currentLang==='en'?'selected':''; ?>>EN</option>
</select>
</div>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i><?php echo $t['refresh_all']; ?></button>
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i><?php echo $t['logout']; ?></button>
</div>
</div>
<div class="container">
<!-- 配置卡片 -->
<div class="base-card">
<div class="title-wrap"><i class="fa fa-bell"></i><?php echo $t['card_title']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_all">
<!-- 邮箱SMTP配置组 -->
<div class="group-title"><?php echo $t['group_mail']; ?></div>
<div class="form-row">
<label><?php echo $t['smtp_host']; ?></label>
<input class="short-width" name="smtp_host" value="<?php echo htmlspecialchars($smtpConfig['smtp_host'] ?? ''); ?>" placeholder="smtp.company.com">
</div>
<div class="form-row">
<label><?php echo $t['smtp_port']; ?></label>
<input class="short-width" name="smtp_port" type="number" value="<?php echo intval($smtpConfig['smtp_port'] ?? 25); ?>" min="1" max="65535">
</div>
<!-- SSL勾选框,通栏输入框容器 -->
<div class="form-row">
<label><?php echo $t['smtp_ssl']; ?></label>
<div class="checkbox-input-box">
<input type="checkbox" name="smtp_ssl" id="smtp_ssl" value="1" <?php echo ($smtpConfig['smtp_ssl'] ?? 0) == 1 ? 'checked' : ''; ?>>
<label for="smtp_ssl"><?php echo $t['smtp_ssl']; ?></label>
</div>
</div>
<div class="form-row">
<label><?php echo $t['smtp_account']; ?></label>
<input class="short-width" name="smtp_user" value="<?php echo htmlspecialchars($smtpConfig['smtp_user'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_pwd']; ?></label>
<input class="short-width" name="smtp_pass" type="password" value="<?php echo htmlspecialchars($smtpConfig['smtp_pass'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_from']; ?></label>
<input class="short-width" name="smtp_from" value="<?php echo htmlspecialchars($smtpConfig['send_from'] ?? ''); ?>">
</div>
<!-- 工单推送配置组 -->
<div class="group-title"><?php echo $t['group_push']; ?></div>
<!-- 推送总开关,通栏输入框容器 -->
<div class="form-row">
<label><?php echo $t['notify_switch']; ?></label>
<div class="checkbox-input-box">
<input type="checkbox" name="notify_open" id="notify_open" value="1" <?php echo ($notifyConfig['notify_open'] ?? 0) == 1 ? 'checked' : ''; ?>>
<label for="notify_open"><?php echo $t['notify_switch']; ?></label>
</div>
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_webhook']; ?></label>
<input class="short-width" name="dingtalk_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_secret']; ?></label>
<input class="short-width" name="dingtalk_secret" placeholder="<?php echo $t['placeholder_secret']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_secret'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_webhook']; ?></label>
<input class="short-width" name="wecom_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_mention']; ?></label>
<input class="short-width" name="wecom_mention" placeholder="<?php echo $t['placeholder_mention']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_mention'] ?? ''); ?>">
</div>
<!-- 统一保存按钮 -->
<div class="form-submit">
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_all']; ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// 实时系统时间
function updateNowTime() {
const d = new Date();
const Y = d.getFullYear();
const M = String(d.getMonth()+1).padStart(2,'0');
const D = String(d.getDate()).padStart(2,'0');
const h = String(d.getHours()).padStart(2,'0');
const m = String(d.getMinutes()).padStart(2,'0');
const s = String(d.getSeconds()).padStart(2,'0');
$("#nowTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
setInterval(updateNowTime,1000);
updateNowTime();
// 语言切换
$("#langSwitch").change(function(){
const lang = $(this).val();
$.post("smtp_config.php", {action:"save_lang",lang:lang}, ()=>location.reload());
})
</script>
</body>
</html>
+444
View File
@@ -0,0 +1,444 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
// 读取配置库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$currentLang = mysqli_fetch_assoc(mysqli_query($conn, "SELECT v FROM sys_config WHERE k='lang'"))['v'] ?? "zh";
// 多语言(统一侧边栏中英对照)
$langMap = [
'zh' => [
'page_subtitle' => '爱德万测试 · 系统通知配置',
'menu_monitor' => '监控面板',
'menu_workorder' => '工单系统',
'menu_system' => '系统管理',
'menu_admin' => '后台配置',
'menu_smtp' => '邮箱SMTP配置',
'menu_notify' => '工单通知告警配置',
'smtp_config' => '邮箱SMTP配置',
'smtp_host' => 'SMTP服务器',
'smtp_port' => '端口',
'smtp_ssl' => '启用SSL/TLS',
'smtp_account' => '发件账号',
'smtp_pwd' => '授权密码',
'smtp_from' => '发件人邮箱',
'save_smtp' => '保存邮箱配置',
'save_success' => '保存成功',
'notify_title' => '工单告警通知配置',
'dingtalk_webhook' => '钉钉Webhook地址',
'dingtalk_secret' => '钉钉加签密钥(可选)',
'wecom_webhook' => '企业微信Webhook地址',
'wecom_mention' => '企业微信@人员(多个用逗号分隔)',
'notify_switch' => '开启工单自动推送告警',
'save_notify' => '保存通知配置',
'placeholder_webhook' => '请输入Webhook完整地址',
'placeholder_secret' => '不使用加签留空',
'placeholder_mention' => 'all代表所有人,或填写手机号'
],
'en' => [
'page_subtitle' => 'Advantest · System Notify Config',
'menu_monitor' => 'Monitor Panel',
'menu_workorder' => 'Work Order System',
'menu_system' => 'System Manage',
'menu_admin' => 'System Config',
'menu_smtp' => 'SMTP Mail Config',
'menu_notify' => 'Work Order Alert Notify',
'smtp_config' => 'SMTP Mail Config',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'Port',
'smtp_ssl' => 'Enable SSL/TLS',
'smtp_account' => 'SMTP Account',
'smtp_pwd' => 'Auth Password',
'smtp_from' => 'Sender Email',
'save_smtp' => 'Save Mail Config',
'save_success' => 'Saved successfully',
'notify_title' => 'Work Order Alert Notification',
'dingtalk_webhook' => 'DingTalk Webhook',
'dingtalk_secret' => 'DingTalk Secret (Optional)',
'wecom_webhook' => 'WeCom Webhook',
'wecom_mention' => 'WeCom Mention User (split by comma)',
'notify_switch' => 'Auto push work order alert',
'save_notify' => 'Save Notify Config',
'placeholder_webhook' => 'Input full webhook url',
'placeholder_secret' => 'Leave empty if no secret',
'placeholder_mention' => 'all for everyone, or mobile number'
]
];
$t = $langMap[$currentLang];
// 保存提交处理
if ($_POST['act'] === "save_smtp") {
$host = trim($_POST['smtp_host'] ?? '');
$port = intval($_POST['smtp_port'] ?? 25);
$ssl = intval($_POST['smtp_ssl'] ?? 0);
$user = trim($_POST['smtp_user'] ?? '');
$pwd = trim($_POST['smtp_pass'] ?? '');
$from = trim($_POST['smtp_from'] ?? '');
mysqli_query($conn, "UPDATE sys_smtp_config SET smtp_host='$host',smtp_port=$port,smtp_ssl=$ssl,smtp_user='$user',smtp_pass='$pwd',send_from='$from' WHERE id=1");
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
if ($_POST['act'] === "save_notify") {
$ding_webhook = trim($_POST['dingtalk_webhook'] ?? '');
$ding_secret = trim($_POST['dingtalk_secret'] ?? '');
$wecom_webhook = trim($_POST['wecom_webhook'] ?? '');
$wecom_mention = trim($_POST['wecom_mention'] ?? '');
$notify_open = intval($_POST['notify_open'] ?? 0);
// 通知配置表,不存在先建表(首次访问自动创建)
mysqli_query($conn, "CREATE TABLE IF NOT EXISTS sys_workorder_notify (
id INT PRIMARY KEY AUTO_INCREMENT,
dingtalk_webhook VARCHAR(500) NOT NULL DEFAULT '',
dingtalk_secret VARCHAR(200) NOT NULL DEFAULT '',
wecom_webhook VARCHAR(500) NOT NULL DEFAULT '',
wecom_mention VARCHAR(300) NOT NULL DEFAULT '',
notify_open TINYINT NOT NULL DEFAULT 0,
update_time DATETIME NOT NULL DEFAULT NOW()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$exist = mysqli_fetch_assoc(mysqli_query($conn, "SELECT id FROM sys_workorder_notify LIMIT 1"));
if ($exist) {
mysqli_query($conn, "UPDATE sys_workorder_notify SET
dingtalk_webhook='$ding_webhook',
dingtalk_secret='$ding_secret',
wecom_webhook='$wecom_webhook',
wecom_mention='$wecom_mention',
notify_open=$notify_open,
update_time=NOW()
WHERE id = {$exist['id']}");
} else {
mysqli_query($conn, "INSERT INTO sys_workorder_notify
(dingtalk_webhook,dingtalk_secret,wecom_webhook,wecom_mention,notify_open)
VALUES ('$ding_webhook','$ding_secret','$wecom_webhook','$wecom_mention',$notify_open)");
}
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
// 读取SMTP配置
$smtpRow = [];
$sql = "SELECT * FROM sys_smtp_config WHERE id = 1";
$res = mysqli_query($conn, $sql);
if ($res instanceof mysqli_result) {
$smtpRow = mysqli_fetch_assoc($res) ?: [];
}
$smtpConfig = $smtpRow;
// 读取工单通知配置
$notifyConfig = [];
$notifyRes = mysqli_query($conn, "SELECT * FROM sys_workorder_notify LIMIT 1");
if ($notifyRes instanceof mysqli_result) {
$notifyConfig = mysqli_fetch_assoc($notifyRes) ?: [];
}
?>
<!DOCTYPE html>
<html lang="<?php echo $currentLang==='zh'?'zh-CN':'en'; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Notify Config - Advantest</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{display:flex;flex:1;overflow:hidden;}
/* 侧边栏升级 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:28px 24px;
border-bottom:1px solid rgba(255,255,255,0.08);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:6px;
line-height:1.4;
}
.sidebar-menu{padding:16px 0 30px;}
.menu-title{
padding:12px 24px 8px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
text-transform:uppercase;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:all 0.22s ease;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{display:flex;align-items:center;gap:14px;}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{font-size:15px;color:#64748b;line-height:1.4;}
.header-right{display:flex;align-items:center;gap:20px;font-size:14px;flex-wrap:wrap;}
.user-info{color:#475569;display:flex;align-items:center;gap:6px;}
/* 按钮通用 */
.btn-base{
border:none;
padding:10px 22px;
border-radius:10px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:8px;
transition:all 0.2s ease;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
box-shadow:0 4px 12px rgba(21,44,91,0.12);
}
/* 容器卡片 */
.container{padding:36px 40px;flex:1;}
.base-card{
background:#fff;
border-radius:18px;
padding:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
max-width:780px;
margin-bottom:32px;
}
.title-wrap{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
margin-bottom:28px;
padding-bottom:18px;
border-bottom:1px solid #eef2fb;
}
.title-wrap i{font-size:20px;color:#3b82f6;}
/* 表单美化 */
.form-row{margin-bottom:24px;}
.form-row label{
display:block;
margin-bottom:10px;
color:#344054;
font-weight:500;
font-size:14px;
}
.form-row input[type="text"],
.form-row input[type="password"],
.form-row input[type="number"]{
width:100%;
padding:12px 16px;
border:1px solid #cbd5e1;
border-radius:10px;
font-size:14px;
transition:border 0.2s;
}
.form-row input:focus{
outline:none;
border-color:#3b82f6;
box-shadow:0 0 0 3px rgba(59,130,246,0.12);
}
.form-row input::placeholder{color:#98a2b3;}
.checkbox-wrap{
display:flex;
align-items:center;
gap:10px;
}
.checkbox-wrap input[type="checkbox"]{
width:18px;height:18px;
cursor:pointer;
}
.form-submit{margin-top:10px;}
</style>
</head>
<body>
<div class="wrap-main">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
<div class="sidebar-menu">
<div class="menu-title"><?php echo $t['menu_monitor']; ?></div>
<div class="menu-item" onclick="location.href='dashboard.php'">
<i class="fa fa-line-chart"></i><span><?php echo $t['menu_monitor']; ?></span>
</div>
<div class="menu-item" onclick="location.href='work_order.php'">
<i class="fa fa-ticket"></i><span><?php echo $t['menu_workorder']; ?></span>
</div>
<div class="menu-title"><?php echo $t['menu_system']; ?></div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i><span><?php echo $t['menu_admin']; ?></span>
</div>
<div class="menu-item active">
<i class="fa fa-envelope-o"></i><span><?php echo $t['menu_smtp']; ?></span>
</div>
<div class="menu-item" onclick="location.href='smtp_config.php#notify'">
<i class="fa fa-bell"></i><span><?php echo $t['menu_notify']; ?></span>
</div>
</div>
</div>
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>User: <?php echo $userName; ?></span>
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>Logout</button>
</div>
</div>
<div class="container">
<!-- 第一块:SMTP邮箱配置 -->
<div class="base-card">
<div class="title-wrap"><i class="fa fa-envelope-o"></i><?php echo $t['smtp_config']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_smtp">
<div class="form-row">
<label><?php echo $t['smtp_host']; ?></label>
<input name="smtp_host" value="<?php echo htmlspecialchars($smtpConfig['smtp_host'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_port']; ?></label>
<input name="smtp_port" type="number" value="<?php echo intval($smtpConfig['smtp_port'] ?? 25); ?>">
</div>
<div class="form-row checkbox-wrap">
<input type="checkbox" name="smtp_ssl" value="1" <?php echo ($smtpConfig['smtp_ssl'] ?? 0)==1?'checked':''; ?>>
<label><?php echo $t['smtp_ssl']; ?></label>
</div>
<div class="form-row">
<label><?php echo $t['smtp_account']; ?></label>
<input name="smtp_user" value="<?php echo htmlspecialchars($smtpConfig['smtp_user'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_pwd']; ?></label>
<input name="smtp_pass" type="password" value="<?php echo htmlspecialchars($smtpConfig['smtp_pass'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_from']; ?></label>
<input name="smtp_from" value="<?php echo htmlspecialchars($smtpConfig['send_from'] ?? ''); ?>">
</div>
<div class="form-submit">
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_smtp']; ?></button>
</div>
</form>
</div>
<!-- 第二块:工单钉钉/企业微信通知配置 -->
<div class="base-card" id="notify">
<div class="title-wrap"><i class="fa fa-bell"></i><?php echo $t['notify_title']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_notify">
<div class="form-row checkbox-wrap">
<input type="checkbox" name="notify_open" value="1" <?php echo ($notifyConfig['notify_open'] ?? 0)==1?'checked':''; ?>>
<label><?php echo $t['notify_switch']; ?></label>
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_webhook']; ?></label>
<input name="dingtalk_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_secret']; ?></label>
<input name="dingtalk_secret" placeholder="<?php echo $t['placeholder_secret']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_secret'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_webhook']; ?></label>
<input name="wecom_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_mention']; ?></label>
<input name="wecom_mention" placeholder="<?php echo $t['placeholder_mention']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_mention'] ?? ''); ?>">
</div>
<div class="form-submit">
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_notify']; ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
+452
View File
@@ -0,0 +1,452 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
// 读取配置库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$currentLang = mysqli_fetch_assoc(mysqli_query($conn, "SELECT v FROM sys_config WHERE k='lang'"))['v'] ?? "zh";
// 多语言(统一侧边栏中英对照)
$langMap = [
'zh' => [
'page_subtitle' => '爱德万测试 · 系统通知配置',
'menu_monitor' => '监控面板',
'menu_workorder' => '工单系统',
'menu_system' => '系统管理',
'menu_admin' => '后台配置',
'menu_smtp' => '邮箱SMTP配置',
'menu_notify' => '工单通知告警配置',
'smtp_config' => '邮箱SMTP配置',
'smtp_host' => 'SMTP服务器',
'smtp_port' => '端口',
'smtp_ssl' => '启用SSL/TLS',
'smtp_account' => '发件账号',
'smtp_pwd' => '授权密码',
'smtp_from' => '发件人邮箱',
'save_smtp' => '保存邮箱配置',
'save_success' => '保存成功',
'notify_title' => '工单告警通知配置',
'dingtalk_webhook' => '钉钉Webhook地址',
'dingtalk_secret' => '钉钉加签密钥(可选)',
'wecom_webhook' => '企业微信Webhook地址',
'wecom_mention' => '企业微信@人员(多个用逗号分隔)',
'notify_switch' => '开启工单自动推送告警',
'save_notify' => '保存通知配置',
'placeholder_webhook' => '请输入Webhook完整地址',
'placeholder_secret' => '不使用加签留空',
'placeholder_mention' => 'all代表所有人,或填写手机号'
],
'en' => [
'page_subtitle' => 'Advantest · System Notify Config',
'menu_monitor' => 'Monitor Panel',
'menu_workorder' => 'Work Order System',
'menu_system' => 'System Manage',
'menu_admin' => 'System Config',
'menu_smtp' => 'SMTP Mail Config',
'menu_notify' => 'Work Order Alert Notify',
'smtp_config' => 'SMTP Mail Config',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'Port',
'smtp_ssl' => 'Enable SSL/TLS',
'smtp_account' => 'SMTP Account',
'smtp_pwd' => 'Auth Password',
'smtp_from' => 'Sender Email',
'save_smtp' => 'Save Mail Config',
'save_success' => 'Saved successfully',
'notify_title' => 'Work Order Alert Notification',
'dingtalk_webhook' => 'DingTalk Webhook',
'dingtalk_secret' => 'DingTalk Secret (Optional)',
'wecom_webhook' => 'WeCom Webhook',
'wecom_mention' => 'WeCom Mention User (split by comma)',
'notify_switch' => 'Auto push work order alert',
'save_notify' => 'Save Notify Config',
'placeholder_webhook' => 'Input full webhook url',
'placeholder_secret' => 'Leave empty if no secret',
'placeholder_mention' => 'all for everyone, or mobile number'
]
];
$t = $langMap[$currentLang];
// 保存提交处理
if ($_POST['act'] === "save_smtp") {
$host = trim($_POST['smtp_host'] ?? '');
$port = intval($_POST['smtp_port'] ?? 25);
$ssl = intval($_POST['smtp_ssl'] ?? 0);
$user = trim($_POST['smtp_user'] ?? '');
$pwd = trim($_POST['smtp_pass'] ?? '');
$from = trim($_POST['smtp_from'] ?? '');
mysqli_query($conn, "UPDATE sys_smtp_config SET smtp_host='$host',smtp_port=$port,smtp_ssl=$ssl,smtp_user='$user',smtp_pass='$pwd',send_from='$from' WHERE id=1");
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
if ($_POST['act'] === "save_notify") {
$ding_webhook = trim($_POST['dingtalk_webhook'] ?? '');
$ding_secret = trim($_POST['dingtalk_secret'] ?? '');
$wecom_webhook = trim($_POST['wecom_webhook'] ?? '');
$wecom_mention = trim($_POST['wecom_mention'] ?? '');
$notify_open = intval($_POST['notify_open'] ?? 0);
mysqli_query($conn, "CREATE TABLE IF NOT EXISTS sys_workorder_notify (
id INT PRIMARY KEY AUTO_INCREMENT,
dingtalk_webhook VARCHAR(500) NOT NULL DEFAULT '',
dingtalk_secret VARCHAR(200) NOT NULL DEFAULT '',
wecom_webhook VARCHAR(500) NOT NULL DEFAULT '',
wecom_mention VARCHAR(300) NOT NULL DEFAULT '',
notify_open TINYINT NOT NULL DEFAULT 0,
update_time DATETIME NOT NULL DEFAULT NOW()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$exist = mysqli_fetch_assoc(mysqli_query($conn, "SELECT id FROM sys_workorder_notify LIMIT 1"));
if ($exist) {
mysqli_query($conn, "UPDATE sys_workorder_notify SET
dingtalk_webhook='$ding_webhook',
dingtalk_secret='$ding_secret',
wecom_webhook='$wecom_webhook',
wecom_mention='$wecom_mention',
notify_open=$notify_open,
update_time=NOW()
WHERE id = {$exist['id']}");
} else {
mysqli_query($conn, "INSERT INTO sys_workorder_notify
(dingtalk_webhook,dingtalk_secret,wecom_webhook,wecom_mention,notify_open)
VALUES ('$ding_webhook','$ding_secret','$wecom_webhook','$wecom_mention',$notify_open)");
}
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
// 读取SMTP配置
$smtpRow = [];
$sql = "SELECT * FROM sys_smtp_config WHERE id = 1";
$res = mysqli_query($conn, $sql);
if ($res instanceof mysqli_result) {
$smtpRow = mysqli_fetch_assoc($res) ?: [];
}
$smtpConfig = $smtpRow;
// 读取工单通知配置
$notifyConfig = [];
$notifyRes = mysqli_query($conn, "SELECT * FROM sys_workorder_notify LIMIT 1");
if ($notifyRes instanceof mysqli_result) {
$notifyConfig = mysqli_fetch_assoc($notifyRes) ?: [];
}
?>
<!DOCTYPE html>
<html lang="<?php echo $currentLang==='zh'?'zh-CN':'en'; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Notify Config - Advantest</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{display:flex;flex:1;overflow:hidden;}
/* 侧边栏 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:28px 24px;
border-bottom:1px solid rgba(255,255,255,0.08);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:12px;
color:#a5b4fc;
margin-top:6px;
line-height:1.4;
}
.sidebar-menu{padding:16px 0 30px;}
.menu-title{
padding:12px 24px 8px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
text-transform:uppercase;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:all 0.22s ease;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{display:flex;align-items:center;gap:14px;}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{font-size:13px;color:#64748b;line-height:1.4;}
.header-right{display:flex;align-items:center;gap:20px;font-size:14px;flex-wrap:wrap;}
.user-info{color:#475569;display:flex;align-items:center;gap:6px;}
/* 按钮统一规范:圆角10px,统一尺寸,无夸张圆润 */
.btn-base{
border:none;
padding:10px 22px;
border-radius:10px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:8px;
transition:all 0.2s ease;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
box-shadow:0 4px 12px rgba(21,44,91,0.12);
}
/* 容器 */
.container{padding:36px 40px;flex:1;}
/* 卡片核心修改:圆角12px,摒弃大圆,轻薄阴影 */
.base-card{
background:#fff;
border-radius:12px;
padding:32px;
box-shadow:0 2px 12px rgba(21,44,91,0.04);
border:1px solid #eef2fb;
max-width:780px;
margin-bottom:28px;
}
.title-wrap{
display:flex;
align-items:center;
gap:12px;
font-size:18px;
font-weight:600;
color:#152c5b;
margin-bottom:24px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-wrap i{font-size:19px;color:#3b82f6;}
/* 表单全局统一,端口/密码/文本框完全一致 */
.form-row{margin-bottom:22px;}
.form-row label{
display:block;
margin-bottom:8px;
color:#344054;
font-weight:500;
font-size:13px;
}
/* 所有输入框统一样式,无差异化 */
.form-row input[type="text"],
.form-row input[type="password"],
.form-row input[type="number"]{
width:100%;
padding:11px 14px;
border:1px solid #cbd5e1;
border-radius:10px;
font-size:14px;
transition:border 0.2s,box-shadow 0.2s;
background:#fff;
}
.form-row input:focus{
outline:none;
border-color:#3b82f6;
box-shadow:0 0 0 3px rgba(59,130,246,0.12);
}
.form-row input::placeholder{color:#98a2b3;font-size:13px;}
.checkbox-wrap{
display:flex;
align-items:center;
gap:10px;
}
.checkbox-wrap input[type="checkbox"]{
width:18px;height:18px;
cursor:pointer;
}
.form-submit{margin-top:8px;}
</style>
</head>
<body>
<div class="wrap-main">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
<div class="sidebar-menu">
<div class="menu-title"><?php echo $t['menu_monitor']; ?></div>
<div class="menu-item" onclick="location.href='dashboard.php'">
<i class="fa fa-line-chart"></i><span><?php echo $t['menu_monitor']; ?></span>
</div>
<div class="menu-item" onclick="location.href='work_order.php'">
<i class="fa fa-ticket"></i><span><?php echo $t['menu_workorder']; ?></span>
</div>
<div class="menu-title"><?php echo $t['menu_system']; ?></div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i><span><?php echo $t['menu_admin']; ?></span>
</div>
<div class="menu-item active">
<i class="fa fa-envelope-o"></i><span><?php echo $t['menu_smtp']; ?></span>
</div>
<div class="menu-item" onclick="location.href='smtp_config.php#notify'">
<i class="fa fa-bell"></i><span><?php echo $t['menu_notify']; ?></span>
</div>
</div>
</div>
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>User: <?php echo $userName; ?></span>
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>Logout</button>
</div>
</div>
<div class="container">
<!-- SMTP邮箱配置 -->
<div class="base-card">
<div class="title-wrap"><i class="fa fa-envelope-o"></i><?php echo $t['smtp_config']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_smtp">
<div class="form-row">
<label><?php echo $t['smtp_host']; ?></label>
<input name="smtp_host" value="<?php echo htmlspecialchars($smtpConfig['smtp_host'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_port']; ?></label>
<input name="smtp_port" type="number" value="<?php echo intval($smtpConfig['smtp_port'] ?? 25); ?>">
</div>
<div class="form-row checkbox-wrap">
<input type="checkbox" name="smtp_ssl" value="1" <?php echo ($smtpConfig['smtp_ssl'] ?? 0)==1?'checked':''; ?>>
<label><?php echo $t['smtp_ssl']; ?></label>
</div>
<div class="form-row">
<label><?php echo $t['smtp_account']; ?></label>
<input name="smtp_user" value="<?php echo htmlspecialchars($smtpConfig['smtp_user'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_pwd']; ?></label>
<input name="smtp_pass" type="password" value="<?php echo htmlspecialchars($smtpConfig['smtp_pass'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_from']; ?></label>
<input name="smtp_from" value="<?php echo htmlspecialchars($smtpConfig['send_from'] ?? ''); ?>">
</div>
<div class="form-submit">
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_smtp']; ?></button>
</div>
</form>
</div>
<!-- 工单钉钉/企业微信通知配置 -->
<div class="base-card" id="notify">
<div class="title-wrap"><i class="fa fa-bell"></i><?php echo $t['notify_title']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_notify">
<div class="form-row checkbox-wrap">
<input type="checkbox" name="notify_open" value="1" <?php echo ($notifyConfig['notify_open'] ?? 0)==1?'checked':''; ?>>
<label><?php echo $t['notify_switch']; ?></label>
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_webhook']; ?></label>
<input name="dingtalk_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_secret']; ?></label>
<input name="dingtalk_secret" placeholder="<?php echo $t['placeholder_secret']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_secret'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_webhook']; ?></label>
<input name="wecom_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_mention']; ?></label>
<input name="wecom_mention" placeholder="<?php echo $t['placeholder_mention']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_mention'] ?? ''); ?>">
</div>
<div class="form-submit">
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_notify']; ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
+545
View File
@@ -0,0 +1,545 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
// 兼容低PHP版本 str_contains
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return $needle === '' || strpos($haystack, $needle) !== false;
}
}
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRoleId = $_SESSION['role_id'];
// 全局配置库连接
$dbConfigHost = "10.150.117.190";
$dbConfigUser = "root";
$dbConfigPwd = "hp93000";
$dbConfigName = "alert_mail_stat";
$connConfig = mysqli_connect($dbConfigHost, $dbConfigUser, $dbConfigPwd, $dbConfigName);
if(!$connConfig){
die("配置库连接失败");
}
mysqli_set_charset($connConfig, "utf8mb4");
// 自动创建SMTP配置表,不存在则新建
mysqli_query($connConfig, "CREATE TABLE IF NOT EXISTS sys_smtp_config (
id INT PRIMARY KEY AUTO_INCREMENT,
smtp_host VARCHAR(200) NOT NULL DEFAULT '',
smtp_port INT NOT NULL DEFAULT 25,
smtp_ssl TINYINT NOT NULL DEFAULT 0,
smtp_user VARCHAR(200) NOT NULL DEFAULT '',
smtp_pass VARCHAR(200) NOT NULL DEFAULT '',
send_from VARCHAR(200) NOT NULL DEFAULT ''
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$checkRow = mysqli_fetch_assoc(mysqli_query($connConfig, "SELECT id FROM sys_smtp_config LIMIT 1"));
if (empty($checkRow)) {
mysqli_query($connConfig, "INSERT INTO sys_smtp_config (id) VALUES (1)");
}
// 自动创建工单推送通知表
mysqli_query($connConfig, "CREATE TABLE IF NOT EXISTS sys_workorder_notify (
id INT PRIMARY KEY AUTO_INCREMENT,
dingtalk_webhook VARCHAR(500) NOT NULL DEFAULT '',
dingtalk_secret VARCHAR(200) NOT NULL DEFAULT '',
wecom_webhook VARCHAR(500) NOT NULL DEFAULT '',
wecom_mention VARCHAR(300) NOT NULL DEFAULT '',
notify_open TINYINT NOT NULL DEFAULT 0,
update_time DATETIME NOT NULL DEFAULT NOW()
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
// 安全读取SMTP配置,杜绝mysqli警告
$smtpRow = [];
$sqlSmtp = "SELECT * FROM sys_smtp_config WHERE id=1";
if (!empty(trim($sqlSmtp))) {
$smtpQuery = mysqli_query($connConfig, $sqlSmtp);
if ($smtpQuery !== false && $smtpQuery instanceof mysqli_result) {
$smtpRow = mysqli_fetch_assoc($smtpQuery) ?: [];
}
}
$smtpConfig = $smtpRow ?? [];
// 安全读取推送通知配置
$notifyConfig = [];
$notifyQuery = mysqli_query($connConfig, "SELECT * FROM sys_workorder_notify LIMIT 1");
if ($notifyQuery instanceof mysqli_result) {
$notifyConfig = mysqli_fetch_assoc($notifyQuery) ?: [];
}
$pageTitle = htmlspecialchars($sysConfig['page_subtitle'] ?? "工单消息通知配置");
$currentLang = $sysConfig['lang'] ?? "zh";
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
mysqli_close($connConfig);
// 多语言词典(菜单文字全部替换:SMTP配置 → 工单消息通知配置)
$langMap = [
'zh' => [
'html_lang' => 'zh-CN',
'page_subtitle' => '工单消息通知配置',
'monitor_panel' => '监控面板',
'alert_overview' => '告警总览',
'quick_link' => '快捷外链',
'system_manage' => '系统管理',
'notify_config' => '工单消息通知配置',
'current_user' => '当前用户:',
'sys_time' => '系统时间:',
'refresh_all' => '刷新页面',
'export_csv' => '导出CSV',
'logout' => '退出登录',
'lang_text' => '语言:',
'work_order_mgr' => '工单系统',
'save_all' => '保存全部通知配置',
'card_title' => '工单消息推送 & 发件邮箱统一配置',
'group_mail' => '邮箱SMTP发件设置',
'smtp_host' => 'SMTP服务器',
'smtp_port' => '端口',
'smtp_ssl' => '启用SSL/TLS加密',
'smtp_account' => '登录账号',
'smtp_pwd' => '授权密码',
'smtp_from' => '发件人邮箱',
'group_push' => '工单变更推送配置',
'notify_switch' => '开启工单变更自动推送',
'dingtalk_webhook' => '钉钉Webhook地址',
'dingtalk_secret' => '钉钉安全密钥(可选)',
'wecom_webhook' => '企业微信Webhook地址',
'wecom_mention' => '@提醒人员(多个逗号分隔,all=全员)',
'placeholder_webhook' => '输入完整Webhook链接',
'placeholder_secret' => '无安全校验则留空',
'placeholder_mention' => '填写手机号,all代表所有人',
'save_success' => '配置保存成功'
],
'en' => [
'html_lang' => 'en',
'page_subtitle' => 'Work Order Notification Config',
'monitor_panel' => 'Monitor Panel',
'alert_overview' => 'Alert Overview',
'quick_link' => 'Quick Links',
'system_manage' => 'System Manage',
'notify_config' => 'Work Order Notification Config',
'current_user' => 'User: ',
'sys_time' => 'System Time: ',
'refresh_all' => 'Refresh',
'export_csv' => 'Export CSV',
'logout' => 'Logout',
'lang_text' => 'Lang: ',
'work_order_mgr' => 'Work Order System',
'save_all' => 'Save All Notification Config',
'card_title' => 'Work Order Push & Mail Sender Config',
'group_mail' => 'SMTP Mail Sender',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'Port',
'smtp_ssl' => 'Enable SSL/TLS',
'smtp_account' => 'Account',
'smtp_pwd' => 'Auth Password',
'smtp_from' => 'Sender Email',
'group_push' => 'Work Order Push Setting',
'notify_switch' => 'Auto push on order change',
'dingtalk_webhook' => 'DingTalk Webhook',
'dingtalk_secret' => 'DingTalk Secret (Optional)',
'wecom_webhook' => 'WeCom Webhook',
'wecom_mention' => 'Mention user (split by comma, all=everyone)',
'placeholder_webhook' => 'Full webhook url',
'placeholder_secret' => 'Leave empty if no secret',
'placeholder_mention' => 'Mobile number or all',
'save_success' => 'Config saved successfully'
]
];
$t = $langMap[$currentLang];
// 解析外链菜单
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line, 2);
if (count($item) === 2) {
$sidebarLinkList[] = ["name" => trim($item[0]), "url" => trim($item[1])];
}
}
}
// 统一保存表单提交逻辑(SMTP+推送一次保存)
if ($_POST['act'] === "save_all") {
// 更新SMTP邮箱配置
$host = trim($_POST['smtp_host'] ?? '');
$port = intval($_POST['smtp_port'] ?? 25);
$ssl = intval($_POST['smtp_ssl'] ?? 0);
$user = trim($_POST['smtp_user'] ?? '');
$pwd = trim($_POST['smtp_pass'] ?? '');
$from = trim($_POST['smtp_from'] ?? '');
mysqli_query($connConfig, "UPDATE sys_smtp_config SET smtp_host='$host',smtp_port=$port,smtp_ssl=$ssl,smtp_user='$user',smtp_pass='$pwd',send_from='$from' WHERE id=1");
// 更新工单推送配置
$ding_webhook = trim($_POST['dingtalk_webhook'] ?? '');
$ding_secret = trim($_POST['dingtalk_secret'] ?? '');
$wecom_webhook = trim($_POST['wecom_webhook'] ?? '');
$wecom_mention = trim($_POST['wecom_mention'] ?? '');
$notify_open = intval($_POST['notify_open'] ?? 0);
$existNotify = mysqli_fetch_assoc(mysqli_query($connConfig, "SELECT id FROM sys_workorder_notify LIMIT 1"));
if ($existNotify) {
mysqli_query($connConfig, "UPDATE sys_workorder_notify SET dingtalk_webhook='$ding_webhook', dingtalk_secret='$ding_secret', wecom_webhook='$wecom_webhook', wecom_mention='$wecom_mention', notify_open=$notify_open, update_time=NOW() WHERE id = {$existNotify['id']}");
} else {
mysqli_query($connConfig, "INSERT INTO sys_workorder_notify (dingtalk_webhook,dingtalk_secret,wecom_webhook,wecom_mention,notify_open) VALUES ('$ding_webhook','$ding_secret','$wecom_webhook','$wecom_mention',$notify_open)");
}
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
?>
<!DOCTYPE html>
<html lang="<?php echo $t['html_lang']; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle; ?></title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{ background:#f8fafc; font-family:"Inter","PingFang SC","Microsoft YaHei",system-ui,sans-serif; color:#1e293b; line-height:1.55; display:flex; flex-direction:column; height:100vh; overflow:hidden; }
.wrap-main{display:flex;flex:1;overflow:hidden;}
/* 侧边栏 深蓝导航 */
.sidebar{ width:240px; background:#152c5b; color:#e2e8f0; flex-shrink:0; overflow-y:auto; }
.sidebar-brand{ padding:30px 24px; border-bottom:1px solid rgba(255,255,255,0.06); }
.sidebar-brand-en{ font-size:20px; font-weight:600; letter-spacing:0.5px; color:#ffffff; }
.sidebar-brand-cn{ font-size:12px; color:#a5b4fc; margin-top:6px; opacity:0.9; }
.sidebar-menu{padding:20px 0;}
.menu-title{ padding:10px 24px 6px; font-size:11px; color:#94a3b8; letter-spacing:0.5px; text-transform:uppercase; }
.menu-item{ display:flex; align-items:center; gap:12px; padding:13px 24px; cursor:pointer; font-size:14px; transition:background 0.2s ease; color:#cbd5e1; }
.menu-item i{ font-size:16px; width:20px; text-align:center; }
.menu-item.active{ background:rgba(59,130,246,0.15); color:#ffffff; border-left:4px solid #3b82f6; }
.menu-item:hover:not(.active){ background:rgba(255,255,255,0.06); color:#ffffff; }
/* 顶部Header */
.main-content{ flex:1; overflow-y:auto; display:flex; flex-direction:column; }
.header{ background:#ffffff; border-bottom:1px solid #e2e8f0; padding:20px 40px; display:flex; justify-content:space-between; align-items:center; flex-shrink:0; }
.header-left{display:flex;align-items:center;gap:14px;}
.header-brand-en{ font-size:24px; font-weight:700; color:#152c5b; letter-spacing:1px; }
.header-brand-cn{font-size:13px;color:#64748b;}
.header-right{display:flex;align-items:center;gap:20px;font-size:14px;}
.user-info,.time-info,.lang-box{color:#475569;display:flex;align-items:center;gap:6px;}
.lang-box select{padding:4px 8px;border:1px solid #cbd5e1;border-radius:6px;}
/* 通用按钮基础 */
.btn-base{ border:none; padding:10px 24px; border-radius:999px; cursor:pointer; font-size:14px; font-weight:500; display:flex; align-items:center; gap:8px; transition:all 0.2s ease; }
.btn-outline{
background:#fff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
}
.container{padding:36px 40px;flex:1;}
/* ====================== 四、升级卡片样式 ====================== */
.base-card{
background:#fff;
border-radius:18px;
padding:36px;
border:1px solid #e5e7eb;
box-shadow:
0 1px 3px rgba(0,0,0,.04),
0 8px 24px rgba(15,23,42,.04);
max-width:900px;
}
.title-wrap{ display:flex; align-items:center; gap:12px; font-size:17px; font-weight:600; color:#152c5b; margin-bottom:26px; padding-bottom:16px; border-bottom:1px solid #f1f5f9; }
.title-wrap i{font-size:18px;color:#2563eb;}
.group-title{ font-size:15px; font-weight:600; color:#334155; margin:32px 0 18px; padding-bottom:10px; border-bottom:1px solid #f1f5f9; }
.group-title:first-of-type{margin-top:0;}
/* ====================== 一、企业级输入框统一样式 ====================== */
.form-row{
margin-bottom:22px;
}
.form-row label{
display:block;
margin-bottom:8px;
font-size:13px;
font-weight:600;
color:#334155;
letter-spacing:.2px;
}
/* 通栏完整宽度输入框:端口 */
.form-row input.full-width{
width:100%;
height:46px;
padding:0 16px;
border:1px solid #d6dce5;
border-radius:10px;
background:#ffffff;
font-size:14px;
color:#1e293b;
transition:all .2s ease;
}
/* 短款输入框:其余所有文本项 */
.form-row input.short-width{
width:360px;
height:46px;
padding:0 16px;
border:1px solid #d6dce5;
border-radius:10px;
background:#ffffff;
font-size:14px;
color:#1e293b;
transition:all .2s ease;
}
.form-row input[type="text"]:hover,
.form-row input[type="password"]:hover,
.form-row input[type="number"]:hover{
border-color:#94a3b8;
}
.form-row input[type="text"]:focus,
.form-row input[type="password"]:focus,
.form-row input[type="number"]:focus{
outline:none;
border-color:#2563eb;
box-shadow:0 0 0 3px rgba(37,99,235,.12);
background:#fff;
}
.form-row input::placeholder{
color:#94a3b8;
font-size:13px;
}
.form-row input:disabled{
background:#f8fafc;
color:#94a3b8;
cursor:not-allowed;
}
/* 数字框去除上下箭头 */
input[type="number"]::-webkit-outer-spin-button,
input[type="number"]::-webkit-inner-spin-button{
-webkit-appearance:none;
margin:0;
}
input[type="number"]{
-moz-appearance:textfield;
}
/* 勾选框容器,和输入框完全同尺寸同样式、通栏 */
.checkbox-input-box{
display:flex;
align-items:center;
gap:10px;
width:100%;
height:46px;
padding:0 16px;
border:1px solid #d6dce5;
border-radius:10px;
background:#fff;
transition:all .2s ease;
}
.checkbox-input-box:hover{
border-color:#94a3b8;
}
.checkbox-input-box input[type="checkbox"]{
width:18px;
height:18px;
accent-color:#2563eb;
cursor:pointer;
}
.checkbox-input-box label{
margin:0;
cursor:pointer;
font-size:14px;
color:#334155;
}
/* ====================== 三、主保存渐变按钮升级 ====================== */
.btn-primary{
background:linear-gradient(135deg,#1d4ed8,#2563eb);
color:#fff;
min-width:220px;
justify-content:center;
box-shadow:0 6px 18px rgba(37,99,235,.25);
}
.btn-primary:hover{
transform:translateY(-1px);
box-shadow:0 10px 24px rgba(37,99,235,.32);
background:linear-gradient(135deg,#1d4ed8,#2563eb);
}
.form-submit{margin-top:16px;}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 侧边栏菜单:工单消息通知配置放入系统管理分组内 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
<div class="sidebar-menu">
<div class="menu-title"><?php echo $t['monitor_panel']; ?></div>
<!-- 1.告警总览 -->
<div class="menu-item" onclick="location.href='dashboard.php'">
<i class="fa fa-line-chart"></i><span><?php echo $t['alert_overview']; ?></span>
</div>
<!-- 2.工单系统 -->
<div class="menu-item" onclick="location.href='work_order.php'">
<i class="fa fa-ticket"></i><span><?php echo $t['work_order_mgr']; ?></span>
</div>
<!-- 快捷外链 -->
<?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title"><?php echo $t['quick_link']; ?></div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>','_blank')">
<i class="fa fa-external-link"></i><span><?php echo htmlspecialchars($link['name']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 系统管理分组,工单通知配置放这里 -->
<div class="menu-title"><?php echo $t['system_manage']; ?></div>
<div class="menu-item active">
<i class="fa fa-bell"></i><span><?php echo $t['notify_config']; ?></span>
</div>
</div>
</div>
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $t['current_user']; ?><?php echo $userName; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i><span id="nowTime">--</span></span>
<div class="lang-box">
<span><?php echo $t['lang_text']; ?></span>
<select id="langSwitch">
<option value="zh" <?php echo $currentLang==='zh'?'selected':''; ?>>中文</option>
<option value="en" <?php echo $currentLang==='en'?'selected':''; ?>>EN</option>
</select>
</div>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i><?php echo $t['refresh_all']; ?></button>
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i><?php echo $t['logout']; ?></button>
</div>
</div>
<div class="container">
<!-- 配置卡片 -->
<div class="base-card">
<div class="title-wrap"><i class="fa fa-bell"></i><?php echo $t['card_title']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_all">
<!-- 邮箱SMTP配置组 -->
<div class="group-title"><?php echo $t['group_mail']; ?></div>
<div class="form-row">
<label><?php echo $t['smtp_host']; ?></label>
<input class="short-width" name="smtp_host" value="<?php echo htmlspecialchars($smtpConfig['smtp_host'] ?? ''); ?>" placeholder="smtp.company.com">
</div>
<div class="form-row">
<label><?php echo $t['smtp_port']; ?></label>
<input class="full-width" name="smtp_port" type="number" value="<?php echo intval($smtpConfig['smtp_port'] ?? 25); ?>" min="1" max="65535">
</div>
<!-- SSL勾选框,通栏输入框容器 -->
<div class="form-row">
<label><?php echo $t['smtp_ssl']; ?></label>
<div class="checkbox-input-box">
<input type="checkbox" name="smtp_ssl" id="smtp_ssl" value="1" <?php echo ($smtpConfig['smtp_ssl'] ?? 0) == 1 ? 'checked' : ''; ?>>
<label for="smtp_ssl"><?php echo $t['smtp_ssl']; ?></label>
</div>
</div>
<div class="form-row">
<label><?php echo $t['smtp_account']; ?></label>
<input class="short-width" name="smtp_user" value="<?php echo htmlspecialchars($smtpConfig['smtp_user'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_pwd']; ?></label>
<input class="full-width" name="smtp_pass" type="password" value="<?php echo htmlspecialchars($smtpConfig['smtp_pass'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_from']; ?></label>
<input class="short-width" name="smtp_from" value="<?php echo htmlspecialchars($smtpConfig['send_from'] ?? ''); ?>">
</div>
<!-- 工单推送配置组 -->
<div class="group-title"><?php echo $t['group_push']; ?></div>
<!-- 推送总开关,通栏输入框容器 -->
<div class="form-row">
<label><?php echo $t['notify_switch']; ?></label>
<div class="checkbox-input-box">
<input type="checkbox" name="notify_open" id="notify_open" value="1" <?php echo ($notifyConfig['notify_open'] ?? 0) == 1 ? 'checked' : ''; ?>>
<label for="notify_open"><?php echo $t['notify_switch']; ?></label>
</div>
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_webhook']; ?></label>
<input class="short-width" name="dingtalk_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['dingtalk_secret']; ?></label>
<input class="short-width" name="dingtalk_secret" placeholder="<?php echo $t['placeholder_secret']; ?>" value="<?php echo htmlspecialchars($notifyConfig['dingtalk_secret'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_webhook']; ?></label>
<input class="short-width" name="wecom_webhook" placeholder="<?php echo $t['placeholder_webhook']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_webhook'] ?? ''); ?>">
</div>
<div class="form-row">
<label><?php echo $t['wecom_mention']; ?></label>
<input class="short-width" name="wecom_mention" placeholder="<?php echo $t['placeholder_mention']; ?>" value="<?php echo htmlspecialchars($notifyConfig['wecom_mention'] ?? ''); ?>">
</div>
<!-- 统一保存按钮 -->
<div class="form-submit">
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_all']; ?></button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
<script>
// 实时系统时间
function updateNowTime() {
const d = new Date();
const Y = d.getFullYear();
const M = String(d.getMonth()+1).padStart(2,'0');
const D = String(d.getDate()).padStart(2,'0');
const h = String(d.getHours()).padStart(2,'0');
const m = String(d.getMinutes()).padStart(2,'0');
const s = String(d.getSeconds()).padStart(2,'0');
$("#nowTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
setInterval(updateNowTime,1000);
updateNowTime();
// 语言切换
$("#langSwitch").change(function(){
const lang = $(this).val();
$.post("smtp_config.php", {action:"save_lang",lang:lang}, ()=>location.reload());
})
</script>
</body>
</html>
+192
View File
@@ -0,0 +1,192 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
// 读取配置库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$currentLang = mysqli_fetch_assoc(mysqli_query($conn, "SELECT v FROM sys_config WHERE k='lang'"))['v'] ?? "zh";
// 多语言
$langMap = [
'zh' => [
'page_subtitle' => '爱德万测试 · SMTP邮箱配置',
'smtp_config' => '邮箱SMTP配置',
'smtp_host' => 'SMTP服务器',
'smtp_port' => '端口',
'smtp_ssl' => '启用SSL/TLS',
'smtp_account' => '发件账号',
'smtp_pwd' => '授权密码',
'smtp_from' => '发件人邮箱',
'save_smtp' => '保存配置',
'save_success' => '保存成功',
],
'en' => [
'page_subtitle' => 'Advantest · SMTP Mail Config',
'smtp_config' => 'SMTP Mail Config',
'smtp_host' => 'SMTP Host',
'smtp_port' => 'Port',
'smtp_ssl' => 'Enable SSL/TLS',
'smtp_account' => 'SMTP Account',
'smtp_pwd' => 'Auth Password',
'smtp_from' => 'Sender Email',
'save_smtp' => 'Save Config',
'save_success' => 'Saved successfully',
]
];
$t = $langMap[$currentLang];
// 保存提交
if ($_POST['act'] === "save_smtp") {
$host = trim($_POST['smtp_host'] ?? '');
$port = intval($_POST['smtp_port'] ?? 25);
$ssl = intval($_POST['smtp_ssl'] ?? 0);
$user = trim($_POST['smtp_user'] ?? '');
$pwd = trim($_POST['smtp_pass'] ?? '');
$from = trim($_POST['smtp_from'] ?? '');
mysqli_query($conn, "UPDATE sys_smtp_config SET smtp_host='$host',smtp_port=$port,smtp_ssl=$ssl,smtp_user='$user',smtp_pass='$pwd',send_from='$from' WHERE id=1");
echo "<script>alert('".$t['save_success']."');location.href='smtp_config.php';</script>";
exit;
}
// 1. 先判断SQL不为空,避免空查询警告
$sql = "SELECT * FROM sys_smtp_config WHERE id = 1";
if (!empty($sql)) {
$res = mysqli_query($conn, $sql);
// 2. 判断查询成功且返回有效结果集再执行fetch
if ($res instanceof mysqli_result) {
$smtpRow = mysqli_fetch_assoc($res);
} else {
$smtpRow = [];
}
} else {
$smtpRow = [];
}
$smtpConfig = $smtpRow ?? [];
?>
<!DOCTYPE html>
<html lang="<?php echo $currentLang==='zh'?'zh-CN':'en'; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SMTP Config - Advantest</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f9fafc;font-family:"Inter","PingFang SC","Microsoft YaHei";color:#1d2939;line-height:1.6;display:flex;flex-direction:column;height:100vh;overflow:hidden;}
.wrap-main{display:flex;flex:1;overflow:hidden;}
.sidebar{width:240px;background:#152c5b;color:#e2e8f0;flex-shrink:0;overflow-y:auto;}
.sidebar-brand{padding:26px 24px;border-bottom:1px solid rgba(255,255,255,0.1);}
.sidebar-brand-en{font-size:20px;font-weight:700;letter-spacing:1px;color:#fff;}
.sidebar-brand-cn{font-size:13px;color:#a5b4fc;margin-top:4px;}
.sidebar-menu{padding:16px 0;}
.menu-title{padding:10px 24px;font-size:12px;color:#94a3b8;letter-spacing:1px;}
.menu-item{display:flex;align-items:center;gap:12px;padding:14px 24px;cursor:pointer;font-size:14px;transition:0.2s;color:#cbd5e1;}
.menu-item i{font-size:16px;width:20px;text-align:center;}
.menu-item.active{background:rgba(255,255,255,0.12);color:#fff;border-left:4px solid #3b82f6;}
.menu-item:hover:not(.active){background:rgba(255,255,255,0.06);color:#fff;}
.main-content{flex:1;overflow-y:auto;display:flex;flex-direction:column;}
.header{background:#ffffff;border-bottom:1px solid #eef2fb;color:#152c5b;padding:20px 40px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 12px rgba(21,44,91,0.05);flex-shrink:0;}
.header-left{display:flex;align-items:center;gap:14px;}
.header-brand-en{font-size:24px;font-weight:700;letter-spacing:2px;color:#152c5b;}
.header-brand-cn{font-size:15px;color:#64748b;}
.header-right{display:flex;align-items:center;gap:18px;font-size:14px;flex-wrap:wrap;}
.user-info{color:#475569;display:flex;align-items:center;gap:6px;}
.btn-base{border:none;padding:9px 20px;border-radius:8px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:all 0.2s ease;}
.btn-primary{background:#152c5b;color:#fff;}
.container{padding:36px 40px;flex:1;}
.base-card{background:#fff;border-radius:16px;padding:32px;box-shadow:0 3px 18px rgba(21,44,91,0.06);border:1px solid #eef2fb;max-width:700px;}
.title-wrap{display:flex;align-items:center;gap:12px;font-size:19px;font-weight:600;color:#152c5b;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid #eef2fb;}
.form-row{margin-bottom:20px;}
.form-row label{display:block;margin-bottom:8px;color:#475569;font-weight:500;}
.form-row input{width:100%;padding:10px 14px;border:1px solid #cbd5e1;border-radius:8px;font-size:14px;}
</style>
</head>
<body>
<div class="wrap-main">
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
<div class="sidebar-menu">
<div class="menu-title">Monitor Panel</div>
<div class="menu-item" onclick="location.href='dashboard.php'">
<i class="fa fa-line-chart"></i><span>Alert Overview</span>
</div>
<div class="menu-item" onclick="location.href='work_order.php'">
<i class="fa fa-ticket"></i><span>Work Order System</span>
</div>
<div class="menu-title">System Manage</div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i><span>System Config</span>
</div>
<div class="menu-item active">
<i class="fa fa-envelope-o"></i><span><?php echo $t['smtp_config']; ?></span>
</div>
</div>
</div>
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn"><?php echo $t['page_subtitle']; ?></div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>User: <?php echo $userName; ?></span>
<button class="btn-base btn-primary" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>Logout</button>
</div>
</div>
<div class="container">
<div class="base-card">
<div class="title-wrap"><i class="fa fa-envelope-o"></i><?php echo $t['smtp_config']; ?></div>
<form method="post">
<input type="hidden" name="act" value="save_smtp">
<div class="form-row">
<label><?php echo $t['smtp_host']; ?></label>
<input name="smtp_host" value="<?php echo htmlspecialchars($cfg['smtp_host']); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_port']; ?></label>
<input name="smtp_port" type="number" value="<?php echo intval($cfg['smtp_port']); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_ssl']; ?></label>
<input type="checkbox" name="smtp_ssl" value="1" <?php echo $cfg['smtp_ssl']==1?'checked':''; ?>>
</div>
<div class="form-row">
<label><?php echo $t['smtp_account']; ?></label>
<input name="smtp_user" value="<?php echo htmlspecialchars($cfg['smtp_user']); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_pwd']; ?></label>
<input name="smtp_pass" type="password" value="<?php echo htmlspecialchars($cfg['smtp_pass']); ?>">
</div>
<div class="form-row">
<label><?php echo $t['smtp_from']; ?></label>
<input name="smtp_from" value="<?php echo htmlspecialchars($cfg['send_from']); ?>">
</div>
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i><?php echo $t['save_smtp']; ?></button>
</form>
</div>
</div>
</div>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More