Initial commit

This commit is contained in:
2026-07-07 13:29:22 +08:00
commit 3fdd9ec927
154 changed files with 56566 additions and 0 deletions
File diff suppressed because one or more lines are too long
+20
View File
@@ -0,0 +1,20 @@
services:
php:
image: php:7.4-fpm-alpine
volumes:
- ./www:/usr/share/nginx/html
- ./www/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf
command: >
sh -c "apk add --no-cache $PHPIZE_DEPS && docker-php-ext-install mysqli pdo_mysql && apk del $PHPIZE_DEPS && php-fpm"
nginx:
image: nginx:stable-alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./www:/usr/share/nginx/html
depends_on:
- php
# 使用nc探测tcp端口,不再用wgetwget不兼容fastcgi
command: ["/bin/sh", "-c", "until nc -z php 9000; do echo '等待php-fpm端口就绪...'; sleep 1; done; nginx -g 'daemon off;'"]
+23
View File
@@ -0,0 +1,23 @@
services:
php:
image: php:7.4-fpm-alpine
user: "101:101"
volumes:
- ./www:/usr/share/nginx/html
# 关键:覆盖fpm监听配置
- ./www/php-fpm/www.conf:/usr/local/etc/php-fpm.d/www.conf
# 直接apk安装mysqli,不编译,避免gcc全家桶
command: >
sh -c "apk add --no-cache php7.4-mysqli && php-fpm"
nginx:
image: nginx:stable-alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./www:/usr/share/nginx/html
depends_on:
- php
# alpine自带wget等待php端口就绪
command: ["/bin/sh", "-c", "until wget -q -T1 php:9000; do echo '等待php-fpm就绪...'; sleep 1; done; nginx -g 'daemon off;'"]
+17
View File
@@ -0,0 +1,17 @@
services:
php:
image: php:7.4-fpm-alpine
volumes:
- ./www:/usr/share/nginx/html
# 新增安装mysqli扩展
command: >
sh -c "docker-php-ext-install mysqli && docker-php-ext-enable mysqli && php-fpm"
nginx:
image: nginx:stable-alpine
ports:
- "8080:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./www:/usr/share/nginx/html
depends_on:
- php
+54
View File
@@ -0,0 +1,54 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm index.php;
# 开启Gzip全局压缩(文本类压缩60%-80%)
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
# PHP统一解析 + 优化参数
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# PHP-FPM优化:减少等待超时
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 128k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
}
# 首页通用路由
location / {
try_files $uri $uri/;
}
# api目录转发
location /api/ {
try_files $uri $uri/;
}
# 静态资源统一长期缓存(图片、样式、脚本、字体合并规则)
location ~* \.(jpg|jpeg|png|gif|webp|ico|css|js|woff|woff2|ttf)$ {
expires 7d;
add_header Cache-Control "public";
access_log off;
}
# 禁止外部直接访问cache缓存目录,返回403
location ^~ /cache/ {
deny all;
return 403;
}
}
+25
View File
@@ -0,0 +1,25 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm index.php;
# 统一PHP解析规则(所有目录下.php文件都交给php-fpm解析)
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
# 自动获取当前请求php文件真实路径,支持任意php文件
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 前端静态页面、目录访问
location / {
try_files $uri $uri/;
}
# /api 仅路由,不用重复配置php,上面正则会自动处理
location /api/ {
try_files $uri $uri/;
}
}
+53
View File
@@ -0,0 +1,53 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm index.php;
# 开启Gzip全局压缩(文本类压缩60%-80%)
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
# PHP统一解析 + 优化参数
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# PHP-FPM优化:减少等待超时
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 128k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
}
# 首页通用路由
location / {
try_files $uri $uri/;
}
# api目录转发
location /api/ {
try_files $uri $uri/;
}
# 静态资源专项缓存(头像、js、css、字体)
location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
access_log off;
}
location ~* \.(js|css|woff|woff2|ttf)$ {
expires 3d;
add_header Cache-Control "public, max-age=259200";
access_log off;
}
}
+45
View File
@@ -0,0 +1,45 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm index.php;
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 128k;
}
location / {
try_files $uri $uri/;
}
location /api/ {
try_files $uri $uri/;
}
# 静态资源长期缓存
location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
access_log off;
}
location ~* \.(js|css|woff|woff2|ttf)$ {
expires 3d;
add_header Cache-Control "public, max-age=259200";
access_log off;
}
}
+59
View File
@@ -0,0 +1,59 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm index.php;
# 开启Gzip压缩,提升页面加载速度
gzip on;
gzip_min_length 1k;
gzip_buffers 4 16k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
gzip_vary on;
# PHP解析配置
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# 优化PHP连接超时、缓冲区
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
fastcgi_buffer_size 128k;
fastcgi_buffers 4 128k;
fastcgi_busy_buffers_size 256k;
fastcgi_temp_file_write_size 256k;
}
# 首页通用路由
location / {
try_files $uri $uri/;
}
# API目录
location /api/ {
try_files $uri $uri/;
}
# 静态资源长期缓存(本地static目录js/css/图片)
location ~* \.(jpg|jpeg|png|gif|webp|ico)$ {
expires 7d;
add_header Cache-Control "public, max-age=604800";
access_log off;
}
location ~* \.(js|css|woff|woff2|ttf)$ {
expires 3d;
add_header Cache-Control "public, max-age=259200";
access_log off;
}
# 禁止访问缓存目录cache,防止外部直接下载缓存文件
location ^~ /cache/ {
deny all;
return 403;
}
}
+19
View File
@@ -0,0 +1,19 @@
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html index.htm index.php;
# 前端静态页面
location / {
try_files $uri $uri/;
}
# PHP告警接口
location /api {
fastcgi_pass php:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME /usr/share/nginx/html/api/index.php;
include fastcgi_params;
}
}
+150
View File
@@ -0,0 +1,150 @@
import poplib
import ssl
import email
from email.header import decode_header
import time
import pymysql
# 关闭SSL校验,解决老Python握手重置
#ssl._create_default_https_context = ssl._create_unverified_context
# ========== 邮箱配置 自行修改 ==========
MAIL_HOST = "pop.163.com"
MAIL_PORT = 995
MAIL_USER = "lyudream@163.com"
MAIL_PWD = "DVZxyF5NKCkR6fL5"
# ========== 本地MariaDB配置 ==========
DB_HOST = "127.0.0.1"
DB_USER = "root"
DB_PWD = "hp93000"
DB_NAME = "alert_mail_stat"
# 解码邮件标题、正文
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
# 获取邮件正文
def get_email_content(msg):
content = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
if content_type == 'text/plain' and 'attachment' not in content_disposition:
payload = part.get_payload(decode=True)
charset = part.get_charset()
if charset is None:
charset = 'utf-8'
content = payload.decode(charset, errors='ignore')
break
else:
payload = msg.get_payload(decode=True)
charset = msg.get_charset()
if charset is None:
charset = 'utf-8'
content = payload.decode(charset, errors='ignore')
return content
# 简单解析告警关键字(适配Alertmanager中文邮件模板:告警名称、实例、级别)
def parse_alert_info(text):
alert_name = ""
instance = ""
severity = "unknown"
lines = text.splitlines()
for line in lines:
if "告警名称" in line:
alert_name = line.split("")[-1].strip()
if "实例" in line:
instance = line.split("")[-1].strip()
if "级别" in line:
s_val = line.split("")[-1].strip()
if s_val.lower() in ["critical", "严重"]:
severity = "critical"
elif s_val.lower() in ["warning", "警告"]:
severity = "warning"
return alert_name, instance, severity
# 主抓取逻辑
def crawl_mail():
# 连接数据库
db = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PWD, database=DB_NAME, charset='utf8mb4')
cursor = db.cursor()
# 连接POP3邮箱
pop_conn = poplib.POP3_SSL(MAIL_HOST, MAIL_PORT)
pop_conn.user(MAIL_USER)
pop_conn.pass_(MAIL_PWD)
total_num, total_size = pop_conn.stat()
print(f"当前邮箱总邮件数量: {total_num}")
if total_num == 0:
print("暂无新邮件")
pop_conn.quit()
db.close()
return
# 从最新一封开始遍历
for idx in range(total_num, 0, -1):
# 获取邮件原始内容
resp, raw_lines, octet = pop_conn.retr(idx)
raw_msg = b'\r\n'.join(raw_lines).decode('utf-8', errors='ignore')
msg = email.message_from_string(raw_msg)
# 生成唯一标识去重
mail_uid = msg.get("Message-ID", str(time.time()))
# 查重,避免重复入库
cursor.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (mail_uid,))
if cursor.fetchone():
print(f"邮件{idx}已存在数据库,跳过")
continue
# 解析标题、发件人
subject = decode_str(msg.get("Subject", ""))
from_addr = msg.get("From", "")
# ===================== 临时关闭发件人过滤,全部邮件放行测试 =====================
# if "prometheusalert" not in from_addr.lower():
# print(f"邮件{idx}发件人不匹配,跳过,发件人:{from_addr}")
# continue
# 调试打印所有邮件基础信息
print("\n========================================")
print(f"【调试】正在处理第 {idx} 封邮件")
print(f"发件人:{from_addr}")
print(f"邮件标题:{subject}")
print("========================================")
# 正文解析告警字段
body = get_email_content(msg)
alert_name, instance, severity = parse_alert_info(body)
if not alert_name:
print(f"邮件{idx}未识别到【告警名称】关键字,跳过")
continue
# 区分触发/恢复告警
alert_type = 1
if "恢复" in subject or "resolved" in subject.lower():
alert_type = 2
now = time.strftime("%Y-%m-%d %H:%M:%S")
sql = """
INSERT INTO alert_log
(mail_uid, alert_type, alert_name, instance, severity, starts_at, ends_at, content, receive_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(sql, (mail_uid, alert_type, alert_name, instance, severity, now, None, body, now))
db.commit()
print(f"入库成功 | 告警:{alert_name} 实例:{instance}")
pop_conn.quit()
cursor.close()
db.close()
print("\n本次抓取全部完成")
if __name__ == "__main__":
crawl_mail()
@@ -0,0 +1,140 @@
import poplib
import ssl
import email
from email.header import decode_header
import time
import pymysql
# 关闭SSL校验,解决老Python握手重置
ssl._create_default_https_context = ssl._create_unverified_context
# ========== 邮箱配置 自行修改 ==========
MAIL_HOST = "pop.163.com"
MAIL_PORT = 995
MAIL_USER = "lyudream@163.com"
MAIL_PWD = "DVZxyF5NKCkR6fL5"
# ========== 本地MariaDB配置 ==========
DB_HOST = "127.0.0.1"
DB_USER = "root"
DB_PWD = "hp93000"
DB_NAME = "alert_mail_stat"
# 解码邮件标题、正文
def decode_str(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
# 获取邮件正文
def get_email_content(msg):
content = ""
if msg.is_multipart():
for part in msg.walk():
content_type = part.get_content_type()
content_disposition = str(part.get("Content-Disposition"))
if content_type == 'text/plain' and 'attachment' not in content_disposition:
payload = part.get_payload(decode=True)
charset = part.get_charset()
if charset is None:
charset = 'utf-8'
content = payload.decode(charset, errors='ignore')
break
else:
payload = msg.get_payload(decode=True)
charset = msg.get_charset()
if charset is None:
charset = 'utf-8'
content = payload.decode(charset, errors='ignore')
return content
# 简单解析告警关键字(适配Alertmanager中文邮件模板:告警名称、实例、级别)
def parse_alert_info(text):
alert_name = ""
instance = ""
severity = "unknown"
lines = text.splitlines()
for line in lines:
if "告警名称" in line:
alert_name = line.split("")[-1].strip()
if "实例" in line:
instance = line.split("")[-1].strip()
if "级别" in line:
s_val = line.split("")[-1].strip()
if s_val.lower() in ["critical", "严重"]:
severity = "critical"
elif s_val.lower() in ["warning", "警告"]:
severity = "warning"
return alert_name, instance, severity
# 主抓取逻辑
def crawl_mail():
# 连接数据库
db = pymysql.connect(host=DB_HOST, user=DB_USER, password=DB_PWD, database=DB_NAME, charset='utf8mb4')
cursor = db.cursor()
# 连接POP3邮箱
pop_conn = poplib.POP3_SSL(MAIL_HOST, MAIL_PORT)
pop_conn.user(MAIL_USER)
pop_conn.pass_(MAIL_PWD)
total_num, total_size = pop_conn.stat()
print(f"当前邮箱总邮件数量: {total_num}")
if total_num == 0:
print("暂无新邮件")
pop_conn.quit()
db.close()
return
# 从最新一封开始遍历
for idx in range(total_num, 0, -1):
# 获取邮件原始内容
resp, raw_lines, octet = pop_conn.retr(idx)
raw_msg = b'\r\n'.join(raw_lines).decode('utf-8', errors='ignore')
msg = email.message_from_string(raw_msg)
# 生成唯一标识去重
mail_uid = msg.get("Message-ID", str(time.time()))
# 查重,避免重复入库
cursor.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (mail_uid,))
if cursor.fetchone():
continue
# 解析标题、发件人
subject = decode_str(msg.get("Subject", ""))
from_addr = msg.get("From", "")
# 过滤非告警发件人(自行修改匹配你的Alertmanager发件地址)
if "prometheusalert" not in from_addr.lower():
continue
# 正文解析告警字段
body = get_email_content(msg)
alert_name, instance, severity = parse_alert_info(body)
if not alert_name:
print(f"邮件{idx}未识别到告警,跳过")
continue
# 区分触发/恢复告警
alert_type = 1
if "恢复" in subject or "resolved" in subject.lower():
alert_type = 2
now = time.strftime("%Y-%m-%d %H:%M:%S")
sql = """
INSERT INTO alert_log
(mail_uid, alert_type, alert_name, instance, severity, starts_at, ends_at, content, receive_time)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(sql, (mail_uid, alert_type, alert_name, instance, severity, now, None, body, now))
db.commit()
print(f"入库成功 | 告警:{alert_name} 实例:{instance}")
pop_conn.quit()
cursor.close()
db.close()
print("本次抓取完成\n")
if __name__ == "__main__":
crawl_mail()
+77
View File
@@ -0,0 +1,77 @@
import poplib
import ssl
import email
from email.header import decode_header
# SSL兼容修复
try:
ctx = ssl._create_unverified_context
except:
ctx = None
if ctx:
ssl._create_default_https_context = ctx
# ========= 修改这里你的邮箱信息 =========
MAIL_HOST = "pop.163.com"
MAIL_PORT = 995
MAIL_USER = "lyudream@163.com"
MAIL_PWD = "DVZxyF5NKCkR6fL5"
def decode_text(s):
value, charset = decode_header(s)[0]
if charset:
value = value.decode(charset)
return value
if __name__ == "__main__":
print("=== 开始连接pop.163.com:995 ===")
try:
pop = poplib.POP3_SSL(MAIL_HOST, MAIL_PORT)
print("连接服务器成功,正在登录...")
pop.user(MAIL_USER)
pop.pass_(MAIL_PWD)
print("登录邮箱成功!")
total, _ = pop.stat()
print(f"邮箱共有邮件:{total}\n")
if total == 0:
print("邮箱无邮件")
pop.quit()
exit()
# 从最新邮件遍历
for i in range(total, 0, -1):
resp, lines, octet = pop.retr(i)
msg_raw = b"\r\n".join(lines).decode("utf-8", errors="ignore")
msg = email.message_from_string(msg_raw)
subject = decode_text(msg.get("Subject", ""))
from_addr = msg.get("From", "")
# 取正文
body = ""
if msg.is_multipart():
for part in msg.walk():
t = part.get_content_type()
disp = str(part.get("Content-Disposition"))
if t == "text/plain" and "attachment" not in disp:
payload = part.get_payload(decode=True)
body = payload.decode("utf-8", errors="ignore")
break
else:
payload = msg.get_payload(decode=True)
body = payload.decode("utf-8", errors="ignore")
print("========================================")
print(f"{i}封邮件")
print(f"发件人:{from_addr}")
print(f"标题:{subject}")
print(f"正文内容:\n{body}")
print("========================================\n")
pop.quit()
print("全部读取完成")
except Exception as e:
print("连接/登录失败,错误信息:")
print(str(e))
+205
View File
@@ -0,0 +1,205 @@
nohup: ignoring input
* Serving Flask app "webhook" (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:909/ (Press CTRL+C to quit)
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:34:17] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:35:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:42] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:42] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:42:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:41] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:43:56] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:11] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:11] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:11] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:44:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:45:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:36] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:56:46] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:58:57] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:59:12] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:59:12] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 14:59:12] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 17:28:04] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 19:23:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [02/Jul/2026 19:24:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 12:30:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 12:30:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 15:19:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [03/Jul/2026 15:19:53] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:44:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:50:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:56:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 13:57:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:26:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:28:53] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:33:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [04/Jul/2026 14:33:38] "POST /alert HTTP/1.1" 200 -
10.150.44.16 - - [06/Jul/2026 09:36:01] "GET / HTTP/1.1" 404 -
10.150.44.16 - - [06/Jul/2026 09:36:02] "GET /favicon.ico HTTP/1.1" 404 -
10.150.117.190 - - [06/Jul/2026 11:21:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:21:31] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:25:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:28:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:28:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:30:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:30:31] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 11:31:46] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 12:21:51] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 13:54:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 13:55:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:44:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:45:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:45:26] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:45:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:50:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:50:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:52:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:52:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 14:54:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:13:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:13:33] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:16:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:16:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:18:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:18:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:49:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 15:50:48] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 16:00:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 16:01:08] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [06/Jul/2026 17:22:05] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:42:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:43:38] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:46:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 09:46:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:11:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:11:28] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:13:08] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:13:08] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:23:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:24:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:25:18] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:29:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 10:50:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:21:01] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:24:13] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:24:23] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:37:21] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:37:31] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:43:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:44:06] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:44:43] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:45:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 11:46:03] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 12:12:58] "POST /alert HTTP/1.1" 200 -
10.150.117.190 - - [07/Jul/2026 12:13:48] "POST /alert HTTP/1.1" 200 -
+65
View File
@@ -0,0 +1,65 @@
from flask import Flask, request, jsonify
import pymysql
app = Flask(__name__)
def get_db():
return pymysql.connect(
host="127.0.0.1",
user="root",
password="hp93000",
database="alert_mail_stat",
charset="utf8mb4"
)
@app.route("/alert", methods=["POST"])
def alert_receive():
try:
data = request.get_json()
alerts = data.get("alerts", [])
insert_num = 0
update_num = 0
db = get_db()
cur = db.cursor()
for alert in alerts:
fp = alert["fingerprint"]
alert_name = alert["labels"].get("alertname", "")
instance = alert["labels"].get("instance", "")
severity = alert["labels"].get("severity", "warning")
status = alert["status"]
start_at = alert.get("startsAt", "")
end_at = alert.get("endsAt")
alert_type = 2 if status == "resolved" else 1
content_str = str(alert)
# 1. 原有告警入库逻辑不变
sql = """
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
"""
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
insert_num += 1
# 2. 新增:同步监控规则,存在则置为启用1
if alert_name:
sync_rule_sql = """
INSERT INTO monitor_rule (rule_name, status)
VALUES (%s, 1)
ON DUPLICATE KEY UPDATE status = 1
"""
cur.execute(sync_rule_sql, [alert_name])
db.commit()
cur.close()
db.close()
return jsonify({"code": 200, "insert": insert_num, "update": update_num})
except Exception as e:
if "db" in locals():
db.rollback()
db.close()
return jsonify({"code": 500, "msg": str(e)}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=909, debug=False)
+47
View File
@@ -0,0 +1,47 @@
from flask import Flask, request, jsonify
import pymysql
app = Flask(__name__)
def get_db():
return pymysql.connect(
host="127.0.0.1",
user="root",
password="hp93000",
database="alert_mail_stat",
charset="utf8mb4"
)
@app.route("/alert", methods=["POST"])
def alert_in():
raw = request.get_json()
alerts = raw.get("alerts", [])
add_cnt = 0
db = get_db()
cur = db.cursor()
for alert in alerts:
fp = alert["fingerprint"]
alert_name = alert["labels"].get("alertname", "")
instance = alert["labels"].get("instance", "")
severity = alert["labels"].get("severity", "warning")
status = alert["status"]
start = alert.get("startsAt", "")
end = alert.get("endsAt")
type_val = 2 if status == "resolved" else 1
cur.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (fp,))
if cur.fetchone():
continue
sql = """
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
"""
cur.execute(sql, (fp, type_val, alert_name, instance, severity, start, end, str(alert)))
add_cnt += 1
db.commit()
cur.close()
db.close()
return jsonify({"code": 200, "insert_count": add_cnt})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=909, debug=False)
+47
View File
@@ -0,0 +1,47 @@
from flask import Flask, request, jsonify
import pymysql
app = Flask(__name__)
def get_db():
return pymysql.connect(
host="127.0.0.1",
user="root",
password="hp93000",
database="alert_mail_stat",
charset="utf8mb4"
)
@app.route("/alert", methods=["POST"])
def alert_receive():
data = request.get_json()
alerts = data.get("alerts", [])
insert_num = 0
db = get_db()
cur = db.cursor()
for alert in alerts:
fp = alert["fingerprint"]
alert_name = alert["labels"].get("alertname", "")
instance = alert["labels"].get("instance", "")
severity = alert["labels"].get("severity", "warning")
status = alert["status"]
start_at = alert.get("startsAt", "")
end_at = alert.get("endsAt")
alert_type = 2 if status == "resolved" else 1
cur.execute("SELECT id FROM alert_log WHERE mail_uid=%s", (fp,))
if cur.fetchone():
continue
sql = """
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
"""
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, str(alert)))
insert_num += 1
db.commit()
cur.close()
db.close()
return jsonify({"code": 200, "insert": insert_num})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=909, debug=False)
+72
View File
@@ -0,0 +1,72 @@
from flask import Flask, request, jsonify
import pymysql
app = Flask(__name__)
def get_db():
return pymysql.connect(
host="127.0.0.1",
user="root",
password="hp93000",
database="alert_mail_stat",
charset="utf8mb4"
)
@app.route("/alert", methods=["POST"])
def alert_receive():
try:
data = request.get_json()
alerts = data.get("alerts", [])
insert_num = 0
update_num = 0
db = get_db()
cur = db.cursor()
for alert in alerts:
fp = alert["fingerprint"]
alert_name = alert["labels"].get("alertname", "")
instance = alert["labels"].get("instance", "")
severity = alert["labels"].get("severity", "warning")
status = alert["status"]
start_at = alert.get("startsAt", "")
end_at = alert.get("endsAt")
alert_type = 2 if status == "resolved" else 1
content_str = str(alert)
cur.execute("""
SELECT id FROM alert_log
WHERE mail_uid=%s AND DATE(receive_time) = CURDATE()
ORDER BY id DESC LIMIT 1
""", (fp,))
row = cur.fetchone()
if status == "resolved":
if row:
cur.execute("""
UPDATE alert_log
SET alert_type=%s, ends_at=%s, content=%s
WHERE mail_uid=%s AND id=%s
""", (alert_type, end_at, content_str, fp, row[0]))
update_num += 1
else:
# 删掉唯一索引后,每次故障直接插入,不再判断是否存在
sql = """
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
"""
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
insert_num += 1
db.commit()
cur.close()
db.close()
return jsonify({"code": 200, "insert": insert_num, "update": update_num})
except Exception as e:
if "db" in locals():
db.rollback()
db.close()
return jsonify({"code": 500, "msg": str(e)}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=909, debug=False)
+55
View File
@@ -0,0 +1,55 @@
from flask import Flask, request, jsonify
import pymysql
app = Flask(__name__)
def get_db():
return pymysql.connect(
host="127.0.0.1",
user="root",
password="hp93000",
database="alert_mail_stat",
charset="utf8mb4"
)
@app.route("/alert", methods=["POST"])
def alert_receive():
try:
data = request.get_json()
alerts = data.get("alerts", [])
insert_num = 0
update_num = 0
db = get_db()
cur = db.cursor()
for alert in alerts:
fp = alert["fingerprint"]
alert_name = alert["labels"].get("alertname", "")
instance = alert["labels"].get("instance", "")
severity = alert["labels"].get("severity", "warning")
status = alert["status"]
start_at = alert.get("startsAt", "")
end_at = alert.get("endsAt")
alert_type = 2 if status == "resolved" else 1
content_str = str(alert)
sql = """
INSERT INTO alert_log(mail_uid,alert_type,alert_name,instance,severity,starts_at,ends_at,content,receive_time)
VALUES(%s,%s,%s,%s,%s,%s,%s,%s,NOW())
"""
cur.execute(sql, (fp, alert_type, alert_name, instance, severity, start_at, end_at, content_str))
insert_num += 1
db.commit()
cur.close()
db.close()
return jsonify({"code": 200, "insert": insert_num, "update": update_num})
except Exception as e:
if "db" in locals():
db.rollback()
db.close()
return jsonify({"code": 500, "msg": str(e)}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=909, debug=False)
+619
View File
@@ -0,0 +1,619 @@
<?php
session_start();
// 新增:禁止页面缓存,解决刷新读旧数据
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
// 会话超时30分钟统一校验,和dashboard共用
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 数据库读取现有配置,页面打开自动回填
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$res = mysqli_query($conn, "SELECT k,v FROM sys_config");
$config = [];
while ($row = mysqli_fetch_assoc($res)) {
$config[$row['k']] = $row['v'];
}
mysqli_close($conn);
// 赋值表单默认值
$pageTitle = $config['page_title'] ?? "告警监控系统";
$themeColor = $config['theme_color'] ?? "#409eff";
$pageSize = $config['page_size'] ?? "20";
$footerText = $config['footer_text'] ?? "";
$sidebarLinks = $config['sidebar_links'] ?? "";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统后台管理 - Advantest 运维告警平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
/* 侧边栏 与dashboard完全同款深蓝色侧边 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 和dashboard完全一致 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
.btn-danger:hover{
background:#e53e3e;
}
/* 页面容器 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
.page-title{
font-size:24px;
font-weight:700;
color:#152c5b;
margin-bottom:26px;
display:flex;
align-items:center;
gap:12px;
}
.page-title i{
color:#2563eb;
font-size:26px;
}
/* Tab切换 */
.tab-box{
display:flex;
border-bottom:1px solid #eef2fb;
margin-bottom:36px;
}
.tab-item{
padding:14px 26px;
cursor:pointer;
font-size:15px;
color:#64748b;
border-bottom:3px solid transparent;
transition:0.2s;
}
.tab-item.active{
color:#2563eb;
border-bottom:3px solid #2563eb;
font-weight:600;
}
.tab-item:hover:not(.active){
color:#152c5b;
}
/* 统一企业卡片样式,和dashboard base-card完全一致 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.form-row{
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
flex-wrap:wrap;
}
.form-row label{
width:160px;
font-size:14px;
color:#475569;
font-weight:500;
}
.form-row input,.form-row textarea{
padding:10px 14px;
border:1px solid #dcdfe6;
border-radius:8px;
flex:1;
max-width:480px;
font-size:14px;
}
.form-row textarea{
min-height:120px;
resize:vertical;
}
.tip-text{
font-size:13px;
color:#94a3b8;
margin-top:-12px;
margin-bottom:16px;
padding-left:160px;
}
/* 提示消息框 */
#msg{
margin:20px 0;
padding:12px 16px;
border-radius:8px;
display:none;
font-size:14px;
}
.success{
background:#dcfce7;
color:#16a34a;
border:1px solid #bbf7d0;
}
.error{
background:#fee2e2;
color:#dc2626;
border:1px solid #fecdd3;
}
/* JSON返回结果框 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:160px;
font-family:Consolas,monospace;
font-size:13px;
}
.hide{display:none}
.link-group{
margin-top:30px;
padding-top:20px;
border-top:1px solid #eef2fb;
display:flex;
gap:20px;
}
.link-group a{
color:#2563eb;
text-decoration:none;
display:flex;
align-items:center;
gap:6px;
}
.link-group a:hover{
text-decoration:underline;
}
/* 响应式适配 和dashboard统一 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.card{padding:24px}
.form-row{flex-direction:column;align-items:flex-start}
.form-row label{width:auto}
.form-row input,.form-row textarea{width:100%;max-width:100%}
.tip-text{padding-left:0}
.tab-item{padding:12px 16px}
}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">系统管理</div>
<div class="menu-item active" data-tab="pwd">
<i class="fa fa-key"></i>
<span>修改登录密码</span>
</div>
<div class="menu-item" data-tab="theme">
<i class="fa fa-paint-brush"></i>
<span>界面外观配置</span>
</div>
<div class="menu-item" data-tab="link">
<i class="fa fa-link"></i>
<span>侧边栏外链管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">系统后台管理面板</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新配置</button>
<button class="btn-base btn-primary" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
<button class="btn-base btn-danger" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="page-title"><i class="fa fa-cog"></i>系统全局配置管理</div>
<!-- Tab切换 -->
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块 BCrypt兼容 -->
<div class="card tab-content" id="tab-pwd">
<h3><i class="fa fa-key"></i>账户密码修改(BCrypt哈希加密,与登录算法统一)</h3>
<div class="form-row">
<label>原登录密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前正在使用的密码">
</div>
<div class="tip-text">输入你现在登录的密码用于身份校验</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置全新登录密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码,保持一致">
</div>
<div class="form-row">
<button class="btn-base btn-primary" onclick="updatePwd()"><i class="fa fa-check"></i>提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置(保存后dashboard实时生效) -->
<div class="card tab-content hide" id="tab-theme">
<h3><i class="fa fa-paint-brush"></i>监控大屏界面外观全局配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" value="<?php echo htmlspecialchars($pageTitle); ?>" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题主色值:</label>
<input id="theme_color" value="<?php echo htmlspecialchars($themeColor); ?>" placeholder="#409eff">
</div>
<div class="form-row">
<label>表格每页展示条数:</label>
<input id="page_size" value="<?php echo htmlspecialchars($pageSize); ?>" placeholder="20">
</div>
<div class="form-row">
<label>页面底部自定义文案:</label>
<textarea id="footer_text"><?php echo htmlspecialchars($footerText); ?></textarea>
</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveTheme()"><i class="fa fa-save"></i>保存外观配置</button>
</div>
<div class="tip-text">保存后刷新告警监控大屏dashboard.php即可生效,前端标题、分页、底部文案同步更新</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理(保存后dashboard侧边同步更新) -->
<div class="card tab-content hide" id="tab-link">
<h3><i class="fa fa-link"></i>监控大屏侧边导航外链配置</h3>
<div class="form-row">
<label>外链列表(格式:名称|地址,一行一条):</label>
<textarea id="sidebar_links"><?php echo htmlspecialchars($sidebarLinks); ?></textarea>
</div>
<div class="tip-text">示例:Prometheus|http://10.150.117.190:9090<br>保存后刷新dashboard侧边栏立即加载新外链菜单</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveLink()"><i class="fa fa-save"></i>保存侧边外链配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 底部跳转链接 -->
<div class="link-group">
<a href="./api/panel.php" target="_blank"><i class="fa fa-code"></i>进入开发者调试面板(API可视化调用)</a>
<a href="dashboard.php"><i class="fa fa-line-chart"></i>返回告警监控总览大屏</a>
</div>
</div>
</div>
</div>
<script>
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 侧边菜单同步Tab切换
$(".menu-item").click(function(){
const tabId = $(this).data("tab");
$(".tab-item").removeClass("active");
$(`.tab-item[data-tab="${tabId}"]`).addClass("active");
$(".tab-content").addClass("hide");
$("#tab-"+tabId).removeClass("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
})
// 通用提示弹窗
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// 1 修改密码 POST 提交
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码输入项不能为空","error");
if(new1 !== new2) return showMsg("两次输入的新密码不一致,请核对","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,即将跳转登录页重新验证");
setTimeout(()=>location.href="login.php",1800);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 2 保存外观配置 POST 提交
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功!2秒后自动刷新大屏页面");
setTimeout(()=>{
window.location.href = "dashboard.php?_t="+new Date().getTime();
},2000);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 3 保存侧边外链 POST 提交
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功!刷新dashboard侧边栏立刻展示新增外链菜单");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
</script>
</body>
</html>
+203
View File
@@ -0,0 +1,203 @@
<?php
session_start();
// 校验登录,无session跳转登录页
if(empty($_SESSION['user_id'])){
header("Location: login.php");
exit;
}
// 简单权限控制,如需管理员账号可自行加判断
$isAdmin = true;
if(!$isAdmin){
echo "无后台管理权限";
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>系统后台管理 Admin</title>
<style>
*{box-sizing:border-box;margin:0;padding:0;font-family:Microsoft Yahei}
body{background:#f0f2f5;padding:24px}
.wrap{max-width:1000px;margin:0 auto}
h1{margin-bottom:20px;color:#1f2937}
.tab-box{display:flex;border-bottom:1px solid #dcdfe6;margin-bottom:16px}
.tab-item{padding:10px 20px;cursor:pointer;border:1px solid transparent;border-bottom:none;background:#fff}
.tab-item.active{border:1px solid #409eff;border-bottom:1px solid #f0f2f5;color:#409eff}
.card{background:#fff;padding:20px;border-radius:6px;border:1px solid #e4e7ed;margin-bottom:16px}
.card h3{margin-bottom:14px;color:#303133}
.form-row{margin-bottom:12px;display:flex;align-items:center;gap:10px}
label{width:140px}
input,textarea{padding:8px 10px;border:1px solid #dcdfe6;border-radius:4px;flex:1;max-width:400px}
textarea{min-height:80px}
button{padding:8px 18px;background:#409eff;color:#fff;border:none;border-radius:4px;cursor:pointer}
#msg{margin-top:10px;padding:8px;border-radius:4px;display:none}
.success{background:#e1f3d8;color:#67c23a}
.error{background:#fef0f0;color:#f56c6c}
#result{margin-top:16px;padding:12px;background:#1e1e1e;color:#fff;white-space:pre-wrap;min-height:100px}
.hide{display:none}
</style>
</head>
<body>
<div class="wrap">
<h1>系统后台管理面板</h1>
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块(BCrypt哈希兼容原有密码) -->
<div class="card tab-content" id="tab-pwd">
<h3>账户密码修改(采用bcrypt哈希加密,和历史密码算法统一)</h3>
<div class="form-row">
<label>原密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前登录密码">
</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置新密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码">
</div>
<div class="form-row">
<button onclick="updatePwd()">提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置 -->
<div class="card tab-content hide" id="tab-theme">
<h3>大屏界面外观参数配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题色值:</label>
<input id="theme_color" placeholder="#409eff">
</div>
<div class="form-row">
<label>每页展示条数:</label>
<input id="page_size" placeholder="20">
</div>
<div class="form-row">
<label>自定义底部文案:</label>
<textarea id="footer_text"></textarea>
</div>
<div class="form-row">
<button onclick="saveTheme()">保存外观配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理 -->
<div class="card tab-content hide" id="tab-link">
<h3>侧边导航外链配置(一行一条,名称|链接)</h3>
<div class="form-row">
<textarea id="sidebar_links" placeholder="示例:
Prometheus|http://10.150.117.190:9090
Alertmanager|http://10.150.117.190:9093"></textarea>
</div>
<div class="form-row">
<button onclick="saveLink()">保存侧边外链</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<div style="margin-top:20px;padding-top:16px;border-top:1px solid #eee">
<a href="./api/dev_panel.php" target="_blank">👉 进入开发者调试面板(所有接口可视化调用)</a>
<br><br>
<a href="dashboard.php">返回监控告警大屏</a>
</div>
</div>
<script>
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 通用提示
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// 1 修改密码接口请求
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码项不能为空","error");
if(new1 !== new2) return showMsg("两次新密码输入不一致","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,请重新登录");
setTimeout(()=>location.href="login.php",1500);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("请求接口失败:"+e,"error");
})
}
// 2 保存外观配置
function saveTheme(){
const params = new URLSearchParams();
params.append("act","save_theme");
params.append("title",document.getElementById("page_title").value);
params.append("color",document.getElementById("theme_color").value);
params.append("pagesize",document.getElementById("page_size").value);
params.append("footer",document.getElementById("footer_text").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
json.code===0 ? showMsg("外观配置保存成功") : showMsg(json.msg,"error");
})
}
// 3 保存侧边外链
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
json.code===0 ? showMsg("侧边外链保存成功") : showMsg(json.msg,"error");
})
}
</script>
</body>
</html>
+605
View File
@@ -0,0 +1,605 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,和dashboard逻辑统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 预加载现有配置,页面打开自动回填表单
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($conn, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($conn);
// 读取已有配置默认值
$pageTitle = $sysConfig['page_title'] ?? "告警监控系统";
$themeColor = $sysConfig['theme_color'] ?? "#409eff";
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = $sysConfig['footer_text'] ?? "";
$sidebarLinks = $sysConfig['sidebar_links'] ?? "";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统后台管理 - Advantest 爱德万测试运维告警平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
/* 侧边栏 同dashboard深蓝色侧边 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 和dashboard完全一致 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
.btn-danger:hover{
background:#e53e3e;
}
/* 页面容器 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
.page-title{
font-size:24px;
font-weight:700;
color:#152c5b;
margin-bottom:26px;
display:flex;
align-items:center;
gap:12px;
}
.page-title i{
color:#2563eb;
font-size:26px;
}
/* Tab切换 */
.tab-box{
display:flex;
border-bottom:1px solid #eef2fb;
margin-bottom:36px;
}
.tab-item{
padding:14px 26px;
cursor:pointer;
font-size:15px;
color:#64748b;
border-bottom:3px solid transparent;
transition:0.2s;
}
.tab-item.active{
color:#2563eb;
border-bottom:3px solid #2563eb;
font-weight:600;
}
.tab-item:hover:not(.active){
color:#152c5b;
}
/* 统一企业卡片样式,和dashboard base-card完全一致 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.form-row{
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
flex-wrap:wrap;
}
.form-row label{
width:160px;
font-size:14px;
color:#475569;
font-weight:500;
}
.form-row input,.form-row textarea{
padding:10px 14px;
border:1px solid #dcdfe6;
border-radius:8px;
flex:1;
max-width:480px;
font-size:14px;
}
.form-row textarea{
min-height:120px;
resize:vertical;
}
.tip-text{
font-size:13px;
color:#94a3b8;
margin-top:-12px;
margin-bottom:16px;
padding-left:160px;
}
/* 提示消息框 */
#msg{
margin:20px 0;
padding:12px 16px;
border-radius:8px;
display:none;
font-size:14px;
}
.success{
background:#dcfce7;
color:#16a34a;
border:1px solid #bbf7d0;
}
.error{
background:#fee2e2;
color:#dc2626;
border:1px solid #fecdd3;
}
/* JSON返回结果框 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:160px;
font-family:Consolas,monospace;
font-size:13px;
}
.hide{display:none}
.link-group{
margin-top:30px;
padding-top:20px;
border-top:1px solid #eef2fb;
display:flex;
gap:20px;
}
.link-group a{
color:#2563eb;
text-decoration:none;
display:flex;
align-items:center;
gap:6px;
}
.link-group a:hover{
text-decoration:underline;
}
/* 响应式适配 和dashboard统一 */
@media (max-width:1440px){
}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.card{padding:24px}
.form-row{flex-direction:column;align-items:flex-start}
.form-row label{width:auto}
.form-row input,.form-row textarea{width:100%;max-width:100%}
.tip-text{padding-left:0}
.tab-item{padding:12px 16px}
}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">系统管理</div>
<div class="menu-item active" data-tab="pwd">
<i class="fa fa-key"></i>
<span>修改登录密码</span>
</div>
<div class="menu-item" data-tab="theme">
<i class="fa fa-paint-brush"></i>
<span>界面外观配置</span>
</div>
<div class="menu-item" data-tab="link">
<i class="fa fa-link"></i>
<span>侧边栏外链管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">系统后台管理面板</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新配置</button>
<button class="btn-base btn-primary" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
<button class="btn-base btn-danger" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="page-title"><i class="fa fa-cog"></i>系统全局配置管理</div>
<!-- Tab切换 -->
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块 BCrypt兼容 -->
<div class="card tab-content" id="tab-pwd">
<h3><i class="fa fa-key"></i>账户密码修改(BCrypt哈希加密,与登录算法统一)</h3>
<div class="form-row">
<label>原登录密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前正在使用的密码">
</div>
<div class="tip-text">输入你现在登录的密码用于身份校验</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置全新登录密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码,保持一致">
</div>
<div class="form-row">
<button class="btn-base btn-primary" onclick="updatePwd()"><i class="fa fa-check"></i>提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置(保存后dashboard实时生效) -->
<div class="card tab-content hide" id="tab-theme">
<h3><i class="fa fa-paint-brush"></i>监控大屏界面外观全局配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" value="<?php echo htmlspecialchars($pageTitle); ?>" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题主色值:</label>
<input id="theme_color" value="<?php echo htmlspecialchars($themeColor); ?>" placeholder="#409eff">
</div>
<div class="form-row">
<label>表格每页展示条数:</label>
<input id="page_size" value="<?php echo htmlspecialchars($pageSize); ?>" placeholder="20">
</div>
<div class="form-row">
<label>页面底部自定义文案:</label>
<textarea id="footer_text"><?php echo htmlspecialchars($footerText); ?></textarea>
</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveTheme()"><i class="fa fa-save"></i>保存外观配置</button>
</div>
<div class="tip-text">保存后刷新告警监控大屏dashboard.php即可生效</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理(保存后dashboard侧边同步更新) -->
<div class="card tab-content hide" id="tab-link">
<h3><i class="fa fa-link"></i>监控大屏侧边导航外链配置</h3>
<div class="form-row">
<label>外链列表(格式:名称|地址,一行一条):</label>
<textarea id="sidebar_links"><?php echo htmlspecialchars($sidebarLinks); ?></textarea>
</div>
<div class="tip-text">示例:Prometheus|http://10.150.117.190:9090<br>保存后刷新dashboard侧边栏立即加载</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveLink()"><i class="fa fa-save"></i>保存侧边外链配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 底部跳转链接 -->
<div class="link-group">
<a href="./api/dev_panel.php" target="_blank"><i class="fa fa-code"></i>进入开发者调试面板(API可视化调用)</a>
<a href="dashboard.php"><i class="fa fa-line-chart"></i>返回告警监控总览大屏</a>
</div>
</div>
</div>
</div>
<script>
// 相对路径接口,适配站点部署
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 侧边菜单同步Tab切换
$(".menu-item").click(function(){
const tabId = $(this).data("tab");
$(".tab-item").removeClass("active");
$(`.tab-item[data-tab="${tabId}"]`).addClass("active");
$(".tab-content").addClass("hide");
$("#tab-"+tabId).removeClass("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
})
// 通用提示弹窗
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// 1 修改密码接口请求
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码输入项不能为空","error");
if(new1 !== new2) return showMsg("两次输入的新密码不一致,请核对","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,即将跳转登录页重新验证");
setTimeout(()=>location.href="login.php",1800);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 2 保存外观配置(存入sys_configdashboard读取渲染)
function saveTheme(){
const params = new URLSearchParams();
params.append("act","save_theme");
params.append("title",document.getElementById("page_title").value);
params.append("color",document.getElementById("theme_color").value);
params.append("pagesize",document.getElementById("page_size").value);
params.append("footer",document.getElementById("footer_text").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("外观配置保存成功,刷新监控大屏即可生效");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 3 保存侧边外链(存入sys_configdashboard侧边读取渲染)
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(`${api}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功,刷新监控大屏侧边栏立即更新");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
</script>
</body>
</html>
+625
View File
@@ -0,0 +1,625 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,和dashboard逻辑统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 数据库连接读取全局配置,页面打开自动回填
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($conn, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($conn, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($conn);
// 读取配置默认值
$pageTitle = $sysConfig['page_title'] ?? "告警监控系统";
$themeColor = $sysConfig['theme_color'] ?? "#409eff";
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = $sysConfig['footer_text'] ?? "";
$sidebarLinks = $sysConfig['sidebar_links'] ?? "";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统后台管理 - Advantest 爱德万测试运维告警平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
/* 侧边栏 与dashboard完全同款深蓝色侧边 */
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 和dashboard完全一致 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
.btn-danger:hover{
background:#e53e3e;
}
/* 页面容器 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
.page-title{
font-size:24px;
font-weight:700;
color:#152c5b;
margin-bottom:26px;
display:flex;
align-items:center;
gap:12px;
}
.page-title i{
color:#2563eb;
font-size:26px;
}
/* Tab切换 */
.tab-box{
display:flex;
border-bottom:1px solid #eef2fb;
margin-bottom:36px;
}
.tab-item{
padding:14px 26px;
cursor:pointer;
font-size:15px;
color:#64748b;
border-bottom:3px solid transparent;
transition:0.2s;
}
.tab-item.active{
color:#2563eb;
border-bottom:3px solid #2563eb;
font-weight:600;
}
.tab-item:hover:not(.active){
color:#152c5b;
}
/* 统一企业卡片样式,和dashboard base-card完全一致 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.form-row{
margin-bottom:20px;
display:flex;
align-items:center;
gap:12px;
flex-wrap:wrap;
}
.form-row label{
width:160px;
font-size:14px;
color:#475569;
font-weight:500;
}
.form-row input,.form-row textarea{
padding:10px 14px;
border:1px solid #dcdfe6;
border-radius:8px;
flex:1;
max-width:480px;
font-size:14px;
}
.form-row textarea{
min-height:120px;
resize:vertical;
}
.tip-text{
font-size:13px;
color:#94a3b8;
margin-top:-12px;
margin-bottom:16px;
padding-left:160px;
}
/* 提示消息框 */
#msg{
margin:20px 0;
padding:12px 16px;
border-radius:8px;
display:none;
font-size:14px;
}
.success{
background:#dcfce7;
color:#16a34a;
border:1px solid #bbf7d0;
}
.error{
background:#fee2e2;
color:#dc2626;
border:1px solid #fecdd3;
}
/* JSON返回结果框 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:160px;
font-family:Consolas,monospace;
font-size:13px;
}
.hide{display:none}
.link-group{
margin-top:30px;
padding-top:20px;
border-top:1px solid #eef2fb;
display:flex;
gap:20px;
}
.link-group a{
color:#2563eb;
text-decoration:none;
display:flex;
align-items:center;
gap:6px;
}
.link-group a:hover{
text-decoration:underline;
}
/* 响应式适配 和dashboard统一 */
@media (max-width:1440px){
}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.card{padding:24px}
.form-row{flex-direction:column;align-items:flex-start}
.form-row label{width:auto}
.form-row input,.form-row textarea{width:100%;max-width:100%}
.tip-text{padding-left:0}
.tab-item{padding:12px 16px}
}
</style>
</head>
<body>
<div class="wrap-main">
<!-- 左侧侧边栏 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">系统管理</div>
<div class="menu-item active" data-tab="pwd">
<i class="fa fa-key"></i>
<span>修改登录密码</span>
</div>
<div class="menu-item" data-tab="theme">
<i class="fa fa-paint-brush"></i>
<span>界面外观配置</span>
</div>
<div class="menu-item" data-tab="link">
<i class="fa fa-link"></i>
<span>侧边栏外链管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">系统后台管理面板</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新配置</button>
<button class="btn-base btn-primary" onclick="location.href='dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
<button class="btn-base btn-danger" onclick="location.href='logout.php'"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="page-title"><i class="fa fa-cog"></i>系统全局配置管理</div>
<!-- Tab切换 -->
<div class="tab-box">
<div class="tab-item active" data-tab="pwd">修改登录密码</div>
<div class="tab-item" data-tab="theme">界面外观配置</div>
<div class="tab-item" data-tab="link">侧边栏外链管理</div>
</div>
<!-- 1 修改密码模块 BCrypt兼容 -->
<div class="card tab-content" id="tab-pwd">
<h3><i class="fa fa-key"></i>账户密码修改(BCrypt哈希加密,与登录算法统一)</h3>
<div class="form-row">
<label>原登录密码:</label>
<input type="password" id="old_pwd" placeholder="输入当前正在使用的密码">
</div>
<div class="tip-text">输入你现在登录的密码用于身份校验</div>
<div class="form-row">
<label>新密码:</label>
<input type="password" id="new_pwd" placeholder="设置全新登录密码">
</div>
<div class="form-row">
<label>确认新密码:</label>
<input type="password" id="re_pwd" placeholder="再次输入新密码,保持一致">
</div>
<div class="form-row">
<button class="btn-base btn-primary" onclick="updatePwd()"><i class="fa fa-check"></i>提交修改密码</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 2 界面外观配置(保存后dashboard实时生效) -->
<div class="card tab-content hide" id="tab-theme">
<h3><i class="fa fa-paint-brush"></i>监控大屏界面外观全局配置</h3>
<div class="form-row">
<label>页面标题:</label>
<input id="page_title" value="<?php echo htmlspecialchars($pageTitle); ?>" placeholder="告警监控系统">
</div>
<div class="form-row">
<label>主题主色值:</label>
<input id="theme_color" value="<?php echo htmlspecialchars($themeColor); ?>" placeholder="#409eff">
</div>
<div class="form-row">
<label>表格每页展示条数:</label>
<input id="page_size" value="<?php echo htmlspecialchars($pageSize); ?>" placeholder="20">
</div>
<div class="form-row">
<label>页面底部自定义文案:</label>
<textarea id="footer_text"><?php echo htmlspecialchars($footerText); ?></textarea>
</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveTheme()"><i class="fa fa-save"></i>保存外观配置</button>
</div>
<div class="tip-text">保存后刷新告警监控大屏dashboard.php即可生效</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 3 侧边栏外链管理(保存后dashboard侧边同步更新) -->
<div class="card tab-content hide" id="tab-link">
<h3><i class="fa fa-link"></i>监控大屏侧边导航外链配置</h3>
<div class="form-row">
<label>外链列表(格式:名称|地址,一行一条):</label>
<textarea id="sidebar_links"><?php echo htmlspecialchars($sidebarLinks); ?></textarea>
</div>
<div class="tip-text">示例:Prometheus|http://10.150.117.190:9090<br>保存后刷新dashboard侧边栏立即加载</div>
<div class="form-row">
<button class="btn-base btn-success" onclick="saveLink()"><i class="fa fa-save"></i>保存侧边外链配置</button>
</div>
<div id="msg"></div>
<div id="result"></div>
</div>
<!-- 底部跳转链接 -->
<div class="link-group">
<a href="./api/dev_panel.php" target="_blank"><i class="fa fa-code"></i>进入开发者调试面板(API可视化调用)</a>
<a href="dashboard.php"><i class="fa fa-line-chart"></i>返回告警监控总览大屏</a>
</div>
</div>
</div>
</div>
<script>
// 相对路径接口,适配站点部署
const api = "./api/index.php";
const msgBox = document.getElementById("msg");
const resultBox = document.getElementById("result");
// Tab切换逻辑
document.querySelectorAll(".tab-item").forEach(item=>{
item.onclick = ()=>{
document.querySelectorAll(".tab-item").forEach(t=>t.classList.remove("active"));
item.classList.add("active");
const tabId = item.dataset.tab;
document.querySelectorAll(".tab-content").forEach(c=>c.classList.add("hide"));
document.getElementById("tab-"+tabId).classList.remove("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
}
})
// 侧边菜单同步Tab切换
$(".menu-item").click(function(){
const tabId = $(this).data("tab");
$(".tab-item").removeClass("active");
$(`.tab-item[data-tab="${tabId}"]`).addClass("active");
$(".tab-content").addClass("hide");
$("#tab-"+tabId).removeClass("hide");
msgBox.style.display = "none";
resultBox.innerText = "";
})
// 通用提示弹窗
function showMsg(text, type="success"){
msgBox.className = type;
msgBox.innerText = text;
msgBox.style.display = "block";
}
// ========== 核心修复:全部改用POST标准表单提交,补全编码头,解决参数丢失/乱码 ==========
// 1 修改密码接口请求
function updatePwd(){
const old = document.getElementById("old_pwd").value.trim();
const new1 = document.getElementById("new_pwd").value.trim();
const new2 = document.getElementById("re_pwd").value.trim();
if(!old || !new1 || !new2) return showMsg("所有密码输入项不能为空","error");
if(new1 !== new2) return showMsg("两次输入的新密码不一致,请核对","error");
const params = new URLSearchParams();
params.append("act","change_pwd");
params.append("old_pwd",old);
params.append("new_pwd",new1);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("密码修改成功,即将跳转登录页重新验证");
setTimeout(()=>location.href="login.php",1800);
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 2 保存外观配置(存入sys_configdashboard读取渲染)
function saveTheme(){
const params = new URLSearchParams();
params.append("act","save_theme");
params.append("title",document.getElementById("page_title").value);
params.append("color",document.getElementById("theme_color").value);
params.append("pagesize",document.getElementById("page_size").value);
params.append("footer",document.getElementById("footer_text").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("外观配置保存成功,刷新监控大屏即可生效");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
// 3 保存侧边外链(存入sys_configdashboard侧边读取渲染)
function saveLink(){
const params = new URLSearchParams();
params.append("act","save_sidebar_link");
params.append("link_text",document.getElementById("sidebar_links").value);
fetch(api, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded; charset=utf-8"
},
body: params.toString()
})
.then(r=>r.json())
.then(json=>{
resultBox.innerText = JSON.stringify(json,null,2);
if(json.code===0){
showMsg("侧边外链保存成功,刷新监控大屏侧边栏立即更新");
}else{
showMsg(json.msg,"error");
}
})
.catch(e=>{
showMsg("接口请求失败:"+e,"error");
})
}
</script>
</body>
</html>
+375
View File
@@ -0,0 +1,375 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$userName = $_SESSION['user_name'] ?? '';
$today = date("Y-m-d");
// Grafana固定看板地址
$grafanaBase = "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警时序监控面板</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/chart.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1400px;margin:0 auto;padding:20px;}
/* 头部布局修改 */
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.nav-left{display:flex;gap:12px;align-items:center;}
.nav-right{display:flex;gap:10px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
.filter-full{width:100%;}
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
label{font-size:13px;color:#4E5969;}
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-primary:hover{background:#0E4BDB;}
.btn-warning{background:#FF7D00;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
.btn-danger{background:#F53F3F;color:#fff;}
.btn-danger:hover{background:#D83636;}
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
.stat-card{padding:16px;border-radius:8px;text-align:center;}
.stat-1{background:#E8F3FF;}
.stat-2{background:#FFECE8;}
.stat-3{background:#E6FFEA;}
.stat-4{background:#FFF7E6;}
.stat-label{font-size:13px;color:#4E5969;}
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
.chart-box{width:100%;height:260px;position:relative;overflow:hidden;}
canvas{width:100% !important;height:100% !important;}
.table-wrap{overflow-x:auto;}
table{width:100%;border-collapse:collapse;font-size:14px;}
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
.row-firing{background:#FFF7F7;}
.row-resolved{background:#F7FFF9;}
.storm{color:#F53F3F;font-weight:500;}
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
.empty-box{padding:60px;text-align:center;color:#86909C;}
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<div class="nav-left">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
</div>
<!-- 右侧:订阅管理 + 用户名 + 退出登录 -->
<div class="nav-right">
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
</div>
</div>
<!-- 筛选区域 -->
<div class="card">
<div class="filter-row filter-full">
<input type="text" id="globalSearch" placeholder="全局检索(告警/实例/级别)" style="width:100%;">
</div>
<div class="filter-row">
<div class="filter-item">
<label>起始日期</label>
<input type="date" id="startDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>结束日期</label>
<input type="date" id="endDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>实例模糊匹配</label>
<input type="text" id="filterInstance" placeholder="10.150.10.82">
</div>
<div class="filter-item">
<label>告警级别</label>
<div class="check-group">
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
<label><input type="checkbox" name="sev" value="info"> Info</label>
</div>
</div>
<div class="filter-item">
<label>告警状态</label>
<div class="check-group">
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
</div>
</div>
</div>
<div class="filter-row">
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> 查询</button>
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> 重置筛选</button>
<button class="btn btn-warning" id="btnToggleMode"><i class="fa fa-list"></i> 切换详细模式</button>
<span class="tip-text" id="modeTip">当前:去重汇总模式</span>
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> 导出CSV报表</button>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="autoRefresh"> 60秒自动刷新
</label>
</div>
</div>
<!-- 统计图表 -->
<div class="card" id="statCard" style="display:none;">
<div class="stat-row">
<div class="stat-card stat-1">
<div class="stat-label">告警种类</div>
<div class="stat-num" id="statTotal">0</div>
</div>
<div class="stat-card stat-2">
<div class="stat-label">触发中</div>
<div class="stat-num" id="statFiring">0</div>
</div>
<div class="stat-card stat-3">
<div class="stat-label">已恢复</div>
<div class="stat-num" id="statResolved">0</div>
</div>
<div class="stat-card stat-4">
<div class="stat-label">严重告警</div>
<div class="stat-num" id="statCritical">0</div>
</div>
</div>
<div class="chart-row">
<div>
<h4 class="card-title">24小时告警触发趋势</h4>
<div class="chart-box">
<canvas id="hourChart"></canvas>
</div>
</div>
<div>
<h4 class="card-title">告警级别分布占比</h4>
<div class="chart-box">
<canvas id="pieChart"></canvas>
</div>
</div>
</div>
</div>
<!-- 告警表格 -->
<div class="card">
<div class="table-wrap">
<table>
<thead id="tableHead"></thead>
<tbody id="tableBody">
<tr><td colspan="6" class="empty-box">数据加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pag-info">总告警条数:<span id="pageTotal">0</span></div>
<div style="display:flex;gap:8px;align-items:center;">
<span>每页展示</span>
<select id="pageSizeSel" class="pag-btn">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst">首页</button>
<button class="pag-btn" id="pagePrev">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext">下一页</button>
<button class="pag-btn" id="pageLast">末页</button>
</div>
</div>
</div>
<div class="shortcut-tip">快捷键:Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索</div>
</div>
<script>
const grafanaFullBase = "<?php echo $grafanaBase; ?>";
let page = 1;
let pageSize = 20;
let total = 0;
let distinctMode = 1;
let autoRefreshTimer = null;
let hourChart = null;
let pieChart = null;
const stormThreshold = 1000;
const t = {
col_state:"状态",col_alert:"告警名称",col_instance:"实例",
col_severity:"级别",col_count:"触发次数",col_time:"最新时间",
empty_tip:"暂无匹配告警数据,请调整筛选条件",storm_tip:"风暴告警"
};
// UTC秒级时间戳转北京时间
function utcTsToCst(ts) {
const date = new Date(ts * 1000);
const cstTime = date.getTime() + 8 * 3600 * 1000;
const cstDate = new Date(cstTime);
const Y = cstDate.getFullYear();
const m = String(cstDate.getMonth() + 1).padStart(2, '0');
const d = String(cstDate.getDate()).padStart(2, '0');
const H = String(cstDate.getHours()).padStart(2, '0');
const i = String(cstDate.getMinutes()).padStart(2, '0');
const s = String(cstDate.getSeconds()).padStart(2, '0');
return `${Y}-${m}-${d} ${H}:${i}:${s}`;
}
function saveFilter(){
const d={start:$("#startDate").val(),end:$("#endDate").val(),instance:$("#filterInstance").val(),global:$("#globalSearch").val(),sev:$("input[name=sev]:checked").map((i,e)=>e.value).get(),sta:$("input[name=sta]:checked").map((i,e)=>e.value).get(),distinct:distinctMode,pageSize:pageSize};
localStorage.setItem("alertFilter",JSON.stringify(d));
}
function loadFilter(){
const s=localStorage.getItem("alertFilter");if(!s)return;
const d=JSON.parse(s);
$("#startDate").val(d.start);$("#endDate").val(d.end);$("#filterInstance").val(d.instance);$("#globalSearch").val(d.global);
pageSize=d.pageSize;$("#pageSizeSel").val(pageSize);distinctMode=d.distinct;
$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");
$("input[name=sev]").prop("checked",false);d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
$("input[name=sta]").prop("checked",false);d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
}
function renderHead(){
let h="";
if(distinctMode===1){
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_count}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}else{
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}
$("#tableHead").html(h);
}
function drawChart(stat){
$("#statCard").show();
$("#statTotal").text(stat.total_all);$("#statFiring").text(stat.total_firing);
$("#statResolved").text(stat.total_resolved);$("#statCritical").text(stat.total_critical);
const hourData=stat.hour_data,hourLabels=[],hourVals=[];
for(let i=0;i<24;i++){hourLabels.push(i+":00");hourVals.push(hourData[i]);}
if(hourChart) hourChart.destroy();
const hCtx=document.getElementById("hourChart").getContext("2d");
hourChart=new Chart(hCtx,{type:"line",data:{labels:hourLabels,datasets:[{label:"触发量",data:hourVals,borderColor:"#165DFF",backgroundColor:"rgba(22,93,255,0.08)",fill:true,tension:0.2,pointRadius:2}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"top",labels:{boxWidth:12,font:{size:12}}}},scales:{x:{grid:{display:false}},y:{grid:{color:"#F2F3F5"}}}}});
if(pieChart) pieChart.destroy();
const pCtx=document.getElementById("pieChart").getContext("2d");
let c=0,w=0,i=0;$("#tableBody tr").each((idx,tr)=>{const s=$(tr).find("td:nth-child(4)").text();if(s.includes("critical"))c++;else if(s.includes("warning"))w++;else i++;});
pieChart=new Chart(pCtx,{type:"doughnut",data:{labels:["Critical","Warning","Info"],datasets:[{data:[c,w,i],backgroundColor:["#F53F3F","#FF7D00","#165DFF"],borderWidth:0}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"bottom"}},cutout:"60%"}});
}
function loadData(){
saveFilter();renderHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">数据加载中...</td></tr>`);
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("page",page);params.append("page_size",pageSize);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">${res.msg}</td></tr>`);$("#statCard").hide();return;}
total=res.total;$("#pageTotal").text(total);const tp=Math.ceil(total/pageSize)||1;$("#pageInfo").text(`${page} / ${tp}`);
if(res.stat.total_all>0) drawChart(res.stat);else $("#statCard").hide();
const list=res.list;if(list.length===0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);return;}
let html="";
list.forEach(row=>{
const cls=row.state==="firing"?"row-firing":"row-resolved";
const tagText=row.state==="firing"?"Firing":"Resolved";
let sevCls="";if(row.severity==="critical")sevCls="color:#F53F3F";else if(row.severity==="warning")sevCls="color:#FF7D00";else sevCls="color:#165DFF";
let storm="";if(distinctMode===1&&row.total_count>stormThreshold)storm=`<span class="storm">${t.storm_tip}</span>`;
const cstTimeStr = utcTsToCst(row.timestamp);
const grafanaUrl = `${grafanaFullBase}&var-instance=${encodeURIComponent(row.instance)}&var-alertname=${encodeURIComponent(row.metric)}`;
const btnHtml = `
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button>
<a class="btn btn-sm btn-primary" target="_blank" href="${grafanaUrl}">查看图表</a>
`;
if(distinctMode===1){
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${row.total_count} ${storm}</td><td>${cstTimeStr}</td>
<td>${btnHtml}</td>
</tr>`;
}else{
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${cstTimeStr}</td>
<td>${btnHtml}</td>
</tr>`;
}
});
$("#tableBody").html(html);
}).catch(()=>{$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">接口请求失败</td></tr>`);$("#statCard").hide();});
}
function exportExcel(){
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("export",1);params.append("page_size",9999);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0) return alert(res.msg);
let csv="";
if(distinctMode===1) csv="状态,告警名称,实例,级别,触发次数,最新时间\n";
else csv="状态,告警名称,实例,级别,触发时间\n";
res.list.forEach(r=>{
if(distinctMode===1) csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${utcTsToCst(r.timestamp)}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${utcTsToCst(r.timestamp)}\n`;
});
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`告警报表_${new Date().getTime()}.csv`;a.click();
});
}
function toggleAutoRefresh(){
if($("#autoRefresh").is(":checked")) autoRefreshTimer=setInterval(()=>loadData(),60000);
else clearInterval(autoRefreshTimer);
}
$(function(){
loadFilter();loadData();
// 退出登录按钮
$("#logoutBtn").click(function(){
if(confirm("确定退出当前账号?")){
location.href="login.php";
}
});
$("#btnSearch").click(()=>{page=1;loadData();});
$("#btnReset").click(()=>{
const today=new Date().toISOString().split('T')[0];
$("#startDate").val(today);$("#endDate").val(today);$("#filterInstance").val("");$("#globalSearch").val("");
$("input[name=sev]").prop("checked",false);$("input[name=sta]").prop("checked",false);
distinctMode=1;$("#modeTip").text("当前:去重汇总模式");page=1;loadData();
});
$("#btnToggleMode").click(()=>{distinctMode=distinctMode===1?0:1;$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");page=1;loadData();});
$("#btnExport").click(exportExcel);
$("#autoRefresh").change(toggleAutoRefresh);
$("#pageSizeSel").change(function(){pageSize=parseInt($(this).val());page=1;loadData();});
$("#pageFirst").click(()=>{page=1;loadData();});
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
$(document).on("click",".copy-name",function(){
const name=$(this).data("name");
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
});
$(document).keydown(function(e){
if(e.key==="Enter"){e.preventDefault();page=1;loadData();}
if(e.ctrlKey&&e.key.toLowerCase()==="f"){e.preventDefault();$("#globalSearch").focus();}
});
})
</script>
</body>
</html>
+340
View File
@@ -0,0 +1,340 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$userName = $_SESSION['user_name'] ?? '';
$today = date("Y-m-d");
$grafanaUrl = "http://10.150.117.190:3000";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警时序监控面板</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/chart.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1400px;margin:0 auto;padding:20px;}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.nav-group{display:flex;gap:12px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.user-info{display:flex;gap:12px;align-items:center;}
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
.filter-full{width:100%;}
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
label{font-size:13px;color:#4E5969;}
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-primary:hover{background:#0E4BDB;}
.btn-warning{background:#FF7D00;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
.stat-card{padding:16px;border-radius:8px;text-align:center;}
.stat-1{background:#E8F3FF;}
.stat-2{background:#FFECE8;}
.stat-3{background:#E6FFEA;}
.stat-4{background:#FFF7E6;}
.stat-label{font-size:13px;color:#4E5969;}
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
.chart-box{width:100%;height:260px;position:relative;overflow:hidden;}
canvas{width:100% !important;height:100% !important;}
.table-wrap{overflow-x:auto;}
table{width:100%;border-collapse:collapse;font-size:14px;}
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
.row-firing{background:#FFF7F7;}
.row-resolved{background:#F7FFF9;}
.storm{color:#F53F3F;font-weight:500;}
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
.empty-box{padding:60px;text-align:center;color:#86909C;}
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<div class="nav-group">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
</div>
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
</div>
<!-- 筛选区域 -->
<div class="card">
<div class="filter-row filter-full">
<input type="text" id="globalSearch" placeholder="全局检索(告警/实例/级别)" style="width:100%;">
</div>
<div class="filter-row">
<div class="filter-item">
<label>起始日期</label>
<input type="date" id="startDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>结束日期</label>
<input type="date" id="endDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>实例模糊匹配</label>
<input type="text" id="filterInstance" placeholder="10.150.10.82">
</div>
<div class="filter-item">
<label>告警级别</label>
<div class="check-group">
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
<label><input type="checkbox" name="sev" value="info"> Info</label>
</div>
</div>
<div class="filter-item">
<label>告警状态</label>
<div class="check-group">
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
</div>
</div>
</div>
<div class="filter-row">
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> 查询</button>
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> 重置筛选</button>
<button class="btn btn-warning" id="btnToggleMode"><i class="fa fa-list"></i> 切换详细模式</button>
<span class="tip-text" id="modeTip">当前:去重汇总模式</span>
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> 导出CSV报表</button>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="autoRefresh"> 60秒自动刷新
</label>
</div>
</div>
<!-- 统计图表 -->
<div class="card" id="statCard" style="display:none;">
<div class="stat-row">
<div class="stat-card stat-1">
<div class="stat-label">告警种类</div>
<div class="stat-num" id="statTotal">0</div>
</div>
<div class="stat-card stat-2">
<div class="stat-label">触发中</div>
<div class="stat-num" id="statFiring">0</div>
</div>
<div class="stat-card stat-3">
<div class="stat-label">已恢复</div>
<div class="stat-num" id="statResolved">0</div>
</div>
<div class="stat-card stat-4">
<div class="stat-label">严重告警</div>
<div class="stat-num" id="statCritical">0</div>
</div>
</div>
<div class="chart-row">
<div>
<h4 class="card-title">24小时告警触发趋势</h4>
<div class="chart-box">
<canvas id="hourChart"></canvas>
</div>
</div>
<div>
<h4 class="card-title">告警级别分布占比</h4>
<div class="chart-box">
<canvas id="pieChart"></canvas>
</div>
</div>
</div>
</div>
<!-- 告警表格 -->
<div class="card">
<div class="table-wrap">
<table>
<thead id="tableHead"></thead>
<tbody id="tableBody">
<tr><td colspan="6" class="empty-box">数据加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pag-info">总告警条数:<span id="pageTotal">0</span></div>
<div style="display:flex;gap:8px;align-items:center;">
<span>每页展示</span>
<select id="pageSizeSel" class="pag-btn">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst">首页</button>
<button class="pag-btn" id="pagePrev">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext">下一页</button>
<button class="pag-btn" id="pageLast">末页</button>
</div>
</div>
</div>
<div class="shortcut-tip">快捷键:Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索</div>
</div>
<script>
// 原有告警查询、图表、分页、导出逻辑不变
const grafanaBase = "<?php echo $grafanaUrl; ?>";
let page = 1;
let pageSize = 20;
let total = 0;
let distinctMode = 1;
let autoRefreshTimer = null;
let hourChart = null;
let pieChart = null;
const stormThreshold = 1000;
const t = {
col_state:"状态",col_alert:"告警名称",col_instance:"实例",
col_severity:"级别",col_count:"触发次数",col_time:"最新时间",
empty_tip:"暂无匹配告警数据,请调整筛选条件",storm_tip:"风暴告警"
};
function saveFilter(){
const d={start:$("#startDate").val(),end:$("#endDate").val(),instance:$("#filterInstance").val(),global:$("#globalSearch").val(),sev:$("input[name=sev]:checked").map((i,e)=>e.value).get(),sta:$("input[name=sta]:checked").map((i,e)=>e.value).get(),distinct:distinctMode,pageSize:pageSize};
localStorage.setItem("alertFilter",JSON.stringify(d));
}
function loadFilter(){
const s=localStorage.getItem("alertFilter");if(!s)return;
const d=JSON.parse(s);
$("#startDate").val(d.start);$("#endDate").val(d.end);$("#filterInstance").val(d.instance);$("#globalSearch").val(d.global);
pageSize=d.pageSize;$("#pageSizeSel").val(pageSize);distinctMode=d.distinct;
$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");
$("input[name=sev]").prop("checked",false);d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
$("input[name=sta]").prop("checked",false);d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
}
function renderHead(){
let h="";
if(distinctMode===1){
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_count}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}else{
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}
$("#tableHead").html(h);
}
function drawChart(stat){
$("#statCard").show();
$("#statTotal").text(stat.total_all);$("#statFiring").text(stat.total_firing);
$("#statResolved").text(stat.total_resolved);$("#statCritical").text(stat.total_critical);
const hourData=stat.hour_data,hourLabels=[],hourVals=[];
for(let i=0;i<24;i++){hourLabels.push(i+":00");hourVals.push(hourData[i]);}
if(hourChart) hourChart.destroy();
const hCtx=document.getElementById("hourChart").getContext("2d");
hourChart=new Chart(hCtx,{type:"line",data:{labels:hourLabels,datasets:[{label:"触发量",data:hourVals,borderColor:"#165DFF",backgroundColor:"rgba(22,93,255,0.08)",fill:true,tension:0.2,pointRadius:2}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"top",labels:{boxWidth:12,font:{size:12}}}},scales:{x:{grid:{display:false}},y:{grid:{color:"#F2F3F5"}}}}});
if(pieChart) pieChart.destroy();
const pCtx=document.getElementById("pieChart").getContext("2d");
let c=0,w=0,i=0;$("#tableBody tr").each((idx,tr)=>{const s=$(tr).find("td:nth-child(4)").text();if(s.includes("critical"))c++;else if(s.includes("warning"))w++;else i++;});
pieChart=new Chart(pCtx,{type:"doughnut",data:{labels:["Critical","Warning","Info"],datasets:[{data:[c,w,i],backgroundColor:["#F53F3F","#FF7D00","#165DFF"],borderWidth:0}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"bottom"}},cutout:"60%"}});
}
function loadData(){
saveFilter();renderHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">数据加载中...</td></tr>`);
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]:checked").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("page",page);params.append("page_size",pageSize);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">${res.msg}</td></tr>`);$("#statCard").hide();return;}
total=res.total;$("#pageTotal").text(total);const tp=Math.ceil(total/pageSize)||1;$("#pageInfo").text(`${page} / ${tp}`);
if(res.stat.total_all>0) drawChart(res.stat);else $("#statCard").hide();
const list=res.list;if(list.length===0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);return;}
let html="";
list.forEach(row=>{
const cls=row.state==="firing"?"row-firing":"row-resolved";
const tagText=row.state==="firing"?"Firing":"Resolved";
let sevCls="";if(row.severity==="critical")sevCls="color:#F53F3F";else if(row.severity==="warning")sevCls="color:#FF7D00";else sevCls="color:#165DFF";
let storm="";if(distinctMode===1&&row.total_count>stormThreshold)storm=`<span class="storm">${t.storm_tip}</span>`;
if(distinctMode===1){
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${row.total_count} ${storm}</td><td>${row.time}</td>
<td><button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button></td>
</tr>`;
}else{
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${row.time}</td>
<td><button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button></td>
</tr>`;
}
});
$("#tableBody").html(html);
}).catch(()=>{$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">接口请求失败</td></tr>`);$("#statCard").hide();});
}
function exportExcel(){
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("export",1);params.append("page_size",9999);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0) return alert(res.msg);
let csv="";
if(distinctMode===1) csv="状态,告警名称,实例,级别,触发次数,最新时间\n";
else csv="状态,告警名称,实例,级别,触发时间\n";
res.list.forEach(r=>{
if(distinctMode===1) csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${r.time}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.time}\n`;
});
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`告警报表_${new Date().getTime()}.csv`;a.click();
});
}
function toggleAutoRefresh(){
if($("#autoRefresh").is(":checked")) autoRefreshTimer=setInterval(()=>loadData(),60000);
else clearInterval(autoRefreshTimer);
}
$(function(){
loadFilter();loadData();
$("#btnSearch").click(()=>{page=1;loadData();});
$("#btnReset").click(()=>{
const today=new Date().toISOString().split('T')[0];
$("#startDate").val(today);$("#endDate").val(today);$("#filterInstance").val("");$("#globalSearch").val("");
$("input[name=sev]").prop("checked",false);$("input[name=sta]").prop("checked",false);
distinctMode=1;$("#modeTip").text("当前:去重汇总模式");page=1;loadData();
});
$("#btnToggleMode").click(()=>{distinctMode=distinctMode===1?0:1;$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");page=1;loadData();});
$("#btnExport").click(exportExcel);
$("#autoRefresh").change(toggleAutoRefresh);
$("#pageSizeSel").change(function(){pageSize=parseInt($(this).val());page=1;loadData();});
$("#pageFirst").click(()=>{page=1;loadData();});
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
$(document).on("click",".copy-name",function(){
const name=$(this).data("name");
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
});
$(document).keydown(function(e){
if(e.key==="Enter"){e.preventDefault();page=1;loadData();}
if(e.ctrlKey&&e.key.toLowerCase()==="f"){e.preventDefault();$("#globalSearch").focus();}
});
})
</script>
</body>
</html>
+367
View File
@@ -0,0 +1,367 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$userName = $_SESSION['user_name'] ?? '';
$today = date("Y-m-d");
$grafanaBase = "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s";
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警时序监控面板</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/chart.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1400px;margin:0 auto;padding:20px;}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.nav-left{display:flex;gap:12px;align-items:center;}
.nav-right{display:flex;gap:10px;align-items:center;}
.back-link{color:#165DFF;text-decoration:none;}
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
.filter-full{width:100%;}
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
label{font-size:13px;color:#4E5969;}
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-primary:hover{background:#0E4BDB;}
.btn-warning{background:#FF7D00;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
.btn-danger{background:#F53F3F;color:#fff;}
.btn-danger:hover{background:#D83636;}
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
.stat-card{padding:16px;border-radius:8px;text-align:center;}
.stat-1{background:#E8F3FF;}
.stat-2{background:#FFECE8;}
.stat-3{background:#E6FFEA;}
.stat-4{background:#FFF7E6;}
.stat-label{font-size:13px;color:#4E5969;}
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
.chart-box{width:100%;height:260px;position:relative;overflow:hidden;}
canvas{width:100% !important;height:100% !important;}
.table-wrap{overflow-x:auto;}
table{width:100%;border-collapse:collapse;font-size:14px;}
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
.row-firing{background:#FFF7F7;}
.row-resolved{background:#F7FFF9;}
.storm{color:#F53F3F;font-weight:500;}
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
.empty-box{padding:60px;text-align:center;color:#86909C;}
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<div class="nav-left">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
</div>
<div class="nav-right">
<a href="alert_subscribe.php" class="btn btn-outline"><i class="fa fa-bell"></i> 告警订阅管理</a>
<span style="color:#4E5969;"><i class="fa fa-user"></i> <?php echo $userName; ?></span>
<button class="btn btn-danger" id="logoutBtn"><i class="fa fa-sign-out"></i> 退出登录</button>
</div>
</div>
<div class="card">
<div class="filter-row filter-full">
<input type="text" id="globalSearch" placeholder="全局检索(告警/实例/级别)" style="width:100%;">
</div>
<div class="filter-row">
<div class="filter-item">
<label>起始日期</label>
<input type="date" id="startDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>结束日期</label>
<input type="date" id="endDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label>实例模糊匹配</label>
<input type="text" id="filterInstance" placeholder="10.150.10.82">
</div>
<div class="filter-item">
<label>告警级别</label>
<div class="check-group">
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
<label><input type="checkbox" name="sev" value="info"> Info</label>
</div>
</div>
<div class="filter-item">
<label>告警状态</label>
<div class="check-group">
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
</div>
</div>
</div>
<div class="filter-row">
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> 查询</button>
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> 重置筛选</button>
<button class="btn btn-warning" id="btnToggleMode"><i class="fa fa-list"></i> 切换详细模式</button>
<span class="tip-text" id="modeTip">当前:去重汇总模式</span>
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> 导出CSV报表</button>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="autoRefresh"> 60秒自动刷新
</label>
</div>
</div>
<div class="card" id="statCard" style="display:none;">
<div class="stat-row">
<div class="stat-card stat-1">
<div class="stat-label">告警种类</div>
<div class="stat-num" id="statTotal">0</div>
</div>
<div class="stat-card stat-2">
<div class="stat-label">触发中</div>
<div class="stat-num" id="statFiring">0</div>
</div>
<div class="stat-card stat-3">
<div class="stat-label">已恢复</div>
<div class="stat-num" id="statResolved">0</div>
</div>
<div class="stat-card stat-4">
<div class="stat-label">严重告警</div>
<div class="stat-num" id="statCritical">0</div>
</div>
</div>
<div class="chart-row">
<div>
<h4 class="card-title">24小时告警触发趋势</h4>
<div class="chart-box">
<canvas id="hourChart"></canvas>
</div>
</div>
<div>
<h4 class="card-title">告警级别分布占比</h4>
<div class="chart-box">
<canvas id="pieChart"></canvas>
</div>
</div>
</div>
</div>
<div class="card">
<div class="table-wrap">
<table>
<thead id="tableHead"></thead>
<tbody id="tableBody">
<tr><td colspan="6" class="empty-box">数据加载中...</td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pag-info">总告警条数:<span id="pageTotal">0</span></div>
<div style="display:flex;gap:8px;align-items:center;">
<span>每页展示</span>
<select id="pageSizeSel" class="pag-btn">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst">首页</button>
<button class="pag-btn" id="pagePrev">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext">下一页</button>
<button class="pag-btn" id="pageLast">末页</button>
</div>
</div>
</div>
<div class="shortcut-tip">快捷键:Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索</div>
</div>
<script>
const grafanaFullBase = "<?php echo $grafanaBase; ?>";
let page = 1;
let pageSize = 20;
let total = 0;
let distinctMode = 1;
let autoRefreshTimer = null;
let hourChart = null;
let pieChart = null;
const stormThreshold = 1000;
const t = {
col_state:"状态",col_alert:"告警名称",col_instance:"实例",
col_severity:"级别",col_count:"触发次数",col_time:"最新时间",
empty_tip:"暂无匹配告警数据,请调整筛选条件",storm_tip:"风暴告警"
};
// 时区回滚:原生浏览器本地时区,删除手动+8偏移
function utcTsToCst(ts) {
const date = new Date(ts * 1000);
const Y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const H = String(date.getHours()).padStart(2, '0');
const i = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${Y}-${m}-${d} ${H}:${i}:${s}`;
}
function saveFilter(){
const d={start:$("#startDate").val(),end:$("#endDate").val(),instance:$("#filterInstance").val(),global:$("#globalSearch").val(),sev:$("input[name=sev]:checked").map((i,e)=>e.value).get(),sta:$("input[name=sta]:checked").map((i,e)=>e.value).get(),distinct:distinctMode,pageSize:pageSize};
localStorage.setItem("alertFilter",JSON.stringify(d));
}
function loadFilter(){
const s=localStorage.getItem("alertFilter");if(!s)return;
const d=JSON.parse(s);
$("#startDate").val(d.start);$("#endDate").val(d.end);$("#filterInstance").val(d.instance);$("#globalSearch").val(d.global);
pageSize=d.pageSize;$("#pageSizeSel").val(pageSize);distinctMode=d.distinct;
$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");
$("input[name=sev]").prop("checked",false);d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
$("input[name=sta]").prop("checked",false);d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
}
function renderHead(){
let h="";
if(distinctMode===1){
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_count}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}else{
h=`<tr><th>${t.col_state}</th><th>${t.col_alert}</th><th>${t.col_instance}</th><th>${t.col_severity}</th><th>${t.col_time}</th><th>操作</th></tr>`;
}
$("#tableHead").html(h);
}
function drawChart(stat){
$("#statCard").show();
$("#statTotal").text(stat.total_all);$("#statFiring").text(stat.total_firing);
$("#statResolved").text(stat.total_resolved);$("#statCritical").text(stat.total_critical);
const hourData=stat.hour_data,hourLabels=[],hourVals=[];
for(let i=0;i<24;i++){hourLabels.push(i+":00");hourVals.push(hourData[i]);}
if(hourChart) hourChart.destroy();
const hCtx=document.getElementById("hourChart").getContext("2d");
hourChart=new Chart(hCtx,{type:"line",data:{labels:hourLabels,datasets:[{label:"触发量",data:hourVals,borderColor:"#165DFF",backgroundColor:"rgba(22,93,255,0.08)",fill:true,tension:0.2,pointRadius:2}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"top",labels:{boxWidth:12,font:{size:12}}}},scales:{x:{grid:{display:false}},y:{grid:{color:"#F2F3F5"}}}}});
if(pieChart) pieChart.destroy();
const pCtx=document.getElementById("pieChart").getContext("2d");
let c=0,w=0,i=0;$("#tableBody tr").each((idx,tr)=>{const s=$(tr).find("td:nth-child(4)").text();if(s.includes("critical"))c++;else if(s.includes("warning"))w++;else i++;});
pieChart=new Chart(pCtx,{type:"doughnut",data:{labels:["Critical","Warning","Info"],datasets:[{data:[c,w,i],backgroundColor:["#F53F3F","#FF7D00","#165DFF"],borderWidth:0}]},
options:{responsive:true,maintainAspectRatio:false,plugins:{legend:{position:"bottom"}},cutout:"60%"}});
}
function loadData(){
saveFilter();renderHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">数据加载中...</td></tr>`);
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("page",page);params.append("page_size",pageSize);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">${res.msg}</td></tr>`);$("#statCard").hide();return;}
total=res.total;$("#pageTotal").text(total);const tp=Math.ceil(total/pageSize)||1;$("#pageInfo").text(`${page} / ${tp}`);
if(res.stat.total_all>0) drawChart(res.stat);else $("#statCard").hide();
const list=res.list;if(list.length===0){$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);return;}
let html="";
list.forEach(row=>{
const cls=row.state==="firing"?"row-firing":"row-resolved";
const tagText=row.state==="firing"?"Firing":"Resolved";
let sevCls="";if(row.severity==="critical")sevCls="color:#F53F3F";else if(row.severity==="warning")sevCls="color:#FF7D00";else sevCls="color:#165DFF";
let storm="";if(distinctMode===1&&row.total_count>stormThreshold)storm=`<span class="storm">${t.storm_tip}</span>`;
const cstTimeStr = utcTsToCst(row.timestamp);
const grafanaUrl = `${grafanaFullBase}&var-instance=${encodeURIComponent(row.instance)}&var-alertname=${encodeURIComponent(row.metric)}`;
// 统一btn-sm btn-outline,无蓝色主按钮
const btnHtml = `
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">复制告警</button>
<a class="btn btn-sm btn-outline" target="_blank" href="${grafanaUrl}">查看图表</a>
`;
if(distinctMode===1){
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${row.total_count} ${storm}</td><td>${cstTimeStr}</td>
<td>${btnHtml}</td>
</tr>`;
}else{
html+=`<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td>${tagText}</td><td>${row.metric}</td><td>${row.instance}</td>
<td style="${sevCls}">${row.severity}</td><td>${cstTimeStr}</td>
<td>${btnHtml}</td>
</tr>`;
}
});
$("#tableBody").html(html);
}).catch(()=>{$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;">接口请求失败</td></tr>`);$("#statCard").hide();});
}
function exportExcel(){
const sevArr=$("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr=$("input[name=sta]").map((i,e)=>e.value).get();
const params=new URLSearchParams();
params.append("export",1);params.append("page_size",9999);
params.append("start_date",$("#startDate").val());params.append("end_date",$("#endDate").val());
params.append("instance",$("#filterInstance").val());params.append("global",$("#globalSearch").val());
params.append("distinct",distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));staArr.forEach(v=>params.append("status[]",v));
fetch(`./api/api_alert_list.php?${params.toString()}`).then(r=>r.json()).then(res=>{
if(res.code!==0) return alert(res.msg);
let csv="";
if(distinctMode===1) csv="状态,告警名称,实例,级别,触发次数,最新时间\n";
else csv="状态,告警名称,实例,级别,触发时间\n";
res.list.forEach(r=>{
if(distinctMode===1) csv+=`${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${utcTsToCst(r.timestamp)}\n`;
else csv+=`${r.state},${r.metric},${r.instance},${r.severity},${utcTsToCst(r.timestamp)}\n`;
});
const blob=new Blob([csv],{type:"text/csv;charset=utf-8"});
const a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download=`告警报表_${new Date().getTime()}.csv`;a.click();
});
}
function toggleAutoRefresh(){
if($("#autoRefresh").is(":checked")) autoRefreshTimer=setInterval(()=>loadData(),60000);
else clearInterval(autoRefreshTimer);
}
$(function(){
loadFilter();loadData();
$("#logoutBtn").click(function(){
if(confirm("确定退出当前账号?")){
location.href="login.php";
}
});
$("#btnSearch").click(()=>{page=1;loadData();});
$("#btnReset").click(()=>{
const today=new Date().toISOString().split('T')[0];
$("#startDate").val(today);$("#endDate").val(today);$("#filterInstance").val("");$("#globalSearch").val("");
$("input[name=sev]").prop("checked",false);$("input[name=sta]").prop("checked",false);
distinctMode=1;$("#modeTip").text("当前:去重汇总模式");page=1;loadData();
});
$("#btnToggleMode").click(()=>{distinctMode=distinctMode===1?0:1;$("#modeTip").text(distinctMode===1?"当前:去重汇总模式":"当前:逐条时序模式");page=1;loadData();});
$("#btnExport").click(exportExcel);
$("#autoRefresh").change(toggleAutoRefresh);
$("#pageSizeSel").change(function(){pageSize=parseInt($(this).val());page=1;loadData();});
$("#pageFirst").click(()=>{page=1;loadData();});
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
$(document).on("click",".copy-name",function(){
const name=$(this).data("name");
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
});
$(document).keydown(function(e){
if(e.key==="Enter"){e.preventDefault();page=1;loadData();}
if(e.ctrlKey&&e.key.toLowerCase()==="f"){e.preventDefault();$("#globalSearch").focus();}
})
})
</script>
</body>
</html>
+650
View File
@@ -0,0 +1,650 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username'] ?? "");
$userRole = $_SESSION['role'] ?? "user";
// 固定配置
$grafanaUrl = "http://10.150.117.190:3000";
$currentLang = "zh";
$langMap = [
'zh' => [
'page_title' => '告警时序监控面板',
'global_search' => '全局检索(告警/实例/级别)',
'filter_instance' => '实例IP模糊匹配',
'start_date' => '起始日期',
'end_date' => '结束日期',
'severity_title' => '告警级别',
'status_title' => '告警状态',
'btn_query' => '查询',
'btn_reset' => '重置筛选',
'btn_detail_mode' => '切换详细模式',
'tip_distinct' => '当前:去重汇总模式',
'tip_raw' => '当前:逐条时序模式',
'stat_total' => '总告警种类',
'stat_firing' => '触发中',
'stat_resolved' => '已恢复',
'stat_critical' => '严重告警',
'hour_chart_title' => '24小时告警触发趋势',
'pie_title' => '告警级别分布占比',
'col_state' => '状态',
'col_alert' => '告警名称',
'col_instance' => '实例地址',
'col_severity' => '级别',
'col_count' => '当日触发次数',
'col_time' => '最新触发时间',
'btn_copy_name' => '复制告警名',
'btn_copy_link' => '复制筛选链接',
'btn_export' => '导出CSV报表',
'btn_refresh_auto' => '60秒自动刷新',
'modal_title' => '告警完整详情',
'modal_grafana' => '跳转Grafana大盘',
'modal_labels' => '全量标签信息',
'empty_tip' => '暂无匹配告警数据,请调整筛选条件',
'storm_tip' => '风暴告警',
'shortcut_tip' => 'Enter快速查询 | ESC关闭弹窗 | Ctrl+F全局搜索'
]
];
$t = $langMap[$currentLang];
$today = date("Y-m-d");
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $t['page_title']; ?></title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/chart.js"></script>
<style>
/* 全局企业化基础样式 */
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",system-ui,sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1400px;margin:0 auto;padding:20px;}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.back-link{color:#165DFF;text-decoration:none;font-size:14px;}
.user-info{font-size:14px;color:#4E5969;}
/* 卡片通用 */
.card{background:#FFFFFF;border-radius:8px;padding:20px;box-shadow:0 1px 2px rgba(0,0,0,0.06);margin-bottom:16px;}
.card-title{font-size:15px;font-weight:500;color:#1D2129;margin-bottom:14px;}
/* 筛选区域 */
.filter-row{display:flex;flex-wrap:wrap;gap:16px;align-items:flex-end;margin-bottom:16px;}
.filter-full{width:100%;}
.filter-item{display:flex;flex-direction:column;gap:6px;min-width:160px;}
label{font-size:13px;color:#4E5969;}
input[type="date"],input[type="text"]{height:36px;padding:0 12px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;color:#1D2129;}
.check-group{display:flex;gap:12px;flex-wrap:wrap;}
.check-group label{display:flex;align-items:center;gap:4px;cursor:pointer;}
/* 按钮规范 */
.btn{height:36px;padding:0 16px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;transition:0.15s;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-primary:hover{background:#0E4BDB;}
.btn-warning{background:#FF7D00;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-outline:hover{border-color:#165DFF;color:#165DFF;}
.btn-sm{height:28px;padding:0 8px;font-size:13px;}
/* 统计指标卡片 */
.stat-row{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-bottom:16px;}
.stat-card{padding:16px;border-radius:8px;text-align:center;}
.stat-1{background:#E8F3FF;}
.stat-2{background:#FFECE8;}
.stat-3{background:#E6FFEA;}
.stat-4{background:#FFF7E6;}
.stat-label{font-size:13px;color:#4E5969;}
.stat-num{font-size:26px;font-weight:600;margin:8px 0;}
/* 图表区域 固定高度 防止溢出 */
.chart-row{display:grid;grid-template-columns:2fr 1fr;gap:16px;margin-bottom:16px;}
.chart-box{width:100%;height:260px;position:relative;}
canvas{width:100% !important;height:100% !important;}
/* 表格 */
.table-wrap{overflow-x:auto;}
table{width:100%;border-collapse:collapse;font-size:14px;}
th{background:#F5F7FA;padding:12px 10px;text-align:left;border-bottom:1px solid #E5E6EB;color:#4E5969;font-weight:500;white-space:nowrap;}
td{padding:12px 10px;border-bottom:1px solid #F2F3F5;color:#1D2129;}
.row-firing{background:#FFF7F7;}
.row-resolved{background:#F7FFF9;}
.storm{color:#F53F3F;font-weight:500;}
/* 分页 */
.pagination{display:flex;justify-content:space-between;align-items:center;margin-top:16px;flex-wrap:wrap;gap:12px;}
.pag-btn{height:32px;padding:0 12px;border:1px solid #DCDFE6;background:#fff;border-radius:6px;cursor:pointer;font-size:14px;}
.pag-info{color:#4E5969;}
/* 弹窗 */
.modal-mask{position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.4);display:none;align-items:center;justify-content:center;z-index:999;}
.modal-box{width:760px;max-height:80vh;background:#fff;border-radius:8px;padding:24px;overflow-y:auto;}
.modal-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.modal-close{cursor:pointer;font-size:20px;color:#4E5969;}
.labels-block{background:#F5F7FA;padding:12px;border-radius:6px;margin:12px 0;white-space:pre-wrap;font-family:monospace;font-size:13px;}
/* 空数据、提示文字 */
.empty-box{padding:60px;text-align:center;color:#86909C;}
.tip-text{font-size:13px;color:#86909C;margin-left:8px;}
.shortcut-tip{text-align:center;color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<a href="dashboard.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回监控大盘</a>
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
</div>
<!-- 筛选区域 -->
<div class="card">
<div class="filter-row filter-full">
<input type="text" id="globalSearch" placeholder="<?php echo $t['global_search']; ?>" style="width:100%;">
</div>
<div class="filter-row">
<div class="filter-item">
<label><?php echo $t['start_date']; ?></label>
<input type="date" id="startDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label><?php echo $t['end_date']; ?></label>
<input type="date" id="endDate" value="<?php echo $today; ?>">
</div>
<div class="filter-item">
<label><?php echo $t['filter_instance']; ?></label>
<input type="text" id="filterInstance" placeholder="10.150.10.82">
</div>
<div class="filter-item">
<label><?php echo $t['severity_title']; ?></label>
<div class="check-group">
<label><input type="checkbox" name="sev" value="critical"> Critical</label>
<label><input type="checkbox" name="sev" value="warning"> Warning</label>
<label><input type="checkbox" name="sev" value="info"> Info</label>
</div>
</div>
<div class="filter-item">
<label><?php echo $t['status_title']; ?></label>
<div class="check-group">
<label><input type="checkbox" name="sta" value="firing"> Firing</label>
<label><input type="checkbox" name="sta" value="resolved"> Resolved</label>
</div>
</div>
</div>
<div class="filter-row">
<button class="btn btn-primary" id="btnSearch"><i class="fa fa-search"></i> <?php echo $t['btn_query']; ?></button>
<button class="btn btn-outline" id="btnReset"><i class="fa fa-refresh"></i> <?php echo $t['btn_reset']; ?></button>
<button class="btn btn-warning" id="btnToggleDistinct"><i class="fa fa-list"></i> <?php echo $t['btn_detail_mode']; ?></button>
<span class="tip-text" id="modeTip"><?php echo $t['tip_distinct']; ?></span>
<button class="btn btn-outline" id="btnExport"><i class="fa fa-file-excel-o"></i> <?php echo $t['btn_export']; ?></button>
<button class="btn btn-outline" id="btnCopyLink"><i class="fa fa-link"></i> <?php echo $t['btn_copy_link']; ?></button>
<label style="display:flex;align-items:center;gap:6px;">
<input type="checkbox" id="autoRefresh"> <?php echo $t['btn_refresh_auto']; ?>
</label>
</div>
</div>
<!-- 统计卡片 + 图表 -->
<div class="card" id="statCard" style="display:none;">
<div class="stat-row">
<div class="stat-card stat-1">
<div class="stat-label"><?php echo $t['stat_total']; ?></div>
<div class="stat-num" id="statTotal">0</div>
</div>
<div class="stat-card stat-2">
<div class="stat-label"><?php echo $t['stat_firing']; ?></div>
<div class="stat-num" id="statFiring">0</div>
</div>
<div class="stat-card stat-3">
<div class="stat-label"><?php echo $t['stat_resolved']; ?></div>
<div class="stat-num" id="statResolved">0</div>
</div>
<div class="stat-card stat-4">
<div class="stat-label"><?php echo $t['stat_critical']; ?></div>
<div class="stat-num" id="statCritical">0</div>
</div>
</div>
<div class="chart-row">
<div>
<h4 class="card-title"><?php echo $t['hour_chart_title']; ?></h4>
<div class="chart-box">
<canvas id="hourChart"></canvas>
</div>
</div>
<div>
<h4 class="card-title"><?php echo $t['pie_title']; ?></h4>
<div class="chart-box">
<canvas id="pieChart"></canvas>
</div>
</div>
</div>
</div>
<!-- 告警表格 -->
<div class="card">
<div class="table-wrap">
<table>
<thead id="tableHead"></thead>
<tbody id="tableBody">
<tr><td colspan="6" class="empty-box">Loading...</td></tr>
</tbody>
</table>
</div>
<div class="pagination">
<div class="pag-info"><?php echo $t['stat_total']; ?><span id="pageTotal">0</span></div>
<div style="display:flex;gap:8px;align-items:center;">
<span>每页</span>
<select id="pageSizeSel" class="pag-btn">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst">首页</button>
<button class="pag-btn" id="pagePrev">上一页</button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext">下一页</button>
<button class="pag-btn" id="pageLast">末页</button>
</div>
</div>
</div>
<div class="shortcut-tip"><?php echo $t['shortcut_tip']; ?></div>
</div>
<!-- 详情弹窗 -->
<div class="modal-mask" id="modalMask">
<div class="modal-box">
<div class="modal-head">
<h3 id="modalTitle"><?php echo $t['modal_title']; ?></h3>
<span class="modal-close" id="modalClose">×</span>
</div>
<div id="modalContent"></div>
<div style="margin-top:16px;">
<a target="_blank" id="grafanaLink" class="btn btn-primary"><i class="fa fa-chart-line"></i> <?php echo $t['modal_grafana']; ?></a>
</div>
</div>
</div>
<script>
const apiUrl = "./api/api_alert_list.php";
const grafanaBase = "<?php echo $grafanaUrl; ?>";
let page = 1;
let pageSize = 20;
let total = 0;
let distinctMode = 1;
let autoRefreshTimer = null;
let hourChart = null;
let pieChart = null;
const stormThreshold = 1000;
const t = <?php echo json_encode($t); ?>;
// 本地筛选记忆
function saveFilter() {
const data = {
start: $("#startDate").val(),
end: $("#endDate").val(),
instance: $("#filterInstance").val(),
global: $("#globalSearch").val(),
sev: $("input[name=sev]:checked").map((i,e)=>e.value).get(),
sta: $("input[name=sta]").map((i,e)=>e.value).get(),
distinct: distinctMode,
pageSize: pageSize
};
localStorage.setItem("alertFilter", JSON.stringify(data));
}
function loadFilter() {
const s = localStorage.getItem("alertFilter");
if (!s) return;
const d = JSON.parse(s);
$("#startDate").val(d.start);
$("#endDate").val(d.end);
$("#filterInstance").val(d.instance);
$("#globalSearch").val(d.global);
pageSize = d.pageSize;
$("#pageSizeSel").val(pageSize);
distinctMode = d.distinct;
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_raw);
$("input[name=sev]").prop("checked", false);
d.sev.forEach(v=>$(`input[name=sev][value="${v}"]`).prop("checked",true));
$("input[name=sta]").prop("checked", false);
d.sta.forEach(v=>$(`input[name=sta][value="${v}"]`).prop("checked",true));
}
// 渲染表头
function renderHead() {
let h = "";
if (distinctMode === 1) {
h = `<tr>
<th>${t['col_state']}</th>
<th>${t['col_alert']}</th>
<th>${t['col_instance']}</th>
<th>${t['col_severity']}</th>
<th>${t['col_count']}</th>
<th>${t['col_time']}</th>
<th>操作</th>
</tr>`;
} else {
h = `<tr>
<th>${t['col_state']}</th>
<th>${t['col_alert']}</th>
<th>${t['col_instance']}</th>
<th>${t['col_severity']}</th>
<th>${t['col_time']}</th>
<th>操作</th>
</tr>`;
}
$("#tableHead").html(h);
}
// 绘图 严格限制尺寸、精简配置 解决撑满页面问题
function drawChart(stat) {
$("#statCard").show();
$("#statTotal").text(stat.total_all);
$("#statFiring").text(stat.total_firing);
$("#statResolved").text(stat.total_resolved);
$("#statCritical").text(stat.total_critical);
const hourData = stat.hour_data;
const hourLabels = [];
const hourVals = [];
for (let i=0;i<24;i++) {
hourLabels.push(i+":00");
hourVals.push(hourData[i]);
}
// 销毁旧图表防止叠加溢出
if (hourChart) hourChart.destroy();
const hCtx = document.getElementById("hourChart").getContext("2d");
hourChart = new Chart(hCtx, {
type: "line",
data: {
labels: hourLabels,
datasets: [{
label: "触发量",
data: hourVals,
borderColor: "#165DFF",
backgroundColor: "rgba(22, 93, 255, 0.08)",
fill: true,
tension: 0.2,
pointRadius: 2
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {display:true,position:"top",labels:{boxWidth:12,font:{size:12}}},
tooltip: {mode:"index",intersect:false}
},
scales: {
x: {grid:{display:false},ticks:{font:{size:11}}},
y: {grid:{color:"#F2F3F5"},ticks:{font:{size:11}}}
}
}
});
if (pieChart) pieChart.destroy();
const pCtx = document.getElementById("pieChart").getContext("2d");
let c=0,w=0,i=0;
$("#tableBody tr").each((idx,tr)=>{
const s = $(tr).find("td:nth-child(4)").text();
if (s.includes("critical")) c++;
else if (s.includes("warning")) w++;
else i++;
})
pieChart = new Chart(pCtx, {
type:"doughnut",
data:{
labels:["Critical","Warning","Info"],
datasets:[{
data:[c,w,i],
backgroundColor:["#F53F3F","#FF7D00","#165DFF"],
borderWidth:0
}]
},
options:{
responsive:true,
maintainAspectRatio:false,
plugins:{legend:{position:"bottom",labels:{boxWidth:12,font:{size:12}}}},
cutout:"60%" // 缩小饼图半径,不会超大
}
});
}
// 加载数据
function loadData() {
saveFilter();
renderHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">Loading...</td></tr>`);
const sevArr = $("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr = $("input[name=sta]").map((i,e)=>e.value).get();
const params = new URLSearchParams();
params.append("page", page);
params.append("page_size", pageSize);
params.append("start_date", $("#startDate").val());
params.append("end_date", $("#endDate").val());
params.append("instance", $("#filterInstance").val());
params.append("global", $("#globalSearch").val());
params.append("distinct", distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));
staArr.forEach(v=>params.append("status[]",v));
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(res=>{
if (res.code !== 0) {
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;"><i class="fa fa-exclamation-circle"></i> ${res.msg}</td></tr>`);
$("#statCard").hide();
return;
}
total = res.total;
$("#pageTotal").text(total);
const totalPage = Math.ceil(total / pageSize) || 1;
$("#pageInfo").text(`${page} / ${totalPage}`);
if(res.stat.total_all > 0){
drawChart(res.stat);
}else{
$("#statCard").hide();
}
const list = res.list;
if (list.length === 0) {
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box">${t.empty_tip}</td></tr>`);
return;
}
let html = "";
list.forEach(row=>{
const cls = row.state === "firing" ? "row-firing" : "row-resolved";
const tagClass = row.state === "firing" ? "tag-firing" : "tag-resolved";
const tagText = row.state === "firing" ? "Firing" : "Resolved";
let sevCls = "";
if (row.severity === "critical") sevCls = "severity-critical";
else if (row.severity === "warning") sevCls = "severity-warning";
else sevCls = "severity-info";
let storm = "";
if (distinctMode === 1 && row.total_count > stormThreshold) storm = `<span class="storm">${t.storm_tip}</span>`;
if (distinctMode === 1) {
html += `<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td><span class="${tagClass}">${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevCls}">${row.severity}</span></td>
<td>${row.total_count} ${storm}</td>
<td>${row.time}</td>
<td>
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">${t.btn_copy_name}</button>
<button class="btn btn-sm btn-outline detail-open">详情</button>
</td>
</tr>`;
} else {
html += `<tr class="${cls}" data-row='${JSON.stringify(row)}'>
<td><span class="${tagClass}">${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevCls}">${row.severity}</span></td>
<td>${row.time}</td>
<td>
<button class="btn btn-sm btn-outline copy-name" data-name="${row.metric}">${t.btn_copy_name}</button>
<button class="btn btn-sm btn-outline detail-open">详情</button>
</td>
</tr>`;
}
})
$("#tableBody").html(html);
})
.catch(()=>{
$("#tableBody").html(`<tr><td colspan="${distinctMode===1?7:6}" class="empty-box" style="color:#F53F3F;"><i class="fa fa-exclamation-circle"></i> 接口请求失败</td></tr>`);
$("#statCard").hide();
})
}
// 导出CSV
function exportExcel() {
const sevArr = $("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr = $("input[name=sta]").map((i,e)=>e.value).get();
const params = new URLSearchParams();
params.append("export",1);
params.append("page_size", 9999);
params.append("start_date", $("#startDate").val());
params.append("end_date", $("#endDate").val());
params.append("instance", $("#filterInstance").val());
params.append("global", $("#globalSearch").val());
params.append("distinct", distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));
staArr.forEach(v=>params.append("status[]",v));
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(res=>{
if (res.code !== 0) return alert(res.msg);
let csv = "";
if (distinctMode === 1) {
csv = "状态,告警名称,实例,级别,触发次数,最新时间\n";
res.list.forEach(r=>csv += `${r.state},${r.metric},${r.instance},${r.severity},${r.total_count},${r.time}\n`);
} else {
csv = "状态,告警名称,实例,级别,触发时间\n";
res.list.forEach(r=>csv += `${r.state},${r.metric},${r.instance},${r.severity},${r.time}\n`);
}
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "alert_export_"+new Date().getTime()+".csv";
a.click();
})
}
// 复制筛选链接
function copyLink() {
const sevArr = $("input[name=sev]:checked").map((i,e)=>e.value).get();
const staArr = $("input[name=sta]").map((i,e)=>e.value).get();
const params = new URLSearchParams();
params.append("start_date", $("#startDate").val());
params.append("end_date", $("#endDate").val());
params.append("instance", $("#filterInstance").val());
params.append("global", $("#globalSearch").val());
params.append("distinct", distinctMode);
sevArr.forEach(v=>params.append("severity[]",v));
staArr.forEach(v=>params.append("status[]",v));
const link = location.origin + location.pathname + "?" + params.toString();
navigator.clipboard.writeText(link).then(()=>alert("筛选链接已复制"));
}
// 弹窗详情
function openModal(row) {
const metric = row.metric;
const inst = row.instance;
const grafanaLink = `${grafanaBase}/d/alert-overview?var-alert=${encodeURIComponent(metric)}&var-instance=${encodeURIComponent(inst)}`;
$("#grafanaLink").attr("href", grafanaLink);
let html = `<h4>${metric}</h4>
<p>实例:${inst}|级别:${row.severity}|状态:${row.state}</p>
<div class="labels-block">${JSON.stringify(row.labels, null, 2)}</div>`;
$("#modalContent").html(html);
$("#modalMask").css("display","flex");
}
// 自动刷新开关
function toggleAutoRefresh() {
if ($("#autoRefresh").is(":checked")) {
autoRefreshTimer = setInterval(()=>loadData(), 60000);
} else {
clearInterval(autoRefreshTimer);
}
}
$(function(){
loadFilter();
loadData();
// 切换汇总/详细模式
$("#btnToggleDistinct").click(()=>{
distinctMode = distinctMode === 1 ? 0 : 1;
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_raw);
page = 1;
loadData();
})
// 查询按钮
$("#btnSearch").click(()=>{page=1;loadData();});
// 重置筛选
$("#btnReset").click(()=>{
const today = new Date().toISOString().split('T')[0];
$("#startDate").val(today);
$("#endDate").val(today);
$("#filterInstance").val("");
$("#globalSearch").val("");
$("input[name=sev]").prop("checked",false);
$("input[name=sta]").prop("checked",false);
distinctMode = 1;
$("#modeTip").text(t.tip_distinct);
page = 1;
loadData();
})
// 导出、复制链接、自动刷新
$("#btnExport").click(exportExcel);
$("#btnCopyLink").click(copyLink);
$("#autoRefresh").change(toggleAutoRefresh);
// 每页条数切换
$("#pageSizeSel").change(function(){
pageSize = parseInt($(this).val());
page = 1;
loadData();
})
// 分页按钮
$("#pageFirst").click(()=>{page=1;loadData();});
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}});
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}});
$("#pageLast").click(()=>{page=Math.ceil(total/pageSize);loadData();});
// 复制告警名、打开详情弹窗
$(document).on("click",".copy-name",function(){
const name = $(this).data("name");
navigator.clipboard.writeText(name).then(()=>alert("已复制告警名称:"+name));
})
$(document).on("click",".detail-open",function(){
const rowJson = $(this).closest("tr").attr("data-row");
const row = JSON.parse(rowJson);
openModal(row);
})
// 关闭弹窗
$("#modalClose, #modalMask").click(function(e){
if (e.target === this) $("#modalMask").hide();
})
// 全局快捷键
$(document).keydown(function(e){
if (e.key === "Enter") {
e.preventDefault();
page = 1;
loadData();
}
if (e.key === "Escape") {
$("#modalMask").hide();
}
if (e.ctrlKey && e.key.toLowerCase() === "f") {
e.preventDefault();
$("#globalSearch").focus();
}
})
})
</script>
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($connConfig, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) $sysConfig[$row['k']] = $row['v'];
mysqli_close($connConfig);
$currentLang = $sysConfig['lang'] ?? "zh";
$langMap = [
'zh' => [
'html_lang' => 'zh-CN',
'page_title_list' => '告警分页列表',
'filter_date' => '日期筛选',
'filter_alert_name' => '告警名称',
'filter_severity' => '告警级别',
'filter_status' => '告警状态',
'btn_search' => '查询',
'btn_reset' => '重置',
'page_total' => '共',
'page_record' => '条记录',
'page_size' => '每页',
'page_prev' => '上一页',
'page_next' => '下一页',
'page_first' => '首页',
'page_last' => '末页',
'empty_no_data' => '暂无告警时序数据',
'server_fail' => 'Prometheus 接口请求失败',
'tag' => '状态',
'alert_name' => '告警指标',
'instance_addr' => '实例',
'severity' => '级别',
'occur_time_group' => '最新触发时间',
'occur_time_detail' => '触发时间',
'firing' => '触发',
'resolved' => '恢复',
'critical' => '严重',
'warning' => '警告',
'info' => '信息',
'btn_detail' => '详细模式',
'tip_distinct' => '当前:合并去重(汇总次数)',
'tip_all' => '当前:详细时序(逐条展示触发时间)',
'col_count' => '总触发次数'
],
'en' => [
'html_lang' => 'en',
'page_title_list' => 'Alert Pagination List',
'filter_date' => 'Date',
'filter_alert_name' => 'Metric',
'filter_severity' => 'Severity',
'filter_status' => 'Status',
'btn_search' => 'Search',
'btn_reset' => 'Reset',
'page_total' => 'Total ',
'page_record' => ' records',
'page_size' => 'Per page',
'page_prev' => 'Prev',
'page_next' => 'Next',
'page_first' => 'First',
'page_last' => 'Last',
'empty_no_data' => 'No alert time series data',
'server_fail' => 'Prometheus API request failed',
'tag' => 'Status',
'alert_name' => 'Metric',
'instance_addr' => 'Instance',
'severity' => 'Severity',
'occur_time_group' => 'Last Trigger Time',
'occur_time_detail' => 'Trigger Time',
'firing' => 'Firing',
'resolved' => 'Resolved',
'critical' => 'Critical',
'warning' => 'Warning',
'info' => 'Info',
'btn_detail' => 'Detail Mode',
'tip_distinct' => 'Mode: Deduplicated (Total Count)',
'tip_all' => 'Mode: Raw Time Series (Each Trigger)',
'col_count' => 'Total Triggers'
]
];
$t = $langMap[$currentLang];
?>
<!DOCTYPE html>
<html lang="<?php echo $t['html_lang']; ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $t['page_title_list']; ?></title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f9fafc;font-family:"Inter","PingFang SC","Microsoft YaHei";color:#1d2939;line-height:1.6;height:100vh;overflow:auto;padding:24px;}
.base-card{background:#fff;border-radius:16px;padding:32px;box-shadow:0 3px 18px rgba(21,44,91,0.06);border:1px solid #eef2fb;max-width:1600px;margin:0 auto;}
.title-wrap{display:flex;justify-content:space-between;align-items:center;margin-bottom:26px;padding-bottom:16px;border-bottom:1px solid #eef2fb;}
.title-left{display:flex;align-items:center;gap:12px;font-size:19px;font-weight:600;color:#152c5b;}
.title-left i{color:#2563eb;font-size:21px}
.filter-bar{display:flex;flex-wrap:wrap;gap:16px;margin-bottom:24px;align-items:center;}
.filter-bar label{font-size:14px;color:#475569;display:flex;align-items:center;gap:8px;}
.filter-bar input,.filter-bar select{padding:6px 10px;border:1px solid #cbd5e1;border-radius:6px;}
.btn-base{border:none;padding:9px 16px;border-radius:8px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;}
.btn-success{background:#10b981;color:#fff;}
.btn-outline{background:#fff;color:#152c5b;border:1px solid #cbd5e1;}
.btn-warning{background:#f59e0b;color:#fff;}
.table-wrap{overflow-x:auto;margin-bottom:24px;}
table{width:100%;border-collapse:collapse;font-size:14px}
th{background:#f9fafc;color:#475569;font-weight:600;padding:16px 18px;text-align:left;border-bottom:2px solid #e2e8f0;white-space:nowrap;}
td{padding:18px;border-bottom:1px solid #f1f5f9;color:#334155;}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
.status-tag{display:inline-flex;align-items:center;gap:5px;padding:6px 14px;border-radius:24px;font-size:13px;font-weight:500;}
.tag-firing{background:#fee2e2;color:#dc2626}
.tag-resolved{background:#dcfce7;color:#16a34a}
.severity-critical{background:#fee2e2;color:#dc2626;padding:5px 12px;border-radius:8px;font-size:13px;}
.severity-warning{background:#ffedd5;color:#ea580c;padding:5px 12px;border-radius:8px;font-size:13px;}
.severity-info{background:#dbeafe;color:#2563eb;padding:5px 12px;border-radius:8px;font-size:13px;}
.empty-state{text-align:center;padding:80px 20px;color:#94a3b8;font-size:15px;}
.empty-icon{font-size:56px;margin-bottom:18px;opacity:0.35;}
.pagination{display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:12px;}
.pag-btn{padding:4px 10px;border:1px solid #cbd5e1;border-radius:6px;background:#fff;cursor:pointer;}
.pag-info{margin:0 12px;}
.header-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;max-width:1600px;margin-left:auto;margin-right:auto;}
.user-bar{display:flex;gap:16px;align-items:center;font-size:14px;color:#475569;}
.back-btn{color:#2563eb;text-decoration:none;}
.mode-tip{color:#64748b;font-size:13px;margin-left:10px;}
.count-badge{font-weight:bold;color:#dc2626;}
</style>
</head>
<body>
<div class="header-top">
<a href="dashboard.php" class="back-btn"><i class="fa fa-arrow-left"></i> 返回监控大屏</a>
<div class="user-bar">
<span><i class="fa fa-user-circle"></i> <?php echo $userName; ?> (<?php echo $roleText; ?>)</span>
<a href="logout.php" class="back-btn">退出登录</a>
</div>
</div>
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list"></i><?php echo $t['page_title_list']; ?></div>
<div style="display:flex;align-items:center;">
<button class="btn-base btn-warning" id="btnToggleDetail"><?php echo $t['btn_detail']; ?></button>
<span class="mode-tip" id="modeTip"><?php echo $t['tip_distinct']; ?></span>
</div>
</div>
<div class="filter-bar">
<label><?php echo $t['filter_date']; ?>
<input type="date" id="filterDate">
</label>
<label><?php echo $t['filter_alert_name']; ?>
<input type="text" id="filterMetric" placeholder="metric name">
</label>
<label><?php echo $t['filter_severity']; ?>
<select id="filterSeverity">
<option value="">--</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
<option value="info">Info</option>
</select>
</label>
<label><?php echo $t['filter_status']; ?>
<select id="filterStatus">
<option value="">--</option>
<option value="firing"><?php echo $t['firing']; ?></option>
<option value="resolved"><?php echo $t['resolved']; ?></option>
</select>
</label>
<button class="btn-base btn-success" id="btnSearch"><?php echo $t['btn_search']; ?></button>
<button class="btn-base btn-outline" id="btnReset"><?php echo $t['btn_reset']; ?></button>
</div>
<div class="table-wrap">
<table id="dataTable">
<thead id="tableHead">
<!-- JS动态渲染表头 -->
</thead>
<tbody id="tableBody">
<tr>
<td colspan="6" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>Loading...</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination">
<div>
<?php echo $t['page_total']; ?><span id="totalCnt">0</span><?php echo $t['page_record']; ?>
</div>
<div style="display:flex;gap:8px;align-items:center;">
<span><?php echo $t['page_size']; ?></span>
<select id="pageSize">
<option value="10">10</option>
<option value="20" selected>20</option>
<option value="50">50</option>
</select>
<button class="pag-btn" id="pageFirst"><?php echo $t['page_first']; ?></button>
<button class="pag-btn" id="pagePrev"><?php echo $t['page_prev']; ?></button>
<span id="pageInfo">1 / 1</span>
<button class="pag-btn" id="pageNext"><?php echo $t['page_next']; ?></button>
<button class="pag-btn" id="pageLast"><?php echo $t['page_last']; ?></button>
</div>
</div>
</div>
<script>
const apiUrl = "./api/api_alert_list.php";
let page = 1;
let pageSize = 20;
let total = 0;
const defaultDate = new Date().toISOString().split('T')[0];
let filter = {
date: defaultDate,
metric: "",
severity: "",
status: ""
};
let distinctMode = 1; // 1汇总去重,0详细逐条
const t = <?php echo json_encode($t); ?>;
$(function(){
$("#filterDate").val(defaultDate);
loadData();
})
// 切换模式按钮
$("#btnToggleDetail").click(function(){
distinctMode = distinctMode === 1 ? 0 : 1;
$("#modeTip").text(distinctMode === 1 ? t.tip_distinct : t.tip_all);
page = 1;
loadData();
})
// 渲染表头
function renderTableHead(){
let headHtml = "";
if(distinctMode === 1){
// 汇总模式:带总次数列
headHtml = `<tr>
<th>${t.tag}</th>
<th>${t.alert_name}</th>
<th>${t.instance_addr}</th>
<th>${t.severity}</th>
<th>${t.col_count}</th>
<th>${t.occur_time_group}</th>
</tr>`;
}else{
// 详细模式:无次数列,纯触发时间
headHtml = `<tr>
<th>${t.tag}</th>
<th>${t.alert_name}</th>
<th>${t.instance_addr}</th>
<th>${t.severity}</th>
<th>${t.occur_time_detail}</th>
</tr>`;
}
$("#tableHead").html(headHtml);
}
function loadData(){
renderTableHead();
$("#tableBody").html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>Loading...</div></td></tr>`);
const params = new URLSearchParams();
params.append("page", page);
params.append("page_size", pageSize);
params.append("date", filter.date);
params.append("metric", filter.metric);
params.append("severity", filter.severity);
params.append("status", filter.status);
params.append("distinct", distinctMode);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(res=>{
if(res.code !== 0){
$("#tableBody").html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>${res.msg}</div></td></tr>`);
return;
}
const rawList = res.list;
total = res.total;
$("#totalCnt").text(total);
const totalPage = Math.ceil(total / pageSize) || 1;
$("#pageInfo").text(`${page} / ${totalPage}`);
const tbody = $("#tableBody");
if(rawList.length === 0){
tbody.html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>${t.empty_no_data}</div></td></tr>`);
return;
}
let html = "";
rawList.forEach(row=>{
const tagClass = row.state === "firing" ? "tag-firing" : "tag-resolved";
const tagText = row.state === "firing" ? t.firing : t.resolved;
let sevClass = "severity-info";
if(row.severity === "critical") sevClass = "severity-critical";
if(row.severity === "warning") sevClass = "severity-warning";
if(distinctMode === 1){
// 汇总行,带计数
html += `<tr>
<td><span class="status-tag ${tagClass}"><i class="fa fa-bolt"></i>${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevClass}">${row.severity}</span></td>
<td><span class="count-badge">${row.total_count}</span></td>
<td>${row.time}</td>
</tr>`;
}else{
// 详细模式,无计数
html += `<tr>
<td><span class="status-tag ${tagClass}"><i class="fa fa-bolt"></i>${tagText}</span></td>
<td>${row.metric}</td>
<td>${row.instance}</td>
<td><span class="${sevClass}">${row.severity}</span></td>
<td>${row.time}</td>
</tr>`;
}
})
tbody.html(html);
})
.catch(()=>{
$("#tableBody").html(`<tr><td colspan="${distinctMode === 1 ? 6 : 5}" class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>${t.server_fail}</div></td></tr>`);
})
}
// 查询按钮
$("#btnSearch").click(function(){
filter.date = $("#filterDate").val();
filter.metric = $("#filterMetric").val().trim();
filter.severity = $("#filterSeverity").val();
filter.status = $("#filterStatus").val();
page = 1;
loadData();
})
// 重置:恢复默认去重汇总模式
$("#btnReset").click(function(){
$("#filterDate").val(defaultDate);
$("#filterMetric").val("");
$("#filterSeverity").val("");
$("#filterStatus").val("");
filter = {date: defaultDate, metric: "", severity: "", status: ""};
distinctMode = 1;
$("#modeTip").text(t.tip_distinct);
page = 1;
loadData();
})
$("#pageSize").change(function(){
pageSize = parseInt($(this).val());
page = 1;
loadData();
})
$("#pageFirst").click(()=>{if(page!==1){page=1;loadData();}})
$("#pagePrev").click(()=>{if(page>1){page--;loadData();}})
$("#pageNext").click(()=>{const tp=Math.ceil(total/pageSize);if(page<tp){page++;loadData();}})
$("#pageLast").click(()=>{const tp=Math.ceil(total/pageSize);if(page!==tp){page=tp;loadData();}})
</script>
</body>
</html>
+132
View File
@@ -0,0 +1,132 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Pragma: no-cache");
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
$userName = $_SESSION['user_name'] ?? '';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警订阅管理</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#F5F7FA;font-family:"Microsoft YaHei",sans-serif;color:#1D2129;font-size:14px;}
.container{max-width:1000px;margin:0 auto;padding:20px;}
.header-bar{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;}
.back-link{color:#165DFF;text-decoration:none;}
.user-info{display:flex;gap:12px;align-items:center;}
.card{background:#fff;border-radius:8px;padding:24px;box-shadow:0 1px 2px rgba(0,0,0,0.06);}
.card-title{font-size:16px;font-weight:500;margin-bottom:20px;}
.form-row{margin-bottom:18px;}
.form-row label{display:block;margin-bottom:6px;color:#4E5969;}
.form-row input{width:100%;height:38px;padding:0 14px;border:1px solid #DCDFE6;border-radius:6px;font-size:14px;}
.form-desc{font-size:12px;color:#86909C;margin-top:4px;}
.switch-row{display:flex;align-items:center;gap:8px;margin-bottom:24px;font-size:14px;}
.btn{height:38px;padding:0 20px;border:none;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px;}
.btn-primary{background:#165DFF;color:#fff;}
.btn-outline{background:#fff;color:#4E5969;border:1px solid #DCDFE6;}
.btn-group{display:flex;gap:12px;}
.tip{color:#86909C;font-size:13px;margin-top:12px;}
</style>
</head>
<body>
<div class="container">
<div class="header-bar">
<a href="alert_index.php" class="back-link"><i class="fa fa-arrow-left"></i> 返回告警监控面板</a>
<div class="user-info"><i class="fa fa-user"></i> <?php echo $userName; ?></div>
</div>
<div class="card">
<h3 class="card-title">告警推送订阅配置</h3>
<div class="switch-row">
<input type="checkbox" id="subEnable">
<label for="subEnable">全局启用告警推送订阅</label>
</div>
<div class="form-row">
<label>订阅告警级别(多值逗号分隔,为空匹配全部)</label>
<input type="text" id="subSeverity" placeholder="critical,warning">
<div class="form-desc">可选:critical / warning / info</div>
</div>
<div class="form-row">
<label>订阅实例模糊匹配(多实例逗号分隔)</label>
<input type="text" id="subInstance" placeholder="10.150,192.168">
<div class="form-desc">填写实例片段,为空订阅所有实例告警</div>
</div>
<div class="form-row">
<label>订阅告警名称模糊匹配(多告警逗号分隔)</label>
<input type="text" id="subAlertname" placeholder="CPU使用率过高,磁盘满">
<div class="form-desc">只推送包含该名称的告警,为空订阅全部告警</div>
</div>
<div class="form-row">
<label>钉钉机器人 Webhook</label>
<input type="text" id="subDingtalk" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxx">
</div>
<div class="form-row">
<label>企业微信机器人 Webhook</label>
<input type="text" id="subWecom" placeholder="https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx">
</div>
<div class="form-row">
<label>接收邮箱(多邮箱逗号分隔)</label>
<input type="text" id="subMail" placeholder="ops@company.com,admin@company.com">
</div>
<div class="btn-group">
<button class="btn btn-primary" id="saveSubscribe"><i class="fa fa-save"></i> 保存订阅配置</button>
<button class="btn btn-outline" id="resetForm"><i class="fa fa-refresh"></i> 重置表单</button>
</div>
<div class="tip">说明:配置保存后,符合筛选条件的新增告警将通过配置渠道推送;关闭全局开关则暂停所有推送。</div>
</div>
</div>
<script>
const apiSub = "./api/subscribe_api.php";
// 页面加载读取当前订阅
$(function(){
$.getJSON(apiSub,{action:"get_subscribe"},function(res){
if(res.code===0){
const d=res.data;
$("#subEnable").prop("checked",d.enable==1);
$("#subSeverity").val(d.severity);
$("#subInstance").val(d.instance);
$("#subAlertname").val(d.alertname);
$("#subDingtalk").val(d.dingtalk_webhook);
$("#subWecom").val(d.wecom_webhook);
$("#subMail").val(d.mail_receiver);
}
});
});
// 保存配置
$("#saveSubscribe").click(function(){
const data={
severity:$("#subSeverity").val().trim(),
instance:$("#subInstance").val().trim(),
alertname:$("#subAlertname").val().trim(),
dingtalk_webhook:$("#subDingtalk").val().trim(),
wecom_webhook:$("#subWecom").val().trim(),
mail_receiver:$("#subMail").val().trim(),
enable:$("#subEnable").prop("checked")?1:0
};
$.ajax({
url:apiSub+"?action=save_subscribe",
method:"POST",
contentType:"application/json",
data:JSON.stringify(data),
dataType:"json",
success:function(res){alert(res.msg);}
});
});
// 重置表单
$("#resetForm").click(function(){
$("#subSeverity,#subInstance,#subAlertname,#subDingtalk,#subWecom,#subMail").val("");
$("#subEnable").prop("checked",false);
});
</script>
</body>
</html>
+198
View File
@@ -0,0 +1,198 @@
<?php
session_start();
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,OPTIONS");
header("Access-Control-Allow-Headers: *");
if($_SERVER['REQUEST_METHOD'] === 'OPTIONS'){
echo json_encode(array('code'=>0));
exit;
}
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(array('code'=>401,'msg'=>'登录失效','list'=>array(),'total'=>0,'stat'=>array()));
exit;
}
$_SESSION['login_time'] = time();
$promHost = "10.150.117.190";
$promPort = "9090";
$promApi = "http://".$promHost.":".$promPort."/api/v1";
// 修复低版本PHP不支持 ?? 运算符,改用三元判断
$page = isset($_REQUEST['page']) ? (int)$_REQUEST['page'] : 1;
$pageSize = isset($_REQUEST['page_size']) ? (int)$_REQUEST['page_size'] : 20;
$startDate = isset($_REQUEST['start_date']) ? $_REQUEST['start_date'] : "";
$endDate = isset($_REQUEST['end_date']) ? $_REQUEST['end_date'] : "";
$filterMetric = isset($_REQUEST['metric']) ? trim($_REQUEST['metric']) : "";
$filterInstance = isset($_REQUEST['instance']) ? trim($_REQUEST['instance']) : "";
$filterGlobal = isset($_REQUEST['global']) ? trim($_REQUEST['global']) : "";
$distinct = isset($_REQUEST['distinct']) ? (int)$_REQUEST['distinct'] : 1;
$filterSeverity = isset($_REQUEST['severity']) ? $_REQUEST['severity'] : array();
$filterStatus = isset($_REQUEST['status']) ? $_REQUEST['status'] : array();
$isExport = (isset($_REQUEST['export']) && $_REQUEST['export'] == 1) ? 1 : 0;
$startTs = 0;
$endTs = 0;
if (!empty($startDate) && !empty($endDate)) {
$startTs = strtotime($startDate);
$endTs = strtotime($endDate . " 23:59:59");
} else {
$today = date("Y-m-d");
$startTs = strtotime($today);
$endTs = strtotime($today . " 23:59:59");
}
$promQL = 'sort_desc(ALERTS)';
$params = array('query' => $promQL);
$params['start'] = $startTs;
$params['end'] = $endTs;
$params['step'] = 60;
$queryUrl = $promApi . "/query_range";
$url = $queryUrl . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch);
$err = curl_error($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if (!empty($err) || empty($resp)) {
echo json_encode(array(
'code' => 500,
'msg' => 'Prometheus连接失败:' . $err,
'list' => array(),
'total' => 0,
'stat' => array()
));
exit;
}
$promData = json_decode($resp, true);
if (!isset($promData['status']) || $promData['status'] !== 'success') {
echo json_encode(array(
'code' => 500,
'msg' => 'Prometheus查询异常:'.$resp,
'list' => array(),
'total' => 0,
'stat' => array()
));
exit;
}
$rawAll = array();
$hourCount = array();
for ($h = 0; $h < 24; $h++){
$hourCount[$h] = 0;
}
if (!empty($promData['data']['result'])) {
foreach ($promData['data']['result'] as $item) {
$metric = $item['metric'];
$state = isset($metric['alertstate']) ? $metric['alertstate'] : "";
$sev = isset($metric['severity']) ? $metric['severity'] : "info";
$alertName = isset($metric['alertname']) ? $metric['alertname'] : "";
$inst = isset($metric['instance']) ? $metric['instance'] : "";
$values = isset($item['values']) ? $item['values'] : array();
foreach ($values as $v) {
$ts = (int)$v[0];
$dt = date("Y-m-d H:i:s", $ts);
$hour = date("G", $ts);
$hourCount[$hour]++;
$rawAll[] = array(
'state' => $state,
'severity' => $sev,
'metric' => $alertName,
'instance' => $inst,
'time' => $dt,
'timestamp' => $ts,
'labels' => $metric
);
}
}
}
$filterFunc = function ($row) use ($filterMetric, $filterInstance, $filterGlobal, $filterSeverity, $filterStatus) {
if (!empty($filterGlobal)) {
$g = strtolower($filterGlobal);
$match = (stripos(strtolower($row['metric']), $g) !== false)
|| (stripos(strtolower($row['instance']), $g) !== false)
|| (stripos(strtolower($row['severity']), $g) !== false);
if (!$match) return false;
}
if (!empty($filterMetric) && stripos($row['metric'], $filterMetric) === false) return false;
if (!empty($filterInstance) && stripos($row['instance'], $filterInstance) === false) return false;
if (!empty($filterSeverity) && !in_array($row['severity'], $filterSeverity)) return false;
if (!empty($filterStatus) && !in_array($row['state'], $filterStatus)) return false;
return true;
};
$rawAll = array_filter($rawAll, $filterFunc);
usort($rawAll, function($a, $b){
return $b['timestamp'] - $a['timestamp'];
});
$listRaw = array();
$stat = array(
'total_all' => count($rawAll),
'total_firing' => 0,
'total_resolved' => 0,
'total_critical' => 0,
'unique_count' => 0,
'hour_data' => $hourCount
);
if ($distinct !== 1) {
$listRaw = $rawAll;
foreach ($rawAll as $r) {
if ($r['state'] == 'firing') $stat['total_firing']++;
else $stat['total_resolved']++;
if ($r['severity'] == 'critical') $stat['total_critical']++;
}
} else {
$group = array();
foreach ($rawAll as $row) {
$key = md5($row['metric'] . "|" . $row['instance'] . "|" . $row['severity'] . "|" . $row['state']);
if (!isset($group[$key])) {
$group[$key] = array(
'state' => $row['state'],
'severity' => $row['severity'],
'metric' => $row['metric'],
'instance' => $row['instance'],
'time' => $row['time'],
'timestamp' => $row['timestamp'],
'total_count' => 1,
'labels' => $row['labels']
);
} else {
$group[$key]['total_count']++;
if ($row['timestamp'] > $group[$key]['timestamp']) {
$group[$key]['time'] = $row['time'];
$group[$key]['timestamp'] = $row['timestamp'];
}
}
}
$listRaw = array_values($group);
$stat['unique_count'] = count($listRaw);
foreach ($listRaw as $r) {
if ($r['state'] == 'firing') $stat['total_firing']++;
else $stat['total_resolved']++;
if ($r['severity'] == 'critical') $stat['total_critical']++;
}
}
$total = count($listRaw);
if ($isExport) {
$pageData = $listRaw;
} else {
$offset = ($page - 1) * $pageSize;
$pageData = array_slice($listRaw, $offset, $pageSize);
}
echo json_encode(array(
'code' => 0,
'msg' => 'ok',
'total' => $total,
'stat' => $stat,
'list' => $pageData
), JSON_UNESCAPED_UNICODE);
exit;
?>
@@ -0,0 +1,143 @@
<?php
session_start();
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if($_SERVER['REQUEST_METHOD'] === 'OPTIONS') exit(json_encode(['code'=>0]));
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
exit(json_encode(['code'=>401,'msg'=>'登录失效','list'=>[],'total'=>0]));
}
$_SESSION['login_time'] = time();
$promHost = "10.150.117.190";
$promPort = "9090";
$promApi = "http://{$promHost}:{$promPort}/api/v1";
$page = (int)($_REQUEST['page'] ?? 1);
$pageSize = (int)($_REQUEST['page_size'] ?? 20);
$filterDate = $_REQUEST['date'] ?? "";
$filterMetric = trim($_REQUEST['metric'] ?? "");
$filterSeverity = $_REQUEST['severity'] ?? "";
$filterStatus = $_REQUEST['status'] ?? "";
$distinct = (int)($_REQUEST['distinct'] ?? 1);
$promQL = 'sort_desc(ALERTS)';
$params = ['query' => $promQL];
if (!empty($filterDate)) {
$end = strtotime($filterDate . " 23:59:59");
$start = $end - 86400;
$params['start'] = $start;
$params['end'] = $end;
$params['step'] = 60;
$queryUrl = $promApi . "/query_range";
} else {
$queryUrl = $promApi . "/query";
}
$url = $queryUrl . "?" . http_build_query($params);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch);
$err = curl_error($ch);
curl_close($ch);
if (!empty($err) || empty($resp)) {
exit(json_encode([
'code' => 500,
'msg' => 'Prometheus连接失败:'.$err,
'list' => [],
'total' => 0
]));
}
$promData = json_decode($resp, true);
if ($promData['status'] !== 'success') {
exit(json_encode([
'code' => 500,
'msg' => 'Prometheus查询异常',
'list' => [],
'total' => 0
]));
}
$rawAll = [];
if (!empty($promData['data']['result'])) {
foreach ($promData['data']['result'] as $item) {
$metric = $item['metric'];
$state = $metric['alertstate'] ?? "";
$sev = $metric['severity'] ?? "info";
$alertName = $metric['alertname'] ?? "";
$inst = $metric['instance'] ?? "";
$values = $item['values'] ?? [];
foreach ($values as $v) {
$ts = (int)$v[0];
$dt = date("Y-m-d H:i:s", $ts);
$rawAll[] = [
'state' => $state,
'severity' => $sev,
'metric' => $alertName,
'instance' => $inst,
'time' => $dt,
'timestamp' => $ts
];
}
}
}
// ==========修复过滤逻辑==========
if (!empty($filterMetric)) {
$rawAll = array_filter($rawAll, function($r) use ($filterMetric){
return stripos($r['metric'], $filterMetric) !== false;
});
}
if (!empty($filterSeverity)) {
$rawAll = array_filter($rawAll, fn($r) => $r['severity'] === $filterSeverity);
}
if (!empty($filterStatus)) {
$rawAll = array_filter($rawAll, fn($r) => $r['state'] === $filterStatus);
}
usort($rawAll, fn($a,$b) => $b['timestamp'] - $a['timestamp']);
if ($distinct !== 1) {
$list = $rawAll;
} else {
$group = [];
foreach ($rawAll as $row) {
$key = md5($row['metric'] . "|" . $row['instance'] . "|" . $row['severity'] . "|" . $row['state']);
if (!isset($group[$key])) {
$group[$key] = [
'state' => $row['state'],
'severity' => $row['severity'],
'metric' => $row['metric'],
'instance' => $row['instance'],
'time' => $row['time'],
'timestamp' => $row['timestamp'],
'total_count' => 1
];
} else {
$group[$key]['total_count']++;
if ($row['timestamp'] > $group[$key]['timestamp']) {
$group[$key]['time'] = $row['time'];
$group[$key]['timestamp'] = $row['timestamp'];
}
}
}
$list = array_values($group);
}
$total = count($list);
$offset = ($page - 1) * $pageSize;
$pageData = array_slice($list, $offset, $pageSize);
echo json_encode([
'code' => 0,
'msg' => 'ok',
'total' => $total,
'list' => $pageData
], JSON_UNESCAPED_UNICODE);
exit;
?>
+40
View File
@@ -0,0 +1,40 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(["code"=>401,"msg"=>"未登录"]);
exit;
}
header("Content-Type:application/json;charset=utf-8");
$configFile = "./system_config.json";
if(!file_exists($configFile)){
echo json_encode(["code"=>500,"msg"=>"配置文件缺失"]);
exit;
}
$config = json_decode(file_get_contents($configFile),true);
$act = $_POST['act'] ?? '';
if($act == "save_style"){
$config['theme_color'] = $_POST['theme_color'];
$config['card_bg'] = $_POST['card_bg'];
$config['body_bg'] = $_POST['body_bg'];
$config['font_family'] = $_POST['font_family'];
file_put_contents($configFile,json_encode($config,JSON_UNESCAPED_UNICODE));
echo json_encode(["code"=>0,"msg"=>"样式保存成功,刷新页面生效"]);
exit;
}
if($act == "save_sidebar_link"){
$list = json_decode($_POST['link_list'],true);
$config['sidebar_links'] = $list;
file_put_contents($configFile,json_encode($config,JSON_UNESCAPED_UNICODE));
echo json_encode(["code"=>0,"msg"=>"侧边栏链接更新成功"]);
exit;
}
if($act == "get_config"){
echo json_encode(["code"=>0,"data"=>$config]);
exit;
}
echo json_encode(["code"=>400,"msg"=>"非法操作"]);
?>
+3
View File
@@ -0,0 +1,3 @@
<?php
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>200,"msg"=>"PHP正常,mysqli已加载"]);
+205
View File
@@ -0,0 +1,205 @@
<?php
session_start();
if(empty($_SESSION['user_id'])){
header("Location: ../login.php");
exit;
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>开发者模式 - 接口可视化调试面板</title>
<style>
*{box-sizing: border-box;margin:0;padding:0;font-family:Microsoft Yahei}
body{background:#f5f7fa;padding:20px}
.container{max-width:1400px;margin:0 auto}
h1{color:#333;margin-bottom:24px;font-size:22px}
.card{background:#fff;border-radius:8px;padding:16px;margin-bottom:16px;border:1px solid #e4e7ed}
.card h3{margin-bottom:12px;color:#409eff;font-size:16px}
.row{display:flex;gap:12px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
input{padding:8px 10px;border:1px solid #dcdfe6;border-radius:4px;width:260px}
button{padding:8px 16px;border:none;border-radius:4px;cursor:pointer;color:#fff;background:#409eff}
button.warn{background:#e6a23c}
button.danger{background:#f56c6c}
button.success{background:#67c23a}
#result{margin-top:20px;padding:16px;background:#1e1e1e;color:#fff;border-radius:6px;white-space:pre-wrap;min-height:200px;font-family:Consolas}
.tip{font-size:12px;color:#999}
</style>
</head>
<body>
<div class="container">
<h1>开发者调试面板 | 全部告警接口可视化调用</h1>
<a href="../admin.php">← 返回系统后台管理页</a>
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button onclick="req('clear_single_instance','c_rule','c_ins')">执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;冒号自动编码无需手动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="danger" onclick="req('offline_single_instance','off_rule','off_ins')">永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不恢复永远不告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="success" onclick="req('restore_single_instance','res_rule','res_ins')">恢复实例告警</button>
</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="danger" onclick="reqSingle('clear_offline_rule','cr_rule')">下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="success" onclick="reqSingle('restore_rule','rr_rule')">恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="warn" onclick="req('delete_single_instance_log','del_rule','del_ins')">清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button onclick="simpleReq('sync_prom_alerts')">立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端不显示告警bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3>8. 数据查询接口(查看面板原始数据)</h3>
<div class="row">
<button onclick="simpleReq('day_total')">今日统计 day_total</button>
<button onclick="simpleReq('top_alert')">告警TOP10 top_alert</button>
<button onclick="simpleReq('log_list')">今日明细 log_list</button>
<button onclick="simpleReq('active_firing')">当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表 -->
<div class="card">
<h3>9. 导出今日告警CSV报表</h3>
<div class="row">
<button onclick="exportCsv()">下载报表 export_csv</button>
</div>
</div>
<!-- 结果输出区域 -->
<div class="card">
<h3>接口返回结果(JSON)</h3>
<div id="result">等待操作...</div>
</div>
</div>
<script>
// 当前文件在/api/下,接口同目录index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV新窗口
function exportCsv(){
window.open(`${apiUrl}?act=export_csv`,"_blank");
}
</script>
</body>
</html>
+411
View File
@@ -0,0 +1,411 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: ../login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者调试面板 - Advantest 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-title{
font-size:24px;
font-weight:700;
letter-spacing:1px;
color:#152c5b;
}
.header-sub{
font-size:14px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 18px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-warning{
background:#e6a23c;
color:#fff;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1600px;
margin:0 auto;
}
h1{
display:none;
}
/* 卡片样式 统一企业面板风格 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.row{
display:flex;
gap:12px;
align-items:center;
margin-bottom:10px;
flex-wrap:wrap;
}
label{
font-size:14px;
color:#475569;
width:110px;
}
input{
padding:8px 12px;
border:1px solid #dcdfe6;
border-radius:8px;
width:280px;
font-size:14px;
}
input[type="date"]{
width:200px;
}
button{
padding:9px 18px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
}
button.normal{
background:#409eff;
color:#fff;
}
button.normal:hover{
background:#2979e0;
}
.tip{
font-size:13px;
color:#94a3b8;
margin-top:8px;
padding-left:110px;
}
/* 返回结果区块 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:260px;
font-family:Consolas,monospace;
font-size:13px;
}
/* 响应式适配 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.container{padding:24px}
.card{padding:24px}
.row{flex-direction:column;align-items:flex-start}
label{width:auto}
input{width:100% !important;max-width:320px}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-title"><i class="fa fa-code"></i> 开发者调试面板</div>
<div class="header-sub">全部告警接口可视化调用 · 后端API调试工具</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.href='../admin.php'"><i class="fa fa-arrow-left"></i>返回系统后台</button>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新页面</button>
<button class="btn-base btn-primary" onclick="location.href='../dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
</div>
</div>
<div class="container">
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3><i class="fa fa-eraser"></i>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button class="normal" onclick="req('clear_single_instance','c_rule','c_ins')"><i class="fa fa-play"></i>执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号无需手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3><i class="fa fa-ban"></i>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="btn-danger" onclick="req('offline_single_instance','off_rule','off_ins')"><i class="fa fa-ban"></i>永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不执行恢复接口则永久不展示该实例告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3><i class="fa fa-check-circle"></i>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="btn-success" onclick="req('restore_single_instance','res_rule','res_ins')"><i class="fa fa-unlock"></i>恢复实例告警</button>
</div>
<div class="tip">解除单实例永久屏蔽状态,新故障会正常展示</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-danger" onclick="reqSingle('clear_offline_rule','cr_rule')"><i class="fa fa-power-off"></i>下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-success" onclick="reqSingle('restore_rule','rr_rule')"><i class="fa fa-refresh"></i>恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3><i class="fa fa-trash"></i>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="btn-warning" onclick="req('delete_single_instance_log','del_rule','del_ins')"><i class="fa fa-trash"></i>清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3><i class="fa fa-refresh"></i>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('sync_prom_alerts')"><i class="fa fa-sync"></i>立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端“故障存在但页面不显示”bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3><i class="fa fa-database"></i>8. 数据查询接口(获取面板原始数据)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('day_total')"><i class="fa fa-bar-chart"></i>今日统计 day_total</button>
<button class="normal" onclick="simpleReq('top_alert')"><i class="fa fa-pie-chart"></i>告警TOP10 top_alert</button>
<button class="normal" onclick="simpleReq('log_list')"><i class="fa fa-list"></i>今日明细 log_list</button>
<button class="normal" onclick="simpleReq('active_firing')"><i class="fa fa-exclamation-circle"></i>当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表(带日历选择) -->
<div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
<div class="row">
<label>选择日期:</label>
<input type="date" id="csv_date" placeholder="留空导出今日">
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
</div>
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
</div>
<!-- 返回结果输出 -->
<div class="card">
<h3><i class="fa fa-terminal"></i>接口返回结果(JSON)</h3>
<div id="result">等待执行接口操作...</div>
</div>
</div>
<script>
// 接口路径 同目录 index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV 支持日历选择日期
function exportCsv(){
const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`;
if(dateVal){
url += `&date=${encodeURIComponent(dateVal)}`;
}
window.open(url,"_blank");
}
</script>
</body>
</html>
+60
View File
@@ -0,0 +1,60 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"],JSON_UNESCAPED_UNICODE);
exit;
}
header("Content-Type:application/json; charset=utf-8");
$promAddr = "http://10.150.117.190:9090";
$gbUnit = 1073741824;
$promQuery = urlencode('node_directory_size_bytes');
$apiUrl = "{$promAddr}/api/v1/query?query={$promQuery}";
// 改用curl,不依赖allow_url_fopen
$ch = curl_init($apiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
$resp = curl_exec($ch);
$curlErr = curl_error($ch);
curl_close($ch);
if(!$resp || !empty($curlErr)){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus".$curlErr],JSON_UNESCAPED_UNICODE);
exit;
}
$raw = json_decode($resp, true);
$allData = [];
if ($raw['status'] === 'success' && !empty($raw['data']['result'])) {
foreach ($raw['data']['result'] as $item) {
$dir = $item['metric']['directory'];
$instanceIp = $item['metric']['instance'];
$instanceIp = explode(':', $instanceIp)[0];
$userName = basename($dir);
$byteNum = floatval($item['value'][1]);
$gbNum = round($byteNum / $gbUnit, 2);
$allData[] = [
"instance_ip" => $instanceIp,
"username" => $userName,
"directory" => $dir,
"size_gb" => $gbNum
];
}
usort($allData, function ($a, $b) {
return $b['size_gb'] <=> $a['size_gb'];
});
}
echo json_encode([
"code" => 0,
"msg" => "查询成功",
"list" => $allData
], JSON_UNESCAPED_UNICODE);
exit;
?>
+51
View File
@@ -0,0 +1,51 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"],JSON_UNESCAPED_UNICODE);
exit;
}
header("Content-Type:application/json; charset=utf-8");
$promAddr = "http://10.150.117.190:9090";
$gbUnit = 1073741824;
$promQuery = urlencode('node_directory_size_bytes');
$apiUrl = "{$promAddr}/api/v1/query?query={$promQuery}";
$resp = @file_get_contents($apiUrl);
if(!$resp){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus"],JSON_UNESCAPED_UNICODE);
exit;
}
$raw = json_decode($resp, true);
$allData = [];
if ($raw['status'] === 'success' && !empty($raw['data']['result'])) {
foreach ($raw['data']['result'] as $item) {
$dir = $item['metric']['directory'];
$instanceIp = $item['metric']['instance'];
$instanceIp = explode(':', $instanceIp)[0];
$userName = basename($dir);
$byteNum = floatval($item['value'][1]);
$gbNum = round($byteNum / $gbUnit, 2);
$allData[] = [
"instance_ip" => $instanceIp,
"username" => $userName,
"directory" => $dir,
"size_gb" => $gbNum
];
}
usort($allData, function ($a, $b) {
return $b['size_gb'] <=> $a['size_gb'];
});
}
echo json_encode([
"code" => 0,
"msg" => "查询成功",
"list" => $allData
], JSON_UNESCAPED_UNICODE);
exit;
?>
+407
View File
@@ -0,0 +1,407 @@
<?php
session_start();
$act = $_GET['act'] ?? '';
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"], JSON_UNESCAPED_UNICODE);
exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
// CSV导出接口(支持指定历史日期下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
// 接收自定义日期参数,不传则取今日
$targetDate = $_GET['date'] ?? date("Y-m-d");
// 简单日期格式校验 Y-m-d
if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $targetDate)) {
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>400,"msg"=>"日期格式错误,请使用 Y-m-d,例如 2026-07-01"], JSON_UNESCAPED_UNICODE);
exit;
}
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$targetDate' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $targetDate . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 调试代码:打印GET入参,测试完成后删除此段
// $getAct = $_GET['act'] ?? '';
//echo json_encode([
// "debug_act" => $getAct,
// "debug_rule" => $_GET['rule_name'] ?? '',
// "debug_instance" => $_GET['instance'] ?? ''
//], JSON_UNESCAPED_UNICODE);
//exit;
// 接口1:下线整条监控规则(该规则下所有实例一并清理)
if($act === "clear_offline_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
// 标记该规则下全部实例为下线
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
// 批量插入恢复记录,消除页面残留告警
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc'
AND (alert_name,instance) NOT IN (
SELECT alert_name,instance FROM alert_log WHERE alert_type=2
)
";
mysqli_query($conn, $insertSql);
echo json_encode(["code"=>0,"msg"=>"整条规则下线完成,所有实例告警已清除"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口2:单独下线单台实例(仅隐藏指定机器,同规则其他主机不受影响)
if($act === "clear_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 第一步:删除当前实例旧的恢复记录,避免重复拦截
$delSql = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
// 第二步:插入最新恢复记录,移除NOT IN限制,强制生成
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
$affected = mysqli_affected_rows($conn);
echo json_encode([
"code"=>0,
"msg"=>"单实例告警清理完成",
"insert_rows" => $affected
],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口3:恢复整条已下线规则(status改为1,重新展示所有实例告警)
if($act === "restore_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"监控规则已恢复,新故障会正常展示"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口4:恢复单台实例(仅把指定rule+instance置为启用)
if($act === "restore_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"单实例已恢复"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口5:删除单实例全部告警日志
if($act === "delete_single_instance_log"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$delSql = "DELETE FROM alert_log WHERE alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
$aff = mysqli_affected_rows($conn);
echo json_encode(["code"=>0,"msg"=>"单实例告警日志已删除","affected_rows"=>$aff],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口:永久屏蔽单台实例(monitor_rule 该条实例status=0,不手动恢复永远不告警)
if($act === "offline_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 把这条【规则+实例】状态改为0,永久屏蔽
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
$aff = mysqli_affected_rows($conn);
// 同步生成恢复日志,界面立刻消失旧告警
$delOld = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delOld);
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
echo json_encode([
"code"=>0,
"msg"=>"单实例已永久屏蔽,需调用restore_single_instance恢复",
"update_rows"=>$aff
],JSON_UNESCAPED_UNICODE);
exit;
}
// 新增:同步Prometheus实时告警,自动清理残留恢复记录
if($act === "sync_prom_alerts"){
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
$firingMap = [];
foreach($promData['data']['alerts'] as $item){
$an = $item['labels']['alertname'];
$ins = $item['labels']['instance'];
$key = "$an||$ins";
$firingMap[$key] = 1;
}
$res = mysqli_query($conn,"SELECT alert_name,instance FROM alert_log WHERE alert_type=2");
$delCnt = 0;
while($row = mysqli_fetch_assoc($res)){
$k = $row['alert_name']."||".$row['instance'];
if(isset($firingMap[$k])){
$n = mysqli_real_escape_string($conn,$row['alert_name']);
$i = mysqli_real_escape_string($conn,$row['instance']);
mysqli_query($conn,"DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$n' AND instance='$i'");
$delCnt++;
}
}
echo json_encode(["code"=>0,"msg"=>"同步完成","delete_clear_log"=>$delCnt],JSON_UNESCAPED_UNICODE);
exit;
}
// ============后台管理专用接口 start============
// 修改密码 BCrypt哈希加密,匹配历史密码算法
if($act === "change_pwd"){
$uid = $_SESSION['user_id'];
$oldPwdRaw = $_GET['old_pwd'] ?? '';
$newPwdRaw = $_GET['new_pwd'] ?? '';
if(empty($oldPwdRaw) || empty($newPwdRaw)){
echo json_encode(["code"=>400,"msg"=>"密码不能为空"],JSON_UNESCAPED_UNICODE);
exit;
}
// 查询原密码哈希
$userSql = "SELECT password FROM admin_user WHERE user_id='$uid' LIMIT 1";
$userRes = mysqli_query($conn,$userSql);
$userRow = mysqli_fetch_assoc($userRes);
if(!$userRow){
echo json_encode(["code"=>401,"msg"=>"账户不存在"],JSON_UNESCAPED_UNICODE);
exit;
}
$oldHash = $userRow['password'];
// 校验旧密码(和原有登录逻辑一致)
if(!password_verify($oldPwdRaw, $oldHash)){
echo json_encode(["code"=>400,"msg"=>"原密码输入错误"],JSON_UNESCAPED_UNICODE);
exit;
}
// 生成新bcrypt哈希
$newHash = password_hash($newPwdRaw, PASSWORD_DEFAULT);
$updateSql = "UPDATE admin_user SET password='$newHash', update_at=NOW() WHERE user_id='$uid'";
mysqli_query($conn,$updateSql);
echo json_encode(["code"=>0,"msg"=>"密码修改成功"],JSON_UNESCAPED_UNICODE);
exit;
}
// 保存界面外观配置
if($act === "save_theme"){
// 兼容POST/GET
$title = mysqli_real_escape_string($conn,$_POST['title'] ?? $_GET['title'] ?? '');
$color = mysqli_real_escape_string($conn,$_POST['color'] ?? $_GET['color'] ?? '');
$pagesize = mysqli_real_escape_string($conn,$_POST['pagesize'] ?? $_GET['pagesize'] ?? '20');
$footer = mysqli_real_escape_string($conn,$_POST['footer'] ?? $_GET['footer'] ?? '');
$lang = mysqli_real_escape_string($conn,$_POST['lang'] ?? $_GET['lang'] ?? 'zh');
$sql = "REPLACE INTO sys_config (k,v) VALUES
('page_title','$title'),
('theme_color','$color'),
('page_size','$pagesize'),
('footer_text','$footer'),
('lang','$lang')";
mysqli_query($conn,$sql);
echo json_encode(["code"=>0,"msg"=>"外观配置已保存"],JSON_UNESCAPED_UNICODE);
exit;
}
// 保存侧边栏外链
if($act === "save_sidebar_link"){
$linkText = mysqli_real_escape_string($conn,$_GET['link_text'] ?? '');
$sql = "REPLACE INTO sys_config (k,v) VALUES ('sidebar_links','$linkText')";
mysqli_query($conn,$sql);
echo json_encode(["code"=>0,"msg"=>"侧边外链保存成功"],JSON_UNESCAPED_UNICODE);
exit;
}
// 单独切换语言接口
if ($_POST['act'] === 'save_lang') {
$lang = mysqli_real_escape_string($conn, $_POST['lang']);
$sql = "REPLACE INTO sys_config(k, v) VALUES ('lang', '$lang')";
mysqli_query($conn, $sql);
echo json_encode(["code" => 0]);
exit;
}
// ============后台管理专用接口 end============
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 4. 全局跨天未恢复告警(过滤整条下线规则,单实例仅靠恢复日志隐藏)
if ($act === "active_firing") {
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($maxRow = mysqli_fetch_assoc($maxRes)) {
$an = $conn->real_escape_string($maxRow['alert_name']);
$ins = $conn->real_escape_string($maxRow['instance']);
$mrt = $conn->real_escape_string($maxRow['max_rt']);
// 判断该规则是否整条下线,下线直接跳过
$ruleCheck = mysqli_query($conn, "SELECT status FROM monitor_rule WHERE rule_name='$an' LIMIT 1");
$ruleStatus = 1;
if($ruleCheck && $ruleRow = mysqli_fetch_assoc($ruleCheck)){
$ruleStatus = intval($ruleRow['status']);
}
if($ruleStatus === 0){
continue;
}
$rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log
WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
$rowRes = mysqli_query($conn, $rowSql);
$data = mysqli_fetch_assoc($rowRes);
if ($data && (int)$data['alert_type'] === 1) {
$activeList[] = $data;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
@@ -0,0 +1,45 @@
<?php
header("Content-Type:application/json;charset=utf-8");
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
// 1. 获取今日总统计:触发次数、恢复次数、故障实例数
if($act == "day_total"){
$today = date("Y-m-d");
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 2. 今日告警TOP排行(按发生次数)
if($act == "top_alert"){
$today = date("Y-m-d");
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 3. 今日明细列表(前端表格)
if($act == "log_list"){
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
@@ -0,0 +1,49 @@
<?php
header("Content-Type:application/json;charset=utf-8");
// 宿主机内网IP,容器访问MySQL
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
echo json_encode(["code"=>500,"msg"=>"数据库连接失败:".mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
$today = date("Y-m-d");
// 当日总统计
if($act == "day_total"){
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 告警TOP10
if($act == "top_alert"){
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 明细列表
if($act == "log_list"){
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
@@ -0,0 +1,49 @@
<?php
header("Content-Type:application/json;charset=utf-8");
// 宿主机内网IP,容器访问MySQL
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
echo json_encode(["code"=>500,"msg"=>"数据库连接失败:".mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
$today = date("Y-m-d");
// 当日总统计
if($act == "day_total"){
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 告警TOP10
if($act == "top_alert"){
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 明细列表
if($act == "log_list"){
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
+106
View File
@@ -0,0 +1,106 @@
<?php
$act = $_GET['act'] ?? '';
if($act == "export_csv"){
// 先不输出JSON头,单独处理下载
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:".mysqli_connect_error();
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$today = date("Y-m-d");
// 查询今日告警,去掉ends_at恢复时间字段
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn,$sql);
// 下载头
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_".$today.".csv");
// UTF8 BOMWindows Excel不乱码
echo "\xEF\xBB\xBF";
// CSV表头
$header = ["告警状态","告警名称","实例","级别","故障开始时间","接收时间"];
echo implode(",",$header)."\r\n";
// 逐行输出数据
while($row = mysqli_fetch_assoc($res)){
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
// 字段逗号转义,防止内容逗号破坏CSV格式
foreach($line as &$v){
$v = '"'.str_replace('"','""',$v).'"';
}
echo implode(",",$line)."\r\n";
}
mysqli_close($conn);
exit;
}
// 以下普通JSON接口
header("Content-Type:application/json;charset=utf-8");
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if(!$conn){
echo json_encode(["code"=>500,"msg"=>"数据库连接失败:".mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$today = date("Y-m-d");
// 当日总统计
if($act == "day_total"){
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 告警TOP10
if($act == "top_alert"){
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 明细列表
if($act == "log_list"){
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 无效参数兜底
echo json_encode(["code"=>400,"msg"=>"无效请求参数act"],JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+140
View File
@@ -0,0 +1,140 @@
<?php
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
$act = $_GET['act'] ?? '';
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 4. 全局跨天未恢复告警(解决6.30残留故障不显示)
if ($act === "active_firing") {
// 查询全部触发告警
$fireSql = "SELECT DISTINCT alert_name,instance,severity,starts_at,receive_time FROM alert_log WHERE alert_type=1";
$fireRes = mysqli_query($conn, $fireSql);
$fireMap = [];
while ($row = mysqli_fetch_assoc($fireRes)) {
$key = $row['alert_name'] . "|" . $row['instance'];
$fireMap[$key] = $row;
}
if (empty($fireMap)) {
echo json_encode([], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 查询全部恢复告警唯一标识
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'|',instance) as k FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($conn, $resolveSql);
$resolveKeys = [];
while ($r = mysqli_fetch_assoc($resolveRes)) {
$resolveKeys[] = $r['k'];
}
// 过滤:存在触发、无对应恢复 = 当前活跃故障
$activeList = [];
foreach ($fireMap as $key => $row) {
if (!in_array($key, $resolveKeys)) {
$row['alert_type'] = 1;
$activeList[] = $row;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+139
View File
@@ -0,0 +1,139 @@
<?php
session_start();
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"]);
exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
$act = $_GET['act'] ?? '';
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 4. 全局跨天未恢复告警【修复版:只看每条告警最新状态,兼容低版本MariaDB】
if ($act === "active_firing") {
// 第一步:分组获取每个告警+实例的最新入库时间
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($maxRow = mysqli_fetch_assoc($maxRes)) {
// 转义防止SQL注入
$an = $conn->real_escape_string($maxRow['alert_name']);
$ins = $conn->real_escape_string($maxRow['instance']);
$mrt = $conn->real_escape_string($maxRow['max_rt']);
// 根据告警名、实例、最新时间取出最后一条记录
$rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log
WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
$rowRes = mysqli_query($conn, $rowSql);
$data = mysqli_fetch_assoc($rowRes);
// 仅最后一条状态为触发,才判定为当前活跃未恢复告警
if ($data && (int)$data['alert_type'] === 1) {
$activeList[] = $data;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+330
View File
@@ -0,0 +1,330 @@
<?php
session_start();
$act = $_GET['act'] ?? '';
// 未登录直接拒绝所有接口访问
if(empty($_SESSION['user_id'])){
header("Content-Type:application/json;charset=utf-8");
echo json_encode(["code"=>401,"msg"=>"未登录,请前往登录页面"], JSON_UNESCAPED_UNICODE);
exit;
}
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 调试代码:打印GET入参,测试完成后删除此段
// $getAct = $_GET['act'] ?? '';
//echo json_encode([
// "debug_act" => $getAct,
// "debug_rule" => $_GET['rule_name'] ?? '',
// "debug_instance" => $_GET['instance'] ?? ''
//], JSON_UNESCAPED_UNICODE);
//exit;
// 接口1:下线整条监控规则(该规则下所有实例一并清理)
if($act === "clear_offline_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
// 标记该规则下全部实例为下线
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
// 批量插入恢复记录,消除页面残留告警
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc'
AND (alert_name,instance) NOT IN (
SELECT alert_name,instance FROM alert_log WHERE alert_type=2
)
";
mysqli_query($conn, $insertSql);
echo json_encode(["code"=>0,"msg"=>"整条规则下线完成,所有实例告警已清除"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口2:单独下线单台实例(仅隐藏指定机器,同规则其他主机不受影响)
if($act === "clear_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 第一步:删除当前实例旧的恢复记录,避免重复拦截
$delSql = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
// 第二步:插入最新恢复记录,移除NOT IN限制,强制生成
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
$affected = mysqli_affected_rows($conn);
echo json_encode([
"code"=>0,
"msg"=>"单实例告警清理完成",
"insert_rows" => $affected
],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口3:恢复整条已下线规则(status改为1,重新展示所有实例告警)
if($act === "restore_rule"){
$rule = $_GET['rule_name'] ?? '';
if(empty($rule)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"监控规则已恢复,新故障会正常展示"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口4:恢复单台实例(仅把指定rule+instance置为启用)
if($act === "restore_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$updateSql = "UPDATE monitor_rule SET status=1, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
echo json_encode(["code"=>0,"msg"=>"单实例已恢复"],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口5:删除单实例全部告警日志
if($act === "delete_single_instance_log"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
$delSql = "DELETE FROM alert_log WHERE alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delSql);
$aff = mysqli_affected_rows($conn);
echo json_encode(["code"=>0,"msg"=>"单实例告警日志已删除","affected_rows"=>$aff],JSON_UNESCAPED_UNICODE);
exit;
}
// 接口:永久屏蔽单台实例(monitor_rule 该条实例status=0,不手动恢复永远不告警)
if($act === "offline_single_instance"){
$rule = $_GET['rule_name'] ?? '';
$inst = $_GET['instance'] ?? '';
if(empty($rule) || empty($inst)){
echo json_encode(["code"=>400,"msg"=>"缺少rule_name或instance参数"],JSON_UNESCAPED_UNICODE);
exit;
}
$ruleEsc = mysqli_real_escape_string($conn, $rule);
$instEsc = mysqli_real_escape_string($conn, $inst);
// 把这条【规则+实例】状态改为0,永久屏蔽
$updateSql = "UPDATE monitor_rule SET status=0, update_at=NOW() WHERE rule_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $updateSql);
$aff = mysqli_affected_rows($conn);
// 同步生成恢复日志,界面立刻消失旧告警
$delOld = "DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$ruleEsc' AND instance='$instEsc'";
mysqli_query($conn, $delOld);
$insertSql = "
INSERT INTO alert_log (alert_type,alert_name,instance,severity,starts_at,receive_time)
SELECT DISTINCT 2,alert_name,instance,severity,NOW(),NOW()
FROM alert_log
WHERE alert_name='$ruleEsc' AND instance='$instEsc'
";
mysqli_query($conn, $insertSql);
echo json_encode([
"code"=>0,
"msg"=>"单实例已永久屏蔽,需调用restore_single_instance恢复",
"update_rows"=>$aff
],JSON_UNESCAPED_UNICODE);
exit;
}
// 新增:同步Prometheus实时告警,自动清理残留恢复记录
if($act === "sync_prom_alerts"){
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
$firingMap = [];
foreach($promData['data']['alerts'] as $item){
$an = $item['labels']['alertname'];
$ins = $item['labels']['instance'];
$key = "$an||$ins";
$firingMap[$key] = 1;
}
$res = mysqli_query($conn,"SELECT alert_name,instance FROM alert_log WHERE alert_type=2");
$delCnt = 0;
while($row = mysqli_fetch_assoc($res)){
$k = $row['alert_name']."||".$row['instance'];
if(isset($firingMap[$k])){
$n = mysqli_real_escape_string($conn,$row['alert_name']);
$i = mysqli_real_escape_string($conn,$row['instance']);
mysqli_query($conn,"DELETE FROM alert_log WHERE alert_type=2 AND alert_name='$n' AND instance='$i'");
$delCnt++;
}
}
echo json_encode(["code"=>0,"msg"=>"同步完成","delete_clear_log"=>$delCnt],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// 2. 今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 3. 今日全量告警明细表格
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
exit;
}
// 4. 全局跨天未恢复告警(过滤整条下线规则,单实例仅靠恢复日志隐藏)
if ($act === "active_firing") {
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($maxRow = mysqli_fetch_assoc($maxRes)) {
$an = $conn->real_escape_string($maxRow['alert_name']);
$ins = $conn->real_escape_string($maxRow['instance']);
$mrt = $conn->real_escape_string($maxRow['max_rt']);
// 判断该规则是否整条下线,下线直接跳过
$ruleCheck = mysqli_query($conn, "SELECT status FROM monitor_rule WHERE rule_name='$an' LIMIT 1");
$ruleStatus = 1;
if($ruleCheck && $ruleRow = mysqli_fetch_assoc($ruleCheck)){
$ruleStatus = intval($ruleRow['status']);
}
if($ruleStatus === 0){
continue;
}
$rowSql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log
WHERE alert_name='$an' AND instance='$ins' AND receive_time='$mrt'";
$rowRes = mysqli_query($conn, $rowSql);
$data = mysqli_fetch_assoc($rowRes);
if ($data && (int)$data['alert_type'] === 1) {
$activeList[] = $data;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
exit;
}
// 非法请求兜底
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
@@ -0,0 +1,46 @@
<?php
header("Content-Type:application/json;charset=utf-8");
// 改为宿主机真实内网IP,不要写localhost/mysql容器名
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
mysqli_set_charset($conn,"utf8mb4");
$act = $_GET['act'] ?? '';
// 1. 获取今日总统计:触发次数、恢复次数、故障实例数
if($act == "day_total"){
$today = date("Y-m-d");
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$res = mysqli_fetch_assoc(mysqli_query($conn,$sql));
echo json_encode($res,JSON_UNESCAPED_UNICODE);
}
// 2. 今日告警TOP排行(按发生次数)
if($act == "top_alert"){
$today = date("Y-m-d");
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
// 3. 今日明细列表(前端表格)
if($act == "log_list"){
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$data = [];
$q = mysqli_query($conn,$sql);
while($r = mysqli_fetch_assoc($q)) $data[] = $r;
echo json_encode($data,JSON_UNESCAPED_UNICODE);
}
mysqli_close($conn);
?>
+12
View File
@@ -0,0 +1,12 @@
<?php
// 后台存放真实密码,前端完全看不到
$realPwd = "hp93000";
$inputPwd = $_POST['pwd'] ?? '';
header("Content-Type:application/json;charset=utf-8");
if($inputPwd === $realPwd){
// 生成临时token返回前端
echo json_encode(["code"=>200,"token"=>"alert_".md5(time().uniqid())]);
}else{
echo json_encode(["code"=>400,"msg"=>"密码错误"]);
}
+126
View File
@@ -0,0 +1,126 @@
<?php
// 统一数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
// 统一数据库连接函数
function getDbConn() {
global $dbHost, $dbUser, $dbPass, $dbName;
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
mysqli_set_charset($conn, "utf8mb4");
return $conn;
}
$act = $_GET['act'] ?? '';
// CSV导出接口(独立流式下载,不输出JSON头)
if ($act === "export_csv") {
$conn = getDbConn();
if (!$conn) {
header("Content-Type:text/html;charset=utf-8");
echo "数据库连接失败:" . mysqli_connect_error();
exit;
}
$today = date("Y-m-d");
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,receive_time
FROM alert_log WHERE DATE(receive_time) = '$today' ORDER BY receive_time DESC";
$res = mysqli_query($conn, $sql);
ob_clean();
header("Content-Type: text/csv; charset=utf-8");
header("Content-Disposition: attachment; filename=告警报表_" . $today . ".csv");
echo "\xEF\xBB\xBF";
$header = ["告警状态", "告警名称", "实例", "级别", "故障开始时间", "接收时间"];
echo implode(",", $header) . "\r\n";
while ($row = mysqli_fetch_assoc($res)) {
$status = $row['alert_type'] == 1 ? "触发" : "恢复";
$line = [
$status,
$row['alert_name'],
$row['instance'],
$row['severity'],
$row['starts_at'],
$row['receive_time']
];
foreach ($line as &$v) {
$v = '"' . str_replace('"', '""', $v) . '"';
}
echo implode(",", $line) . "\r\n";
}
mysqli_close($conn);
exit;
}
// 所有JSON接口统一返回头
header("Content-Type:application/json;charset=utf-8");
$conn = getDbConn();
if (!$conn) {
echo json_encode(["code" => 500, "msg" => "数据库连接失败:" . mysqli_connect_error()], JSON_UNESCAPED_UNICODE);
exit;
}
$today = date("Y-m-d");
// 1.今日统计卡片
if ($act === "day_total") {
$sql = "SELECT
SUM(IF(alert_type=1,1,0)) as fire_count,
SUM(IF(alert_type=2,1,0)) as resolve_count,
COUNT(DISTINCT instance) as instance_num
FROM alert_log WHERE DATE(receive_time) = '$today'";
$data = mysqli_fetch_assoc(mysqli_query($conn, $sql));
echo json_encode($data, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 2.今日告警TOP10图表
if ($act === "top_alert") {
$sql = "SELECT alert_name,COUNT(*) cnt FROM alert_log WHERE DATE(receive_time)='$today' GROUP BY alert_name ORDER BY cnt DESC LIMIT 10";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 3.今日全量告警明细
if ($act === "log_list") {
$sql = "SELECT alert_type,alert_name,instance,severity,starts_at,ends_at,receive_time
FROM alert_log WHERE DATE(receive_time)='$today' ORDER BY receive_time DESC";
$list = [];
$q = mysqli_query($conn, $sql);
while ($r = mysqli_fetch_assoc($q)) $list[] = $r;
echo json_encode($list, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 4.全局跨天未恢复告警(修复版,取每组最新一条判断状态)
if ($act === "active_firing") {
$maxSql = "SELECT alert_name,instance,MAX(receive_time) max_rt FROM alert_log GROUP BY alert_name,instance";
$maxRes = mysqli_query($conn, $maxSql);
$activeList = [];
while ($row = mysqli_fetch_assoc($maxRes)) {
$an = mysqli_real_escape_string($conn, $row['alert_name']);
$ins = mysqli_real_escape_string($conn, $row['instance']);
$rt = mysqli_real_escape_string($conn, $row['max_rt']);
$sql = "SELECT alert_name,instance,severity,starts_at,receive_time,alert_type
FROM alert_log WHERE alert_name='$an' AND instance='$ins' AND receive_time='$rt' LIMIT 1";
$one = mysqli_fetch_assoc(mysqli_query($conn, $sql));
if ($one && (int)$one['alert_type'] === 1) {
$activeList[] = $one;
}
}
echo json_encode($activeList, JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
// 非法act
echo json_encode(["code" => 400, "msg" => "无效请求参数act"], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+411
View File
@@ -0,0 +1,411 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: ../login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者调试面板 - Advantest 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-title{
font-size:24px;
font-weight:700;
letter-spacing:1px;
color:#152c5b;
}
.header-sub{
font-size:14px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 18px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-warning{
background:#e6a23c;
color:#fff;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1600px;
margin:0 auto;
}
h1{
display:none;
}
/* 卡片样式 统一企业面板风格 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.row{
display:flex;
gap:12px;
align-items:center;
margin-bottom:10px;
flex-wrap:wrap;
}
label{
font-size:14px;
color:#475569;
width:110px;
}
input{
padding:8px 12px;
border:1px solid #dcdfe6;
border-radius:8px;
width:280px;
font-size:14px;
}
input[type="date"]{
width:200px;
}
button{
padding:9px 18px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
}
button.normal{
background:#409eff;
color:#fff;
}
button.normal:hover{
background:#2979e0;
}
.tip{
font-size:13px;
color:#94a3b8;
margin-top:8px;
padding-left:110px;
}
/* 返回结果区块 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:260px;
font-family:Consolas,monospace;
font-size:13px;
}
/* 响应式适配 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.container{padding:24px}
.card{padding:24px}
.row{flex-direction:column;align-items:flex-start}
label{width:auto}
input{width:100% !important;max-width:320px}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-title"><i class="fa fa-code"></i> 开发者调试面板</div>
<div class="header-sub">全部告警接口可视化调用 · 后端API调试工具</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.href='../admin.php'"><i class="fa fa-arrow-left"></i>返回系统后台</button>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新页面</button>
<button class="btn-base btn-primary" onclick="location.href='../dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
</div>
</div>
<div class="container">
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3><i class="fa fa-eraser"></i>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button class="normal" onclick="req('clear_single_instance','c_rule','c_ins')"><i class="fa fa-play"></i>执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号无需手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3><i class="fa fa-ban"></i>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="btn-danger" onclick="req('offline_single_instance','off_rule','off_ins')"><i class="fa fa-ban"></i>永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不执行恢复接口则永久不展示该实例告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3><i class="fa fa-check-circle"></i>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="btn-success" onclick="req('restore_single_instance','res_rule','res_ins')"><i class="fa fa-unlock"></i>恢复实例告警</button>
</div>
<div class="tip">解除单实例永久屏蔽状态,新故障会正常展示</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-danger" onclick="reqSingle('clear_offline_rule','cr_rule')"><i class="fa fa-power-off"></i>下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-success" onclick="reqSingle('restore_rule','rr_rule')"><i class="fa fa-refresh"></i>恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3><i class="fa fa-trash"></i>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="btn-warning" onclick="req('delete_single_instance_log','del_rule','del_ins')"><i class="fa fa-trash"></i>清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3><i class="fa fa-refresh"></i>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('sync_prom_alerts')"><i class="fa fa-sync"></i>立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端“故障存在但页面不显示”bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3><i class="fa fa-database"></i>8. 数据查询接口(获取面板原始数据)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('day_total')"><i class="fa fa-bar-chart"></i>今日统计 day_total</button>
<button class="normal" onclick="simpleReq('top_alert')"><i class="fa fa-pie-chart"></i>告警TOP10 top_alert</button>
<button class="normal" onclick="simpleReq('log_list')"><i class="fa fa-list"></i>今日明细 log_list</button>
<button class="normal" onclick="simpleReq('active_firing')"><i class="fa fa-exclamation-circle"></i>当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表(带日历选择) -->
<div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
<div class="row">
<label>选择日期:</label>
<input type="date" id="csv_date" placeholder="留空导出今日">
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
</div>
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
</div>
<!-- 返回结果输出 -->
<div class="card">
<h3><i class="fa fa-terminal"></i>接口返回结果(JSON)</h3>
<div id="result">等待执行接口操作...</div>
</div>
</div>
<script>
// 接口路径 同目录 index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV 支持日历选择日期
function exportCsv(){
const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`;
if(dateVal){
url += `&date=${encodeURIComponent(dateVal)}`;
}
window.open(url,"_blank");
}
</script>
</body>
</html>
+411
View File
@@ -0,0 +1,411 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: ../login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发者调试面板 - Advantest 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-title{
font-size:24px;
font-weight:700;
letter-spacing:1px;
color:#152c5b;
}
.header-sub{
font-size:14px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 18px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
.btn-warning{
background:#e6a23c;
color:#fff;
}
.btn-danger{
background:#f56c6c;
color:#fff;
}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1600px;
margin:0 auto;
}
h1{
display:none;
}
/* 卡片样式 统一企业面板风格 */
.card{
background:#fff;
border-radius:16px;
padding:32px;
margin-bottom:36px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.card h3{
display:flex;
align-items:center;
gap:10px;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.card h3 i{
color:#2563eb;
font-size:20px;
}
.row{
display:flex;
gap:12px;
align-items:center;
margin-bottom:10px;
flex-wrap:wrap;
}
label{
font-size:14px;
color:#475569;
width:110px;
}
input{
padding:8px 12px;
border:1px solid #dcdfe6;
border-radius:8px;
width:280px;
font-size:14px;
}
input[type="date"]{
width:200px;
}
button{
padding:9px 18px;
border:none;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
}
button.normal{
background:#409eff;
color:#fff;
}
button.normal:hover{
background:#2979e0;
}
.tip{
font-size:13px;
color:#94a3b8;
margin-top:8px;
padding-left:110px;
}
/* 返回结果区块 */
#result{
margin-top:20px;
padding:20px;
background:#1e1e1e;
color:#fff;
border-radius:12px;
white-space:pre-wrap;
min-height:260px;
font-family:Consolas,monospace;
font-size:13px;
}
/* 响应式适配 */
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.container{padding:24px}
.card{padding:24px}
.row{flex-direction:column;align-items:flex-start}
label{width:auto}
input{width:100% !important;max-width:320px}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<div class="header-left">
<div>
<div class="header-title"><i class="fa fa-code"></i> 开发者调试面板</div>
<div class="header-sub">全部告警接口可视化调用 · 后端API调试工具</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i><?php echo $userName; ?><?php echo $roleText; ?></span>
<button class="btn-base btn-outline" onclick="location.href='../admin.php'"><i class="fa fa-arrow-left"></i>返回系统后台</button>
<button class="btn-base btn-outline" onclick="location.reload()"><i class="fa fa-refresh"></i>刷新页面</button>
<button class="btn-base btn-primary" onclick="location.href='../dashboard.php'"><i class="fa fa-line-chart"></i>告警监控大屏</button>
</div>
</div>
<div class="container">
<!-- 1 临时清理单实例告警 clear_single_instance -->
<div class="card">
<h3><i class="fa fa-eraser"></i>1. 临时隐藏单实例告警 clear_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="c_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="c_ins" placeholder="例 10.150.10.82:389">
<button class="normal" onclick="req('clear_single_instance','c_rule','c_ins')"><i class="fa fa-play"></i>执行清理</button>
</div>
<div class="tip">仅插入恢复日志,下次故障自动重新展示;instance冒号无需手动编码,JS自动处理</div>
</div>
<!-- 2 永久屏蔽单实例 offline_single_instance -->
<div class="card">
<h3><i class="fa fa-ban"></i>2. 永久屏蔽单实例 offline_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="off_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="off_ins" placeholder="例 10.150.10.82:389">
<button class="btn-danger" onclick="req('offline_single_instance','off_rule','off_ins')"><i class="fa fa-ban"></i>永久屏蔽</button>
</div>
<div class="tip">修改monitor_rule状态0,不执行恢复接口则永久不展示该实例告警</div>
</div>
<!-- 3 恢复单实例 restore_single_instance -->
<div class="card">
<h3><i class="fa fa-check-circle"></i>3. 恢复单实例 restore_single_instance</h3>
<div class="row">
<label>rule_name</label>
<input id="res_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="res_ins" placeholder="例 10.150.10.82:389">
<button class="btn-success" onclick="req('restore_single_instance','res_rule','res_ins')"><i class="fa fa-unlock"></i>恢复实例告警</button>
</div>
<div class="tip">解除单实例永久屏蔽状态,新故障会正常展示</div>
</div>
<!-- 4 整条规则下线 clear_offline_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>4. 整条规则全部下线 clear_offline_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="cr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-danger" onclick="reqSingle('clear_offline_rule','cr_rule')"><i class="fa fa-power-off"></i>下线整条规则</button>
</div>
</div>
<!-- 5 恢复整条规则 restore_rule -->
<div class="card">
<h3><i class="fa fa-cog"></i>5. 恢复整条规则 restore_rule</h3>
<div class="row">
<label>rule_name</label>
<input id="rr_rule" placeholder="例 Host_Ping_Offline">
<button class="btn-success" onclick="reqSingle('restore_rule','rr_rule')"><i class="fa fa-refresh"></i>恢复整条规则</button>
</div>
</div>
<!-- 6 删除单实例全部日志 delete_single_instance_log -->
<div class="card">
<h3><i class="fa fa-trash"></i>6. 删除单实例所有告警日志 delete_single_instance_log</h3>
<div class="row">
<label>rule_name</label>
<input id="del_rule" placeholder="例 Port_Down">
<label>instance</label>
<input id="del_ins" placeholder="例 10.150.10.82:389">
<button class="btn-warning" onclick="req('delete_single_instance_log','del_rule','del_ins')"><i class="fa fa-trash"></i>清空日志</button>
</div>
</div>
<!-- 7 同步Prometheus实时告警 sync_prom_alerts -->
<div class="card">
<h3><i class="fa fa-refresh"></i>7. 手动同步Prometheus告警(清除脏恢复记录)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('sync_prom_alerts')"><i class="fa fa-sync"></i>立即同步校准数据库</button>
</div>
<div class="tip">自动删除故障中残留的手动恢复记录,修复前端“故障存在但页面不显示”bug</div>
</div>
<!-- 8 数据查询接口 -->
<div class="card">
<h3><i class="fa fa-database"></i>8. 数据查询接口(获取面板原始数据)</h3>
<div class="row">
<button class="normal" onclick="simpleReq('day_total')"><i class="fa fa-bar-chart"></i>今日统计 day_total</button>
<button class="normal" onclick="simpleReq('top_alert')"><i class="fa fa-pie-chart"></i>告警TOP10 top_alert</button>
<button class="normal" onclick="simpleReq('log_list')"><i class="fa fa-list"></i>今日明细 log_list</button>
<button class="normal" onclick="simpleReq('active_firing')"><i class="fa fa-exclamation-circle"></i>当前未恢复 active_firing</button>
</div>
</div>
<!-- 9 导出CSV报表(带日历选择) -->
<div class="card">
<h3><i class="fa fa-download"></i>9. 导出告警CSV报表(支持历史日期)</h3>
<div class="row">
<label>选择日期:</label>
<input type="date" id="csv_date" placeholder="留空导出今日">
<button class="btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>下载对应日期报表</button>
</div>
<div class="tip">不选择日期默认导出今日告警;选择历史日期可下载过往报表</div>
</div>
<!-- 返回结果输出 -->
<div class="card">
<h3><i class="fa fa-terminal"></i>接口返回结果(JSON)</h3>
<div id="result">等待执行接口操作...</div>
</div>
</div>
<script>
// 接口路径 同目录 index.php
const apiUrl = "./index.php";
const resultDom = document.getElementById("result");
// 双参数接口 act + rule_name + instance
function req(act, ruleId, insId){
const rule = document.getElementById(ruleId).value.trim();
const ins = document.getElementById(insId).value.trim();
if(!rule || !ins){
resultDom.innerText = "错误:rule_name 和 instance 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
params.append("instance", ins);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 单参数接口 act + rule_name
function reqSingle(act, ruleId){
const rule = document.getElementById(ruleId).value.trim();
if(!rule){
resultDom.innerText = "错误:rule_name 不能为空";
return;
}
const params = new URLSearchParams();
params.append("act", act);
params.append("rule_name", rule);
fetch(`${apiUrl}?${params.toString()}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 无参数接口
function simpleReq(act){
fetch(`${apiUrl}?act=${act}`)
.then(r=>r.json())
.then(json=>{
resultDom.innerText = JSON.stringify(json,null,2);
})
.catch(e=>{
resultDom.innerText = "请求异常:"+e;
})
}
// 导出CSV 支持日历选择日期
function exportCsv(){
const dateVal = document.getElementById("csv_date").value.trim();
let url = `${apiUrl}?act=export_csv`;
if(dateVal){
url += `&date=${encodeURIComponent(dateVal)}`;
}
window.open(url,"_blank");
}
</script>
</body>
</html>
+89
View File
@@ -0,0 +1,89 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(json_encode(['code' => 0, 'msg' => 'ok']));
}
// 数据库配置
$dbHost = '127.0.0.1';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效,请重新登录']);
exit;
}
$uid = $_SESSION['user_id'];
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
if (!$conn) {
echo json_encode(['code' => 500, 'msg' => '数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
// 获取当前用户订阅配置
if ($action === 'get_subscribe') {
$sql = "SELECT * FROM alert_subscribe WHERE uid = ? LIMIT 1";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $uid);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$data = mysqli_fetch_assoc($res);
if (empty($data)) {
$data = [
'severity' => '',
'instance' => '',
'alertname' => '',
'dingtalk_webhook' => '',
'wecom_webhook' => '',
'mail_receiver' => '',
'enable' => 0
];
}
echo json_encode(['code' => 0, 'msg' => 'ok', 'data' => $data], JSON_UNESCAPED_UNICODE);
exit;
}
// 保存/更新订阅配置
if ($action === 'save_subscribe') {
$post = json_decode(file_get_contents('php://input'), true);
$severity = $post['severity'] ?? '';
$instance = $post['instance'] ?? '';
$alertname = $post['alertname'] ?? '';
$dingtalk = $post['dingtalk_webhook'] ?? '';
$wecom = $post['wecom_webhook'] ?? '';
$mail = $post['mail_receiver'] ?? '';
$enable = intval($post['enable'] ?? 0);
$check = mysqli_query($conn, "SELECT id FROM alert_subscribe WHERE uid = '$uid' LIMIT 1");
if (mysqli_num_rows($check) > 0) {
$sql = "UPDATE alert_subscribe SET severity=?,instance=?,alertname=?,dingtalk_webhook=?,wecom_webhook=?,mail_receiver=?,enable=? WHERE uid=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssis', $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable, $uid);
} else {
$sql = "INSERT INTO alert_subscribe(uid,severity,instance,alertname,dingtalk_webhook,wecom_webhook,mail_receiver,enable) VALUES (?,?,?,?,?,?,?,?)";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $uid, $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable);
}
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '订阅配置保存成功'], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode(['code' => 500, 'msg' => '保存失败:' . mysqli_error($conn)], JSON_UNESCAPED_UNICODE);
}
exit;
}
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
?>
@@ -0,0 +1,130 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(json_encode(['code' => 0, 'msg' => 'ok']));
}
// 数据库配置
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
// 登录会话校验(和你原有登录体系一致)
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效,请重新登录']);
exit;
}
$uid = $_SESSION['user_id'];
// 连接数据库
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
if (!$conn) {
echo json_encode(['code' => 500, 'msg' => '数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
$action = isset($_REQUEST['action']) ? $_REQUEST['action'] : '';
// 1、获取当前用户订阅配置
if ($action === 'get_subscribe') {
$sql = "SELECT * FROM alert_subscribe WHERE uid = ? LIMIT 1";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 's', $uid);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$data = mysqli_fetch_assoc($res);
if (empty($data)) {
$data = [
'severity' => '',
'instance' => '',
'alertname' => '',
'dingtalk_webhook' => '',
'wecom_webhook' => '',
'mail_receiver' => '',
'enable' => 0
];
}
echo json_encode(['code' => 0, 'msg' => 'ok', 'data' => $data], JSON_UNESCAPED_UNICODE);
exit;
}
// 2、保存/更新订阅配置
if ($action === 'save_subscribe') {
$post = json_decode(file_get_contents('php://input'), true);
$severity = $post['severity'] ?? '';
$instance = $post['instance'] ?? '';
$alertname = $post['alertname'] ?? '';
$dingtalk = $post['dingtalk_webhook'] ?? '';
$wecom = $post['wecom_webhook'] ?? '';
$mail = $post['mail_receiver'] ?? '';
$enable = intval($post['enable'] ?? 0);
// 判断是否已有记录
$check = mysqli_query($conn, "SELECT id FROM alert_subscribe WHERE uid = '$uid' LIMIT 1");
if (mysqli_num_rows($check) > 0) {
$sql = "UPDATE alert_subscribe SET severity=?,instance=?,alertname=?,dingtalk_webhook=?,wecom_webhook=?,mail_receiver=?,enable=? WHERE uid=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssis', $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable, $uid);
} else {
$sql = "INSERT INTO alert_subscribe(uid,severity,instance,alertname,dingtalk_webhook,wecom_webhook,mail_receiver,enable) VALUES (?,?,?,?,?,?,?,?)";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $uid, $severity, $instance, $alertname, $dingtalk, $wecom, $mail, $enable);
}
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '订阅配置保存成功'], JSON_UNESCAPED_UNICODE);
} else {
echo json_encode(['code' => 500, 'msg' => '保存失败:' . mysqli_error($conn)], JSON_UNESCAPED_UNICODE);
}
exit;
}
// 3、分页查询推送日志
if ($action === 'list_push_log') {
$page = isset($_REQUEST['page']) ? intval($_REQUEST['page']) : 1;
$pageSize = isset($_REQUEST['page_size']) ? intval($_REQUEST['page_size']) : 10;
$offset = ($page - 1) * $pageSize;
// 总条数
$cntSql = "SELECT COUNT(*) as total FROM alert_push_log WHERE uid = ?";
$stmtCnt = mysqli_prepare($conn, $cntSql);
mysqli_stmt_bind_param($stmtCnt, 's', $uid);
mysqli_stmt_execute($stmtCnt);
$cntRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtCnt));
$total = $cntRow['total'];
// 分页数据
$sql = "SELECT * FROM alert_push_log WHERE uid = ? ORDER BY push_time DESC LIMIT ?,?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sii', $uid, $offset, $pageSize);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode([
'code' => 0,
'msg' => 'ok',
'data' => [
'list' => $list,
'page' => $page,
'page_size' => $pageSize,
'total' => $total,
'pages' => ceil($total / $pageSize)
]
], JSON_UNESCAPED_UNICODE);
exit;
}
// 无匹配操作
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
@@ -0,0 +1,14 @@
{
"theme_color": "#152c5b",
"card_bg": "#ffffff",
"body_bg": "#f9fafc",
"font_family": "Inter,PingFang SC,Microsoft YaHei",
"sidebar_links": [
{
"icon": "fa-github",
"title": "运维监控中心",
"url": "http://10.150.117.190:3000/d/linux-server-alert-fix-startsat/linux-fu-wu-qi-gao-jing-zhong-xin?orgId=1&refresh=10s",
"target": "_blank"
}
]
}
+6
View File
@@ -0,0 +1,6 @@
{
"user_id": "admin001",
"username": "admin",
"role": "admin",
"password": "e10adc3949ba59abbe56e057f20f883e"
}
+35
View File
@@ -0,0 +1,35 @@
<?php
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(["code"=>401,"msg"=>"未登录"]);
exit;
}
header("Content-Type:application/json;charset=utf-8");
$userFile = "./user_info.json";
if(!file_exists($userFile)){
echo json_encode(["code"=>500,"msg"=>"账号配置文件缺失"]);
exit;
}
$userData = json_decode(file_get_contents($userFile),true);
$act = $_POST['act'] ?? '';
if($act == "change_pwd"){
$old = $_POST['old_pwd'];
$new = $_POST['new_pwd'];
$confirm = $_POST['confirm_pwd'];
if($userData['password'] != md5($old)){
echo json_encode(["code"=>400,"msg"=>"原密码错误"]);
exit;
}
if($new != $confirm){
echo json_encode(["code"=>400,"msg"=>"两次新密码不一致"]);
exit;
}
$userData['password'] = md5($new);
file_put_contents($userFile,json_encode($userData,JSON_UNESCAPED_UNICODE));
echo json_encode(["code"=>0,"msg"=>"密码修改成功,请重新登录"]);
exit;
}
echo json_encode(["code"=>400,"msg"=>"非法操作"]);
?>
+64
View File
@@ -0,0 +1,64 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
header("Content-Type:application/json;charset=utf-8");
// 改成宿主机IP
$host = "10.150.117.190";
$user = "root";
$pass = "hp93000";
$dbname = "alert_mail_stat";
$conn = mysqli_connect($host,$user,$pass,$dbname);
if (!$conn) {
echo json_encode([
"code" => 500,
"msg" => "数据库连接失败:" . mysqli_connect_error()
], JSON_UNESCAPED_UNICODE);
exit;
}
mysqli_set_charset($conn,"utf8mb4");
$rawBody = file_get_contents('php://input');
$alertData = json_decode($rawBody, true);
if (!is_array($alertData) || empty($alertData['alerts'])) {
echo json_encode([
"code" => 400,
"msg" => "未获取到有效告警数据"
], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
exit;
}
$insertCount = 0;
foreach ($alertData['alerts'] as $alert) {
$alertName = mysqli_real_escape_string($conn, $alert['labels']['alertname'] ?? "未知告警");
$instance = mysqli_real_escape_string($conn, $alert['labels']['instance'] ?? "");
$severity = mysqli_real_escape_string($conn, $alert['labels']['severity'] ?? "warning");
$status = $alert['status'] ?? "firing";
$startsAt = mysqli_real_escape_string($conn, $alert['startsAt'] ?? date("Y-m-d H:i:s"));
$endsAt = isset($alert['endsAt']) ? "'" . mysqli_real_escape_string($conn, $alert['endsAt']) . "'" : "NULL";
$fingerprint= mysqli_real_escape_string($conn, $alert['fingerprint'] ?? md5($alertName.$instance.$startsAt));
$content = mysqli_real_escape_string($conn, json_encode($alert, JSON_UNESCAPED_UNICODE));
$alertType = $status === "resolved" ? 2 : 1;
$checkSql = "SELECT id FROM alert_log WHERE mail_uid = '$fingerprint'";
$checkRes = mysqli_query($conn, $checkSql);
if (mysqli_num_rows($checkRes) > 0) continue;
$insertSql = "INSERT INTO alert_log
(mail_uid, alert_type, alert_name, instance, severity, starts_at, ends_at, content, receive_time)
VALUES ('$fingerprint', $alertType, '$alertName', '$instance', '$severity', '$startsAt', $endsAt, '$content', NOW())";
if (mysqli_query($conn, $insertSql)) $insertCount++;
}
echo json_encode([
"code" => 200,
"msg" => "告警处理完成",
"insert_num" => $insertCount
], JSON_UNESCAPED_UNICODE);
mysqli_close($conn);
?>
+427
View File
@@ -0,0 +1,427 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
// 新增工单默认未读 is_read=0
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,0,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
// 修改状态自动标记已读
$sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
// 修复BUG:仅按工单唯一ID更新,不再使用标题模糊匹配,选中哪一行只修改该行工单
$sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 一键标记当前用户全部待处理工单为已读
if ($action === 'mark_all_read') {
$uid = $post['uid'] ?? '';
$sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, "s", $uid);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']);
exit;
}
// 语言切换
if ($action === 'save_lang') {
$lang = $post['lang'] ?? 'zh';
$sql = "UPDATE sys_config SET v=? WHERE k='lang'";
$stmt = mysqli_prepare($connConf, $sql);
mysqli_stmt_bind_param($stmt, "s", $lang);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新
$syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($action === "sync_alert_auto_close"){
// 1、拉取仪表盘实时未恢复告警
$alertApi = "./index.php?act=active_firing";
$raw = @file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "|" . $item['instance'];
}
// 2、查询所有未完成、绑定告警的工单
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($connWork, $sql);
while($row = mysqli_fetch_assoc($res)){
// 工单关联告警不在当前活跃列表=已恢复,自动设为完成
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($connWork, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
@@ -0,0 +1,117 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 数据库本地连接,避免远程拦截
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error()
]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// 获取当前用户是否管理员
$isAdmin = 0;
$roleSql = "SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($conn, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) $isAdmin = intval($roleRow['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、新建工单(修复第50行引用报错)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$status = 1; // 单独定义变量,不能直接写1传入bind_param
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,status,create_time,update_time) VALUES (?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn, $sql);
// 全部使用变量传参,禁止直接写数字/字符串
mysqli_stmt_bind_param($stmt, 'ssssi', $title, $content, $loginUid, $assign, $status);
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '工单提交成功']);
} else {
echo json_encode(['code' => 500, 'msg' => '提交失败:' . mysqli_error($conn)]);
}
exit;
}
// 2、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
// 普通用户权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 3、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 无效操作
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
?>
@@ -0,0 +1,144 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 数据库本地连接
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error()
]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// 获取当前用户是否管理员
$isAdmin = 0;
$roleSql = "SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($conn, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) $isAdmin = intval($roleRow['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、新建工单
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,status,create_time,update_time) VALUES (?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ssssi', $title, $content, $loginUid, $assign, $status);
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '工单提交成功']);
} else {
echo json_encode(['code' => 500, 'msg' => '提交失败:' . mysqli_error($conn)]);
}
exit;
}
// 2、编辑工单(新增,支持修改标题、内容备注、分配人)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sssi', $title, $content, $assign, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单修改备注保存成功']);
exit;
}
// 3、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 4、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
echo json_encode(['code' => 400, 'msg' => '无效操作']);
mysqli_close($conn);
exit;
?>
@@ -0,0 +1,146 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单数据库本地连接
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error()
]);
exit;
}
mysqli_set_charset($conn, 'utf8mb4');
// 判断管理员
$isAdmin = 0;
$roleSql = "SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($conn, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) $isAdmin = intval($roleRow['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、新建工单(新增relate_alert_id字段)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,status,create_time,update_time) VALUES (?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $loginUid, $assign, $relateAlertId, $status);
$ok = mysqli_stmt_execute($stmt);
if ($ok) {
echo json_encode(['code' => 0, 'msg' => '工单提交成功']);
} else {
echo json_encode(['code' => 500, 'msg' => '提交失败:' . mysqli_error($conn)]);
}
exit;
}
// 2、编辑工单(不修改关联告警ID)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'sssi', $title, $content, $assign, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单修改备注保存成功']);
exit;
}
// 3、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($conn, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 4、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 无匹配操作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($conn);
exit;
?>
@@ -0,0 +1,247 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = 'localhost';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRow = mysqli_fetch_assoc(mysqli_stmt_get_result($stmtRole));
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、获取当前活跃告警(解决前端下拉空白)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(新增通知用户、通知渠道、关联告警)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
// 发送通知
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发']);
exit;
}
// 3、编辑工单(更新通知、不修改关联告警)
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单
if ($action === "assign_order") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 通知发送统一函数
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 获取SMTP配置
$smtpRow = mysqli_fetch_assoc(mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1"));
$smtpConfig = $smtpRow ?? [];
// 获取工单详情
$orderRow = mysqli_fetch_assoc(mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId"));
$title = $orderRow['title'];
$content = $orderRow['content'];
// 遍历渠道发送
foreach ($channelArr as $ch) {
switch ($ch) {
case "mail":
sendMailNotify($smtpConfig, $userArr, $title, $content);
break;
case "dingtalk":
// 此处填入钉钉机器人发送逻辑,按需补充
break;
case "wecom":
// 此处填入企业微信机器人发送逻辑,按需补充
break;
}
}
}
// 邮件发送函数
function sendMailNotify($smtp, $uidList, $title, $content)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_user']) || empty($smtp['smtp_pass'])) return;
// 查询用户邮箱
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", $emailList);
$subject = "【工单通知】" . $title;
$body = "<h3>工单内容</h3><pre>".$content."</pre>";
// PHPMailer 发送逻辑,环境需安装phpmailer
/*
require_once 'PHPMailer.php';
require_once 'SMTP.php';
$mail = new PHPMailer\PHPMailer\PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = $smtp['smtp_host'];
$mail->SMTPAuth = true;
$mail->Username = $smtp['smtp_user'];
$mail->Password = $smtp['smtp_pass'];
$mail->SMTPSecure = $smtp['smtp_ssl'] ? PHPMailer\PHPMailer\PHPMailer::ENCRYPTION_SMTPS : '';
$mail->Port = $smtp['smtp_port'];
$mail->setFrom($smtp['send_from'], $smtp['send_from']);
$mail->addAddress($to);
$mail->isHTML(true);
$mail->Subject = $subject;
$mail->Body = $body;
$mail->send();
} catch (Exception $e) {}
*/
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
@@ -0,0 +1,267 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发']);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
@@ -0,0 +1,378 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
header("Content-Type: application/json;charset=utf-8");
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新,无需额外调用
$syncWorkUrl = apiOrder . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
@@ -0,0 +1,407 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssssi', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'sssssi', $title, $content, $assign, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
header("Content-Type: application/json;charset=utf-8");
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新,无需额外调用
$syncWorkUrl = apiOrder . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($_POST['action'] === "sync_alert_auto_close"){
// 1、拉取仪表盘实时未恢复告警
$alertApi = "./index.php?act=active_firing";
$raw = file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "|" . $item['instance'];
}
// 2、查询所有未完成、绑定告警的工单
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($conn, $sql);
while($row = mysqli_fetch_assoc($res)){
// 工单关联告警不在当前活跃列表=已恢复,自动设为完成
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($conn, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// 读取通知配置
$notifyRow = mysqli_fetch_assoc(mysqli_query($conn, "SELECT * FROM sys_workorder_notify LIMIT 1"));
if(!$notifyRow || $notifyRow['notify_open'] != 1) return;
// 组装钉钉、企业微信请求,curl发送webhook
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($GLOBALS['connWork'], "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
@@ -0,0 +1,427 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
echo json_encode(['code' => 0, 'msg' => 'ok']);
exit;
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 工单业务库 monitor
$dbWorkHost = '10.150.117.190';
$dbWorkUser = 'root';
$dbWorkPwd = 'hp93000';
$dbWorkName = 'monitor';
$connWork = mysqli_connect($dbWorkHost, $dbWorkUser, $dbWorkPwd, $dbWorkName);
if (!$connWork) {
echo json_encode(['code' => 500, 'msg' => '工单数据库连接失败:' . mysqli_connect_error()]);
exit;
}
mysqli_set_charset($connWork, 'utf8mb4');
// 配置库 alert_mail_stat(告警、通知配置统一此处)
$dbConfHost = "10.150.117.190";
$dbConfUser = "root";
$dbConfPwd = "hp93000";
$dbConfName = "alert_mail_stat";
$connConf = mysqli_connect($dbConfHost, $dbConfUser, $dbConfPwd, $dbConfName);
if(!$connConf){
echo json_encode(['code' => 500, 'msg' => '配置库连接失败']);
exit;
}
mysqli_set_charset($connConf, "utf8mb4");
// 获取当前用户角色权限
$isAdmin = 0;
$permList = "";
$roleSql = "SELECT r.is_admin, r.perm_list FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid = ?";
$stmtRole = mysqli_prepare($connWork, $roleSql);
mysqli_stmt_bind_param($stmtRole, 's', $loginUid);
mysqli_stmt_execute($stmtRole);
$roleRes = mysqli_stmt_get_result($stmtRole);
$roleRow = $roleRes ? mysqli_fetch_assoc($roleRes) : [];
if ($roleRow) {
$isAdmin = intval($roleRow['is_admin']);
$permList = $roleRow['perm_list'];
}
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"), true) ?: [];
// ===================== 新增1:根据告警ID查询关联工单 =====================
if ($action === "get_workorder_by_alertid") {
$alertId = trim($_GET['alert_id'] ?? '');
if (empty($alertId)) {
echo json_encode(['code' => 400, 'msg' => '缺少告警ID参数']);
exit;
}
$alertEsc = mysqli_real_escape_string($connWork, $alertId);
$sql = "SELECT id,title,status,assign_uid,create_time FROM work_order WHERE relate_alert_id = ? ORDER BY create_time DESC";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 's', $alertEsc);
mysqli_stmt_execute($stmt);
$res = mysqli_stmt_get_result($stmt);
$list = [];
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// ===================== 新增2:自动同步告警状态,批量更新工单为已完成 =====================
if ($action === "auto_sync_workorder_status") {
// 1. 查询所有已恢复的告警(alert_type=2),去重获取告警ID
$resolveSql = "SELECT DISTINCT CONCAT(alert_name,'_',instance) as alert_id FROM alert_log WHERE alert_type=2";
$resolveRes = mysqli_query($connConf, $resolveSql);
$resolveAlertIds = [];
while ($row = mysqli_fetch_assoc($resolveRes)) {
$resolveAlertIds[] = $row['alert_id'];
}
if (empty($resolveAlertIds)) {
echo json_encode(['code' => 0, 'msg' => '暂无已恢复告警,无需更新工单', 'update_count' => 0]);
exit;
}
// 2. 拼接IN条件,查询未完成、关联该告警的工单
$inStr = implode("','", array_map(function($v) use ($connWork) {
return mysqli_real_escape_string($connWork, $v);
}, $resolveAlertIds));
// status=1待处理 / status=2处理中,统一改为3已完成
$updateSql = "UPDATE work_order SET status=3,update_time=NOW() WHERE relate_alert_id IN ('$inStr') AND status IN (1,2)";
mysqli_query($connWork, $updateSql);
$updateCnt = mysqli_affected_rows($connWork);
echo json_encode([
'code' => 0,
'msg' => '工单状态自动同步完成,已恢复告警绑定工单全部置为已完成',
'update_count' => $updateCnt
], JSON_UNESCAPED_UNICODE);
exit;
}
// 1、获取实时活跃告警(alert_mail_stat.alert_firing 数据源)
if ($action === "get_firing_alert") {
$alertSql = "SELECT alert_id,alert_name,instance FROM alert_firing WHERE status='firing' ORDER BY receive_time DESC LIMIT 100";
$res = mysqli_query($connConf, $alertSql);
$list = [];
if($res instanceof mysqli_result){
while ($row = mysqli_fetch_assoc($res)) {
$list[] = $row;
}
}
echo json_encode(['code' => 0, 'list' => $list]);
exit;
}
// 2、新建工单(关联告警、多渠道通知 + 自动判断告警是否已恢复)
if ($action === "create") {
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
// 默认待处理1;若关联告警已恢复,直接置完成3
$status = 1;
if (!$title || !$content) {
echo json_encode(['code' => 400, 'msg' => '标题和内容不能为空']);
exit;
}
// 关联了告警,判断该告警是否存在恢复记录
if (!empty($relateAlertId)) {
$alertEsc = mysqli_real_escape_string($connConf, $relateAlertId);
$checkResolveSql = "SELECT 1 FROM alert_log WHERE CONCAT(alert_name,'_',instance) = ? AND alert_type=2 LIMIT 1";
$stmtCheck = mysqli_prepare($connConf, $checkResolveSql);
mysqli_stmt_bind_param($stmtCheck, 's', $alertEsc);
mysqli_stmt_execute($stmtCheck);
$checkRes = mysqli_stmt_get_result($stmtCheck);
if (mysqli_num_rows($checkRes) > 0) {
$status = 3;
}
}
// 新增工单默认未读 is_read=0
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,relate_alert_id,notify_user,notify_channel,status,is_read,create_time,update_time) VALUES (?,?,?,?,?,?,?,?,0,NOW(),NOW())";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssss', $title, $content, $loginUid, $assign, $relateAlertId, $notifyUser, $notifyChannel, $status);
$ok = mysqli_stmt_execute($stmt);
if (!$ok) {
echo json_encode(['code' => 500, 'msg' => '创建失败:' . mysqli_error($connWork)]);
exit;
}
$orderId = mysqli_insert_id($connWork);
sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单提交成功,通知已下发', 'init_status' => $status]);
exit;
}
// 3、编辑工单
if ($action === "edit") {
$id = intval($post['id'] ?? 0);
$title = trim($post['title'] ?? '');
$content = trim($post['content'] ?? '');
$assign = trim($post['assign_uid'] ?? '');
$relateAlertId = trim($post['relate_alert_id'] ?? '');
$notifyUser = trim($post['notify_user'] ?? '');
$notifyChannel = trim($post['notify_channel'] ?? '');
if ($id <= 0 || empty($title) || empty($content)) {
echo json_encode(['code' => 400, 'msg' => '参数不能为空']);
exit;
}
// 非管理员权限校验
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限编辑该工单']);
exit;
}
}
$sql = "UPDATE work_order SET title=?,content=?,assign_uid=?,relate_alert_id=?,notify_user=?,notify_channel=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ssssssi', $title, $content, $assign, $relateAlertId, $notifyUser, $notifyChannel, $id);
mysqli_stmt_execute($stmt);
sendWorkOrderNotify($connConf, $connWork, $id, $notifyUser, $notifyChannel);
echo json_encode(['code' => 0, 'msg' => '工单保存成功,通知已更新下发']);
exit;
}
// 4、修改工单状态
if ($action === "update_status") {
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if ($id <= 0) {
echo json_encode(['code' => 400, 'msg' => '工单ID错误']);
exit;
}
if ($isAdmin !== 1) {
$chkSql = "SELECT id FROM work_order WHERE id=? AND (create_uid=? OR assign_uid=?)";
$stmtChk = mysqli_prepare($connWork, $chkSql);
mysqli_stmt_bind_param($stmtChk, 'iss', $id, $loginUid, $loginUid);
mysqli_stmt_execute($stmtChk);
$chkRes = mysqli_stmt_get_result($stmtChk);
if (mysqli_num_rows($chkRes) === 0) {
echo json_encode(['code' => 403, 'msg' => '无权限操作该工单']);
exit;
}
}
// 修改状态自动标记已读
$sql = "UPDATE work_order SET status=?,is_read=1,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'ii', $status, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '状态更新成功']);
exit;
}
// 5、管理员分发工单(前端统一action=assign,废弃assign_order
if ($action === "assign") {
if ($isAdmin !== 1) {
echo json_encode(['code' => 403, 'msg' => '仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = trim($post['assign_uid'] ?? '');
// 修复BUG:仅按工单唯一ID更新,不再使用标题模糊匹配,选中哪一行只修改该行工单
$sql = "UPDATE work_order SET assign_uid=?,is_read=0,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, 'si', $assignUid, $id);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '工单分发成功']);
exit;
}
// 一键标记当前用户全部待处理工单为已读
if ($action === 'mark_all_read') {
$uid = $post['uid'] ?? '';
$sql = "UPDATE work_order SET is_read=1 WHERE assign_uid=? AND status=1";
$stmt = mysqli_prepare($connWork, $sql);
mysqli_stmt_bind_param($stmt, "s", $uid);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '已全部标记为已读']);
exit;
}
// 语言切换
if ($action === 'save_lang') {
$lang = $post['lang'] ?? 'zh';
$sql = "UPDATE sys_config SET v=? WHERE k='lang'";
$stmt = mysqli_prepare($connConf, $sql);
mysqli_stmt_bind_param($stmt, "s", $lang);
mysqli_stmt_execute($stmt);
echo json_encode(['code' => 0, 'msg' => '成功']);
exit;
}
// 新增:同步Prometheus实时告警,自动清理恢复告警(完全独立,无需index.php)
if ($action === "sync_prom_alerts") {
$promUrl = "http://10.150.117.190:9090/api/v1/alerts";
$resp = @file_get_contents($promUrl);
if($resp === false){
echo json_encode(["code"=>500,"msg"=>"无法连接Prometheus接口"],JSON_UNESCAPED_UNICODE);
exit;
}
$promData = json_decode($resp,true);
if(!isset($promData['data']['alerts'])){
echo json_encode(["code"=>500,"msg"=>"Prometheus返回数据异常"],JSON_UNESCAPED_UNICODE);
exit;
}
// 1. 先将表内所有告警标记为已恢复
mysqli_query($connConf,"UPDATE alert_firing SET status='resolved'");
// 2. 遍历当前Prometheus活跃告警,写入/更新
$alerts = $promData['data']['alerts'] ?? [];
foreach($alerts as $item){
$alertName = $item['labels']['alertname'] ?? "unknown";
$instance = $item['labels']['instance'] ?? "unknown";
$alertId = $alertName."_".$instance;
$sql = "INSERT INTO alert_firing(alert_id,alert_name,instance,status,receive_time) VALUES (?,?,?,'firing',NOW()) ON DUPLICATE KEY UPDATE status='firing',receive_time=NOW()";
$stmt = mysqli_prepare($connConf,$sql);
mysqli_stmt_bind_param($stmt,"sss",$alertId,$alertName,$instance);
mysqli_stmt_execute($stmt);
}
$syncNum = count($alerts);
// 同步完成后自动执行工单状态更新
$syncWorkUrl = $_SERVER['REQUEST_SCHEME'] . "://" . $_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . "?action=auto_sync_workorder_status";
@file_get_contents($syncWorkUrl);
echo json_encode([
"code"=>0,
"msg"=>"Prometheus告警同步完成,已自动同步工单状态",
"active_alert_total"=>$syncNum
],JSON_UNESCAPED_UNICODE);
exit;
}
// 定时同步:告警恢复自动完工单
if($action === "sync_alert_auto_close"){
// 1、拉取仪表盘实时未恢复告警
$alertApi = "./index.php?act=active_firing";
$raw = @file_get_contents($alertApi);
$activeList = json_decode($raw, true) ?: [];
$activeKeys = [];
foreach($activeList as $item){
$activeKeys[] = $item['alert_name'] . "|" . $item['instance'];
}
// 2、查询所有未完成、绑定告警的工单
$sql = "SELECT id, relate_alert_id FROM work_order WHERE status IN(1,2) AND relate_alert_id <> ''";
$res = mysqli_query($connWork, $sql);
while($row = mysqli_fetch_assoc($res)){
// 工单关联告警不在当前活跃列表=已恢复,自动设为完成
if(!in_array($row['relate_alert_id'], $activeKeys)){
$updateSql = "UPDATE work_order SET status=3, update_time=NOW() WHERE id = ".(int)$row['id'];
mysqli_query($connWork, $updateSql);
}
}
echo json_encode(['code'=>0, 'msg'=>'同步完成']);
exit;
}
// ==================== 全局通知统一函数 ====================
function sendWorkOrderNotify($connConf, $connWork, $orderId, $notifyUserStr, $channelStr)
{
if (empty($notifyUserStr) || empty($channelStr)) return;
$userArr = explode(",", $notifyUserStr);
$channelArr = explode(",", $channelStr);
// 读取完整通知配置(邮箱+钉钉+企微)
$cfgRes = mysqli_query($connConf, "SELECT * FROM sys_smtp_config WHERE id=1");
$notifyCfg = $cfgRes ? mysqli_fetch_assoc($cfgRes) : [];
if(empty($notifyCfg)) return;
// 获取工单完整信息
$orderRes = mysqli_query($connWork, "SELECT * FROM work_order WHERE id=$orderId");
$order = $orderRes ? mysqli_fetch_assoc($orderRes) : [];
$title = $order['title'];
$content = $order['content'];
$relateId = $order['relate_alert_id'] ?: "无";
$msgText = "【运维工单通知】\n工单ID$orderId\n关联告警:$relateId\n标题:$title\n详情:$content";
foreach ($channelArr as $ch) {
$ch = trim($ch);
switch ($ch) {
case "mail":
sendMailNotify($notifyCfg, $connWork, $userArr, $title, $content, $relateId, $orderId);
break;
case "dingtalk":
sendDingNotify($notifyCfg, $msgText);
break;
case "wecom":
sendWecomNotify($notifyCfg, $msgText);
break;
}
}
}
// 邮件发送
function sendMailNotify($smtp, $connWork, $uidList, $title, $content, $relateId, $orderId)
{
if (empty($smtp['smtp_host']) || empty($smtp['smtp_account'])) return;
$emailList = [];
foreach ($uidList as $uid) {
$uid = trim($uid);
if (empty($uid)) continue;
$res = mysqli_query($connWork, "SELECT email FROM sys_user WHERE uid='$uid'");
$u = mysqli_fetch_assoc($res);
if (!empty($u['email'])) $emailList[] = $u['email'];
}
if (empty($emailList)) return;
$to = implode(",", array_unique($emailList));
$subject = "【工单通知】#$orderId $title";
$body = "<h3>工单 #$orderId</h3>
<p>关联告警ID$relateId</p>
<p>标题:$title</p>
<pre style='background:#f5f5f5;padding:10px'>$content</pre>";
// 此处接入PHPMailer发送逻辑
}
// 钉钉机器人推送
function sendDingNotify($cfg, $text)
{
$webhook = $cfg['ding_webhook'] ?? '';
$secret = $cfg['ding_secret'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 企业微信机器人推送
function sendWecomNotify($cfg, $text)
{
$webhook = $cfg['wecom_webhook'] ?? '';
if(empty($webhook)) return;
$data = json_encode([
"msgtype" => "text",
"text" => ["content" => $text]
]);
$opts = ["http" => ["method" => "POST", "header" => "Content-Type:application/json", "content" => $data]];
file_get_contents($webhook, false, stream_context_create($opts));
}
// 无匹配动作
echo json_encode(['code' => 400, 'msg' => '无效请求动作']);
mysqli_close($connWork);
mysqli_close($connConf);
exit;
?>
@@ -0,0 +1,104 @@
<?php
header("Content-Type: application/json; charset=utf-8");
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET,POST,OPTIONS");
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit(json_encode(['code' => 0]));
}
session_start();
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
echo json_encode(['code' => 401, 'msg' => '登录失效']);
exit;
}
$loginUid = $_SESSION['user_id'];
// 数据库配置,优先本地localhost
$dbHost = '10.150.117.190';
$dbUser = 'root';
$dbPwd = 'hp93000';
$dbName = 'monitor';
$dbPort = 3306;
// 关键:连接后立刻判断是否失败
$conn = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName, $dbPort);
if (!$conn) {
echo json_encode([
'code' => 500,
'msg' => '数据库连接失败:' . mysqli_connect_error() . ' 错误码:' . mysqli_connect_errno()
]);
exit;
}
// 只有连接成功才执行字符集
mysqli_set_charset($conn, 'utf8mb4');
// 下面原有业务代码不变 ...
// 获取角色
$isAdmin = 0;
$r = mysqli_query($conn,"SELECT r.is_admin FROM sys_user u LEFT JOIN sys_role r ON u.role_id=r.id WHERE u.uid='$loginUid'");
if($rr = mysqli_fetch_assoc($r)) $isAdmin = intval($rr['is_admin']);
$action = $_REQUEST['action'] ?? '';
$post = json_decode(file_get_contents("php://input"),true) ?: [];
// 1、新建工单
if($action === "create"){
$title = $post['title'] ?? '';
$content = $post['content'] ?? '';
$assign = $post['assign_uid'] ?? '';
if(!$title || !$content){
echo json_encode(['code'=>400,'msg'=>'标题和内容不能为空']);
exit;
}
$sql = "INSERT INTO work_order(title,content,create_uid,assign_uid,status,create_time,update_time) VALUES (?,?,?,?,?,NOW(),NOW())";
$stmt = mysqli_prepare($conn,$sql);
mysqli_stmt_bind_param($stmt,'ssssi',$title,$content,$loginUid,$assign,1);
$ok = mysqli_stmt_execute($stmt);
if($ok) echo json_encode(['code'=>0,'msg'=>'工单提交成功']);
else echo json_encode(['code'=>500,'msg'=>'提交失败:'.mysqli_error($conn)]);
exit;
}
// 2、修改工单状态
if($action === "update_status"){
$id = intval($post['id'] ?? 0);
$status = intval($post['status'] ?? 1);
if($id <=0){
echo json_encode(['code'=>400,'msg'=>'工单ID错误']);
exit;
}
// 权限校验:普通用户只能改自己创建/分配给自己的工单
if($isAdmin !== 1){
$chk = mysqli_query($conn,"SELECT id FROM work_order WHERE id=$id AND (create_uid='$loginUid' OR assign_uid='$loginUid')");
if(mysqli_num_rows($chk) === 0){
echo json_encode(['code'=>403,'msg'=>'无权限操作该工单']);
exit;
}
}
$sql = "UPDATE work_order SET status=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn,$sql);
mysqli_stmt_bind_param($stmt,'ii',$status,$id);
mysqli_stmt_execute($stmt);
echo json_encode(['code'=>0,'msg'=>'状态更新成功']);
exit;
}
// 3、管理员分发工单
if($action === "assign_order"){
if($isAdmin !== 1){
echo json_encode(['code'=>403,'msg'=>'仅管理员可分发工单']);
exit;
}
$id = intval($post['id'] ?? 0);
$assignUid = $post['assign_uid'] ?? '';
$sql = "UPDATE work_order SET assign_uid=?,update_time=NOW() WHERE id=?";
$stmt = mysqli_prepare($conn,$sql);
mysqli_stmt_bind_param($stmt,'si',$assignUid,$id);
mysqli_stmt_execute($stmt);
echo json_encode(['code'=>0,'msg'=>'工单分发成功']);
exit;
}
echo json_encode(['code'=>400,'msg'=>'无效操作']);
mysqli_close($conn);
exit;
?>
+145
View File
@@ -0,0 +1,145 @@
<?php
session_start();
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Pragma: no-cache");
header("Expires: Thu, 01 Jan 1970 00:00:00 GMT");
// 兼容低版本str_contains
if (!function_exists('str_contains')) {
function str_contains($haystack, $needle) {
return $needle === '' || strpos($haystack, $needle) !== false;
}
}
$expireTime = 1800;
// 未登录直接跳转登录页
if (empty($_SESSION['user_id']) || empty($_SESSION['login_time']) || (time() - $_SESSION['login_time']) > $expireTime) {
session_destroy();
header("Location: login.php");
exit;
}
$_SESSION['login_time'] = time();
// 登录用户基础信息
$loginUserId = $_SESSION['user_id'];
$userName = htmlspecialchars($_SESSION['username'] ?? '');
$isAdmin = intval($_SESSION['is_admin'] ?? 0);
$userRoleId = intval($_SESSION['role_id'] ?? 0);
// ====================== 双库固定连接(修复null根源) ======================
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
// 业务库 alert_mail_stat(工单、SMTP、大盘)
$dbAlertName = "alert_mail_stat";
$connAlert = mysqli_connect($dbHost, $dbUser, $dbPass, $dbAlertName);
if (!$connAlert) {
die("业务库alert_mail_stat连接失败:" . mysqli_connect_error());
}
mysqli_set_charset($connAlert, "utf8mb4");
// 权限库 monitor(用户、角色、权限、消息)
$dbMonitorName = "monitor";
$connMonitor = mysqli_connect($dbHost, $dbUser, $dbPass, $dbMonitorName);
if (!$connMonitor) {
die("权限库monitor连接失败:" . mysqli_connect_error());
}
mysqli_set_charset($connMonitor, "utf8mb4");
// ========================================================================
// 获取当前访问页面文件名(去掉.php后缀)
$currentPage = basename($_SERVER['SCRIPT_NAME'], '.php');
// 非超级管理员校验页面权限
if ($isAdmin !== 1) {
$permSql = "SELECT page_key FROM sys_role_permission WHERE role_id = $userRoleId";
$permRes = mysqli_query($connMonitor, $permSql);
$allowPages = [];
if ($permRes) {
while ($row = mysqli_fetch_assoc($permRes)) {
$allowPages[] = $row['page_key'];
}
}
// 无权限拦截
if (!in_array($currentPage, $allowPages)) {
echo "<script>alert('无访问权限!');history.back();</script>";
exit;
}
}
// 读取右上角未读消息数量(修复第79/84/86行报错)
$unreadMsgCount = 0;
$msgSql = "SELECT COUNT(id) cnt FROM sys_user_msg WHERE user_id = $loginUserId AND is_read = 0";
$msgRes = mysqli_query($connMonitor, $msgSql);
if ($msgRes) {
$msgRow = mysqli_fetch_assoc($msgRes);
$unreadMsgCount = intval($msgRow['cnt']);
}
// 读取系统基础配置
$sysConfig = [];
$configSql = "SELECT k, v FROM sys_config";
$configRes = mysqli_query($connAlert, $configSql);
if ($configRes) {
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
}
$currentLang = $sysConfig['lang'] ?? "zh";
$sidebarRawLinks = $sysConfig['quick_link'] ?? "";
// 多语言字典
$langMap = [
'zh' => [
'html_lang' => 'zh-CN',
'page_title' => '运维监控平台',
'monitor_panel' => '监控面板',
'alert_overview' => '告警总览',
'quick_link' => '快捷外链',
'system_manage' => '系统管理',
'notify_config' => '消息通知配置',
'role_manage' => '角色管理',
'user_manage' => '用户管理',
'current_user' => '当前用户:',
'sys_time' => '系统时间:',
'refresh' => '刷新页面',
'logout' => '退出登录',
'unread_msg' => '未读消息'
],
'en' => [
'html_lang' => 'en',
'page_title' => "Monitor Platform",
'monitor_panel' => "Monitor Panel",
'alert_overview' => "Alert Dashboard",
'quick_link' => "Quick Link",
'system_manage' => "System Manage",
'notify_config' => "Notify Config",
'role_manage' => "Role Manage",
'user_manage' => "User Manage",
'current_user' => "User: ",
'sys_time' => "Time: ",
'refresh' => "Refresh",
'logout' => "Logout",
'unread_msg' => "Unread Msg"
]
];
$t = $langMap[$currentLang];
// 解析侧边栏外链
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line);
if (count($item) === 2) {
$sidebarLinkList[] = [
'name' => trim($item[0]),
'url' => trim($item[1])
];
}
}
}
?>
File diff suppressed because it is too large Load Diff
+954
View File
@@ -0,0 +1,954 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,与admin后台统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 读取全局系统配置(admin后台保存后同步生效)
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($connConfig, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($connConfig);
// 赋值全局配置
$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统");
$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff");
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = htmlspecialchars($sysConfig['footer_text'] ?? "");
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
// 解析侧边外链列表
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line, 2);
if (count($item) === 2) {
$sidebarLinkList[] = [
"name" => trim($item[0]),
"url" => trim($item[1])
];
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle; ?> - Advantest 爱德万测试 - 运维告警监控平台</title>
<!-- 本地静态资源,已移除CDN外网依赖 -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<script src="/static/js/echarts.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 日期选择框样式 */
#csvDatePicker{
padding:8px 10px;
border:1px solid #cbd5e1;
border-radius:8px;
font-size:14px;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后台配置快捷外链自动渲染 -->
<?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title">快捷外链</div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>')">
<i class="fa fa-external-link"></i>
<span><?php echo htmlspecialchars($link['name']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
<div class="menu-title">系统管理</div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i>
<span>后台配置管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="csvDatePicker">
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
</div>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 修复:使用相对路径,去除硬编码IP
const api = "./api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
if (!pageId) return;
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
function getSeverityTag(severity){
const s = (severity || '').toLowerCase();
if(s==='critical') return `<span class="severity-tag severity-critical">严重</span>`;
if(s==='warning') return `<span class="severity-tag severity-warning">警告</span>`;
if(s==='info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity||'-'}</span>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
const selectDate = $("#csvDatePicker").val().trim();
let exportUrl = api + "?act=export_csv";
if(selectDate){
exportUrl += "&date=" + encodeURIComponent(selectDate);
}
window.open(exportUrl, "_blank");
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
$.getJSON("./api/df.php").done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+707
View File
@@ -0,0 +1,707 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警监控看板 | Alert Dashboard</title>
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #f0f2f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1f2937;
line-height: 1.5;
}
/* 顶部导航栏 */
.header {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #2563eb 100%);
color: #fff;
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.15);
}
.header h1 {
font-size: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.header h1::before {
content: "";
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2); }
50% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0.1); }
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
}
.refresh-btn, .export-btn {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.refresh-btn:hover, .export-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.export-btn {
background: rgba(16, 185, 129, 0.2);
border-color: rgba(16, 185, 129, 0.4);
}
.update-time {
opacity: 0.85;
}
/* 导出加载遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.4);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 24px 32px;
border-radius: 8px;
font-size: 14px;
display: flex;
gap:10px;
align-items: center;
}
/* 主内容区 */
.container {
padding: 24px 32px;
max-width: 1600px;
margin: 0 auto;
}
/* 统计卡片 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
.stat-card.fire::before { background: linear-gradient(180deg, #ef4444, #dc2626); }
.stat-card.resolve::before { background: linear-gradient(180deg, #10b981, #059669); }
.stat-card.instance::before { background: linear-gradient(180deg, #3b82f6, #2563eb); }
.stat-card.rate::before { background: linear-gradient(180deg, #f59e0b, #d97706); }
.stat-card .label {
font-size: 14px;
color: #6b7280;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.stat-card .label .icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
}
.stat-card.fire .icon { background: #fef2f2; color: #dc2626; }
.stat-card.resolve .icon { background: #ecfdf5; color: #059669; }
.stat-card.instance .icon { background: #eff6ff; color: #2563eb; }
.stat-card.rate .icon { background: #fffbeb; color: #d97706; }
.stat-card .value {
font-size: 36px;
font-weight: 700;
line-height: 1.2;
font-family: "DIN Alternate", -apple-system, sans-serif;
}
.stat-card.fire .value { color: #dc2626; }
.stat-card.resolve .value { color: #059669; }
.stat-card.instance .value { color: #2563eb; }
.stat-card.rate .value { color: #d97706; }
.stat-card .sub {
font-size: 12px;
color: #9ca3af;
margin-top: 8px;
}
/* 图表区 */
.chart-section {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 24px;
}
.chart-card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.chart-card .chart-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
display: flex;
align-items: center;
gap: 8px;
}
.chart-card .chart-title::before {
content: "";
width: 3px;
height: 16px;
background: #2563eb;
border-radius: 2px;
}
#chart1 {
height: 380px;
width: 100%;
}
/* 表格区 */
.table-section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.table-title {
font-size: 16px;
font-weight: 600;
color: #111827;
display: flex;
align-items: center;
gap: 8px;
}
.table-title::before {
content: "";
width: 3px;
height: 16px;
background: #2563eb;
border-radius: 2px;
}
.table-count {
font-size: 13px;
color: #6b7280;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
background: #f9fafb;
color: #374151;
font-weight: 600;
padding: 12px 16px;
text-align: left;
border-bottom: 2px solid #e5e7eb;
white-space: nowrap;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
color: #4b5563;
}
tbody tr {
transition: background 0.15s;
}
tbody tr:hover {
background: #f9fafb;
}
tbody tr:nth-child(even) {
background: #fafbfc;
}
tbody tr:nth-child(even):hover {
background: #f3f4f6;
}
/* 状态标签 */
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.status-tag.fire {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.status-tag.resolve {
background: #ecfdf5;
color: #059669;
border: 1px solid #a7f3d0;
}
/* 级别标签 */
.severity-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.severity-critical { background: #fef2f2; color: #dc2626; }
.severity-warning { background: #fffbeb; color: #d97706; }
.severity-info { background: #eff6ff; color: #2563eb; }
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
font-size: 14px;
}
.empty-state .empty-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
/* 加载动画 */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 响应式 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 12px 16px;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.container {
padding: 16px;
}
.stat-cards {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- 导出加载遮罩 -->
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<!-- 顶部导航 -->
<div class="header">
<h1>告警监控看板</h1>
<div class="header-right">
<span class="update-time">最后更新:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()">🔄 刷新数据</button>
<button class="export-btn" onclick="exportCsv()">📥 导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="label">
<span class="icon">!</span>
<span>今日触发告警</span>
</div>
<div class="value" id="fire">0</div>
<div class="sub">Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="label">
<span class="icon"></span>
<span>今日恢复告警</span>
</div>
<div class="value" id="resolve">0</div>
<div class="sub">Resolved Alerts</div>
</div>
<div class="stat-card instance">
<div class="label">
<span class="icon">#</span>
<span>涉及故障实例</span>
</div>
<div class="value" id="instance">0</div>
<div class="sub">Affected Instances</div>
</div>
<div class="stat-card rate">
<div class="label">
<span class="icon">%</span>
<span>恢复率</span>
</div>
<div class="value" id="rate">0%</div>
<div class="sub">Recovery Rate</div>
</div>
</div>
<!-- 图表区 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title">今日告警次数 TOP 10</div>
<div id="chart1"></div>
</div>
</div>
<!-- 表格区 -->
<div class="table-section">
<div class="table-header">
<div class="table-title">告警明细列表</div>
<div class="table-count"><span id="totalCount">0</span> 条记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:80px">状态</th>
<th>告警名称</th>
<th>实例地址</th>
<th style="width:100px">级别</th>
<th style="width:160px">故障开始时间</th>
<th style="width:160px">接收时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="6">
<div class="empty-state">
<div class="empty-icon">📊</div>
<div>数据加载中...</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
// 格式化时间
function formatTime(dateStr) {
if (!dateStr || dateStr === '0000-00-00 00:00:00') return '-';
return dateStr;
}
// 级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 1、顶部统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// 2、TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", function() {
chartInstance.resize();
});
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" }
},
grid: {
left: "3%",
right: "4%",
bottom: "15%",
top: "10%",
containLabel: true
},
xAxis: {
type: "category",
data: names,
axisLabel: {
rotate: 25,
fontSize: 12,
color: "#6b7280",
interval: 0
},
axisLine: { lineStyle: { color: "#e5e7eb" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "次数",
nameTextStyle: { color: "#9ca3af", fontSize: 12 },
axisLabel: { color: "#6b7280", fontSize: 12 },
axisLine: { show: false },
splitLine: { lineStyle: { color: "#f3f4f6", type: "dashed" } }
},
series: [{
type: "bar",
data: counts,
barWidth: "45%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#2563eb" }
]),
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#60a5fa" },
{ offset: 1, color: "#3b82f6" }
])
}
},
label: {
show: true,
position: "top",
fontSize: 12,
color: "#374151"
}
}]
});
});
}
// 3、告警明细表格(已删除恢复时间列,不再渲染ends_at)
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="6">
<div class="empty-state">
<div class="empty-icon">📭</div>
<div>暂无告警记录</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(function(row) {
const typeText = row.alert_type == 1
? '<span class="status-tag fire">触发</span>'
: '<span class="status-tag resolve">恢复</span>';
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#1f2937">${row.alert_name || '-'}</td>
<td style="font-family:monospace;font-size:12px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td>${formatTime(row.starts_at)}</td>
<td style="color:#6b7280">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(function() {
$("#table-body").html(`
<tr>
<td colspan="6">
<div class="empty-state" style="color:#ef4444">
<div class="empty-icon">⚠️</div>
<div>数据加载失败,请检查接口</div>
</div>
</td>
</tr>
`);
});
}
// 更新时间
function updateTime() {
const now = new Date();
const str = now.getFullYear() + "-"
+ String(now.getMonth() + 1).padStart(2, "0") + "-"
+ String(now.getDate()).padStart(2, "0") + " "
+ String(now.getHours()).padStart(2, "0") + ":"
+ String(now.getMinutes()).padStart(2, "0") + ":"
+ String(now.getSeconds()).padStart(2, "0");
$("#updateTime").text(str);
}
// 加载全部数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// ====================== 导出CSV函数 ======================
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>{
mask.hide();
},3000);
}
// 初始化
$(function() {
loadAllData();
// 每60秒自动刷新
setInterval(loadAllData, 60000);
});
</script>
</body>
</html>
+711
View File
@@ -0,0 +1,711 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警监控看板 | Alert Dashboard</title>
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #f0f2f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1f2937;
line-height: 1.5;
}
/* 顶部导航栏 */
.header {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #2563eb 100%);
color: #fff;
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.15);
}
.header h1 {
font-size: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.header h1::before {
content: "";
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2); }
50% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0.1); }
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
}
.refresh-btn, .export-btn {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.refresh-btn:hover, .export-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.export-btn {
background: rgba(16, 185, 129, 0.2);
border-color: rgba(16, 185, 129, 0.4);
}
.update-time {
opacity: 0.85;
}
/* 导出加载遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.4);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 24px 32px;
border-radius: 8px;
font-size: 14px;
display: flex;
gap:10px;
align-items: center;
}
/* 主内容区 */
.container {
padding: 24px 32px;
max-width: 1600px;
margin: 0 auto;
}
/* 统计卡片 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
.stat-card.fire::before { background: linear-gradient(180deg, #ef4444, #dc2626); }
.stat-card.resolve::before { background: linear-gradient(180deg, #10b981, #059669); }
.stat-card.instance::before { background: linear-gradient(180deg, #3b82f6, #2563eb); }
.stat-card.rate::before { background: linear-gradient(180deg, #f59e0b, #d97706); }
.stat-card .label {
font-size: 14px;
color: #6b7280;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.stat-card .label .icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
}
.stat-card.fire .icon { background: #fef2f2; color: #dc2626; }
.stat-card.resolve .icon { background: #ecfdf5; color: #059669; }
.stat-card.instance .icon { background: #eff6ff; color: #2563eb; }
.stat-card.rate .icon { background: #fffbeb; color: #d97706; }
.stat-card .value {
font-size: 36px;
font-weight: 700;
line-height: 1.2;
font-family: "DIN Alternate", -apple-system, sans-serif;
}
.stat-card.fire .value { color: #dc2626; }
.stat-card.resolve .value { color: #059669; }
.stat-card.instance .value { color: #2563eb; }
.stat-card.rate .value { color: #d97706; }
.stat-card .sub {
font-size: 12px;
color: #9ca3af;
margin-top: 8px;
}
/* 图表区 */
.chart-section {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 24px;
}
.chart-card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.chart-card .chart-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
display: flex;
align-items: center;
gap: 8px;
}
.chart-card .chart-title::before {
content: "";
width: 3px;
height: 16px;
background: #2563eb;
border-radius: 2px;
}
#chart1 {
height: 380px;
width: 100%;
}
/* 表格区 */
.table-section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.table-title {
font-size: 16px;
font-weight: 600;
color: #111827;
display: flex;
align-items: center;
gap: 8px;
}
.table-title::before {
content: "";
width: 3px;
height: 16px;
background: #2563eb;
border-radius: 2px;
}
.table-count {
font-size: 13px;
color: #6b7280;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
background: #f9fafb;
color: #374151;
font-weight: 600;
padding: 12px 16px;
text-align: left;
border-bottom: 2px solid #e5e7eb;
white-space: nowrap;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
color: #4b5563;
}
tbody tr {
transition: background 0.15s;
}
tbody tr:hover {
background: #f9fafb;
}
tbody tr:nth-child(even) {
background: #fafbfc;
}
tbody tr:nth-child(even):hover {
background: #f3f4f6;
}
/* 状态标签 */
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.status-tag.fire {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.status-tag.resolve {
background: #ecfdf5;
color: #059669;
border: 1px solid #a7f3d0;
}
/* 级别标签 */
.severity-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.severity-critical { background: #fef2f2; color: #dc2626; }
.severity-warning { background: #fffbeb; color: #d97706; }
.severity-info { background: #eff6ff; color: #2563eb; }
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
font-size: 14px;
}
.empty-state .empty-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
/* 加载动画 */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 响应式 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 12px 16px;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.container {
padding: 16px;
}
.stat-cards {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- 导出加载遮罩 -->
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<!-- 顶部导航 -->
<div class="header">
<h1>告警监控看板</h1>
<div class="header-right">
<span class="update-time">最后更新:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()">🔄 刷新数据</button>
<button class="export-btn" onclick="exportCsv()">📥 导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="label">
<span class="icon">!</span>
<span>今日触发告警</span>
</div>
<div class="value" id="fire">0</div>
<div class="sub">Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="label">
<span class="icon">✓</span>
<span>今日恢复告警</span>
</div>
<div class="value" id="resolve">0</div>
<div class="sub">Resolved Alerts</div>
</div>
<div class="stat-card instance">
<div class="label">
<span class="icon">#</span>
<span>涉及故障实例</span>
</div>
<div class="value" id="instance">0</div>
<div class="sub">Affected Instances</div>
</div>
<div class="stat-card rate">
<div class="label">
<span class="icon">%</span>
<span>恢复率</span>
</div>
<div class="value" id="rate">0%</div>
<div class="sub">Recovery Rate</div>
</div>
</div>
<!-- 图表区 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title">今日告警次数 TOP 10</div>
<div id="chart1"></div>
</div>
</div>
<!-- 表格区 -->
<div class="table-section">
<div class="table-header">
<div class="table-title">告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:80px">状态</th>
<th>告警名称</th>
<th>实例地址</th>
<th style="width:100px">级别</th>
<th style="width:160px">故障开始时间</th>
<th style="width:160px">恢复时间</th>
<th style="width:160px">接收时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-icon">📊</div>
<div>数据加载中...</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api";
let chartInstance = null;
// 格式化时间
function formatTime(dateStr) {
if (!dateStr || dateStr === '0000-00-00 00:00:00') return '-';
return dateStr;
}
// 级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 1、顶部统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// 2、TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", function() {
chartInstance.resize();
});
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" }
},
grid: {
left: "3%",
right: "4%",
bottom: "15%",
top: "10%",
containLabel: true
},
xAxis: {
type: "category",
data: names,
axisLabel: {
rotate: 25,
fontSize: 12,
color: "#6b7280",
interval: 0
},
axisLine: { lineStyle: { color: "#e5e7eb" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "次数",
nameTextStyle: { color: "#9ca3af", fontSize: 12 },
axisLabel: { color: "#6b7280", fontSize: 12 },
axisLine: { show: false },
splitLine: { lineStyle: { color: "#f3f4f6", type: "dashed" } }
},
series: [{
type: "bar",
data: counts,
barWidth: "45%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#2563eb" }
]),
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#60a5fa" },
{ offset: 1, color: "#3b82f6" }
])
}
},
label: {
show: true,
position: "top",
fontSize: 12,
color: "#374151"
}
}]
});
});
}
// 3、告警明细表格
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-icon">📭</div>
<div>暂无告警记录</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(function(row) {
const typeText = row.alert_type == 1
? '<span class="status-tag fire">触发</span>'
: '<span class="status-tag resolve">恢复</span>';
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#1f2937">${row.alert_name || '-'}</td>
<td style="font-family:monospace;font-size:12px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td>${formatTime(row.starts_at)}</td>
<td>${formatTime(row.ends_at)}</td>
<td style="color:#6b7280">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(function() {
$("#table-body").html(`
<tr>
<td colspan="7">
<div class="empty-state" style="color:#ef4444">
<div class="empty-icon">⚠️</div>
<div>数据加载失败,请检查接口</div>
</div>
</td>
</tr>
`);
});
}
// 更新时间
function updateTime() {
const now = new Date();
const str = now.getFullYear() + "-"
+ String(now.getMonth() + 1).padStart(2, "0") + "-"
+ String(now.getDate()).padStart(2, "0") + " "
+ String(now.getHours()).padStart(2, "0") + ":"
+ String(now.getMinutes()).padStart(2, "0") + ":"
+ String(now.getSeconds()).padStart(2, "0");
$("#updateTime").text(str);
}
// 加载全部数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// ====================== 导出CSV函数 ======================
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
// 请求后端导出接口,后端调用 /root/alert_report.py 生成文件并返回下载流
window.location.href = api + "?act=export_csv";
// 3秒后自动关闭加载遮罩
setTimeout(()=>{
mask.hide();
},3000);
}
// 初始化
$(function() {
loadAllData();
// 每60秒自动刷新
setInterval(loadAllData, 60000);
});
</script>
</body>
</html>
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>告警邮件统计看板</title>
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0;}
body{background:#f3f4f6;padding:20px;font-family:Microsoft Yahei}
.head{display:flex;gap:20px;margin-bottom:20px}
.card{background:#fff;padding:20px;border-radius:8px;width:24%;box-shadow:0 1px 3px #ddd}
.chart-box{display:flex;gap:20px;margin-bottom:20px}
.chart{width:50%;height:360px;background:#fff;padding:15px;border-radius:8px}
table{width:100%;background:#fff;border-radius:8px;padding:15px;border-collapse:collapse}
th,td{border:1px solid #eee;padding:10px;text-align:center}
th{background:#2f3542;color:#fff}
.type1{color:#e74c3c;font-weight:bold}
.type2{color:#27ae60;font-weight:bold}
</style>
</head>
<body>
<h1>当日告警邮件统计看板</h1>
<div class="head">
<div class="card">
<h3>今日触发告警总数</h3>
<p id="fire" style="font-size:32px;color:red">0</p>
</div>
<div class="card">
<h3>今日恢复告警总数</h3>
<p id="resolve" style="font-size:32px;color:green">0</p>
</div>
<div class="card">
<h3>涉及故障实例数</h3>
<p id="instance" style="font-size:32px;color:#2980b9">0</p>
</div>
</div>
<div class="chart-box">
<div class="chart" id="chart1"></div>
</div>
<table>
<thead>
<tr>
<th>类型</th>
<th>告警名称</th>
<th>实例</th>
<th>级别</th>
<th>故障开始</th>
<th>恢复时间</th>
<th>邮件接收时间</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
<script>
// 改成你自己的服务器IP端口
const api = "http://10.150.117.190:8080/api";
// 1、顶部统计卡片
$.getJSON(api+"?act=day_total",res=>{
$("#fire").text(res.fire_count);
$("#resolve").text(res.resolve_count);
$("#instance").text(res.instance_num);
})
// 2、TOP告警柱状图
$.getJSON(api+"?act=top_alert",list=>{
let chart = echarts.init(document.getElementById("chart1"));
let name = [],cnt=[];
list.forEach(item=>{name.push(item.alert_name);cnt.push(item.cnt)})
chart.setOption({
title:{text:"今日告警次数TOP10"},
xAxis:{type:"category",data:name,axisLabel:{rotate:30}},
yAxis:{type:"value"},
series:[{type:"bar",data:cnt,color:"#3498db"}]
})
})
// 3、告警明细表格
$.getJSON(api+"?act=log_list",list=>{
let html = "";
list.forEach(row=>{
let typeText = row.alert_type == 1 ? "<span class='type1'>触发</span>" : "<span class='type2'>恢复</span>";
html += `<tr>
<td>${typeText}</td>
<td>${row.alert_name}</td>
<td>${row.instance}</td>
<td>${row.severity}</td>
<td>${row.starts_at || '-'}</td>
<td>${row.ends_at || '-'}</td>
<td>${row.receive_time}</td>
</tr>`
})
$("#table-body").html(html);
})
</script>
</body>
</html>
+663
View File
@@ -0,0 +1,663 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>告警监控看板 | Alert Dashboard</title>
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background: #f0f2f5;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1f2937;
line-height: 1.5;
}
/* 顶部导航栏 */
.header {
background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 50%, #2563eb 100%);
color: #fff;
padding: 16px 32px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 8px rgba(30, 64, 175, 0.15);
}
.header h1 {
font-size: 20px;
font-weight: 600;
display: flex;
align-items: center;
gap: 10px;
}
.header h1::before {
content: "";
width: 8px;
height: 8px;
background: #10b981;
border-radius: 50%;
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2);
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.2); }
50% { box-shadow: 0 0 0 8px rgba(16, 185, 129, 0.1); }
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
font-size: 13px;
}
.refresh-btn {
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: #fff;
padding: 6px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.refresh-btn:hover {
background: rgba(255, 255, 255, 0.25);
}
.update-time {
opacity: 0.85;
}
/* 主内容区 */
.container {
padding: 24px 32px;
max-width: 1600px;
margin: 0 auto;
}
/* 统计卡片 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 20px;
margin-bottom: 24px;
}
.stat-card {
background: #fff;
border-radius: 8px;
padding: 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
position: relative;
overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s;
}
.stat-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.stat-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
}
.stat-card.fire::before { background: linear-gradient(180deg, #ef4444, #dc2626); }
.stat-card.resolve::before { background: linear-gradient(180deg, #10b981, #059669); }
.stat-card.instance::before { background: linear-gradient(180deg, #3b82f6, #2563eb); }
.stat-card.rate::before { background: linear-gradient(180deg, #f59e0b, #d97706); }
.stat-card .label {
font-size: 14px;
color: #6b7280;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.stat-card .label .icon {
width: 28px;
height: 28px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: bold;
}
.stat-card.fire .icon { background: #fef2f2; color: #dc2626; }
.stat-card.resolve .icon { background: #ecfdf5; color: #059669; }
.stat-card.instance .icon { background: #eff6ff; color: #2563eb; }
.stat-card.rate .icon { background: #fffbeb; color: #d97706; }
.stat-card .value {
font-size: 36px;
font-weight: 700;
line-height: 1.2;
font-family: "DIN Alternate", -apple-system, sans-serif;
}
.stat-card.fire .value { color: #dc2626; }
.stat-card.resolve .value { color: #059669; }
.stat-card.instance .value { color: #2563eb; }
.stat-card.rate .value { color: #d97706; }
.stat-card .sub {
font-size: 12px;
color: #9ca3af;
margin-top: 8px;
}
/* 图表区 */
.chart-section {
display: grid;
grid-template-columns: 1fr;
gap: 20px;
margin-bottom: 24px;
}
.chart-card {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.chart-card .chart-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
display: flex;
align-items: center;
gap: 8px;
}
.chart-card .chart-title::before {
content: "";
width: 3px;
height: 16px;
background: #2563eb;
border-radius: 2px;
}
#chart1 {
height: 380px;
width: 100%;
}
/* 表格区 */
.table-section {
background: #fff;
border-radius: 8px;
padding: 20px 24px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
}
.table-title {
font-size: 16px;
font-weight: 600;
color: #111827;
display: flex;
align-items: center;
gap: 8px;
}
.table-title::before {
content: "";
width: 3px;
height: 16px;
background: #2563eb;
border-radius: 2px;
}
.table-count {
font-size: 13px;
color: #6b7280;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
th {
background: #f9fafb;
color: #374151;
font-weight: 600;
padding: 12px 16px;
text-align: left;
border-bottom: 2px solid #e5e7eb;
white-space: nowrap;
}
td {
padding: 12px 16px;
border-bottom: 1px solid #f3f4f6;
color: #4b5563;
}
tbody tr {
transition: background 0.15s;
}
tbody tr:hover {
background: #f9fafb;
}
tbody tr:nth-child(even) {
background: #fafbfc;
}
tbody tr:nth-child(even):hover {
background: #f3f4f6;
}
/* 状态标签 */
.status-tag {
display: inline-block;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
line-height: 1.4;
}
.status-tag.fire {
background: #fef2f2;
color: #dc2626;
border: 1px solid #fecaca;
}
.status-tag.resolve {
background: #ecfdf5;
color: #059669;
border: 1px solid #a7f3d0;
}
/* 级别标签 */
.severity-tag {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.severity-critical { background: #fef2f2; color: #dc2626; }
.severity-warning { background: #fffbeb; color: #d97706; }
.severity-info { background: #eff6ff; color: #2563eb; }
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: #9ca3af;
font-size: 14px;
}
.empty-state .empty-icon {
font-size: 48px;
margin-bottom: 12px;
opacity: 0.5;
}
/* 加载动画 */
.loading {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 响应式 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 12px 16px;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}
.container {
padding: 16px;
}
.stat-cards {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<!-- 顶部导航 -->
<div class="header">
<h1>告警监控看板</h1>
<div class="header-right">
<span class="update-time">最后更新:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()">🔄 刷新数据</button>
</div>
</div>
<div class="container">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="label">
<span class="icon">!</span>
<span>今日触发告警</span>
</div>
<div class="value" id="fire">0</div>
<div class="sub">Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="label">
<span class="icon">✓</span>
<span>今日恢复告警</span>
</div>
<div class="value" id="resolve">0</div>
<div class="sub">Resolved Alerts</div>
</div>
<div class="stat-card instance">
<div class="label">
<span class="icon">#</span>
<span>涉及故障实例</span>
</div>
<div class="value" id="instance">0</div>
<div class="sub">Affected Instances</div>
</div>
<div class="stat-card rate">
<div class="label">
<span class="icon">%</span>
<span>恢复率</span>
</div>
<div class="value" id="rate">0%</div>
<div class="sub">Recovery Rate</div>
</div>
</div>
<!-- 图表区 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title">今日告警次数 TOP 10</div>
<div id="chart1"></div>
</div>
</div>
<!-- 表格区 -->
<div class="table-section">
<div class="table-header">
<div class="table-title">告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:80px">状态</th>
<th>告警名称</th>
<th>实例地址</th>
<th style="width:100px">级别</th>
<th style="width:160px">故障开始时间</th>
<th style="width:160px">恢复时间</th>
<th style="width:160px">接收时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-icon">📊</div>
<div>数据加载中...</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api";
let chartInstance = null;
// 格式化时间
function formatTime(dateStr) {
if (!dateStr || dateStr === '0000-00-00 00:00:00') return '-';
return dateStr;
}
// 级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 1、顶部统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// 2、TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", function() {
chartInstance.resize();
});
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" }
},
grid: {
left: "3%",
right: "4%",
bottom: "15%",
top: "10%",
containLabel: true
},
xAxis: {
type: "category",
data: names,
axisLabel: {
rotate: 25,
fontSize: 12,
color: "#6b7280",
interval: 0
},
axisLine: { lineStyle: { color: "#e5e7eb" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "次数",
nameTextStyle: { color: "#9ca3af", fontSize: 12 },
axisLabel: { color: "#6b7280", fontSize: 12 },
axisLine: { show: false },
splitLine: { lineStyle: { color: "#f3f4f6", type: "dashed" } }
},
series: [{
type: "bar",
data: counts,
barWidth: "45%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#2563eb" }
]),
borderRadius: [4, 4, 0, 0]
},
emphasis: {
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#60a5fa" },
{ offset: 1, color: "#3b82f6" }
])
}
},
label: {
show: true,
position: "top",
fontSize: 12,
color: "#374151"
}
}]
});
});
}
// 3、告警明细表格
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="7">
<div class="empty-state">
<div class="empty-icon">📭</div>
<div>暂无告警记录</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(function(row) {
const typeText = row.alert_type == 1
? '<span class="status-tag fire">触发</span>'
: '<span class="status-tag resolve">恢复</span>';
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#1f2937">${row.alert_name || '-'}</td>
<td style="font-family:monospace;font-size:12px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td>${formatTime(row.starts_at)}</td>
<td>${formatTime(row.ends_at)}</td>
<td style="color:#6b7280">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(function() {
$("#table-body").html(`
<tr>
<td colspan="7">
<div class="empty-state" style="color:#ef4444">
<div class="empty-icon">⚠️</div>
<div>数据加载失败,请检查接口</div>
</div>
</td>
</tr>
`);
});
}
// 更新时间
function updateTime() {
const now = new Date();
const str = now.getFullYear() + "-"
+ String(now.getMonth() + 1).padStart(2, "0") + "-"
+ String(now.getDate()).padStart(2, "0") + " "
+ String(now.getHours()).padStart(2, "0") + ":"
+ String(now.getMinutes()).padStart(2, "0") + ":"
+ String(now.getSeconds()).padStart(2, "0");
$("#updateTime").text(str);
}
// 加载全部数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// 初始化
$(function() {
loadAllData();
// 每60秒自动刷新
setInterval(loadAllData, 60000);
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+601
View File
@@ -0,0 +1,601 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 主容器 */
.container{
padding:36px 40px;
max-width:1800px;
margin:0 auto;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
function getSeverityTag(severity){
const s = (severity || '').toLowerCase();
if(s==='critical') return `<span class="severity-tag severity-critical">严重</span>`;
if(s==='warning') return `<span class="severity-tag severity-warning">警告</span>`;
if(s==='info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity||'-'}</span>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+899
View File
@@ -0,0 +1,899 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后续新增页面直接在这里复制模板
<div class="menu-title">系统管理</div>
<div class="menu-item" data-page="userPage">
<i class="fa fa-users"></i>
<span>用户权限管理</span>
</div>
<div class="menu-item" data-page="rulePage">
<i class="fa fa-cog"></i>
<span>监控规则管理</span>
</div>
-->
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
function getSeverityTag(severity){
const s = (severity || '').toLowerCase();
if(s==='critical') return `<span class="severity-tag severity-critical">严重</span>`;
if(s==='warning') return `<span class="severity-tag severity-warning">警告</span>`;
if(s==='info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity||'-'}</span>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
// 如果当前在磁盘页面同步刷新磁盘数据
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
$.getJSON("/api/df.php",{
headers:{"Accept":"application/json;charset=utf-8"}
}).done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
// 填充IP下拉框
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
// 渲染磁盘表格
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
// 磁盘导出CSV
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
// IP筛选切换
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+916
View File
@@ -0,0 +1,916 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 日期选择框样式 */
#csvDatePicker{
padding:8px 10px;
border:1px solid #cbd5e1;
border-radius:8px;
font-size:14px;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后续新增页面直接在这里复制模板
<div class="menu-title">系统管理</div>
<div class="menu-item" data-page="userPage">
<i class="fa fa-users"></i>
<span>用户权限管理</span>
</div>
<div class="menu-item" data-page="rulePage">
<i class="fa fa-cog"></i>
<span>监控规则管理</span>
</div>
-->
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<!-- 新增日历日期选择框 + 导出按钮 -->
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="csvDatePicker">
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
</div>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
function getSeverityTag(severity){
const s = (severity || '').toLowerCase();
if(s==='critical') return `<span class="severity-tag severity-critical">严重</span>`;
if(s==='warning') return `<span class="severity-tag severity-warning">警告</span>`;
if(s==='info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity||'-'}</span>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
// 如果当前在磁盘页面同步刷新磁盘数据
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
// 已更新:支持日历选择日期导出
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
const selectDate = $("#csvDatePicker").val().trim();
let exportUrl = api + "?act=export_csv";
if(selectDate){
exportUrl += "&date=" + encodeURIComponent(selectDate);
}
window.open(exportUrl, "_blank");
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
$.getJSON("/api/df.php",{
headers:{"Accept":"application/json;charset=utf-8"}
}).done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
// 填充IP下拉框
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
// 渲染磁盘表格
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
// 磁盘导出CSV
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
// IP筛选切换
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+970
View File
@@ -0,0 +1,970 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出,与admin后台统一
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录计时
$_SESSION['login_time'] = time();
$userName = htmlspecialchars($_SESSION['username']);
$userRole = $_SESSION['role'];
$roleText = $userRole === 'admin' ? '管理员' : '只读用户';
// 读取全局系统配置(admin后台保存后同步生效)
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPwd = "hp93000";
$dbName = "alert_mail_stat";
$connConfig = mysqli_connect($dbHost, $dbUser, $dbPwd, $dbName);
mysqli_set_charset($connConfig, "utf8mb4");
$configSql = "SELECT k,v FROM sys_config";
$configRes = mysqli_query($connConfig, $configSql);
$sysConfig = [];
while ($row = mysqli_fetch_assoc($configRes)) {
$sysConfig[$row['k']] = $row['v'];
}
mysqli_close($connConfig);
// 赋值全局配置
$pageTitle = htmlspecialchars($sysConfig['page_title'] ?? "告警监控系统");
$themeColor = htmlspecialchars($sysConfig['theme_color'] ?? "#409eff");
$pageSize = $sysConfig['page_size'] ?? "20";
$footerText = htmlspecialchars($sysConfig['footer_text'] ?? "");
$sidebarRawLinks = $sysConfig['sidebar_links'] ?? "";
// 解析侧边外链列表
$sidebarLinkList = [];
if (!empty($sidebarRawLinks)) {
$lines = explode("\n", $sidebarRawLinks);
foreach ($lines as $line) {
$line = trim($line);
if (empty($line)) continue;
$item = explode("|", $line, 2);
if (count($item) === 2) {
$sidebarLinkList[] = [
"name" => trim($item[0]),
"url" => trim($item[1])
];
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo $pageTitle; ?> - Advantest 爱德万测试 - 运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{
background:#f9fafc;
font-family:"Inter","PingFang SC","Microsoft YaHei";
color:#1d2939;
line-height:1.6;
display:flex;
flex-direction:column;
height:100vh;
overflow:hidden;
}
/* 侧边导航菜单 */
.wrap-main{
display:flex;
flex:1;
overflow:hidden;
}
.sidebar{
width:240px;
background:#152c5b;
color:#e2e8f0;
flex-shrink:0;
overflow-y:auto;
}
.sidebar-brand{
padding:26px 24px;
border-bottom:1px solid rgba(255,255,255,0.1);
}
.sidebar-brand-en{
font-size:20px;
font-weight:700;
letter-spacing:1px;
color:#fff;
}
.sidebar-brand-cn{
font-size:13px;
color:#a5b4fc;
margin-top:4px;
}
.sidebar-menu{
padding:16px 0;
}
.menu-title{
padding:10px 24px;
font-size:12px;
color:#94a3b8;
letter-spacing:1px;
}
.menu-item{
display:flex;
align-items:center;
gap:12px;
padding:14px 24px;
cursor:pointer;
font-size:14px;
transition:0.2s;
color:#cbd5e1;
}
.menu-item i{
font-size:16px;
width:20px;
text-align:center;
}
.menu-item.active{
background:rgba(255,255,255,0.12);
color:#fff;
border-left:4px solid #3b82f6;
}
.menu-item:hover:not(.active){
background:rgba(255,255,255,0.06);
color:#fff;
}
/* 主内容区域 */
.main-content{
flex:1;
overflow-y:auto;
display:flex;
flex-direction:column;
}
/* 顶部导航栏 高端纯白渐变 */
.header{
background:#ffffff;
border-bottom:1px solid #eef2fb;
color:#152c5b;
padding:20px 40px;
display:flex;
justify-content:space-between;
align-items:center;
box-shadow:0 2px 12px rgba(21,44,91,0.05);
flex-shrink:0;
}
.header-left{
display:flex;
align-items:center;
gap:14px;
}
.header-brand-en{
font-size:24px;
font-weight:700;
letter-spacing:2px;
color:#152c5b;
}
.header-brand-cn{
font-size:15px;
color:#64748b;
}
.header-right{
display:flex;
align-items:center;
gap:18px;
font-size:14px;
flex-wrap:wrap;
}
.user-info,.time-info{
color:#475569;
display:flex;
align-items:center;
gap:6px;
}
.btn-base{
border:none;
padding:9px 20px;
border-radius:8px;
cursor:pointer;
font-size:14px;
display:flex;
align-items:center;
gap:6px;
transition:all 0.2s ease;
}
.btn-outline{
background:#ffffff;
color:#152c5b;
border:1px solid #cbd5e1;
}
.btn-outline:hover{
background:#f7f8fc;
border-color:#152c5b;
}
.btn-primary{
background:#152c5b;
color:#fff;
}
.btn-primary:hover{
background:#0f1e42;
}
.btn-success{
background:#10b981;
color:#fff;
}
.btn-success:hover{
background:#059669;
}
/* 日期选择框样式 */
#csvDatePicker{
padding:8px 10px;
border:1px solid #cbd5e1;
border-radius:8px;
font-size:14px;
}
/* 加载遮罩 */
.mask{
position:fixed;
left:0;top:0;
width:100%;height:100%;
background:rgba(255,255,255,0.85);
display:none;
z-index:999;
align-items:center;
justify-content:center;
}
.mask-box{
background:#fff;
padding:30px 40px;
border-radius:14px;
font-size:15px;
display:flex;
gap:14px;
align-items:center;
box-shadow:0 8px 30px rgba(21,44,91,0.12);
border:1px solid #eef2fb;
}
.loading{
display:inline-block;
width:20px;height:20px;
border:2px solid #e2e8f0;
border-top-color:#152c5b;
border-radius:50%;
animation:spin 0.8s linear infinite;
}
@keyframes spin{to{transform:rotate(360deg)}}
/* 容器内边距 */
.container{
padding:36px 40px;
max-width:100%;
flex:1;
}
/* 页面面板控制(切换显示隐藏) */
.page-panel{
display:none;
}
.page-panel.active{
display:block;
}
/* 统计卡片区域 */
.stat-cards{
display:grid;
grid-template-columns:repeat(5,1fr);
gap:28px;
margin-bottom:36px;
}
.stat-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
transition:transform 0.26s,box-shadow 0.26s;
}
.stat-card:hover{
transform:translateY(-6px);
box-shadow:0 10px 32px rgba(21,44,91,0.09);
}
.stat-header{
display:flex;
justify-content:space-between;
align-items:flex-start;
margin-bottom:20px;
}
.stat-title{
font-size:14px;
color:#64748b;
}
.stat-icon{
width:46px;height:46px;
border-radius:12px;
display:flex;
align-items:center;
justify-content:center;
font-size:20px;
}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{
font-size:44px;
font-weight:700;
color:#152c5b;
letter-spacing:1px;
line-height:1.1;
}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{
font-size:12px;
color:#94a3b8;
margin-top:8px;
}
/* 图表、表格通用卡片 */
.chart-section,.active-alert-section,.table-section,.disk-section{
margin-bottom:36px;
}
.base-card{
background:#fff;
border-radius:16px;
padding:32px;
box-shadow:0 3px 18px rgba(21,44,91,0.06);
border:1px solid #eef2fb;
}
.title-wrap{
display:flex;
justify-content:space-between;
align-items:center;
margin-bottom:26px;
padding-bottom:16px;
border-bottom:1px solid #eef2fb;
}
.title-left{
display:flex;
align-items:center;
gap:12px;
font-size:19px;
font-weight:600;
color:#152c5b;
}
.title-left i{color:#2563eb;font-size:21px}
.table-count{font-size:14px;color:#64748b}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
color:#475569;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#diskExportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#diskExportBtn:hover{
background:#2c5282;
}
#chart1{height:420px;width:100%}
.active-alert-section .base-card{border:1px solid #fee2e2}
/* 表格通用样式 */
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{
background:#f9fafc;
color:#475569;
font-weight:600;
padding:16px 18px;
text-align:left;
border-bottom:2px solid #e2e8f0;
white-space:nowrap;
}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{
padding:18px;
border-bottom:1px solid #f1f5f9;
color:#334155;
}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f7f8fc}
tbody tr:nth-child(even){background:#fbfcfe}
/* 状态标签 */
.status-tag{
display:inline-flex;
align-items:center;
gap:5px;
padding:6px 14px;
border-radius:24px;
font-size:13px;
font-weight:500;
}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{
display:inline-block;
padding:5px 12px;
border-radius:8px;
font-size:13px;
font-weight:500;
}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.num-big {
color: #c53030;
font-weight: 500;
}
/* 空数据提示 */
.empty-state{
text-align:center;
padding:80px 20px;
color:#94a3b8;
font-size:15px;
}
.empty-icon{
font-size:56px;
margin-bottom:18px;
opacity:0.35;
}
/* 响应式适配 */
@media (max-width:1440px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:992px){
.wrap-main{flex-direction:column;}
.sidebar{width:100%;height:auto}
}
@media (max-width:768px){
.header{padding:16px 24px;flex-direction:column;gap:16px;align-items:flex-start}
.header-right{flex-wrap:wrap}
.container{padding:24px}
.stat-cards{grid-template-columns:1fr;gap:20px}
.stat-card{padding:24px}
.base-card{padding:24px}
.filter-box{flex-wrap:wrap;}
}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="wrap-main">
<!-- 左侧侧边栏菜单 可无限扩展页面 -->
<div class="sidebar">
<div class="sidebar-brand">
<div class="sidebar-brand-en">Advantest</div>
<div class="sidebar-brand-cn">运维监控管理后台</div>
</div>
<div class="sidebar-menu">
<div class="menu-title">监控面板</div>
<div class="menu-item active" data-page="alertPage">
<i class="fa fa-line-chart"></i>
<span>告警监控总览</span>
</div>
<div class="menu-item" data-page="diskPage">
<i class="fa fa-hdd-o"></i>
<span>服务器磁盘容量</span>
</div>
<!-- 后台配置快捷外链自动渲染 -->
<?php if (!empty($sidebarLinkList)): ?>
<div class="menu-title">快捷外链</div>
<?php foreach ($sidebarLinkList as $link): ?>
<div class="menu-item" onclick="window.open('<?php echo htmlspecialchars($link['url']); ?>')">
<i class="fa fa-external-link"></i>
<span><?php echo htmlspecialchars($link['name']); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
<!-- 后续新增页面直接在这里复制模板
<div class="menu-title">系统管理</div>
<div class="menu-item" data-page="userPage">
<i class="fa fa-users"></i>
<span>用户权限管理</span>
</div>
<div class="menu-item" data-page="rulePage">
<i class="fa fa-cog"></i>
<span>监控规则管理</span>
</div>
-->
<div class="menu-title">系统管理</div>
<div class="menu-item" onclick="location.href='admin.php'">
<i class="fa fa-cog"></i>
<span>后台配置管理</span>
</div>
</div>
</div>
<!-- 右侧主内容区 -->
<div class="main-content">
<div class="header">
<div class="header-left">
<div>
<div class="header-brand-en">Advantest</div>
<div class="header-brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
</div>
<div class="header-right">
<span class="user-info"><i class="fa fa-user-circle"></i>当前用户:<?php echo $userName; ?><?php echo $roleText; ?></span>
<span class="time-info"><i class="fa fa-clock-o"></i>系统时间:<span id="updateTime">--</span></span>
<button class="btn-base btn-outline" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新全部数据</button>
<!-- 新增日历日期选择框 + 导出按钮 -->
<div style="display:flex;align-items:center;gap:8px;">
<input type="date" id="csvDatePicker">
<button class="btn-base btn-success" onclick="exportCsv()"><i class="fa fa-download"></i>导出告警CSV</button>
</div>
<button class="btn-base btn-primary" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<!-- 页面1:原有告警监控面板 默认激活 -->
<div class="page-panel active" id="alertPage">
<!-- 统计卡片 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表 -->
<div class="chart-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-bar-chart"></i>今日告警频次 TOP 10</div>
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 未恢复活跃告警 -->
<div class="active-alert-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- 今日全量明细 -->
<div class="table-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-list-alt"></i>今日全量告警明细列表</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:100px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:120px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:200px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- 页面2:磁盘容量监控面板 切换显示 -->
<div class="page-panel" id="diskPage">
<div class="disk-section">
<div class="base-card">
<div class="title-wrap">
<div class="title-left"><i class="fa fa-hdd-o"></i>用户目录磁盘占用统计</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="diskExportBtn">导出当前表格CSV</button>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="empty-state">
<div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div>
<div>磁盘数据加载中...</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
// 修复:使用相对路径,去除硬编码IP
const api = "./api/index.php";
let chartInstance = null;
let diskSourceData = [];
let currentDiskIp = "all";
// ====================== 侧边菜单页面切换 ======================
$(".menu-item").click(function(){
const pageId = $(this).data("page");
if (!pageId) return;
// 菜单激活切换
$(".menu-item").removeClass("active");
$(this).addClass("active");
// 页面面板切换
$(".page-panel").removeClass("active");
$("#"+pageId).addClass("active");
// 切换到磁盘页自动加载磁盘数据
if(pageId === "diskPage"){
loadDiskData();
}
})
// ====================== 告警页面原有逻辑 ======================
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
function getSeverityTag(severity){
const s = (severity || '').toLowerCase();
if(s==='critical') return `<span class="severity-tag severity-critical">严重</span>`;
if(s==='warning') return `<span class="severity-tag severity-warning">警告</span>`;
if(s==='info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity||'-'}</span>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr>
<td>${t}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"18%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:30,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"48%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[6,6,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
// 如果当前在磁盘页面同步刷新磁盘数据
if($("#diskPage").hasClass("active")){
loadDiskData();
}
}
// 修复导出:相对路径、GET参数稳定兼容后端date接收
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
const selectDate = $("#csvDatePicker").val().trim();
let exportUrl = api + "?act=export_csv";
if(selectDate){
exportUrl += "&date=" + encodeURIComponent(selectDate);
}
window.open(exportUrl, "_blank");
setTimeout(()=>mask.hide(),5000);
}
// ====================== 磁盘容量模块独立逻辑 ======================
function loadDiskData(){
const tbody = $("#diskTableBody");
tbody.html(`<tr><td colspan="4" class="empty-state"><div class="empty-icon"><i class="fa fa-spinner fa-spin"></i></div><div>正在拉取磁盘指标...</div></td></tr>`);
// 修复:本地相对路径,移除headers跨域干扰
$.getJSON("./api/df.php").done(ret=>{
if(ret.code !== 0){
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">磁盘接口异常:${ret.msg}</td></tr>`);
return;
}
diskSourceData = ret.list;
if(diskSourceData.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">暂无磁盘指标数据</td></tr>`);
return;
}
// 填充IP下拉框
const ipSel = $("#ipSelect");
ipSel.find("option:not([value='all'])").remove();
const ipSet = new Set();
diskSourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
ipSel.append(`<option value="${ip}">${ip}</option>`);
})
currentDiskIp = "all";
renderDiskTable("all");
}).fail(()=>{
tbody.html(`<tr><td colspan="4" class="empty-state" style="color:#dc2626">请求df.php接口失败,请检查服务</td></tr>`);
})
}
// 渲染磁盘表格
function renderDiskTable(filterIp){
currentDiskIp = filterIp;
const tbody = $("#diskTableBody");
tbody.html("");
let showList = filterIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === filterIp);
if(showList.length === 0){
tbody.html(`<tr><td colspan="4" class="empty-state">该实例下无目录数据</td></tr>`);
return;
}
showList.forEach(row=>{
tbody.append(`
<tr>
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
</tr>
`)
})
}
// 磁盘导出CSV
$("#diskExportBtn").click(function(){
let showList = currentDiskIp === "all" ? diskSourceData : diskSourceData.filter(i=>i.instance_ip === currentDiskIp);
if(showList.length === 0){
alert("无数据可导出");
return;
}
let csv = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csv += line.join(",") + "\n";
})
const blob = new Blob([csv], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "服务器磁盘容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
// IP筛选切换
$("#ipSelect").change(function(){
renderDiskTable($(this).val());
})
// ====================== 页面初始化 ======================
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+365
View File
@@ -0,0 +1,365 @@
<?php
session_start();
// 会话超时:30分钟无操作自动登出
$expire = 1800;
if (empty($_SESSION['user_id']) || (time() - $_SESSION['login_time'] > $expire)) {
session_destroy();
header("Location: login.php");
exit;
}
// 刷新登录时间
$_SESSION['login_time'] = time();
$userName = $_SESSION['username'];
$userRole = $_SESSION['role'];
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
*{box-sizing:border-box;margin:0;padding:0}
body{background:#f7f8fc;font-family:"Inter","PingFang SC","Microsoft YaHei";color:#1d2939;line-height:1.6}
.header{background:#152c5b;color:#fff;padding:18px 36px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 12px rgba(21,44,91,0.12)}
.header h1{font-size:22px;font-weight:600;display:flex;align-items:center;gap:12px}
.header h1 i{font-size:24px;color:#36d399}
.header-right{display:flex;align-items:center;gap:16px;font-size:14px}
.update-time{color:#cbd5e1;display:flex;align-items:center;gap:6px}
.refresh-btn,.export-btn{border:none;padding:8px 18px;border-radius:6px;cursor:pointer;font-size:14px;display:flex;align-items:center;gap:6px}
.refresh-btn{background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2)}
.refresh-btn:hover{background:rgba(255,255,255,0.18)}
.export-btn{background:#10b981;color:#fff}
.export-btn:hover{background:#059669}
.mask{position:fixed;left:0;top:0;width:100%;height:100%;background:rgba(0,0,0,0.45);display:none;z-index:999;align-items:center;justify-content:center}
.mask-box{background:#fff;padding:28px 36px;border-radius:10px;font-size:15px;display:flex;gap:12px;align-items:center;box-shadow:0 8px 30px rgba(0,0,0,0.15)}
.loading{display:inline-block;width:18px;height:18px;border:2px solid #e2e8f0;border-top-color:#152c5b;border-radius:50%;animation:spin 0.8s linear infinite}
@keyframes spin{to{transform:rotate(360deg)}}
.container{padding:32px 36px;max-width:1700px;margin:0 auto}
.stat-cards{display:grid;grid-template-columns:repeat(5,1fr);gap:24px;margin-bottom:32px}
.stat-card{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #eef2fb;transition:transform 0.24s,box-shadow 0.24s}
.stat-card:hover{transform:translateY(-4px);box-shadow:0 8px 24px rgba(21,44,91,0.1)}
.stat-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:16px}
.stat-title{font-size:14px;color:#64748b}
.stat-icon{width:42px;height:42px;border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:18px}
.stat-card.fire .stat-icon{background:#fee2e2;color:#dc2626}
.stat-card.active .stat-icon{background:#fef3c7;color:#d97706}
.stat-card.resolve .stat-icon{background:#dcfce7;color:#16a34a}
.stat-card.instance .stat-icon{background:#dbeafe;color:#2563eb}
.stat-card.rate .stat-icon{background:#ffedd5;color:#ea580c}
.stat-value{font-size:40px;font-weight:700;color:#152c5b;letter-spacing:1px}
.stat-card.active .stat-value{color:#d97706}
.stat-sub{font-size:12px;color:#94a3b8;margin-top:6px}
.chart-section{margin-bottom:32px}
.chart-card{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #eef2fb}
.chart-title-wrap{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;color:#152c5b;margin-bottom:24px;padding-bottom:14px;border-bottom:1px solid #eef2fb}
.chart-title-wrap i{color:#2563eb;font-size:20px}
#chart1{height:400px;width:100%}
.active-alert-section{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #fecdd3;margin-bottom:32px}
.table-header-wrap{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:14px;border-bottom:1px solid #eef2fb}
.table-title-wrap{display:flex;align-items:center;gap:10px;font-size:18px;font-weight:600;color:#152c5b}
.table-title-wrap i{color:#2563eb;font-size:20px}
.table-count{font-size:14px;color:#64748b}
.table-section{background:#fff;border-radius:12px;padding:28px;box-shadow:0 2px 16px rgba(21,44,91,0.06);border:1px solid #eef2fb;margin-bottom:32px}
.table-wrap{overflow-x:auto}
table{width:100%;border-collapse:collapse;font-size:14px}
th{background:#f8fafc;color:#475569;font-weight:600;padding:14px 16px;text-align:left;border-bottom:2px solid #e2e8f0;white-space:nowrap}
th i{margin-right:6px;font-size:13px;color:#94a3b8}
td{padding:16px;border-bottom:1px solid #f1f5f9;color:#334155}
tbody tr{transition:background 0.2s}
tbody tr:hover{background:#f8fafc}
tbody tr:nth-child(even){background:#fbfcfe}
.status-tag{display:inline-flex;align-items:center;gap:4px;padding:5px 12px;border-radius:20px;font-size:13px;font-weight:500}
.status-tag.fire{background:#fee2e2;color:#dc2626}
.status-tag.resolve{background:#dcfce7;color:#16a34a}
.severity-tag{display:inline-block;padding:4px 10px;border-radius:6px;font-size:13px;font-weight:500}
.severity-critical{background:#fee2e2;color:#dc2626}
.severity-warning{background:#ffedd5;color:#ea580c}
.severity-info{background:#dbeafe;color:#2563eb}
.empty-state{text-align:center;padding:70px 20px;color:#94a3b8;font-size:15px}
.empty-icon{font-size:52px;margin-bottom:16px;opacity:0.4}
@media (max-width:1400px){.stat-cards{grid-template-columns:repeat(3,1fr)}}
@media (max-width:768px){.header{padding:16px 20px;flex-direction:column;gap:14px;align-items:flex-start}.container{padding:20px}.stat-cards{grid-template-columns:1fr;gap:16px}}
</style>
</head>
<body>
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<div class="header">
<h1><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-user-circle"></i>当前用户:<?php echo htmlspecialchars($userName);?><?php echo $userRole==='admin'?'管理员':'只读用户';?></span>
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
<button class="refresh-btn" id="logoutBtn"><i class="fa fa-sign-out"></i>退出登录</button>
</div>
</div>
<div class="container">
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<div class="active-alert-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警
</div>
<div class="table-count">共 <span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>今日全量告警明细列表
</div>
<div class="table-count">共 <span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/index.php";
let chartInstance = null;
function formatTime(dateStr){
if(!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
function getSeverityTag(severity){
const s = (severity || '').toLowerCase();
if(s==='critical') return `<span class="severity-tag severity-critical">严重</span>`;
if(s==='warning') return `<span class="severity-tag severity-warning">警告</span>`;
if(s==='info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity||'-'}</span>`;
}
function loadActiveAlert(){
$.getJSON(api+"?act=active_firing",function(list){
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if(list.length===0){
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-check"></i></div><div>当前无未恢复告警,系统运行正常</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const t=`<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html+=`<tr><td>${t}</td><td style="font-weight:500;color:#152c5b">${row.alert_name||'-'}</td><td style="font-family:Consolas,monospace;font-size:13px">${row.instance||'-'}</td><td>${getSeverityTag(row.severity)}</td><td style="color:#475569">${formatTime(row.receive_time)}</td></tr>`;
});
$("#active-table-body").html(html);
}).fail(()=>{
$("#activeAlert").text("-");
$("#active-table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>活跃告警数据加载失败,请重新登录</div></div></td></tr>`);
});
}
function loadStats(){
$.getJSON(api+"?act=day_total",function(res){
const fire=parseInt(res.fire_count)||0;
const resolve=parseInt(res.resolve_count)||0;
const ins=parseInt(res.instance_num)||0;
const rate=fire>0?Math.round((resolve/fire)*100):0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(ins);
$("#rate").text(rate+"%");
}).fail(()=>{$("#fire,#resolve,#instance,#rate").text("-");});
}
function loadChart(){
$.getJSON(api+"?act=top_alert",function(list){
if(!chartInstance){
chartInstance=echarts.init(document.getElementById("chart1"));
window.addEventListener("resize",()=>chartInstance.resize());
}
const names=list.map(i=>i.alert_name);
const cnt=list.map(i=>i.cnt);
chartInstance.setOption({
tooltip:{trigger:"axis",axisPointer:{type:"shadow"}},
grid:{left:"3%",right:"4%",bottom:"15%",top:"8%",containLabel:true},
xAxis:{
type:"category",
data:names,
axisLabel:{rotate:28,fontSize:12,color:"#64748b"},
axisLine:{lineStyle:{color:"#e2e8f0"}},
axisTick:{show:false}
},
yAxis:{
type:"value",
name:"告警次数",
nameTextStyle:{color:"#64748b",fontSize:13},
axisLabel:{color:"#64748b",fontSize:12},
splitLine:{lineStyle:{color:"#f1f5f9",type:"dashed"}},
axisLine:{show:false}
},
series:[{
type:"bar",
data:cnt,
barWidth:"46%",
itemStyle:{
color:new echarts.graphic.LinearGradient(0,0,0,1,[
{offset:0,color:"#3b82f6"},
{offset:1,color:"#1d4ed8"}
]),
borderRadius:[5,5,0,0]
},
label:{show:true,position:"top",fontSize:12,color:"#334155"}
}]
});
});
}
function loadTable(){
$.getJSON(api+"?act=log_list",function(list){
$("#totalCount").text(list.length);
if(list.length===0){
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state"><div class="empty-icon"><i class="fa fa-folder-open-o"></i></div><div>暂无今日告警数据</div></div></td></tr>`);
return;
}
let html="";
list.forEach(row=>{
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(()=>{
$("#table-body").html(`<tr><td colspan="5"><div class="empty-state" style="color:#dc2626"><div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div><div>接口数据加载失败,请检查后端服务</div></div></td></tr>`);
});
}
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
}
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
// 退出登录跳转登录页销毁会话
$("#logoutBtn").click(function(){
location.href="logout.php";
})
});
</script>
</body>
</html>
+638
View File
@@ -0,0 +1,638 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业运维告警监控平台</title>
<!-- 运维图标库 -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 四类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 表格面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</style>
</head>
<body>
<!-- 导出加载遮罩 -->
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<!-- 顶部导航 -->
<div class="header">
<h1><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-clock-o"></i>最后更新:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Active Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Alerts Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 告警明细表格 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
// 告警级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 顶部统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// 柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", () => chartInstance.resize());
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
grid: { left: "3%", right: "4%", bottom: "15%", top: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 告警表格(已调整字段:只展示接收时间=故障发生时间,删除原startsAt列)
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
});
}
// 顶部时间更新
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth()+1).padStart(2,'0');
const D = String(now.getDate()).padStart(2,'0');
const h = String(now.getHours()).padStart(2,'0');
const m = String(now.getMinutes()).padStart(2,'0');
const s = String(now.getSeconds()).padStart(2,'0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量加载
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// 导出CSV
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
});
</script>
</body>
</html>
+644
View File
@@ -0,0 +1,644 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业运维告警监控平台</title>
<!-- 运维图标库 -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 */
.stat-cards {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 四类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 表格面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1200px) {
.stat-cards {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</style>
</head>
<body>
<!-- 导出加载遮罩 -->
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<!-- 顶部导航 -->
<div class="header">
<h1><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<!-- 修改文字为系统实时时间 -->
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Active Firing Alerts</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Alerts Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 告警明细表格 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
// 告警级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 1、顶部统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// 2、TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", () => chartInstance.resize());
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
grid: { left: "3%", right: "4%", bottom: "15%", top: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 3、告警明细表格
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
});
}
// 实时更新时钟(每秒执行)
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量加载数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// ====================== 导出CSV函数 ======================
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// 初始化
$(function() {
loadAllData();
// 页面数据60秒刷新一次
setInterval(loadAllData, 60000);
// 实时时钟每秒刷新
setInterval(updateTime, 1000);
});
</script>
</body>
</html>
+776
View File
@@ -0,0 +1,776 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业运维告警监控平台</title>
<!-- 运维图标库 -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 改为5列布局 */
.stat-cards {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 五类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.active .stat-icon {
background-color: #fef3c7;
color: #d97706;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-card.active .stat-value {
color: #d97706;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 活跃告警面板(新增) */
.active-alert-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #fecdd3;
margin-bottom: 32px;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
/* 历史全量告警面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
margin-bottom: 32px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1400px) {
.stat-cards {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</style>
</head>
<body>
<!-- 导出加载遮罩 -->
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<!-- 顶部导航 -->
<div class="header">
<h1><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 新增【当前活跃告警】 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<!-- 新增:当前未恢复活跃告警 -->
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 新增:当前未恢复活跃告警列表 -->
<div class="active-alert-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警
</div>
<div class="table-count"><span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 原有:今日全量告警明细列表 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>今日全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
let allAlertList = [];
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
// 告警级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 筛选未恢复告警逻辑:同一条告警有触发无恢复则判定为活跃故障
function getUnresolvedAlert(list) {
// 建立指纹映射
let fireMap = new Map();
let resolveSet = new Set();
list.forEach(item => {
const key = `${item.alert_name}|${item.instance}`;
if(item.alert_type === 1){
fireMap.set(key, item);
}else{
resolveSet.add(key);
}
});
// 过滤只存在触发、无恢复记录的告警
let activeList = [];
fireMap.forEach((val, key) => {
if(!resolveSet.has(key)){
activeList.push(val);
}
});
return activeList;
}
// 渲染活跃未恢复告警表格
function renderActiveTable(activeList) {
$("#activeCount").text(activeList.length);
if (activeList.length === 0) {
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
activeList.forEach(row => {
const typeText = `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
$("#activeAlert").text(activeList.length);
}
// 1、顶部统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// 2、TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", () => chartInstance.resize());
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
grid: { left: "3%", right: "4%", bottom: "15%", top: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 3、告警明细表格 + 同步计算活跃未恢复告警
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
allAlertList = list;
$("#totalCount").text(list.length);
// 计算并渲染当前未恢复告警
const activeList = getUnresolvedAlert(list);
renderActiveTable(activeList);
// 渲染今日全量明细
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
// 活跃表格同步清空
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>数据加载失败</div>
</div>
</td>
</tr>
`);
$("#activeAlert").text("-");
});
}
// 实时更新时钟(每秒执行)
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量加载数据
function loadAllData() {
loadStats();
loadChart();
loadTable();
updateTime();
}
// ====================== 导出CSV函数 ======================
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// 初始化
$(function() {
loadAllData();
// 页面数据60秒刷新一次
setInterval(loadAllData, 60000);
// 实时时钟每秒刷新
setInterval(updateTime, 1000);
});
</script>
</body>
</html>
+749
View File
@@ -0,0 +1,749 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>企业运维告警监控平台</title>
<!-- 运维图标库 -->
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/echarts.min.js"></script>
<script src="/static/js/jquery.min.js"></script>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f7f8fc;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1d2939;
line-height: 1.6;
}
/* 顶部导航栏 企业深蓝主色调 */
.header {
background-color: #152c5b;
color: #ffffff;
padding: 18px 36px;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 2px 12px rgba(21, 44, 91, 0.12);
}
.header h1 {
font-size: 22px;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.header h1 i {
font-size: 24px;
color: #36d399;
}
.header-right {
display: flex;
align-items: center;
gap: 16px;
font-size: 14px;
}
.update-time {
color: #cbd5e1;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn, .export-btn {
border: none;
padding: 8px 18px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.24s ease;
display: flex;
align-items: center;
gap: 6px;
}
.refresh-btn {
background-color: rgba(255,255,255,0.1);
color: #fff;
border: 1px solid rgba(255,255,255,0.2);
}
.refresh-btn:hover {
background-color: rgba(255,255,255,0.18);
}
.export-btn {
background-color: #10b981;
color: #fff;
}
.export-btn:hover {
background-color: #059669;
}
/* 导出遮罩 */
.mask {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.45);
display: none;
z-index: 999;
align-items: center;
justify-content: center;
}
.mask-box {
background: #fff;
padding: 28px 36px;
border-radius: 10px;
font-size: 15px;
display: flex;
gap: 12px;
align-items: center;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
}
.loading {
display: inline-block;
width: 18px;
height: 18px;
border: 2px solid #e2e8f0;
border-top-color: #152c5b;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 主容器 */
.container {
padding: 32px 36px;
max-width: 1700px;
margin: 0 auto;
}
/* 统计卡片模块 改为5列布局 */
.stat-cards {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 24px;
margin-bottom: 32px;
}
.stat-card {
background: #ffffff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
transition: transform 0.24s ease, box-shadow 0.24s ease;
}
.stat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.1);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 16px;
}
.stat-title {
font-size: 14px;
color: #64748b;
}
.stat-icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
/* 五类卡片主题色 */
.stat-card.fire .stat-icon {
background-color: #fee2e2;
color: #dc2626;
}
.stat-card.active .stat-icon {
background-color: #fef3c7;
color: #d97706;
}
.stat-card.resolve .stat-icon {
background-color: #dcfce7;
color: #16a34a;
}
.stat-card.instance .stat-icon {
background-color: #dbeafe;
color: #2563eb;
}
.stat-card.rate .stat-icon {
background-color: #ffedd5;
color: #ea580c;
}
.stat-value {
font-size: 40px;
font-weight: 700;
color: #152c5b;
letter-spacing: 1px;
}
.stat-card.active .stat-value {
color: #d97706;
}
.stat-sub {
font-size: 12px;
color: #94a3b8;
margin-top: 6px;
}
/* 图表模块 */
.chart-section {
margin-bottom: 32px;
}
.chart-card {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
}
.chart-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
margin-bottom: 24px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.chart-title-wrap i {
color: #2563eb;
font-size: 20px;
}
#chart1 {
height: 400px;
width: 100%;
}
/* 活跃告警面板(新增) */
.active-alert-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #fecdd3;
margin-bottom: 32px;
}
.table-header-wrap {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px solid #eef2fb;
}
.table-title-wrap {
display: flex;
align-items: center;
gap: 10px;
font-size: 18px;
font-weight: 600;
color: #152c5b;
}
.table-title-wrap i {
color: #2563eb;
font-size: 20px;
}
.table-count {
font-size: 14px;
color: #64748b;
}
/* 历史全量告警面板 */
.table-section {
background: #fff;
border-radius: 12px;
padding: 28px;
box-shadow: 0 2px 16px rgba(21, 44, 91, 0.06);
border: 1px solid #eef2fb;
margin-bottom: 32px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
}
th {
background-color: #f8fafc;
color: #475569;
font-weight: 600;
padding: 14px 16px;
text-align: left;
border-bottom: 2px solid #e2e8f0;
white-space: nowrap;
}
th i {
margin-right: 6px;
font-size: 13px;
color: #94a3b8;
}
td {
padding: 16px;
border-bottom: 1px solid #f1f5f9;
color: #334155;
}
tbody tr {
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f8fafc;
}
tbody tr:nth-child(even) {
background-color: #fbfcfe;
}
/* 状态标签 */
.status-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 5px 12px;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
.status-tag.fire {
background: #fee2e2;
color: #dc2626;
}
.status-tag.resolve {
background: #dcfce7;
color: #16a34a;
}
/* 告警级别标签 */
.severity-tag {
display: inline-block;
padding: 4px 10px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
}
.severity-critical { background: #fee2e2; color: #dc2626; }
.severity-warning { background: #ffedd5; color: #ea580c; }
.severity-info { background: #dbeafe; color: #2563eb; }
/* 空数据 */
.empty-state {
text-align: center;
padding: 70px 20px;
color: #94a3b8;
font-size: 15px;
}
.empty-state .empty-icon {
font-size: 52px;
margin-bottom: 16px;
opacity: 0.4;
}
/* 响应式适配 */
@media (max-width: 1400px) {
.stat-cards {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.header {
padding: 16px 20px;
flex-direction: column;
gap: 14px;
align-items: flex-start;
}
.container {
padding: 20px;
}
.stat-cards {
grid-template-columns: 1fr;
gap: 16px;
}
}
</style>
</head>
<body>
<!-- 导出加载遮罩 -->
<div class="mask" id="exportMask">
<div class="mask-box">
<span class="loading"></span>
<span>正在生成CSV报表,请稍候...</span>
</div>
</div>
<!-- 顶部导航 -->
<div class="header">
<h1><i class="fa fa-bell"></i>企业运维告警监控平台</h1>
<div class="header-right">
<span class="update-time"><i class="fa fa-clock-o"></i>系统实时时间:<span id="updateTime">--</span></span>
<button class="refresh-btn" onclick="loadAllData()"><i class="fa fa-refresh"></i>刷新数据</button>
<button class="export-btn" onclick="exportCsv()"><i class="fa fa-download"></i>导出CSV报表</button>
</div>
</div>
<div class="container">
<!-- 统计卡片区域 新增【当前活跃告警】 -->
<div class="stat-cards">
<div class="stat-card fire">
<div class="stat-header">
<div class="stat-title">今日触发告警</div>
<div class="stat-icon"><i class="fa fa-exclamation-triangle"></i></div>
</div>
<div class="stat-value" id="fire">0</div>
<div class="stat-sub">Total Firing Today</div>
</div>
<!-- 新增:当前未恢复活跃告警 -->
<div class="stat-card active">
<div class="stat-header">
<div class="stat-title">当前未恢复告警</div>
<div class="stat-icon"><i class="fa fa-bolt"></i></div>
</div>
<div class="stat-value" id="activeAlert">0</div>
<div class="stat-sub">Unresolved Now</div>
</div>
<div class="stat-card resolve">
<div class="stat-header">
<div class="stat-title">今日恢复告警</div>
<div class="stat-icon"><i class="fa fa-check-circle"></i></div>
</div>
<div class="stat-value" id="resolve">0</div>
<div class="stat-sub">Resolved Today</div>
</div>
<div class="stat-card instance">
<div class="stat-header">
<div class="stat-title">故障实例总数</div>
<div class="stat-icon"><i class="fa fa-server"></i></div>
</div>
<div class="stat-value" id="instance">0</div>
<div class="stat-sub">Affected Host Instance</div>
</div>
<div class="stat-card rate">
<div class="stat-header">
<div class="stat-title">告警恢复率</div>
<div class="stat-icon"><i class="fa fa-line-chart"></i></div>
</div>
<div class="stat-value" id="rate">0%</div>
<div class="stat-sub">Daily Recovery Rate</div>
</div>
</div>
<!-- TOP10图表区域 -->
<div class="chart-section">
<div class="chart-card">
<div class="chart-title-wrap">
<i class="fa fa-bar-chart"></i>今日告警频次 TOP 10
</div>
<div id="chart1"></div>
</div>
</div>
<!-- 新增:当前未恢复活跃告警列表 -->
<div class="active-alert-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-exclamation-circle"></i>当前未恢复活跃告警
</div>
<div class="table-count"><span id="activeCount">0</span> 条未恢复故障</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="active-table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- 原有:今日全量告警明细列表 -->
<div class="table-section">
<div class="table-header-wrap">
<div class="table-title-wrap">
<i class="fa fa-list-alt"></i>今日全量告警明细列表
</div>
<div class="table-count"><span id="totalCount">0</span> 条告警记录</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th style="width:90px"><i class="fa fa-tag"></i>告警状态</th>
<th><i class="fa fa-bullhorn"></i>告警名称</th>
<th><i class="fa fa-desktop"></i>实例地址</th>
<th style="width:110px"><i class="fa fa-signal"></i>告警级别</th>
<th style="width:180px"><i class="fa fa-calendar"></i>故障发生时间</th>
</tr>
</thead>
<tbody id="table-body">
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<script>
const api = "http://10.150.117.190:8080/api/oldapi.php";
let chartInstance = null;
let activeAlertList = [];
// 时间格式化
function formatTime(dateStr) {
if (!dateStr || dateStr === '0001-01-01 00:00:00' || dateStr === null) return '-';
return dateStr;
}
// 告警级别标签
function getSeverityTag(severity) {
const s = (severity || '').toLowerCase();
if (s === 'critical') return `<span class="severity-tag severity-critical">严重</span>`;
if (s === 'warning') return `<span class="severity-tag severity-warning">警告</span>`;
if (s === 'info') return `<span class="severity-tag severity-info">信息</span>`;
return `<span class="severity-tag severity-info">${severity || '-'}</span>`;
}
// 加载全局未恢复活跃告警(跨天数据)
function loadActiveAlert() {
$.getJSON(api + "?act=active_firing", function(list) {
activeAlertList = list;
$("#activeAlert").text(list.length);
$("#activeCount").text(list.length);
if (list.length === 0) {
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-check"></i></div>
<div>当前无未恢复告警,系统运行正常</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#active-table-body").html(html);
}).fail(() => {
$("#activeAlert").text("-");
$("#active-table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>活跃告警数据加载失败</div>
</div>
</td>
</tr>
`);
});
}
// 顶部今日统计卡片
function loadStats() {
$.getJSON(api + "?act=day_total", function(res) {
const fire = parseInt(res.fire_count) || 0;
const resolve = parseInt(res.resolve_count) || 0;
const instance = parseInt(res.instance_num) || 0;
const rate = fire > 0 ? Math.round((resolve / fire) * 100) : 0;
$("#fire").text(fire);
$("#resolve").text(resolve);
$("#instance").text(instance);
$("#rate").text(rate + "%");
}).fail(function() {
$("#fire").text("-");
$("#resolve").text("-");
$("#instance").text("-");
$("#rate").text("-");
});
}
// TOP告警柱状图
function loadChart() {
$.getJSON(api + "?act=top_alert", function(list) {
if (!chartInstance) {
chartInstance = echarts.init(document.getElementById("chart1"));
window.addEventListener("resize", () => chartInstance.resize());
}
const names = list.map(item => item.alert_name);
const counts = list.map(item => item.cnt);
chartInstance.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
grid: { left: "3%", right: "4%", bottom: "15%", top: "8%", containLabel: true },
xAxis: {
type: "category",
data: names,
axisLabel: { rotate: 28, fontSize: 12, color: "#64748b" },
axisLine: { lineStyle: { color: "#e2e8f0" } },
axisTick: { show: false }
},
yAxis: {
type: "value",
name: "告警次数",
nameTextStyle: { color: "#64748b", fontSize: 13 },
axisLabel: { color: "#64748b", fontSize: 12 },
splitLine: { lineStyle: { color: "#f1f5f9", type: "dashed" } },
axisLine: { show: false }
},
series: [{
type: "bar",
data: counts,
barWidth: "46%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#3b82f6" },
{ offset: 1, color: "#1d4ed8" }
]),
borderRadius: [5,5,0,0]
},
label: { show: true, position: "top", fontSize:12, color:"#334155" }
}]
});
});
}
// 今日全量明细表格
function loadTable() {
$.getJSON(api + "?act=log_list", function(list) {
$("#totalCount").text(list.length);
if (list.length === 0) {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state">
<div class="empty-icon"><i class="fa fa-folder-open-o"></i></div>
<div>暂无今日告警数据</div>
</div>
</td>
</tr>
`);
return;
}
let html = "";
list.forEach(row => {
const typeText = row.alert_type == 1
? `<span class="status-tag fire"><i class="fa fa-bolt"></i>触发</span>`
: `<span class="status-tag resolve"><i class="fa fa-check"></i>恢复</span>`;
html += `<tr>
<td>${typeText}</td>
<td style="font-weight:500;color:#152c5b">${row.alert_name || '-'}</td>
<td style="font-family:Consolas,monospace;font-size:13px">${row.instance || '-'}</td>
<td>${getSeverityTag(row.severity)}</td>
<td style="color:#475569">${formatTime(row.receive_time)}</td>
</tr>`;
});
$("#table-body").html(html);
}).fail(() => {
$("#table-body").html(`
<tr>
<td colspan="5">
<div class="empty-state" style="color:#dc2626">
<div class="empty-icon"><i class="fa fa-exclamation-circle"></i></div>
<div>接口数据加载失败,请检查后端服务</div>
</div>
</td>
</tr>
`);
});
}
// 实时更新时钟
function updateTime() {
const now = new Date();
const Y = now.getFullYear();
const M = String(now.getMonth() + 1).padStart(2, '0');
const D = String(now.getDate()).padStart(2, '0');
const h = String(now.getHours()).padStart(2, '0');
const m = String(now.getMinutes()).padStart(2, '0');
const s = String(now.getSeconds()).padStart(2, '0');
$("#updateTime").text(`${Y}-${M}-${D} ${h}:${m}:${s}`);
}
// 全量刷新所有模块
function loadAllData() {
loadStats();
loadChart();
loadTable();
loadActiveAlert();
updateTime();
}
// 导出CSV
function exportCsv(){
const mask = $("#exportMask");
mask.css("display","flex");
window.location.href = api + "?act=export_csv";
setTimeout(()=>mask.hide(),5000);
}
// 初始化
$(function() {
loadAllData();
setInterval(loadAllData, 60000);
setInterval(updateTime, 1000);
});
</script>
</body>
</html>
+246
View File
@@ -0,0 +1,246 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务器磁盘目录容量监控 | Dashboard</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", system-ui, -apple-system, sans-serif;
}
body {
background-color: #f4f7fa;
padding: 24px;
color: #2d3748;
}
.dashboard-card {
background: #ffffff;
border-radius: 12px;
box-shadow: 0 2px 16px rgba(0, 30, 80, 0.08);
padding: 24px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 1px solid #e2e8f0;
padding-bottom: 16px;
flex-wrap: wrap;
gap:16px;
}
.card-title {
font-size: 18px;
font-weight: 600;
color: #1a202c;
}
.card-desc {
font-size: 13px;
color: #718096;
margin-top: 4px;
}
.filter-box{
display:flex;
align-items:center;
gap:10px;
}
.filter-box label{
font-size:14px;
}
#ipSelect{
padding:6px 10px;
border:1px solid #cbd5e0;
border-radius:6px;
font-size:14px;
min-width:200px;
}
#exportBtn{
padding:6px 14px;
background:#2b6cb0;
color:#fff;
border:none;
border-radius:6px;
cursor:pointer;
font-size:14px;
}
#exportBtn:hover{
background:#2c5282;
}
table {
width: 100%;
border-collapse: collapse;
margin-top:10px;
}
thead tr {
background-color: #2b6cb0;
color: #fff;
}
th, td {
padding: 14px 16px;
text-align: left;
font-size: 14px;
}
th {
font-weight: 500;
}
tbody tr {
border-bottom: 1px solid #edf2f7;
transition: background 0.2s;
}
tbody tr:hover {
background-color: #f7fafc;
}
.num-big {
color: #c53030;
font-weight: 500;
}
.loading {
text-align: center;
padding: 40px;
color: #718096;
}
</style>
</head>
<body>
<div class="dashboard-card">
<div class="card-header">
<div>
<h2 class="card-title">用户目录磁盘占用统计</h2>
<div class="card-desc">多服务器 /serverhome 目录容量实时采集,支持按实例IP筛选、导出表格</div>
</div>
<div class="filter-box">
<label>筛选实例IP</label>
<select id="ipSelect">
<option value="all">全部实例</option>
</select>
<button id="exportBtn">导出当前表格CSV</button>
</div>
</div>
<table>
<thead>
<tr>
<th>实例IP</th>
<th>用户名</th>
<th>目录路径</th>
<th>占用容量</th>
</tr>
</thead>
<tbody id="diskTableBody">
<tr>
<td colspan="4" class="loading">数据加载中...</td>
</tr>
</tbody>
</table>
</div>
<script>
let sourceData = [];
let currentFilterIp = "all";
const ipSelect = document.getElementById('ipSelect');
const tbody = document.getElementById('diskTableBody');
const exportBtn = document.getElementById('exportBtn');
// 拉取全量数据
fetch('/api/df.php', {
headers: {
"Accept": "application/json;charset=utf-8"
}
})
.then(res => res.json())
.then(ret => {
if(ret.code !== 0){
tbody.innerHTML = `<tr><td colspan="4" class="loading">接口异常:${ret.msg}</td></tr>`;
return;
}
sourceData = ret.list;
if(sourceData.length === 0){
tbody.innerHTML = `<tr><td colspan="4" class="loading">暂无磁盘指标数据</td></tr>`;
return;
}
// 提取所有不重复IP写入下拉框
const ipSet = new Set();
sourceData.forEach(item=>ipSet.add(item.instance_ip));
ipSet.forEach(ip=>{
let opt = document.createElement('option');
opt.value = ip;
opt.innerText = ip;
ipSelect.appendChild(opt);
})
// 初始渲染全部
renderTable("all");
})
.catch(err => {
tbody.innerHTML = `<tr><td colspan="4" class="loading">接口请求失败:${err}</td></tr>`;
})
// 筛选切换事件
ipSelect.addEventListener('change', function(){
currentFilterIp = this.value;
renderTable(currentFilterIp);
})
// 渲染表格
function renderTable(filterIp){
tbody.innerHTML = "";
let showList = [];
if(filterIp === "all"){
showList = sourceData;
}else{
showList = sourceData.filter(item=>item.instance_ip === filterIp);
}
if(showList.length === 0){
tbody.innerHTML = `<tr><td colspan="4" class="loading">该实例下无目录数据</td></tr>`;
return;
}
showList.forEach(row => {
let tr = document.createElement('tr');
tr.innerHTML = `
<td>${row.instance_ip}</td>
<td>${row.username}</td>
<td>${row.directory}</td>
<td class="num-big">${row.size_gb}GB</td>
`;
tbody.appendChild(tr);
})
}
// 导出CSV
exportBtn.addEventListener('click', function(){
let showList = [];
if(currentFilterIp === "all"){
showList = sourceData;
}else{
showList = sourceData.filter(item=>item.instance_ip === currentFilterIp);
}
if(showList.length === 0){
alert("无数据可导出");
return;
}
// CSV表头
let csvContent = "\uFEFF实例IP,用户名,目录路径,占用容量(GB)\n";
showList.forEach(item=>{
const line = [
`"${item.instance_ip}"`,
`"${item.username}"`,
`"${item.directory}"`,
`"${item.size_gb}GB"`
];
csvContent += line.join(",") + "\n";
});
// 下载文件
const blob = new Blob([csvContent], {type:"text/csv;charset=utf-8"});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "磁盘目录容量_"+new Date().getTime()+".csv";
a.click();
URL.revokeObjectURL(url);
})
</script>
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<?php
$raw = "Admin@2026";
$hash = password_hash($raw, PASSWORD_DEFAULT);
echo "哈希:" . $hash . "<br>";
var_dump(password_verify($raw, $hash));
?>
+3
View File
@@ -0,0 +1,3 @@
<?php
$rawPwd = "自定义密码";
echo password_hash($rawPwd, PASSWORD_DEFAULT);
+163
View File
@@ -0,0 +1,163 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 企业运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #152c5b;
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.login-box {
background: #fff;
width: 420px;
padding: 40px 32px;
border-radius: 16px;
box-shadow: 0 8px 40px rgba(0,0,0,0.2);
}
.login-title {
text-align: center;
font-size: 24px;
color: #152c5b;
margin-bottom: 30px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.login-title i {
color: #36d399;
font-size: 26px;
}
.input-item {
margin-bottom: 22px;
}
.input-item label {
display: block;
color: #475569;
margin-bottom: 8px;
font-size: 14px;
}
.input-wrap {
position: relative;
}
.input-wrap i {
position: absolute;
left: 14px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
}
.input-wrap input {
width: 100%;
padding: 14px 14px 14px 42px;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 15px;
transition: border 0.2s;
}
.input-wrap input:focus {
outline: none;
border-color: #152c5b;
}
.login-btn {
width: 100%;
padding: 14px;
background-color: #152c5b;
color: #fff;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.24s;
margin-top: 8px;
}
.login-btn:hover {
background-color: #0f1e42;
}
.msg-tip {
text-align: center;
margin-top: 16px;
font-size: 14px;
display: none;
}
.error {
color: #dc2626;
}
.success {
color: #16a34a;
}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<div class="input-item">
<label>访问密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" id="pwd" placeholder="请输入访问密码" autocomplete="off">
</div>
</div>
<button class="login-btn" id="loginBtn">登录进入平台</button>
<div class="msg-tip" id="tip"></div>
</div>
<script>
$(function(){
// 自定义登录密码,自行修改
const PASSWORD = "hp93000";
const TOKEN_KEY = "alert_admin_token";
// 已有登录凭证直接跳转首页
if(localStorage.getItem(TOKEN_KEY)){
window.location.href = "dashboard6.0.html";
}
function showTip(text, type="error"){
const $tip = $("#tip");
$tip.text(text).removeClass("error success").addClass(type).show();
setTimeout(()=>$tip.hide(),2500);
}
$("#loginBtn").click(function(){
const inputPwd = $("#pwd").val().trim();
if(!inputPwd){
showTip("请输入访问密码");
return;
}
if(inputPwd !== PASSWORD){
showTip("密码错误,请重新输入");
$("#pwd").val("");
return;
}
// 登录成功
localStorage.setItem(TOKEN_KEY, "login_success");
showTip("登录成功,正在跳转...", "success");
setTimeout(()=>{
window.location.href = "dashboard5.0.html";
},800);
});
// 回车登录
$("#pwd").keydown(function(e){
if(e.keyCode === 13) $("#loginBtn").click();
})
})
</script>
</body>
</html>
+275
View File
@@ -0,0 +1,275 @@
<?php
session_start();
// 清除当前IP登录锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳转监控面板
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
// 数据库区分双库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
// 权限用户库 monitor(存放sys_user、sys_role、sys_role_permission
$dbMonitorName = "monitor";
// 业务库 alert_mail_stat(工单、SMTP配置、大盘数据)
$dbAlertName = "alert_mail_stat";
$errMsg = "";
// 5分钟5次错误锁定逻辑
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
}
// 登录提交处理
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请等待5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号与密码不能为空";
} else {
// 连接monitor权限库查询用户
$connMonitor = mysqli_connect($dbHost, $dbUser, $dbPass, $dbMonitorName);
if (!$connMonitor) {
$errMsg = "数据库连接失败:" . mysqli_connect_error();
} else {
mysqli_set_charset($connMonitor, "utf8mb4");
$u = mysqli_real_escape_string($connMonitor, $username);
// 适配真实表字段,新增is_admin查询
$sql = "SELECT id,username,password,role_id,is_admin,enable FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($connMonitor, $sql);
// 新增判断:查询成功才读取数据
if ($res) {
$user = mysqli_fetch_assoc($res);
// 先判断账号是否启用
if (!$user) {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
} elseif ($user['enable'] != 1) {
$errMsg = "该账号已被禁用,无法登录";
} elseif (password_verify($pwd, $user['password'])) {
// 登录成功逻辑
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['is_admin'] = $user['is_admin'];
$_SESSION['login_time'] = time();
mysqli_query($connMonitor, "UPDATE sys_user SET last_login=NOW() WHERE id=" . $user['id']);
mysqli_close($connMonitor);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
}
} else {
// 捕获SQL执行失败原因
$errMsg = "查询用户失败:" . mysqli_error($connMonitor);
}
mysqli_close($connMonitor);
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维监控登录</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
position: relative;
background: url("https://www.advantest.com/img/common/img-ogp.png") center center / cover no-repeat fixed;
display: flex;
align-items: center;
justify-content: center;
}
body::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.86);
z-index: 1;
}
.login-card {
position: relative;
z-index: 2;
width: 480px;
background: #ffffff;
padding: 60px 48px;
border-radius: 24px;
box-shadow: 0 12px 60px rgba(21, 44, 91, 0.08);
border: 1px solid rgba(21, 44, 91, 0.06);
backdrop-filter: blur(6px);
}
.brand-box {
text-align: center;
margin-bottom: 48px;
}
.brand-en {
font-size: 36px;
font-weight: 700;
letter-spacing: 4px;
color: #152c5b;
margin-bottom: 8px;
}
.brand-cn {
font-size: 16px;
color: #64748b;
letter-spacing: 1px;
}
.error-tip {
text-align: center;
padding: 10px;
background: #fef2f2;
color: #dc2626;
border-radius: 10px;
font-size: 14px;
margin-bottom: 24px;
}
.form-item {
margin-bottom: 26px;
}
.form-label {
display: block;
font-size: 14px;
color: #334155;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrap {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 16px;
}
.form-input {
width: 100%;
padding: 18px 18px 18px 52px;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
background: #f9fafc;
transition: all 0.25s ease;
}
.form-input:focus {
outline: none;
border-color: #152c5b;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(21, 44, 91, 0.08);
}
.login-submit {
width: 100%;
padding: 18px;
margin-top: 10px;
background: #152c5b;
color: #ffffff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.login-submit:hover {
background: #0f1e42;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.16);
}
.region-footer {
position: absolute;
z-index: 2;
right: 48px;
bottom: 36px;
font-size: 14px;
color: #152c5b;
font-weight: 500;
letter-spacing: 1px;
}
@media screen and (max-width: 540px) {
.login-card {
width: 92%;
padding: 48px 32px;
}
.brand-en {
font-size: 30px;
}
.region-footer {
right: 24px;
bottom: 20px;
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-box">
<div class="brand-en">Advantest</div>
<div class="brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
<?php if (!empty($errMsg)): ?>
<div class="error-tip"><?php echo $errMsg; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-item">
<label class="form-label">登录账号</label>
<div class="input-wrap">
<i class="fa fa-user input-icon"></i>
<input class="form-input" type="text" name="username" placeholder="请输入登录账号" autocomplete="off" required>
</div>
</div>
<div class="form-item">
<label class="form-label">登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock input-icon"></i>
<input class="form-input" type="password" name="pwd" placeholder="请输入登录密码" autocomplete="off" required>
</div>
</div>
<button class="login-submit" type="submit">登录进入系统</button>
</form>
</div>
<div class="region-footer">中国上海</div>
</body>
</html>
+278
View File
@@ -0,0 +1,278 @@
<?php
session_start();
// 清除当前IP登录锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳转监控面板
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
// 数据库区分双库
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
// 权限用户库 monitor(存放sys_user、sys_role、sys_role_permission
$dbMonitorName = "monitor";
// 业务库 alert_mail_stat(工单、SMTP配置、大盘数据)
$dbAlertName = "alert_mail_stat";
$errMsg = "";
// 5分钟5次错误锁定逻辑
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
}
// 登录提交处理
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请等待5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号与密码不能为空";
} else {
// 连接monitor权限库查询用户
$connMonitor = mysqli_connect($dbHost, $dbUser, $dbPass, $dbMonitorName);
if (!$connMonitor) {
$errMsg = "数据库连接失败";
} else {
mysqli_set_charset($connMonitor, "utf8mb4");
$u = mysqli_real_escape_string($connMonitor, $username);
// 查询完整用户字段:id、账号、密码、角色ID、是否超级管理员
$sql = "SELECT id,username,password,role_id,is_admin FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($connMonitor, $sql);
$user = mysqli_fetch_assoc($res);
// 密码校验(password_hash加密存储)
if ($user && password_verify($pwd, $user['password'])) {
// 清空错误计数
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
// 写入鉴权必需session参数(适配auth.php
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role_id'] = $user['role_id'];
$_SESSION['is_admin'] = $user['is_admin'];
$_SESSION['login_time'] = time();
// 更新最后登录时间(monitor库sys_user表)
mysqli_query($connMonitor, "UPDATE sys_user SET last_login=NOW() WHERE id=" . $user['id']);
mysqli_close($connMonitor);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
}
mysqli_close($connMonitor);
}
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维监控登录</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
position: relative;
/* 官方品牌大图全屏背景 */
background: url("https://www.advantest.com/img/common/img-ogp.png") center center / cover no-repeat fixed;
display: flex;
align-items: center;
justify-content: center;
}
/* 高透纯白遮罩,保留完整背景图,高级柔和不压抑 */
body::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.86);
z-index: 1;
}
/* 登录主卡片 高端质感 */
.login-card {
position: relative;
z-index: 2;
width: 480px;
background: #ffffff;
padding: 60px 48px;
border-radius: 24px;
box-shadow: 0 12px 60px rgba(21, 44, 91, 0.08);
border: 1px solid rgba(21, 44, 91, 0.06);
backdrop-filter: blur(6px);
}
/* 品牌标题区域 */
.brand-box {
text-align: center;
margin-bottom: 48px;
}
.brand-en {
font-size: 36px;
font-weight: 700;
letter-spacing: 4px;
color: #152c5b;
margin-bottom: 8px;
}
.brand-cn {
font-size: 16px;
color: #64748b;
letter-spacing: 1px;
}
/* 错误提示 */
.error-tip {
text-align: center;
padding: 10px;
background: #fef2f2;
color: #dc2626;
border-radius: 10px;
font-size: 14px;
margin-bottom: 24px;
}
/* 输入项通用 */
.form-item {
margin-bottom: 26px;
}
.form-label {
display: block;
font-size: 14px;
color: #334155;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrap {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 16px;
}
.form-input {
width: 100%;
padding: 18px 18px 18px 52px;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
background: #f9fafc;
transition: all 0.25s ease;
}
.form-input:focus {
outline: none;
border-color: #152c5b;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(21, 44, 91, 0.08);
}
/* 登录按钮 */
.login-submit {
width: 100%;
padding: 18px;
margin-top: 10px;
background: #152c5b;
color: #ffffff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.login-submit:hover {
background: #0f1e42;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.16);
}
/* 底部地区文字 */
.region-footer {
position: absolute;
z-index: 2;
right: 48px;
bottom: 36px;
font-size: 14px;
color: #152c5b;
font-weight: 500;
letter-spacing: 1px;
}
/* 移动端自适应 */
@media screen and (max-width: 540px) {
.login-card {
width: 92%;
padding: 48px 32px;
}
.brand-en {
font-size: 30px;
}
.region-footer {
right: 24px;
bottom: 20px;
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-box">
<div class="brand-en">Advantest</div>
<div class="brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
<?php if (!empty($errMsg)): ?>
<div class="error-tip"><?php echo $errMsg; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-item">
<label class="form-label">登录账号</label>
<div class="input-wrap">
<i class="fa fa-user input-icon"></i>
<input class="form-input" type="text" name="username" placeholder="请输入登录账号" autocomplete="off" required>
</div>
</div>
<div class="form-item">
<label class="form-label">登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock input-icon"></i>
<input class="form-input" type="password" name="pwd" placeholder="请输入登录密码" autocomplete="off" required>
</div>
</div>
<button class="login-submit" type="submit">登录进入系统</button>
</form>
</div>
<div class="region-footer">中国上海</div>
</body>
</html>
+259
View File
@@ -0,0 +1,259 @@
<?php
session_start();
// 清除当前IP登录锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳转监控面板
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
// 数据库配置
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 5分钟5次错误锁定逻辑
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$loginErr = $_SESSION[$lockKey];
}
// 登录提交处理
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请等待5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号与密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
$_SESSION[$lockKey] = ['num' => 0, 'time' => time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=" . $user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码输入错误";
}
mysqli_close($conn);
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Advantest 爱德万测试 - 运维监控登录</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
width: 100vw;
height: 100vh;
overflow: hidden;
font-family: "Inter", "PingFang SC", "Microsoft YaHei", sans-serif;
position: relative;
/* 官方品牌大图全屏背景 */
background: url("https://www.advantest.com/img/common/img-ogp.png") center center / cover no-repeat fixed;
display: flex;
align-items: center;
justify-content: center;
}
/* 高透纯白遮罩,保留完整背景图,高级柔和不压抑 */
body::before {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.86);
z-index: 1;
}
/* 登录主卡片 高端质感 */
.login-card {
position: relative;
z-index: 2;
width: 480px;
background: #ffffff;
padding: 60px 48px;
border-radius: 24px;
box-shadow: 0 12px 60px rgba(21, 44, 91, 0.08);
border: 1px solid rgba(21, 44, 91, 0.06);
backdrop-filter: blur(6px);
}
/* 品牌标题区域 */
.brand-box {
text-align: center;
margin-bottom: 48px;
}
.brand-en {
font-size: 36px;
font-weight: 700;
letter-spacing: 4px;
color: #152c5b;
margin-bottom: 8px;
}
.brand-cn {
font-size: 16px;
color: #64748b;
letter-spacing: 1px;
}
/* 错误提示 */
.error-tip {
text-align: center;
padding: 10px;
background: #fef2f2;
color: #dc2626;
border-radius: 10px;
font-size: 14px;
margin-bottom: 24px;
}
/* 输入项通用 */
.form-item {
margin-bottom: 26px;
}
.form-label {
display: block;
font-size: 14px;
color: #334155;
margin-bottom: 10px;
font-weight: 500;
}
.input-wrap {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: #94a3b8;
font-size: 16px;
}
.form-input {
width: 100%;
padding: 18px 18px 18px 52px;
border: 1px solid #e2e8f0;
border-radius: 12px;
font-size: 15px;
background: #f9fafc;
transition: all 0.25s ease;
}
.form-input:focus {
outline: none;
border-color: #152c5b;
background: #ffffff;
box-shadow: 0 0 0 3px rgba(21, 44, 91, 0.08);
}
/* 登录按钮 */
.login-submit {
width: 100%;
padding: 18px;
margin-top: 10px;
background: #152c5b;
color: #ffffff;
border: none;
border-radius: 12px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.25s ease;
}
.login-submit:hover {
background: #0f1e42;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(21, 44, 91, 0.16);
}
/* 底部地区文字 */
.region-footer {
position: absolute;
z-index: 2;
right: 48px;
bottom: 36px;
font-size: 14px;
color: #152c5b;
font-weight: 500;
letter-spacing: 1px;
}
/* 移动端自适应 */
@media screen and (max-width: 540px) {
.login-card {
width: 92%;
padding: 48px 32px;
}
.brand-en {
font-size: 30px;
}
.region-footer {
right: 24px;
bottom: 20px;
font-size: 13px;
}
}
</style>
</head>
<body>
<div class="login-card">
<div class="brand-box">
<div class="brand-en">Advantest</div>
<div class="brand-cn">爱德万测试 · 运维告警监控平台</div>
</div>
<?php if (!empty($errMsg)): ?>
<div class="error-tip"><?php echo $errMsg; ?></div>
<?php endif; ?>
<form method="POST">
<div class="form-item">
<label class="form-label">登录账号</label>
<div class="input-wrap">
<i class="fa fa-user input-icon"></i>
<input class="form-input" type="text" name="username" placeholder="请输入登录账号" autocomplete="off" required>
</div>
</div>
<div class="form-item">
<label class="form-label">登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock input-icon"></i>
<input class="form-input" type="password" name="pwd" placeholder="请输入登录密码" autocomplete="off" required>
</div>
</div>
<button class="login-submit" type="submit">登录进入系统</button>
</form>
</div>
<div class="region-footer">中国上海</div>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<?php
session_start();
// 清空本次IP登录错误锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳监控首页
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 限制5分钟最多5次错误密码,防暴力破解
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
// 超过5分钟重置计数
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
}
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
// 登录成功,清空错误计数,写入会话
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
// 更新最后登录时间
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=".$user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码错误";
}
mysqli_close($conn);
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 企业运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#152c5b;font-family:"PingFang SC","Microsoft YaHei";display:flex;align-items:center;justify-content:center;min-height:100vh}
.login-box{background:#fff;width:420px;padding:40px 32px;border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.2)}
.login-title{text-align:center;font-size:24px;color:#152c5b;margin-bottom:30px;display:flex;align-items:center;justify-content:center;gap:10px}
.login-title i{color:#36d399;font-size:26px}
.input-item{margin-bottom:22px}
.input-item label{display:block;color:#475569;margin-bottom:8px;font-size:14px}
.input-wrap{position:relative}
.input-wrap i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#94a3b8}
.input-wrap input{width:100%;padding:14px 14px 14px 42px;border:1px solid #e2e8f0;border-radius:8px;font-size:15px}
.input-wrap input:focus{outline:none;border-color:#152c5b}
.login-btn{width:100%;padding:14px;background:#152c5b;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-top:8px}
.login-btn:hover{background:#0f1e42}
.tip{text-align:center;margin-top:16px;font-size:14px}
.err{color:#dc2626}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<?php if($errMsg):?>
<div class="tip err"><?php echo $errMsg;?></div>
<?php endif;?>
<form method="post">
<div class="input-item">
<label>登录账号</label>
<div class="input-wrap">
<i class="fa fa-user"></i>
<input type="text" name="username" placeholder="请输入账号" autocomplete="off" required>
</div>
</div>
<div class="input-item">
<label>登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" name="pwd" placeholder="请输入密码" autocomplete="off" required>
</div>
</div>
<button class="login-btn" type="submit">登录进入平台</button>
</form>
</div>
</body>
</html>
+116
View File
@@ -0,0 +1,116 @@
<?php
session_start();
// 清空本次IP登录错误锁定
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
unset($_SESSION[$lockKey]);
// 已登录直接跳监控首页
if (!empty($_SESSION['user_id'])) {
header("Location: dashboard.php");
exit;
}
$dbHost = "10.150.117.190";
$dbUser = "root";
$dbPass = "hp93000";
$dbName = "alert_mail_stat";
$errMsg = "";
// 限制5分钟最多5次错误密码,防暴力破解
$lockKey = "login_err_" . $_SERVER['REMOTE_ADDR'];
if (!isset($_SESSION[$lockKey])) $_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
// 超过5分钟重置计数
if (time() - $loginErr['time'] > 300) {
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$loginErr = $_SESSION[$lockKey];
}
if ($_POST) {
$username = trim($_POST['username'] ?? '');
$pwd = trim($_POST['pwd'] ?? '');
if ($loginErr['num'] >= 5) {
$errMsg = "密码错误次数过多,请5分钟后重试";
} elseif (!$username || !$pwd) {
$errMsg = "账号密码不能为空";
} else {
$conn = mysqli_connect($dbHost, $dbUser, $dbPass, $dbName);
$u = mysqli_real_escape_string($conn, $username);
$sql = "SELECT id,username,password,role FROM sys_user WHERE username='$u' LIMIT 1";
$res = mysqli_query($conn, $sql);
$user = mysqli_fetch_assoc($res);
if ($user && password_verify($pwd, $user['password'])) {
// 登录成功,清空错误计数,写入会话
$_SESSION[$lockKey] = ['num'=>0,'time'=>time()];
$_SESSION['user_id'] = $user['id'];
$_SESSION['username'] = $user['username'];
$_SESSION['role'] = $user['role'];
$_SESSION['login_time'] = time();
// 更新最后登录时间
mysqli_query($conn, "UPDATE sys_user SET last_login=NOW() WHERE id=".$user['id']);
header("Location: dashboard.php");
exit;
} else {
$_SESSION[$lockKey]['num']++;
$errMsg = "账号或密码错误";
}
mysqli_close($conn);
}
}
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>登录 - 企业运维告警监控平台</title>
<link rel="stylesheet" href="/static/css/font-awesome.min.css">
<script src="/static/js/jquery.min.js"></script>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{background:#152c5b;font-family:"PingFang SC","Microsoft YaHei";display:flex;align-items:center;justify-content:center;min-height:100vh}
.login-box{background:#fff;width:420px;padding:40px 32px;border-radius:16px;box-shadow:0 8px 40px rgba(0,0,0,0.2)}
.login-title{text-align:center;font-size:24px;color:#152c5b;margin-bottom:30px;display:flex;align-items:center;justify-content:center;gap:10px}
.login-title i{color:#36d399;font-size:26px}
.input-item{margin-bottom:22px}
.input-item label{display:block;color:#475569;margin-bottom:8px;font-size:14px}
.input-wrap{position:relative}
.input-wrap i{position:absolute;left:14px;top:50%;transform:translateY(-50%);color:#94a3b8}
.input-wrap input{width:100%;padding:14px 14px 14px 42px;border:1px solid #e2e8f0;border-radius:8px;font-size:15px}
.input-wrap input:focus{outline:none;border-color:#152c5b}
.login-btn{width:100%;padding:14px;background:#152c5b;color:#fff;border:none;border-radius:8px;font-size:16px;cursor:pointer;margin-top:8px}
.login-btn:hover{background:#0f1e42}
.tip{text-align:center;margin-top:16px;font-size:14px}
.err{color:#dc2626}
</style>
</head>
<body>
<div class="login-box">
<div class="login-title">
<i class="fa fa-bell"></i>
<span>企业运维告警监控平台</span>
</div>
<?php if($errMsg):?>
<div class="tip err"><?php echo $errMsg;?></div>
<?php endif;?>
<form method="post">
<div class="input-item">
<label>登录账号</label>
<div class="input-wrap">
<i class="fa fa-user"></i>
<input type="text" name="username" placeholder="请输入账号" autocomplete="off" required>
</div>
</div>
<div class="input-item">
<label>登录密码</label>
<div class="input-wrap">
<i class="fa fa-lock"></i>
<input type="password" name="pwd" placeholder="请输入密码" autocomplete="off" required>
</div>
</div>
<button class="login-btn" type="submit">登录进入平台</button>
</form>
</div>
</body>
</html>

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