日志优化

This commit is contained in:
青柠
2026-01-20 20:46:07 +08:00
parent 631baab327
commit c4d673dfe4
4 changed files with 180 additions and 184 deletions

View File

@@ -139,17 +139,40 @@ class AliyunTrafficCheck
return $config;
}
public function getSystemLogs()
// --- 修改:支持按 Tab 获取日志 ---
public function getSystemLogs($tab = 'action')
{
if ($this->initError) return [];
$logs = $this->db->getLogs(50);
if ($tab === 'heartbeat') {
// 心跳日志:只看 heartbeat 类型
$types = ['heartbeat'];
} else {
// 动作日志:只看 info 和 warning排除 error (超时/接口错误)
$types = ['info', 'warning'];
}
// 仅返回最近 20 条
$logs = $this->db->getLogsByTypes($types, 20);
foreach ($logs as &$log) {
$log['time_str'] = date('Y-m-d H:i:s', $log['created_at']);
}
return $logs;
}
// --- 修改:获取流量历史数据(适配新表结构) ---
// --- 新增:清空日志 ---
public function clearSystemLogs($tab = 'action')
{
if ($this->initError) return false;
if ($tab === 'heartbeat') {
return $this->db->clearLogsByTypes(['heartbeat']);
} else {
return $this->db->clearLogsByTypes(['info', 'warning', 'error']); // 清空动作日志时连带Error一起清空保持干净
}
}
public function getAccountHistory($id)
{
if ($this->initError) return [];
@@ -159,7 +182,6 @@ class AliyunTrafficCheck
$ak = $account['access_key_id'];
// 1. 获取最近24小时数据 (Hourly)
$rawHourly = $this->db->getHourlyStats($ak);
$chartHourly = [];
foreach ($rawHourly as $row) {
@@ -170,7 +192,6 @@ class AliyunTrafficCheck
];
}
// 2. 获取最近30天数据 (Daily)
$rawDaily = $this->db->getDailyStats($ak);
$chartDaily = [];
foreach ($rawDaily as $row) {
@@ -193,7 +214,7 @@ class AliyunTrafficCheck
if ($this->initError) return "Error: " . $this->initError;
$this->db->pruneLogs(30);
$this->db->pruneStats(); // 清理旧的统计数据
$this->db->pruneStats();
$logs = [];
$currentUserTime = date('H:i');
@@ -249,14 +270,6 @@ class AliyunTrafficCheck
$shouldCheckApi = $forceRefresh || (($currentTime - $lastUpdate) > $currentInterval);
// 特殊检查:是否需要记录统计数据?
// 策略Monitor每分钟运行尝试插入当前整点和当前0点的数据。
// 由于数据库有 UNIQUE 索引 + INSERT OR IGNORE只有每小时/每天第一次尝试会成功。
// 因此,我们不需要复杂的“是否已记录”判断,直接尝试记录即可。
// 但为了减少API调用我们仅在“应该检查API”或者“尚未记录当前小时/天数据”时才调用API
// 简单起见,利用现有的 $shouldCheckApi 逻辑。
// 如果为了保证整点记录的及时性,我们应该每分钟都检查一下是否到了整点?
// 优化:如果当前分钟是 00 (整点),则强制刷新一次,确保整点数据最准确。
if (date('i') === '00') {
$shouldCheckApi = true;
}
@@ -264,7 +277,6 @@ class AliyunTrafficCheck
$newUpdateTime = $currentTime;
if ($shouldCheckApi) {
// 使用 Safe 方法
$newTraffic = $this->safeGetTraffic($account);
$status = $this->safeGetInstanceStatus($account);
@@ -281,10 +293,7 @@ class AliyunTrafficCheck
$traffic = $newTraffic;
$apiStatusLog = "已更新";
// --- 修改:记录统计数据 ---
// 尝试记录小时数据 (利用DB唯一性去重)
$this->db->addHourlyStat($account['access_key_id'], $traffic);
// 尝试记录天数据 (利用DB唯一性去重)
$this->db->addDailyStat($account['access_key_id'], $traffic);
}
@@ -365,7 +374,11 @@ class AliyunTrafficCheck
}
$actionLog = empty($actions) ? "无动作" : implode(", ", $actions);
$logs[] = sprintf("%s %s | %s | %s | %s", $logPrefix, $actionLog, $trafficDesc, $status, $apiStatusLog);
$logLine = sprintf("%s %s | %s | %s | %s", $logPrefix, $actionLog, $trafficDesc, $status, $apiStatusLog);
// --- 修改:将心跳日志写入数据库 ---
$this->db->addLog('heartbeat', $logLine);
$logs[] = $logLine;
}
$this->configManager->updateLastRunTime(time());
@@ -393,7 +406,6 @@ class AliyunTrafficCheck
$checkInterval = $isTransientState ? 60 : $userInterval;
if (($currentTime - $lastUpdate) > $checkInterval) {
// 使用 Safe 方法
$newTraffic = $this->safeGetTraffic($account);
$status = $this->safeGetInstanceStatus($account);
@@ -407,7 +419,6 @@ class AliyunTrafficCheck
$newUpdateTime = $lastUpdate;
} else {
$traffic = $newTraffic;
// --- 修改:同时尝试记录统计数据 ---
$this->db->addHourlyStat($account['access_key_id'], $traffic);
$this->db->addDailyStat($account['access_key_id'], $traffic);
}
@@ -460,7 +471,6 @@ class AliyunTrafficCheck
if ($traffic < 0) {
$traffic = $targetAccount['traffic_used'];
} else {
// 手动刷新也尝试记录统计数据
$this->db->addHourlyStat($targetAccount['access_key_id'], $traffic);
$this->db->addDailyStat($targetAccount['access_key_id'], $traffic);
}
@@ -571,4 +581,4 @@ class AliyunTrafficCheck
include 'template.html';
return ob_get_clean();
}
}
}

