init 全新仓库,清除全部历史
This commit is contained in:
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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_config,dashboard读取渲染)
|
||||
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_config,dashboard侧边读取渲染)
|
||||
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>
|
||||
@@ -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_config,dashboard读取渲染)
|
||||
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_config,dashboard侧边读取渲染)
|
||||
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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
?>
|
||||
@@ -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"=>"非法操作"]);
|
||||
?>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
header("Content-Type:application/json;charset=utf-8");
|
||||
echo json_encode(["code"=>200,"msg"=>"PHP正常,mysqli已加载"]);
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
?>
|
||||
@@ -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;
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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 BOM,Windows 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);
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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"=>"密码错误"]);
|
||||
}
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"user_id": "admin001",
|
||||
"username": "admin",
|
||||
"role": "admin",
|
||||
"password": "e10adc3949ba59abbe56e057f20f883e"
|
||||
}
|
||||
@@ -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"=>"非法操作"]);
|
||||
?>
|
||||
@@ -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);
|
||||
?>
|
||||
@@ -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;
|
||||
?>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?php
|
||||
$raw = "Admin@2026";
|
||||
$hash = password_hash($raw, PASSWORD_DEFAULT);
|
||||
echo "哈希:" . $hash . "<br>";
|
||||
var_dump(password_verify($raw, $hash));
|
||||
?>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
$rawPwd = "自定义密码";
|
||||
echo password_hash($rawPwd, PASSWORD_DEFAULT);
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
session_start();
|
||||
session_destroy();
|
||||
header("Location: login.php");
|
||||
exit;
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -0,0 +1,20 @@
|
||||
# 1. 系统编译工具
|
||||
yum install gcc gcc-c++ python36-devel -y
|
||||
# 2. 清理冲突socketio包
|
||||
pip3 uninstall flask-socketio python-socketio eventlet greenlet -y
|
||||
# 3. 安装基础依赖
|
||||
pip3 install flask docker --user
|
||||
# 4. docker权限
|
||||
usermod -aG docker root
|
||||
newgrp docker
|
||||
# 5. 验证docker连通
|
||||
python3 -c "import docker;cli=docker.DockerClient();print('Docker连接正常')"
|
||||
# 6. 后台启动服务
|
||||
nohup python3 /opt/alert_dashboard/www/api/docker_api.py > /opt/alert_dashboard/www/api/docker_api.log 2>&1 &
|
||||
# 7. 防火墙放行端口
|
||||
firewall-cmd --add-port=8090/tcp --permanent
|
||||
firewall-cmd --reload
|
||||
# 8. 查看运行状态
|
||||
sleep 2
|
||||
ps aux | grep docker_api.py
|
||||
tail -n 20 /opt/alert_dashboard/www/api/docker_api.log
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
+45
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
@@ -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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user