Files
2026-07-09 16:31:30 +08:00

1115 lines
40 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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>