View File

@@ -101,8 +101,6 @@ class Database
attempt_time INTEGER
)");
// --- 新增:独立的小时级和天级流量表 ---
// 1. 小时级表 (24小时折线图)
$this->pdo->exec("CREATE TABLE IF NOT EXISTS traffic_hourly (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -110,7 +108,6 @@ class Database
traffic REAL,
recorded_at INTEGER
)");
// 唯一索引:确保每个 AK 在每个小时(时间戳归一化后)只有一条记录
$this->pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_traffic_hourly_unique ON traffic_hourly (access_key_id, recorded_at)");
// 2. 天级表 (30天柱状图)
@@ -120,7 +117,6 @@ class Database
traffic REAL,
recorded_at INTEGER
)");
// 唯一索引:确保每个 AK 在每天(时间戳归一化后)只有一条记录
$this->pdo->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_traffic_daily_unique ON traffic_daily (access_key_id, recorded_at)");
$this->ensureColumn('accounts', 'traffic_used', 'REAL DEFAULT 0');
@@ -156,6 +152,30 @@ class Database
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// --- 新增:按类型获取日志 ---
public function getLogsByTypes(array $types, $limit = 20)
{
// 动态构建 IN 查询占位符
$placeholders = implode(',', array_fill(0, count($types), '?'));
$sql = "SELECT * FROM logs WHERE type IN ($placeholders) ORDER BY id DESC LIMIT ?";
// 合并参数
$params = $types;
$params[] = $limit;
$stmt = $this->pdo->prepare($sql);
$stmt->execute($params);
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// --- 新增:按类型删除日志 ---
public function clearLogsByTypes(array $types)
{
$placeholders = implode(',', array_fill(0, count($types), '?'));
$stmt = $this->pdo->prepare("DELETE FROM logs WHERE type IN ($placeholders)");
return $stmt->execute($types);
}
public function pruneLogs($days = 30)
{
$stmt = $this->pdo->prepare("DELETE FROM logs WHERE created_at < ?");
@@ -183,54 +203,33 @@ class Database
$stmt->execute([$ip]);
}
// --- 新的流量记录逻辑 ---
// --- 流量记录逻辑 ---
/**
* 记录小时级数据
* 利用 UNIQUE INDEX 和 INSERT OR IGNORE 实现“每小时只记一条”
*/
public function addHourlyStat($accessKeyId, $traffic)
{
// 归一化到当前小时的整点 (例如 10:23 -> 10:00)
$hourTimestamp = floor(time() / 3600) * 3600;
$stmt = $this->pdo->prepare("INSERT OR IGNORE INTO traffic_hourly (access_key_id, traffic, recorded_at) VALUES (?, ?, ?)");
// 修改为 INSERT OR REPLACE实现当前小时流量实时更新
$stmt = $this->pdo->prepare("INSERT OR REPLACE INTO traffic_hourly (access_key_id, traffic, recorded_at) VALUES (?, ?, ?)");
$stmt->execute([$accessKeyId, $traffic, $hourTimestamp]);
}
/**
* 记录天级数据
* 利用 UNIQUE INDEX 和 INSERT OR IGNORE 实现“每天只记一条”
*/
public function addDailyStat($accessKeyId, $traffic)
{
// 归一化到当天 00:00
// 归一化到当天 00:00:00
$dayTimestamp = strtotime(date('Y-m-d 00:00:00'));
$stmt = $this->pdo->prepare("INSERT OR IGNORE INTO traffic_daily (access_key_id, traffic, recorded_at) VALUES (?, ?, ?)");
// 修改为 INSERT OR REPLACE实现当日流量实时更新直到第二天0点生成新条目
$stmt = $this->pdo->prepare("INSERT OR REPLACE INTO traffic_daily (access_key_id, traffic, recorded_at) VALUES (?, ?, ?)");
$stmt->execute([$accessKeyId, $traffic, $dayTimestamp]);
}
/**
* 获取最近 24 小时的数据
*/
public function getHourlyStats($accessKeyId)
{
// 获取最近 25 条保证覆盖24小时
$stmt = $this->pdo->prepare("SELECT traffic, recorded_at FROM traffic_hourly WHERE access_key_id = ? ORDER BY recorded_at DESC LIMIT 25");
$stmt->execute([$accessKeyId]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 按时间正序排列返回
return array_reverse($data);
<<<<<<< Updated upstream
<<<<<<< Updated upstream
=======
=======
}
/**
* 获取最近 30 天的数据
*/
public function getDailyStats($accessKeyId)
{
$stmt = $this->pdo->prepare("SELECT traffic, recorded_at FROM traffic_daily WHERE access_key_id = ? ORDER BY recorded_at DESC LIMIT 31");
@@ -239,72 +238,12 @@ class Database
return array_reverse($data);
}
/**
* 清理过期统计数据
*/
public function pruneStats()
{
// 1. 清理小时表:保留最近 24+2 小时以外的数据
// 既然我们只取 Limit 24其实可以删掉 48 小时前的
$hourLimit = time() - (48 * 3600);
$this->pdo->exec("DELETE FROM traffic_hourly WHERE recorded_at < $hourLimit");
// 2. 清理天表:保留最近 60 天以外的 (留点余量)
$dayLimit = time() - (60 * 86400);
$this->pdo->exec("DELETE FROM traffic_daily WHERE recorded_at < $dayLimit");
>>>>>>> Stashed changes
}
/**
* 获取最近 30 天的数据
*/
public function getDailyStats($accessKeyId)
{
$stmt = $this->pdo->prepare("SELECT traffic, recorded_at FROM traffic_daily WHERE access_key_id = ? ORDER BY recorded_at DESC LIMIT 31");
$stmt->execute([$accessKeyId]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_reverse($data);
}
/**
* 清理过期统计数据
*/
public function pruneStats()
{
// 1. 清理小时表:保留最近 24+2 小时以外的数据
// 既然我们只取 Limit 24其实可以删掉 48 小时前的
$hourLimit = time() - (48 * 3600);
$this->pdo->exec("DELETE FROM traffic_hourly WHERE recorded_at < $hourLimit");
// 2. 清理天表:保留最近 60 天以外的 (留点余量)
$dayLimit = time() - (60 * 86400);
$this->pdo->exec("DELETE FROM traffic_daily WHERE recorded_at < $dayLimit");
>>>>>>> Stashed changes
}
/**
* 获取最近 30 天的数据
*/
public function getDailyStats($accessKeyId)
{
$stmt = $this->pdo->prepare("SELECT traffic, recorded_at FROM traffic_daily WHERE access_key_id = ? ORDER BY recorded_at DESC LIMIT 31");
$stmt->execute([$accessKeyId]);
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);
return array_reverse($data);
}
/**
* 清理过期统计数据
*/
public function pruneStats()
{
// 1. 清理小时表:保留最近 24+2 小时以外的数据
// 既然我们只取 Limit 24其实可以删掉 48 小时前的
$hourLimit = time() - (48 * 3600);
$this->pdo->exec("DELETE FROM traffic_hourly WHERE recorded_at < $hourLimit");
// 2. 清理天表:保留最近 60 天以外的 (留点余量)
$dayLimit = time() - (60 * 86400);
$this->pdo->exec("DELETE FROM traffic_daily WHERE recorded_at < $dayLimit");
}
}
}

