1115 lines
40 KiB
PHP
1115 lines
40 KiB
PHP
<?php
|
||
/* Hans.wang Copyright All Reserved */
|
||
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' ? '管理员' : '只读用户';
|
||
|
||
// ========== 缓存模块:屏蔽权限警告,无写入权限自动降级 ==========
|
||
$cacheDir = __DIR__ . '/cache';
|
||
$cacheExpireSec = 300;
|
||
$sysConfig = [];
|
||
|
||
// 静默创建缓存目录,抑制权限报错
|
||
if (!is_dir($cacheDir)) {
|
||
@mkdir($cacheDir, 0755, true);
|
||
}
|
||
$configCacheFile = $cacheDir . '/sys_config.cache';
|
||
|
||
// 读取缓存,可读且未过期则使用缓存
|
||
if (file_exists($configCacheFile) && is_readable($configCacheFile) && (time() - filemtime($configCacheFile)) < $cacheExpireSec) {
|
||
$cacheRaw = @file_get_contents($configCacheFile);
|
||
$sysConfig = json_decode($cacheRaw, true) ?: [];
|
||
} else {
|
||
// 缓存失效/无权限,直接查询数据库
|
||
$dbHost = "10.150.117.190";
|
||
$dbUser = "root";
|
||
$dbPwd = "hp93000";
|
||
$dbName = "alert_mail_stat";
|
||
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
|
||
|
||
if ($connConfig) {
|
||
mysqli_set_charset($connConfig, "utf8mb4");
|
||
$configSql = "SELECT k,v FROM sys_config";
|
||
$configRes = mysqli_query($connConfig, $configSql);
|
||
|
||
while ($row = mysqli_fetch_assoc($configRes)) {
|
||
$sysConfig[$row['k']] = trim($row['v'] ?? '');
|
||
}
|
||
mysqli_close($connConfig);
|
||
|
||
// 无写入权限静默跳过,不抛出警告
|
||
@file_put_contents($configCacheFile, json_encode($sysConfig, JSON_UNESCAPED_UNICODE));
|
||
}
|
||
}
|
||
|
||
// ========== 修复标题叠加Bug:纯覆盖赋值,无拼接污染 ==========
|
||
$rawPageTitle = trim($sysConfig['page_title'] ?? '');
|
||
$pageTitle = $rawPageTitle === '' ? "告警监控系统" : $rawPageTitle;
|
||
$pageTitle = htmlspecialchars($pageTitle);
|
||
|
||
$themeColor = htmlspecialchars(trim($sysConfig['theme_color'] ?? "#409eff"));
|
||
$pageSize = trim($sysConfig['page_size'] ?? "20");
|
||
$footerText = htmlspecialchars(trim($sysConfig['footer_text'] ?? ""));
|
||
$sidebarRawLinks = trim($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 && trim($item[0]) !== '' && trim($item[1]) !== '') {
|
||
$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>
|
||
/* Hans.wang Copyright All Reserved */
|
||
* {
|
||
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;
|
||
}
|
||
|
||
/* 底部版权 */
|
||
.footer-copyright {
|
||
text-align: center;
|
||
padding: 16px;
|
||
color: #64748b;
|
||
font-size: 13px;
|
||
border-top: 1px solid #eef2fb;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
/* 响应式 */
|
||
@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">
|
||
<!-- 告警面板 -->
|
||
<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>
|
||
|
||
<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>
|
||
|
||
<!-- 磁盘面板 -->
|
||
<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 class="footer-copyright">
|
||
<?php echo $footerText; ?> © Hans.wang 版权所有
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
/* Hans.wang Copyright All Reserved */
|
||
const api = "./api/index.php";
|
||
let chartInstance = null;
|
||
let diskSourceData = [];
|
||
let currentDiskIp = "all";
|
||
let diskLoaded = false;
|
||
let loadTimer = null;
|
||
|
||
// 侧边菜单切换
|
||
$(".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" && !diskLoaded) {
|
||
loadDiskData();
|
||
diskLoaded = true;
|
||
}
|
||
})
|
||
|
||
// 工具函数
|
||
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>`;
|
||
}
|
||
|
||
// 统一AJAX封装,无痕禁用缓存
|
||
function ajaxGet(url, callback) {
|
||
$.ajax({
|
||
url: url,
|
||
type: "GET",
|
||
cache: false,
|
||
headers: {
|
||
"Cache-Control": "no-cache, no-store, must-revalidate",
|
||
"Pragma": "no-cache",
|
||
"Expires": "0"
|
||
},
|
||
dataType: "json",
|
||
success: callback
|
||
});
|
||
}
|
||
|
||
// 加载活跃告警
|
||
function loadActiveAlert() {
|
||
ajaxGet(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() {
|
||
ajaxGet(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() {
|
||
ajaxGet(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() {
|
||
ajaxGet(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");
|
||
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>
|
||
`);
|
||
ajaxGet("./api/df.php", function (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();
|
||
loadTimer = setInterval(loadAllData, 60000);
|
||
setInterval(updateTime, 1000);
|
||
$("#logoutBtn").click(function () {
|
||
location.href = "logout.php";
|
||
})
|
||
$(window).on("beforeunload", function () {
|
||
clearInterval(loadTimer);
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|