Initial commit

This commit is contained in:
2026-07-07 13:29:22 +08:00
commit 3fdd9ec927
154 changed files with 56566 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>
+367
View File
@@ -0,0 +1,367 @@
<?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");
$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:"风暴告警"
};
// 时区回滚:原生浏览器本地时区,删除手动+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>
+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;
?>
@@ -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;
?>
+407
View File
@@ -0,0 +1,407 @@
<?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;
}
// 接收自定义日期参数,不传则取今日
$targetDate = $_GET['date'] ?? date("Y-m-d");
// 简单日期格式校验 Y-m-d
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) {
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>400,"msg"=>"日期格式错误,请使用 Y-m-d,例如 2026-07-01"], JSON_UNESCAPED_UNICODE);
exit;
}
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$targetDate' 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=告警报表_" . $targetDate . ".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;
}
// ============后台管理专用接口 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);
?>
@@ -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);
?>
@@ -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);
?>
@@ -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);
?>
@@ -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);
?>
+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>
+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;
?>
@@ -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;
@@ -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);
?>
+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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
@@ -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;
?>
+145
View File
@@ -0,0 +1,145 @@
<?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);
// ====================== 双库固定连接(修复null根源) ======================
$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");
// ========================================================================
// 获取当前访问页面文件名(去掉.php后缀)
$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php');
// 非超级管理员校验页面权限
if ($isAdmin !== 1) {
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId";
$permRes = mysqli_query($connMonitor, $permSql);
$allowPages = [];
if ($permRes) {
while ($row = mysqli_fetch_assoc($permRes)) {
$allowPages[] = $row['page_key'];
}
}
// 无权限拦截
if (!in_array($currentPage, $allowPages)) {
echo "<script>alert('无访问权限!');history.back();</script>";
exit;
}
}
// 读取右上角未读消息数量(修复第79/84/86行报错)
$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])
];
}
}
}
?>
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>
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>
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>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
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>
+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
+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>
+533
View File
@@ -0,0 +1,533 @@
<?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;
}
/* 全部文本输入框统一短款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; ?></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>
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 434 KiB

File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+412
View File
@@ -0,0 +1,412 @@
<?php require_once 'auth.php';
$pageTitle = "系统权限管理";
$activeTab = $_GET['tab'] ?? 'role';
$conn = $connMonitor;
// ========== 角色操作逻辑 ==========
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('角色名称不能为空');location.href='system_manage.php?tab=role';</script>";
exit;
}
if ($roleId == 0) {
mysqli_query($conn, "INSERT INTO sys_role(role_name) VALUES('".mysqli_real_escape_string($conn,$roleName)."')");
$roleId = mysqli_insert_id($conn);
} else {
mysqli_query($conn, "UPDATE sys_role SET role_name='".mysqli_real_escape_string($conn,$roleName)."' WHERE id=$roleId");
mysqli_query($conn, "DELETE FROM sys_role_permission WHERE role_id=$roleId");
}
foreach ($pages as $page) {
$page = mysqli_real_escape_string($conn, $page);
mysqli_query($conn, "INSERT IGNORE INTO sys_role_permission(role_id,page_key) VALUES($roleId,'$page')");
}
echo "<script>alert('角色保存成功');location.href='system_manage.php?tab=role';</script>";
exit;
}
if (!empty($_GET['del_role'])) {
$delId = intval($_GET['del_role']);
mysqli_query($conn, "DELETE FROM sys_role_permission WHERE role_id=$delId");
mysqli_query($conn, "DELETE FROM sys_role WHERE id=$delId");
header("Location: system_manage.php?tab=role");
exit;
}
// 修复警告:强制初始化为空数组,in_array不会报null
$editRole = ['id'=>0,'role_name'=>'','perms'=>[]];
if (!empty($_GET['edit_role'])) {
$eid = intval($_GET['edit_role']);
$er = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_role WHERE id=$eid"));
if ($er) {
$editRole = $er;
$permRes = mysqli_query($conn, "SELECT page_key FROM sys_role_permission WHERE role_id=$eid");
$permsArr = [];
while ($p = mysqli_fetch_assoc($permRes)) {
$permsArr[] = $p['page_key'];
}
$editRole['perms'] = $permsArr;
}
}
$roleList = [];
$roleRes = mysqli_query($conn, "SELECT * FROM sys_role ORDER BY id DESC");
while ($r = mysqli_fetch_assoc($roleRes)) $roleList[] = $r;
// ========== 用户操作逻辑(沿用bak可创建逻辑 + 自动生成uid修复空值) ==========
if ($_POST['act'] == "save_user") {
$uid = intval($_POST['user_id'] ?? 0);
$username = trim($_POST['username']);
$realName = trim($_POST['real_name']);
$roleId = intval($_POST['role_id']);
$pwd = trim($_POST['password']);
$status = intval($_POST['status']);
if (empty($username) || empty($realName) || $roleId == 0) {
echo "<script>alert('账号、姓名、角色不能为空');location.href='system_manage.php?tab=user';</script>";
exit;
}
if ($uid == 0) {
if (empty($pwd)) {
echo "<script>alert('新建用户必须填写密码');location.href='system_manage.php?tab=user';</script>";
exit;
}
// 自动生成唯一数字uid,解决空uid问题
$maxUidRow = mysqli_fetch_assoc(mysqli_query($conn, "SELECT MAX(CAST(uid AS UNSIGNED)) max_uid FROM sys_user"));
$newUid = empty($maxUidRow['max_uid']) ? 1000 : intval($maxUidRow['max_uid']) + 1;
$pwdHash = password_hash($pwd, PASSWORD_DEFAULT);
// INSERT 补充uid字段,不再空白
$insertSql = "INSERT INTO sys_user(uid,username,real_name,password,role_id,enable,is_admin)
VALUES('$newUid','".mysqli_real_escape_string($conn,$username)."','".mysqli_real_escape_string($conn,$realName)."','$pwdHash',$roleId,$status,0)";
mysqli_query($conn, $insertSql);
} else {
$updateSql = "UPDATE sys_user SET
username='".mysqli_real_escape_string($conn,$username)."',
real_name='".mysqli_real_escape_string($conn,$realName)."',
role_id=$roleId,
enable=$status
WHERE id=$uid";
if (!empty($pwd)) {
$pwdHash = password_hash($pwd, PASSWORD_DEFAULT);
$updateSql .= ", password='$pwdHash'";
}
mysqli_query($conn, $updateSql);
}
echo "<script>alert('用户保存成功');location.href='system_manage.php?tab=user';</script>";
exit;
}
if (!empty($_GET['del_user'])) {
$delId = intval($_GET['del_user']);
mysqli_query($conn, "DELETE FROM sys_user_msg WHERE user_id=$delId");
mysqli_query($conn, "DELETE FROM sys_user WHERE id=$delId");
header("Location: system_manage.php?tab=user");
exit;
}
$roleOptionHtml = "";
$roleDropRes = mysqli_query($conn, "SELECT id,role_name FROM sys_role");
while ($r = mysqli_fetch_assoc($roleDropRes)) {
$roleOptionHtml .= "<option value='{$r['id']}'>{$r['role_name']}</option>";
}
// 用户列表过滤空uid异常用户
$userList = [];
$uRes = mysqli_query($conn, "SELECT u.*,r.role_name FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid <> '' ORDER BY u.id DESC");
while ($u = mysqli_fetch_assoc($uRes)) $userList[] = $u;
$editUser = ['id'=>0,'username'=>'','real_name'=>'','role_id'=>0,'status'=>1];
if (!empty($_GET['edit_user'])) {
$eid = intval($_GET['edit_user']);
$eu = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_user WHERE id=$eid"));
if ($eu) $editUser = $eu;
}
$allPagePerms = [
['key'=>'dashboard','name'=>'告警总览'],
['key'=>'work_order','name'=>'工单系统'],
['key'=>'disk','name'=>'磁盘容量'],
['key'=>'history_query','name'=>'历史查询'],
['key'=>'smtp_config','name'=>'消息通知配置'],
['key'=>'system_manage','name'=>'系统权限管理'],
];
?>
<!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",sans-serif;color:#1e293b;line-height:1.5;display:flex;flex-direction:column;height:100vh;overflow:hidden;font-size:13px;}
.wrap-main{display:flex;flex:1;overflow:hidden;}
/* 侧边栏 */
.sidebar{width:220px;background:#152c5b;color:#cbd5e1;flex-shrink:0;overflow-y:auto;}
.sidebar-brand{padding:20px 16px;border-bottom:1px solid rgba(255,255,255,0.06);}
.sidebar-brand-en{font-size:18px;font-weight:700;color:#fff;letter-spacing:1px;}
.sidebar-brand-cn{font-size:12px;color:#a5b4fc;margin-top:4px;}
.sidebar-menu{padding:12px 0;}
.menu-title{padding:8px 16px 4px;font-size:11px;color:#94a3b8;text-transform:uppercase;letter-spacing:1px;}
.menu-item{display:flex;align-items:center;gap:10px;padding:10px 16px;cursor:pointer;font-size:13px;color:#cbd5e1;transition:0.2s;}
.menu-item i{width:18px;text-align:center;font-size:14px;}
.menu-item.active{background:rgba(37,99,235,0.15);color:#fff;border-left:4px solid #2563eb;}
.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 紧凑整洁 */
.header{background:#fff;border-bottom:1px solid #e2e8f0;padding:12px 20px;display:flex;justify-content:space-between;align-items:center;flex-shrink:0;}
.header-left{display:flex;align-items:center;gap:12px;}
.header-brand-en{font-size:20px;font-weight:700;color:#152c5b;}
.header-brand-cn{font-size:12px;color:#64748b;}
.header-right{display:flex;align-items:center;gap:12px;font-size:12px;flex-wrap:wrap;}
.user-info,.time-info,.lang-box{display:flex;align-items:center;gap:4px;color:#475569;}
.lang-box select{padding:3px 6px;border:1px solid #cbd5e1;border-radius:4px;font-size:12px;}
.msg-badge{position:relative;cursor:pointer;}
.msg-badge span{position:absolute;top:-5px;right:-5px;background:#ef4444;color:#fff;font-size:10px;border-radius:99px;width:14px;height:14px;text-align:center;line-height:14px;}
/* 按钮 */
.btn-base{border:none;padding:7px 16px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:500;display:flex;align-items:center;gap:6px;transition:0.2s;}
.btn-outline{background:#fff;color:#152c5b;border:1px solid #cbd5e1;}
.btn-outline:hover{background:#f1f5f9;}
.btn-primary{background:#2563eb;color:#fff;box-shadow:0 2px 8px rgba(37,99,235,0.15);}
.btn-primary:hover{background:#1d4ed8;}
/* 容器 */
.container{padding:16px 20px;flex:1;}
/* 卡片100%自适应宽度,移除max-width */
.base-card{background:#fff;border-radius:12px;padding:20px;border:1px solid #e2e8f0;box-shadow:0 1px 2px rgba(0,0,0,0.04);width:100%;margin-bottom:16px;}
.title-wrap{display:flex;align-items:center;gap:10px;font-size:14px;font-weight:600;color:#152c5b;margin-bottom:16px;padding-bottom:10px;border-bottom:1px solid #f1f5f9;}
.title-wrap i{color:#2563eb;font-size:16px;}
/* Tab */
.tab-wrap{display:flex;gap:2px;margin-bottom:16px;border-bottom:1px solid #e2e8f0;}
.tab-item{padding:8px 18px;cursor:pointer;border-bottom:2px solid transparent;color:#64748b;font-size:12px;font-weight:500;}
.tab-item.active{color:#2563eb;border-bottom-color:#2563eb;}
.tab-content{display:none;}
.tab-content.show{display:block;}
/* 表单响应式 */
.form-row{margin-bottom:14px;}
.form-row label{display:block;margin-bottom:4px;font-size:12px;font-weight:500;color:#334155;}
.form-row input,.form-row select{width:320px;height:36px;padding:0 12px;border:1px solid #d1d5db;border-radius:6px;font-size:12px;background:#f8fafc;transition:0.2s;}
.form-row input:hover,.form-row select:hover{border-color:#94a3b8;background:#fff;}
.form-row input:focus,.form-row select:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 2px rgba(37,99,235,0.1);background:#fff;}
/* 权限勾选 完全匹配你要求的3列自适应布局 */
.check-group{
display:grid;
grid-template-columns:repeat(3,max-content);
gap:10px 30px;
margin:8px 0 14px;
}
.check-item{
display:flex;
align-items:center;
gap:4px;
padding:6px 0;
border:none;
background:transparent;
font-size:12px;
white-space:nowrap;
}
/* 表格外层滚动容器,解决溢出 */
.table-wrap{width:100%;overflow-x:auto;margin-top:12px;}
table{min-width:700px;width:100%;border-collapse:collapse;}
th,td{border:1px solid #e2e8f0;padding:10px 12px;font-size:12px;text-align:left;white-space:nowrap;}
th{background:#f8fafc;font-weight:600;color:#1e293b;}
.operate a{margin-right:8px;color:#2563eb;text-decoration:none;font-size:12px;}
.operate a.del{color:#dc2626;}
/* 媒体查询 多分辨率兼容 */
@media screen and (max-width:1366px){
.sidebar{width:200px;}
.form-row input,.form-row select{width:280px;}
}
@media screen and (max-width:1080px){
.sidebar{width:180px;}
.form-row input,.form-row select{width:100%;}
}
@media screen and (max-width:768px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto;}
.container{padding:12px 14px;}
.header-right{gap:8px;flex-wrap:wrap;}
/* 移动端自动改为单列,防止横向溢出 */
.check-group{grid-template-columns:repeat(1, max-content);gap:10px 0;}
}
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;}
</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">业务模块</div>
<div class="menu-item" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i><span>告警总览</span></div>
<div class="menu-item" onclick="location.href='work_order.php'"><i class="fa fa-ticket"></i><span>工单系统</span></div>
<div class="menu-item" onclick="location.href='alert_list.php'"><i class="fa fa-search"></i><span>历史查询</span></div>
<div class="menu-title">系统管理</div>
<div class="menu-item" onclick="location.href='smtp_config.php'"><i class="fa fa-bell"></i><span>消息通知配置</span></div>
<div class="menu-item active"><i class="fa fa-cogs"></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"><?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-o"></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">
<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']; ?></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-cogs"></i><?php echo $pageTitle; ?></div>
<div class="tab-wrap">
<div class="tab-item <?php echo $activeTab=='role'?'active':''; ?>" data-tab="role">角色管理</div>
<div class="tab-item <?php echo $activeTab=='user'?'active':''; ?>" data-tab="user">用户管理</div>
</div>
<!-- 角色Tab -->
<div class="tab-content <?php echo $activeTab=='role'?'show':''; ?>" id="tab-role">
<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 ($allPagePerms 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 class="table-wrap">
<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="?tab=role&edit_role=<?php echo $r['id']; ?>">编辑</a>
<a href="?tab=role&del_role=<?php echo $r['id']; ?>" class="del" onclick="return confirm('确认删除该角色?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
<!-- 用户Tab -->
<div class="tab-content <?php echo $activeTab=='user'?'show':''; ?>" id="tab-user">
<form method="post">
<input type="hidden" name="act" value="save_user">
<input type="hidden" name="user_id" value="<?php echo $editUser['id']; ?>">
<div class="form-row">
<label>登录账号</label>
<input name="username" value="<?php echo htmlspecialchars($editUser['username']); ?>" placeholder="登录账号">
</div>
<div class="form-row">
<label>真实姓名</label>
<input name="real_name" value="<?php echo htmlspecialchars($editUser['real_name']); ?>" placeholder="员工姓名">
</div>
<div class="form-row">
<label>所属角色</label>
<select name="role_id">
<option value="">请选择角色</option>
<?php echo $roleOptionHtml; ?>
</select>
<script>$("select[name=role_id]").val(<?php echo $editUser['role_id']; ?>)</script>
</div>
<div class="form-row">
<label>登录密码(编辑留空不修改)</label>
<input name="password" type="password" placeholder="新建用户必填">
</div>
<div class="form-row">
<label>账号状态</label>
<select name="status">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
<script>$("select[name=status]").val(<?php echo $editUser['status']; ?>)</script>
</div>
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i>保存用户</button>
</form>
<div class="table-wrap">
<table>
<tr>
<th>ID</th>
<th>登录账号</th>
<th>姓名</th>
<th>所属角色</th>
<th>状态</th>
<th>操作</th>
</tr>
<?php foreach ($userList as $u): ?>
<tr>
<td><?php echo $u['id']; ?></td>
<td><?php echo htmlspecialchars($u['username']); ?></td>
<td><?php echo htmlspecialchars($u['real_name']); ?></td>
<td><?php echo htmlspecialchars($u['role_name'] ?? '无'); ?></td>
<td><?php echo $u['enable']==1?'启用':'禁用'; ?></td>
<td class="operate">
<a href="?tab=user&edit_user=<?php echo $u['id']; ?>">编辑</a>
<a href="?tab=user&del_user=<?php echo $u['id']; ?>" class="del" onclick="return confirm('确认删除该用户?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
$(".tab-item").click(function(){
const tab = $(this).data("tab");
location.href = "system_manage.php?tab="+tab;
})
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("system_manage.php",{action:"save_lang",lang:lang},()=>location.reload());
})
</script>
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
<?php require_once 'auth.php';
$pageTitle = "系统权限管理";
$activeTab = $_GET['tab'] ?? 'role';
// 强制绑定权限库monitor,所有用户/角色操作全部走此连接
$conn = $connMonitor;
// ========== 角色操作逻辑 ==========
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('角色名称不能为空');location.href='system_manage.php?tab=role';</script>";
exit;
}
if ($roleId == 0) {
mysqli_query($conn, "INSERT INTO sys_role(role_name) VALUES('".mysqli_real_escape_string($conn,$roleName)."')");
$roleId = mysqli_insert_id($conn);
} else {
mysqli_query($conn, "UPDATE sys_role SET role_name='".mysqli_real_escape_string($conn,$roleName)."' WHERE id=$roleId");
mysqli_query($conn, "DELETE FROM sys_role_permission WHERE role_id=$roleId");
}
foreach ($pages as $page) {
$page = mysqli_real_escape_string($conn, $page);
mysqli_query($conn, "INSERT IGNORE INTO sys_role_permission(role_id,page_key) VALUES($roleId,'$page')");
}
echo "<script>alert('角色保存成功');location.href='system_manage.php?tab=role';</script>";
exit;
}
if (!empty($_GET['del_role'])) {
$delId = intval($_GET['del_role']);
mysqli_query($conn, "DELETE FROM sys_role_permission WHERE role_id=$delId");
mysqli_query($conn, "DELETE FROM sys_role WHERE id=$delId");
header("Location: system_manage.php?tab=role");
exit;
}
$editRole = ['id'=>0,'role_name'=>'','perms'=>[]];
if (!empty($_GET['edit_role'])) {
$eid = intval($_GET['edit_role']);
$er = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_role WHERE id=$eid"));
if ($er) {
$editRole = $er;
$permRes = mysqli_query($conn, "SELECT page_key FROM sys_role_permission WHERE role_id=$eid");
while ($p = mysqli_fetch_assoc($permRes)) $editRole['perms'][] = $p['page_key'];
}
}
$roleList = [];
$roleRes = mysqli_query($conn, "SELECT * FROM sys_role ORDER BY id DESC");
while ($r = mysqli_fetch_assoc($roleRes)) $roleList[] = $r;
// ========== 用户操作逻辑(修复入库逻辑) ==========
if ($_POST['act'] == "save_user") {
$uid = intval($_POST['user_id'] ?? 0);
$username = trim($_POST['username']);
$realName = trim($_POST['real_name']);
$roleId = intval($_POST['role_id']);
$pwd = trim($_POST['password']);
$status = intval($_POST['status']);
if (empty($username) || empty($realName) || $roleId == 0) {
echo "<script>alert('账号、姓名、角色不能为空');location.href='system_manage.php?tab=user';</script>";
exit;
}
if ($uid == 0) {
// 新建用户必须填写密码,使用password_hash加密(和登录校验一致)
if (empty($pwd)) {
echo "<script>alert('新建用户必须填写密码');location.href='system_manage.php?tab=user';</script>";
exit;
}
$pwdHash = password_hash($pwd, PASSWORD_DEFAULT);
// 适配monitor库sys_user真实字段:uid自动生成、enable状态、is_admin默认0
$insertSql = "INSERT INTO sys_user(username,real_name,password,role_id,enable,is_admin)
VALUES('".mysqli_real_escape_string($conn,$username)."','".mysqli_real_escape_string($conn,$realName)."','$pwdHash',$roleId,$status,0)";
mysqli_query($conn, $insertSql);
} else {
// 编辑用户,密码留空不修改
$updateSql = "UPDATE sys_user SET
username='".mysqli_real_escape_string($conn,$username)."',
real_name='".mysqli_real_escape_string($conn,$realName)."',
role_id=$roleId,
enable=$status
WHERE id=$uid";
if (!empty($pwd)) {
$pwdHash = password_hash($pwd, PASSWORD_DEFAULT);
$updateSql .= ", password='$pwdHash'";
}
mysqli_query($conn, $updateSql);
}
echo "<script>alert('用户保存成功');location.href='system_manage.php?tab=user';</script>";
exit;
}
if (!empty($_GET['del_user'])) {
$delId = intval($_GET['del_user']);
mysqli_query($conn, "DELETE FROM sys_user_msg WHERE user_id=$delId");
mysqli_query($conn, "DELETE FROM sys_user WHERE id=$delId");
header("Location: system_manage.php?tab=user");
exit;
}
// 角色下拉选项
$roleOptionHtml = "";
$roleDropRes = mysqli_query($conn, "SELECT id,role_name FROM sys_role");
while ($r = mysqli_fetch_assoc($roleDropRes)) {
$roleOptionHtml .= "<option value='{$r['id']}'>{$r['role_name']}</option>";
}
// 用户列表
$userList = [];
$uRes = mysqli_query($conn, "SELECT u.*,r.role_name FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id ORDER BY u.id DESC");
while ($u = mysqli_fetch_assoc($uRes)) $userList[] = $u;
// 编辑用户数据
$editUser = ['id'=>0,'username'=>'','real_name'=>'','role_id'=>0,'status'=>1];
if (!empty($_GET['edit_user'])) {
$eid = intval($_GET['edit_user']);
$eu = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_user WHERE id=$eid"));
if ($eu) $editUser = $eu;
}
// 全部页面权限清单
$allPagePerms = [
['key'=>'dashboard','name'=>'告警总览'],
['key'=>'work_order','name'=>'工单系统'],
['key'=>'disk','name'=>'磁盘容量'],
['key'=>'history_query','name'=>'历史查询'],
['key'=>'smtp_config','name'=>'消息通知配置'],
['key'=>'system_manage','name'=>'系统权限管理'],
];
?>
<!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",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:#cbd5e1; 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:700; color:#fff; letter-spacing:1px; }
.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; letter-spacing:1px; }
.menu-item{ display:flex; align-items:center; gap:12px; padding:13px 24px; cursor:pointer; font-size:14px; color:#cbd5e1; transition:0.2s; }
.menu-item i{ width:20px; text-align:center; font-size:16px; }
.menu-item.active{ background:rgba(37,99,235,0.15); color:#fff; border-left:4px solid #2563eb; }
.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; flex-shrink:0; }
.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:#f1f5f9;}
.btn-primary{background:linear-gradient(135deg,#1d4ed8,#2563eb);color:#fff;box-shadow:0 6px 18px rgba(37,99,235,0.2);}
.btn-primary:hover{transform:translateY(-1px);box-shadow:0 8px 24px rgba(37,99,235,0.25);}
.container{padding:36px 40px;flex:1;}
.base-card{background:#fff;border-radius:18px;padding:36px;border:1px solid #e2e8f0;box-shadow:0 1px 3px rgba(0,0,0,0.04),0 8px 24px rgba(15,23,42,0.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;font-size:18px;}
.tab-wrap{display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid #e2e8f0;}
.tab-item{padding:12px 24px;cursor:pointer;border-bottom:3px solid transparent;color:#64748b;font-weight:500;transition:0.2s;}
.tab-item.active{color:#2563eb;border-bottom-color:#2563eb;}
.tab-item:hover{color:#152c5b;}
.tab-content{display:none;}
.tab-content.show{display:block;}
.form-row{margin-bottom:22px;}
.form-row label{display:block;margin-bottom:8px;font-size:13px;font-weight:500;color:#334155;letter-spacing:0.2px;}
.form-row input,.form-row select{width:360px;height:46px;padding:0 16px;border:1px solid #d1d5db;border-radius:10px;font-size:14px;transition:all 0.2s ease;background:#f8fafc;}
.form-row input:hover,.form-row select:hover{border-color:#94a3b8;background:#fff;}
.form-row input:focus,.form-row select:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px rgba(37,99,235,0.1);background:#fff;}
.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;background:#f8fafc;}
table{width:100%;border-collapse:collapse;margin-top:20px;}
th,td{border:1px solid #e2e8f0;padding:14px;text-align:left;font-size:14px;}
th{background:#f8fafc;font-weight:600;color:#1e293b;}
.operate a{margin-right:12px;color:#2563eb;text-decoration:none;}
.operate a.del{color:#dc2626;}
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;}
@media screen and (max-width: 768px) {
.sidebar{width:200px;}
.container{padding:24px 20px;}
.form-row input,.form-row select{width:100%;}
.check-group{grid-template-columns:repeat(2,1fr);}
}
</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-cogs"></i><span><?php echo $pageTitle; ?></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-o"></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']; ?></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-cogs"></i><?php echo $pageTitle; ?></div>
<div class="tab-wrap">
<div class="tab-item <?php echo $activeTab=='role'?'active':''; ?>" data-tab="role">角色管理</div>
<div class="tab-item <?php echo $activeTab=='user'?'active':''; ?>" data-tab="user">用户管理</div>
</div>
<!-- 角色Tab -->
<div class="tab-content <?php echo $activeTab=='role'?'show':''; ?>" id="tab-role">
<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 ($allPagePerms 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>
<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="?tab=role&edit_role=<?php echo $r['id']; ?>">编辑</a>
<a href="?tab=role&del_role=<?php echo $r['id']; ?>" class="del" onclick="return confirm('确认删除该角色?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<!-- 用户Tab -->
<div class="tab-content <?php echo $activeTab=='user'?'show':''; ?>" id="tab-user">
<form method="post">
<input type="hidden" name="act" value="save_user">
<input type="hidden" name="user_id" value="<?php echo $editUser['id']; ?>">
<div class="form-row">
<label>登录账号</label>
<input name="username" value="<?php echo htmlspecialchars($editUser['username']); ?>" placeholder="登录账号">
</div>
<div class="form-row">
<label>真实姓名</label>
<input name="real_name" value="<?php echo htmlspecialchars($editUser['real_name']); ?>" placeholder="员工姓名">
</div>
<div class="form-row">
<label>所属角色</label>
<select name="role_id">
<option value="">请选择角色</option>
<?php echo $roleOptionHtml; ?>
</select>
<script>$("select[name=role_id]").val(<?php echo $editUser['role_id']; ?>)</script>
</div>
<div class="form-row">
<label>登录密码(编辑留空不修改)</label>
<input name="password" type="password" placeholder="新建用户必填">
</div>
<div class="form-row">
<label>账号状态</label>
<select name="status">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
<script>$("select[name=status]").val(<?php echo $editUser['status']; ?>)</script>
</div>
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i>保存用户</button>
</form>
<table>
<tr>
<th>ID</th>
<th>登录账号</th>
<th>姓名</th>
<th>所属角色</th>
<th>状态</th>
<th>操作</th>
</tr>
<?php foreach ($userList as $u): ?>
<tr>
<td><?php echo $u['id']; ?></td>
<td><?php echo htmlspecialchars($u['username']); ?></td>
<td><?php echo htmlspecialchars($u['real_name']); ?></td>
<td><?php echo htmlspecialchars($u['role_name'] ?? '无'); ?></td>
<td><?php echo $u['enable']==1?'启用':'禁用'; ?></td>
<td class="operate">
<a href="?tab=user&edit_user=<?php echo $u['id']; ?>">编辑</a>
<a href="?tab=user&del_user=<?php echo $u['id']; ?>" class="del" onclick="return confirm('确认删除该用户?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
$(".tab-item").click(function(){
const tab = $(this).data("tab");
location.href = "system_manage.php?tab="+tab;
})
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("system_manage.php",{action:"save_lang",lang:lang},()=>location.reload());
})
</script>
</body>
</html>
+378
View File
@@ -0,0 +1,378 @@
<?php require_once 'auth.php';
$pageTitle = "系统权限管理";
$activeTab = $_GET['tab'] ?? 'role';
$conn = $connMonitor;
// ========== 角色操作逻辑 ==========
// 保存角色
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('角色名称不能为空');location.href='system_manage.php?tab=role';</script>";
exit;
}
if ($roleId == 0) {
mysqli_query($conn, "INSERT INTO sys_role(role_name) VALUES('$roleName')");
$roleId = mysqli_insert_id($conn);
} else {
mysqli_query($conn, "UPDATE sys_role SET role_name='$roleName' WHERE id=$roleId");
mysqli_query($conn, "DELETE FROM sys_role_permission WHERE role_id=$roleId");
}
foreach ($pages as $page) {
$page = mysqli_real_escape_string($conn, $page);
mysqli_query($conn, "INSERT INTO sys_role_permission(role_id,page_key) VALUES($roleId,'$page')");
}
echo "<script>alert('角色保存成功');location.href='system_manage.php?tab=role';</script>";
exit;
}
// 删除角色
if (!empty($_GET['del_role'])) {
$delId = intval($_GET['del_role']);
mysqli_query($conn, "DELETE FROM sys_role_permission WHERE role_id=$delId");
mysqli_query($conn, "DELETE FROM sys_role WHERE id=$delId");
header("Location: system_manage.php?tab=role");
exit;
}
// 编辑角色数据
$editRole = ['id'=>0,'role_name'=>'','perms'=>[]];
if (!empty($_GET['edit_role'])) {
$eid = intval($_GET['edit_role']);
$er = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_role WHERE id=$eid"));
if ($er) {
$editRole = $er;
$permRes = mysqli_query($conn, "SELECT page_key FROM sys_role_permission WHERE role_id=$eid");
while ($p = mysqli_fetch_assoc($permRes)) $editRole['perms'][] = $p['page_key'];
}
}
// 角色列表
$roleList = [];
$roleRes = mysqli_query($conn, "SELECT * FROM sys_role ORDER BY id DESC");
while ($r = mysqli_fetch_assoc($roleRes)) $roleList[] = $r;
// ========== 用户操作逻辑 ==========
// 保存用户
if ($_POST['act'] == "save_user") {
$uid = intval($_POST['user_id'] ?? 0);
$username = trim($_POST['username']);
$realName = trim($_POST['real_name']);
$roleId = intval($_POST['role_id']);
$pwd = trim($_POST['password']);
$status = intval($_POST['status']);
if (empty($username) || empty($realName) || $roleId == 0) {
echo "<script>alert('账号、姓名、角色不能为空');location.href='system_manage.php?tab=user';</script>";
exit;
}
if ($uid == 0) {
if (empty($pwd)) {
echo "<script>alert('新建用户必须填写密码');location.href='system_manage.php?tab=user';</script>";
exit;
}
$pwdMd5 = md5($pwd);
mysqli_query($conn, "INSERT INTO sys_user(username,password,real_name,role_id,status,is_admin) VALUES('$username','$pwdMd5','$realName',$roleId,$status,0)");
} else {
$updateSql = "UPDATE sys_user SET username='$username',real_name='$realName',role_id=$roleId,status=$status WHERE id=$uid";
if (!empty($pwd)) {
$pwdMd5 = md5($pwd);
$updateSql .= ",password='$pwdMd5'";
}
mysqli_query($conn, $updateSql);
}
echo "<script>alert('用户保存成功');location.href='system_manage.php?tab=user';</script>";
exit;
}
// 删除用户
if (!empty($_GET['del_user'])) {
$delId = intval($_GET['del_user']);
mysqli_query($conn, "DELETE FROM sys_user_msg WHERE user_id=$delId");
mysqli_query($conn, "DELETE FROM sys_user WHERE id=$delId");
header("Location: system_manage.php?tab=user");
exit;
}
// 用户下拉角色选项
$roleOptionHtml = "";
$roleDropRes = mysqli_query($conn, "SELECT id,role_name FROM sys_role");
while ($r = mysqli_fetch_assoc($roleDropRes)) {
$roleOptionHtml .= "<option value='{$r['id']}'>{$r['role_name']}</option>";
}
// 用户列表
$userList = [];
$uRes = mysqli_query($conn, "SELECT u.*,r.role_name FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id ORDER BY u.id DESC");
while ($u = mysqli_fetch_assoc($uRes)) $userList[] = $u;
// 编辑用户数据
$editUser = ['id'=>0,'username'=>'','real_name'=>'','role_id'=>0,'status'=>1];
if (!empty($_GET['edit_user'])) {
$eid = intval($_GET['edit_user']);
$eu = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_user WHERE id=$eid"));
if ($eu) $editUser = $eu;
}
// 全部页面权限清单
$allPagePerms = [
['key'=>'dashboard','name'=>'告警总览'],
['key'=>'work_order','name'=>'工单系统'],
['key'=>'disk','name'=>'磁盘容量'],
['key'=>'history_query','name'=>'历史查询'],
['key'=>'smtp_config','name'=>'消息通知配置'],
['key'=>'system_manage','name'=>'系统权限管理'],
];
mysqli_close($conn);
?>
<!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;}
.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;}
/* Tab切换栏 */
.tab-wrap{display:flex;gap:4px;margin-bottom:24px;border-bottom:1px solid #e2e8f0;}
.tab-item{padding:12px 24px;cursor:pointer;border-bottom:3px solid transparent;color:#64748b;font-weight:500;}
.tab-item.active{color:#2563eb;border-bottom-color:#2563eb;}
.tab-content{display:none;}
.tab-content.show{display:block;}
/* 表单统一样式 */
.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,.form-row select{width:360px;height:46px;padding:0 16px;border:1px solid #d6dce5;border-radius:10px;font-size:14px;transition:all .2s ease;}
.form-row input:hover,.form-row select:hover{border-color:#94a3b8;}
.form-row input:focus,.form-row select:focus{outline:none;border-color:#2563eb;box-shadow:0 0 0 3px rgba(37,99,235,.12);}
/* 权限多选组 */
.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;}
/* 数字输入框去除箭头 */
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;}
</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-cogs"></i><span><?php echo $pageTitle; ?></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-cogs"></i><?php echo $pageTitle; ?></div>
<!-- Tab切换栏 -->
<div class="tab-wrap">
<div class="tab-item <?php echo $activeTab=='role'?'active':''; ?>" data-tab="role">角色管理</div>
<div class="tab-item <?php echo $activeTab=='user'?'active':''; ?>" data-tab="user">用户管理</div>
</div>
<!-- 模块1:角色管理 -->
<div class="tab-content <?php echo $activeTab=='role'?'show':''; ?>" id="tab-role">
<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 ($allPagePerms 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>
<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="?tab=role&edit_role=<?php echo $r['id']; ?>">编辑</a>
<a href="?tab=role&del_role=<?php echo $r['id']; ?>" class="del" onclick="return confirm('确认删除该角色?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
<!-- 模块2:用户管理 -->
<div class="tab-content <?php echo $activeTab=='user'?'show':''; ?>" id="tab-user">
<form method="post">
<input type="hidden" name="act" value="save_user">
<input type="hidden" name="user_id" value="<?php echo $editUser['id']; ?>">
<div class="form-row">
<label>登录账号</label>
<input name="username" value="<?php echo htmlspecialchars($editUser['username']); ?>">
</div>
<div class="form-row">
<label>真实姓名</label>
<input name="real_name" value="<?php echo htmlspecialchars($editUser['real_name']); ?>">
</div>
<div class="form-row">
<label>所属角色</label>
<select name="role_id">
<option value="">请选择角色</option>
<?php echo $roleOptionHtml; ?>
</select>
<script>$("select[name=role_id]").val(<?php echo $editUser['role_id']; ?>)</script>
</div>
<div class="form-row">
<label>登录密码(编辑留空不修改)</label>
<input name="password" type="password">
</div>
<div class="form-row">
<label>账号状态</label>
<select name="status">
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
<script>$("select[name=status]").val(<?php echo $editUser['status']; ?>)</script>
</div>
<button class="btn-base btn-primary" type="submit"><i class="fa fa-save"></i>保存用户</button>
</form>
<table>
<tr>
<th>ID</th>
<th>登录账号</th>
<th>姓名</th>
<th>所属角色</th>
<th>状态</th>
<th>操作</th>
</tr>
<?php foreach ($userList as $u): ?>
<tr>
<td><?php echo $u['id']; ?></td>
<td><?php echo htmlspecialchars($u['username']); ?></td>
<td><?php echo htmlspecialchars($u['real_name']); ?></td>
<td><?php echo htmlspecialchars($u['role_name'] ?? '无'); ?></td>
<td><?php echo $u['status']==1?'启用':'禁用'; ?></td>
<td class="operate">
<a href="?tab=user&edit_user=<?php echo $u['id']; ?>">编辑</a>
<a href="?tab=user&del_user=<?php echo $u['id']; ?>" class="del" onclick="return confirm('确认删除该用户?')">删除</a>
</td>
</tr>
<?php endforeach; ?>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
// Tab切换逻辑
$(".tab-item").click(function(){
const tab = $(this).data("tab");
location.href = "system_manage.php?tab="+tab;
})
// 实时时间
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("system_manage.php",{action:"save_lang",lang:lang},()=>location.reload());
})
</script>
</body>
</html>

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