View File

@@ -118,14 +118,26 @@ if ($action === 'refresh_account') {
exit;
}
// 获取系统日志
// 修改:获取系统日志,支持 Tab
if ($action === 'get_logs') {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['data' => $app->getSystemLogs()]);
$tab = $_GET['tab'] ?? 'action'; // 默认是动作日志
echo json_encode(['data' => $app->getSystemLogs($tab)]);
exit;
}
// 新增:清空日志
if ($action === 'clear_logs') {
$data = json_decode(file_get_contents('php://input'), true);
$tab = $data['tab'] ?? 'action';
if ($app->clearSystemLogs($tab)) {
echo json_encode(['success' => true]);
} else {
echo json_encode(['success' => false, 'message' => 'Clear failed']);
}
exit;
}
// 新增:获取流量历史
if ($action === 'get_history') {
header('Content-Type: application/json; charset=utf-8');
$id = $_GET['id'] ?? 0;

View File

@@ -93,7 +93,6 @@
<div id="app" v-cloak class="min-h-screen p-6 md:p-12 flex flex-col items-center">
<!-- 警告条 -->
<div v-if="cronWarning && initialized && !criticalError" class="w-full max-w-5xl mb-6 bg-yellow-50 border-l-4 border-yellow-400 p-4 rounded-r-xl shadow-sm animate-fade-in">
<div class="flex">
<div class="flex-shrink-0">
@@ -127,7 +126,6 @@
<div v-for="(item, index) in statusData" :key="index"
class="glass-panel rounded-[32px] p-6 flex flex-col justify-between relative overflow-hidden group hover:shadow-2xl transition-all duration-500">
<div class="absolute top-0 right-0 p-6 opacity-50 flex gap-2">
<!-- 图表按钮 (新增) -->
<button v-if="isAdmin" @click="openChart(item.id, item.account)"
class="p-1 rounded-full hover:bg-white/50 text-gray-400 hover:text-blue-500 transition-colors mr-1"
title="查看流量历史">
@@ -136,7 +134,6 @@
</svg>
</button>
<!-- 刷新按钮 -->
<button v-if="isAdmin" @click="refreshSingle(item.id, index)"
class="p-1 rounded-full hover:bg-white/50 text-gray-400 hover:text-gray-600 transition-colors"
:class="{'animate-spin': refreshingMap[index]}"
@@ -191,18 +188,15 @@
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800"></div>
</div>
<!-- Chart Modal (Updated) -->
<div v-if="showChartModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-gray-300/30 backdrop-blur-md" @click="closeChart"></div>
<div class="glass-panel w-full max-w-5xl h-[500px] flex flex-col p-0 rounded-[32px] relative z-10 shadow-2xl bg-white/90 overflow-hidden">
<!-- Header -->
<div class="flex justify-between items-center p-6 border-b border-gray-100">
<div>
<h3 class="text-xl font-bold text-[#1C1C1E]">流量统计</h3>
<p class="text-xs text-gray-400 font-mono mt-1">账号: {{ currentChartAccount }}</p>
</div>
<!-- Tab Switcher -->
<div class="bg-gray-100 p-1 rounded-xl flex gap-1">
<button @click="switchChartMode('24h')"
class="px-4 py-1.5 text-xs font-bold rounded-lg transition-all"
@@ -221,14 +215,12 @@
</button>
</div>
<!-- Chart Body -->
<div class="flex-1 p-6 relative">
<div v-if="chartLoading" class="absolute inset-0 flex items-center justify-center bg-white/50 z-20">
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-800"></div>
</div>
<div id="echarts-container" class="w-full h-full"></div>
<!-- Empty State -->
<div v-if="!chartLoading && isChartEmpty" class="absolute inset-0 flex flex-col items-center justify-center text-gray-400 pointer-events-none">
<svg class="w-12 h-12 mb-2 opacity-20" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg>
<p class="text-sm">暂无统计数据 (等待整点更新)</p>
@@ -237,7 +229,6 @@
</div>
</div>
<!-- Critical Error Modal -->
<div v-if="criticalError" class="fixed inset-0 z-50 flex items-center justify-center p-6 bg-red-50/50 backdrop-blur-sm">
<div class="glass-panel w-full max-w-md p-8 rounded-[40px] shadow-xl border-red-200 bg-red-50/80">
<div class="text-center">
@@ -258,7 +249,6 @@
</div>
</div>
<!-- Login Modal -->
<div v-if="showLoginModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<div class="absolute inset-0 bg-gray-200/30 backdrop-blur-md" @click="showLoginModal = false"></div>
<div class="glass-panel w-full max-w-md p-8 rounded-[40px] relative z-10 transform transition-all shadow-2xl">
@@ -269,9 +259,7 @@
</div>
</div>
<!-- Setup Wizard -->
<div v-if="!initialized && !loadingCheckInit && !criticalError" class="fixed inset-0 z-50 flex flex-col items-center justify-center p-6 bg-[#F2F2F7]">
<!-- ... (Setup Wizard content remains same) ... -->
<div class="glass-panel w-full max-w-lg p-10 rounded-[48px] shadow-2xl border border-white/80">
<div class="text-center mb-8">
<h1 class="text-3xl font-extrabold text-[#1C1C1E] mb-2">欢迎使用 CDT Monitor</h1>
@@ -301,7 +289,6 @@
</div>
</div>
<!-- Admin Config Editor -->
<div v-if="isAdmin && config" class="w-full max-w-5xl mt-12 mb-20 animate-fade-in-up space-y-8">
<div class="flex items-center gap-4">
<h2 class="text-xl font-bold">系统配置</h2>
@@ -309,9 +296,7 @@
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Left Column -->
<div class="lg:col-span-1 space-y-8">
<!-- Global Settings -->
<div class="glass-panel rounded-[32px] p-6 overflow-visible z-20">
<h3 class="text-sm font-bold uppercase tracking-widest text-gray-400 mb-4">全局设置</h3>
<div class="space-y-4">
@@ -324,7 +309,6 @@
<input v-model.number="config.traffic_threshold" type="number" min="1" max="100" class="w-full glass-input rounded-xl px-4 py-2 text-sm">
</div>
<!-- 接口调用频率 -->
<div class="space-y-1">
<label class="text-xs font-medium text-gray-500 ml-2">接口调用频率</label>
<select v-model.number="config.api_interval" class="w-full glass-input rounded-xl px-4 py-2 text-sm appearance-none cursor-pointer">
@@ -337,7 +321,6 @@
<p class="text-[10px] text-gray-400 px-2 mt-1">控制更新状态和流量的频率。开关机操作触发时会自动加速至 1 分钟。</p>
</div>
<!-- 停机模式 (带 Tooltip) -->
<div class="space-y-1 relative">
<div class="flex items-center gap-1">
<label class="text-xs font-medium text-gray-500 ml-2">停机模式</label>
@@ -375,11 +358,9 @@
</select>
</div>
<!-- 实例保活 (新增) -->
<div class="flex items-center justify-between pt-2">
<div class="flex items-center gap-1">
<label class="text-xs font-medium text-gray-500 ml-2">实例保活 (抢占式)</label>
<!-- Tooltip Trigger -->
<div class="group relative flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 text-gray-400 cursor-help hover:text-gray-600 transition-colors">
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
@@ -408,7 +389,6 @@
</div>
</div>
<!-- Notification Settings -->
<div class="glass-panel rounded-[32px] p-6 z-10">
<h3 class="text-sm font-bold uppercase tracking-widest text-gray-400 mb-4">邮件服务器配置</h3>
<div class="space-y-3">
@@ -452,7 +432,6 @@
</div>
</div>
<!-- Right Column (Accounts) -->
<div class="lg:col-span-2 space-y-8">
<div class="glass-panel rounded-[40px] p-8 h-fit z-0">
<div class="space-y-8">
@@ -536,26 +515,49 @@
</div>
</div>
<!-- System Logs (New) -->
<div class="glass-panel rounded-[40px] p-8 relative overflow-hidden">
<h3 class="text-lg font-bold mb-6 text-[#1C1C1E] flex items-center justify-between">
系统执行日志
<button @click="fetchLogs" class="text-xs font-normal text-blue-500 hover:text-blue-600 transition">刷新日志</button>
</h3>
<div class="overflow-y-auto max-h-96 pr-2 no-scrollbar">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-4">
<h3 class="text-lg font-bold text-[#1C1C1E]">系统日志</h3>
<div class="flex bg-gray-100/80 p-1 rounded-xl">
<button @click="currentLogTab = 'action'; fetchLogs()"
class="px-4 py-1.5 text-xs font-bold rounded-lg transition-all"
:class="currentLogTab === 'action' ? 'bg-white text-[#1C1C1E] shadow-sm' : 'text-gray-400 hover:text-gray-600'">
动作日志
</button>
<button @click="currentLogTab = 'heartbeat'; fetchLogs()"
class="px-4 py-1.5 text-xs font-bold rounded-lg transition-all"
:class="currentLogTab === 'heartbeat' ? 'bg-white text-[#1C1C1E] shadow-sm' : 'text-gray-400 hover:text-gray-600'">
心跳日志
</button>
</div>
</div>
<div class="flex items-center gap-3">
<div v-if="logAutoRefresh" class="flex items-center gap-1.5">
<span class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
<span class="text-[10px] text-gray-400 font-mono">LIVE</span>
</div>
<button @click="clearLogs" class="text-xs text-red-400 hover:text-red-500 font-medium transition px-2">清空</button>
</div>
</div>
<div class="overflow-y-auto max-h-96 pr-2 no-scrollbar bg-white/30 rounded-2xl border border-white/40">
<table class="w-full text-left border-collapse">
<thead>
<thead class="bg-gray-50/50 sticky top-0 backdrop-blur-md">
<tr>
<th class="text-xs font-bold text-gray-400 uppercase tracking-wider pb-3 pl-2">时间</th>
<th class="text-xs font-bold text-gray-400 uppercase tracking-wider pb-3">类型</th>
<th class="text-xs font-bold text-gray-400 uppercase tracking-wider pb-3">内容</th>
<th class="text-[10px] font-bold text-gray-400 uppercase tracking-wider py-3 pl-4">时间</th>
<th v-if="currentLogTab === 'action'" class="text-[10px] font-bold text-gray-400 uppercase tracking-wider py-3">类型</th>
<th class="text-[10px] font-bold text-gray-400 uppercase tracking-wider py-3 pr-4">内容</th>
</tr>
</thead>
<tbody class="text-sm text-gray-600">
<tr v-for="(log, i) in systemLogs" :key="i" class="border-t border-gray-100/50 hover:bg-white/40 transition-colors">
<td class="py-3 pl-2 text-gray-500 font-mono text-xs whitespace-nowrap">{{ log.time_str }}</td>
<td class="py-3">
<span class="px-2 py-0.5 rounded-md text-[10px] font-bold uppercase tracking-wide"
<tbody class="text-xs text-gray-600">
<tr v-for="(log, i) in systemLogs" :key="i" class="border-b border-gray-100/50 hover:bg-white/60 transition-colors last:border-0">
<td class="py-3 pl-4 text-gray-400 font-mono whitespace-nowrap w-40">{{ log.time_str }}</td>
<td v-if="currentLogTab === 'action'" class="py-3 w-20">
<span class="px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wide"
:class="{
'bg-blue-100 text-blue-700': log.type === 'info',
'bg-yellow-100 text-yellow-700': log.type === 'warning',
@@ -564,10 +566,15 @@
{{ log.type }}
</span>
</td>
<td class="py-3 pr-2">{{ log.message }}</td>
<td class="py-3 pr-4 font-mono leading-relaxed break-all"
:class="{'text-gray-400': currentLogTab === 'heartbeat'}">
{{ log.message }}
</td>
</tr>
<tr v-if="systemLogs.length === 0">
<td colspan="3" class="py-8 text-center text-gray-400 text-xs">暂无日志记录</td>
<td colspan="3" class="py-12 text-center text-gray-400">
暂无{{ currentLogTab === 'action' ? '动作' : '心跳' }}记录
</td>
</tr>
</tbody>
</table>
@@ -580,7 +587,7 @@
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script>
const { createApp, ref, onMounted, computed } = Vue;
const { createApp, ref, onMounted, onUnmounted, computed } = Vue;
createApp({
setup() {
@@ -598,19 +605,25 @@
const loadingCheckInit = ref(true);
const regionSearchFocus = ref(-1);
const refreshingMap = ref({});
// 日志相关
const systemLogs = ref([]);
const currentLogTab = ref('action');
const logAutoRefresh = ref(false);
let logInterval = null;
const cronWarning = ref(false);
// 图表相关
const showChartModal = ref(false);
const chartLoading = ref(false);
const chartMode = ref('24h'); // 默认为24小时
const chartMode = ref('24h');
const currentChartId = ref(0);
const currentChartAccount = ref('');
let chartInstance = null;
let currentChartData = {};
// 阿里云地域数据 (...省略...)
// 阿里云地域数据
const aliyunRegions = [
{ id: 'cn-hongkong', name: '中国香港' },
{ id: 'ap-southeast-1', name: '新加坡' },
@@ -652,8 +665,6 @@
Accounts: []
});
// ... (checkInitStatus, performSetup, checkLoginStatus remain same) ...
const checkInitStatus = async () => {
loadingCheckInit.value = true;
try {
@@ -708,7 +719,7 @@
if (data.logged_in) {
isAdmin.value = true;
fetchConfig();
fetchLogs();
startLogPolling(); // 登录后开始轮询日志
}
fetchData();
} catch (e) {
@@ -719,7 +730,6 @@
};
const fetchData = async () => {
// 首次加载才显示全局 loading
if (statusData.value.length === 0) loading.value = true;
try {
const res = await fetch('index.php?action=get_status');
@@ -729,16 +739,11 @@
criticalError.value = json.error;
} else {
statusData.value = json.data || [];
// 检查心跳
if (json.system_last_run) {
const now = Math.floor(Date.now() / 1000);
const diff = now - json.system_last_run;
// 改为 180 秒 (3分钟)
cronWarning.value = diff > 180;
} else {
// 如果从来没运行过,也视为异常 (初始化阶段除外,这里简化判断)
// 如果有数据但没心跳记录,说明可能是旧版升级上来第一次运行
if (statusData.value.length > 0) cronWarning.value = true;
}
}
@@ -760,8 +765,6 @@
const data = await res.json();
if (data.success) {
await fetchData();
} else {
console.error('Refresh failed');
}
} catch (e) {
console.error(e);
@@ -776,14 +779,13 @@
chartLoading.value = true;
currentChartId.value = id;
currentChartAccount.value = accountName;
chartMode.value = '24h'; // 默认24小时
chartMode.value = '24h';
try {
const res = await fetch(`index.php?action=get_history&id=${id}`);
const json = await res.json();
currentChartData = json.data || {};
// 等待 DOM 渲染后初始化图表
setTimeout(() => {
initChart();
renderChart();
@@ -825,7 +827,6 @@
let sData = [];
let is30d = chartMode.value === '30d';
// 数据映射
if (is30d) {
if (currentChartData.history_30d) {
xData = currentChartData.history_30d.map(i => i.date);
@@ -843,7 +844,6 @@
return;
}
// ECharts 配置
const option = {
tooltip: {
trigger: 'axis',
@@ -861,7 +861,7 @@
},
xAxis: {
type: 'category',
boundaryGap: is30d, // 30天柱状图留间隙24h折线图不留
boundaryGap: is30d,
data: xData,
axisLine: { lineStyle: { color: '#E5E7EB' } },
axisLabel: { color: '#6B7280', fontSize: 11 },
@@ -878,7 +878,7 @@
data: sData,
type: is30d ? 'bar' : 'line',
smooth: true,
barMaxWidth: 30, // 限制柱状图最大宽度
barMaxWidth: 30,
itemStyle: {
color: is30d ? new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#1C1C1E' },
@@ -897,12 +897,12 @@
{ offset: 1, color: 'rgba(0,122,255,0.0)' }
])
},
showSymbol: !is30d, // 折线图显示点
showSymbol: !is30d,
symbolSize: 6
}]
};
chartInstance.setOption(option, true); // true = 不合并配置,彻底重绘
chartInstance.setOption(option, true);
};
const closeChart = () => {
@@ -919,6 +919,7 @@
isAdmin.value = false;
config.value = null;
systemLogs.value = [];
stopLogPolling();
});
} else {
showLoginModal.value = true;
@@ -937,9 +938,8 @@
showLoginModal.value = false;
passwordInput.value = '';
fetchConfig();
fetchLogs();
startLogPolling();
} else {
// 修复:显示后端返回的具体错误信息,而不是硬编码的“密码错误”
alert(data.message || '登录失败');
}
} catch(e) {
@@ -970,17 +970,47 @@
} catch(e) { alert('保存请求失败'); }
};
// 新增:获取系统日志
// 获取系统日志 (带Tab参数)
const fetchLogs = async () => {
if (!isAdmin.value) return;
try {
const res = await fetch('index.php?action=get_logs');
const res = await fetch(`index.php?action=get_logs&tab=${currentLogTab.value}`);
const data = await res.json();
if (data.data) {
systemLogs.value = data.data;
}
} catch (e) { console.error('Fetch logs failed', e); }
};
// 清空日志
const clearLogs = async () => {
if(!confirm(`确定清空"${currentLogTab.value === 'action' ? '动作' : '心跳'}"日志吗?`)) return;
try {
const res = await fetch('index.php?action=clear_logs', {
method: 'POST',
body: JSON.stringify({ tab: currentLogTab.value })
});
const data = await res.json();
if (data.success) {
fetchLogs();
}
} catch (e) { console.error(e); }
};
const startLogPolling = () => {
fetchLogs(); // 立即执行一次
logAutoRefresh.value = true;
if (logInterval) clearInterval(logInterval);
logInterval = setInterval(fetchLogs, 3000); // 3秒刷新一次
};
const stopLogPolling = () => {
logAutoRefresh.value = false;
if (logInterval) {
clearInterval(logInterval);
logInterval = null;
}
};
const sendTestEmail = async () => {
if (!config.value || !config.value.Notification.email) { alert('请先填写接收邮箱并保存配置'); return; }
@@ -1008,15 +1038,20 @@
onMounted(() => {
checkInitStatus();
});
onUnmounted(() => {
stopLogPolling();
});
return {
statusData, loading, isAdmin, checkingLogin, showLoginModal, passwordInput, config, initialized, loadingCheckInit, setupData, criticalError,
regionSearchFocus, aliyunRegions, refreshingMap, systemLogs, cronWarning,
showChartModal, chartLoading, chartMode, currentChartAccount, openChart, closeChart, switchChartMode, isChartEmpty,
toggleAdmin, performLogin, saveConfig, sendTestEmail, sendingEmail, addAccount, removeAccount, getInstanceStatusColor, performSetup, refreshSingle, fetchLogs
toggleAdmin, performLogin, saveConfig, sendTestEmail, sendingEmail, addAccount, removeAccount, getInstanceStatusColor, performSetup, refreshSingle, fetchLogs,
currentLogTab, clearLogs, logAutoRefresh
};
}
}).mount('#app');
</script>
</body>
</html>
</html>