VPSHUB/view_log.php
2026-08-25 00:07:26 +08:00

314 lines
11 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
require_once __DIR__ . '/app/logger.php';
require_once __DIR__ . '/app/auth.php';
// 加载API_PASS未设置则跳转到配置页
if (!file_exists(__DIR__ . '/app/pass.php')) {
header('Location: config_add.php');
exit;
}
include __DIR__ . '/app/pass.php';
// 统一会话认证前端输入密码验证后建立会话30分钟滑动过期
requireAuth();
// 处理操作
$action = $_GET['action'] ?? '';
if ($action === 'clear' && $_SERVER['REQUEST_METHOD'] === 'POST') {
Logger::clear();
header('Location: view_log.php?cleared=1');
exit;
}
// 读取日志文件
$logFile = Logger::getLogFile();
$logContent = '';
$logLines = [];
if (file_exists($logFile)) {
$logContent = file_get_contents($logFile);
$logLines = array_filter(explode("\n", trim($logContent)));
// 反转数组,最新的在前面
$logLines = array_reverse($logLines);
}
// 过滤级别
$filterLevel = $_GET['level'] ?? 'ALL';
$filteredLines = $logLines;
if ($filterLevel !== 'ALL') {
$filteredLines = array_filter($logLines, function($line) use ($filterLevel) {
return strpos($line, "[{$filterLevel}]") !== false;
});
}
// 搜索关键词
$search = $_GET['search'] ?? '';
if ($search) {
$filteredLines = array_filter($filteredLines, function($line) use ($search) {
return stripos($line, $search) !== false;
});
}
// 限制显示行数
$limit = intval($_GET['limit'] ?? 100);
$filteredLines = array_slice($filteredLines, 0, $limit);
$cleared = isset($_GET['cleared']);
?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VPSHUB - 日志查看器</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #f0f2f5;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 24px;
}
.controls {
padding: 20px 30px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
display: flex;
gap: 15px;
flex-wrap: wrap;
align-items: center;
}
.controls select,
.controls input,
.controls button {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.controls input {
flex: 1;
min-width: 200px;
}
.controls button {
background: #007bff;
color: white;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.controls button:hover {
background: #0056b3;
}
.controls button.danger {
background: #dc3545;
}
.controls button.danger:hover {
background: #c82333;
}
.stats {
padding: 15px 30px;
background: #fff3cd;
border-bottom: 1px solid #ffc107;
display: flex;
gap: 20px;
font-size: 14px;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.log-container {
padding: 20px 30px;
max-height: 70vh;
overflow-y: auto;
}
.log-line {
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.5;
word-break: break-all;
}
.log-line.INFO {
background: #d1ecf1;
border-left: 3px solid #17a2b8;
}
.log-line.WARNING {
background: #fff3cd;
border-left: 3px solid #ffc107;
}
.log-line.ERROR {
background: #f8d7da;
border-left: 3px solid #dc3545;
}
.log-line.DEBUG {
background: #e2e3e5;
border-left: 3px solid #6c757d;
}
.timestamp {
color: #6c757d;
font-weight: bold;
}
.level {
display: inline-block;
padding: 2px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
margin: 0 8px;
}
.level.INFO { background: #17a2b8; color: white; }
.level.WARNING { background: #ffc107; color: black; }
.level.ERROR { background: #dc3545; color: white; }
.level.DEBUG { background: #6c757d; color: white; }
.source {
color: #495057;
font-style: italic;
}
.message {
color: #212529;
}
.empty-state {
text-align: center;
padding: 60px 20px;
color: #6c757d;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 20px;
opacity: 0.3;
}
.alert {
padding: 12px 20px;
margin: 20px 30px 0;
border-radius: 4px;
background: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📋 VPSHUB 日志查看器</h1>
<div>
<span style="font-size: 14px; opacity: 0.9;">
<?php echo date('Y-m-d H:i:s'); ?>
</span>
</div>
</div>
<?php if ($cleared): ?>
<div class="alert">✅ 日志已清空</div>
<?php endif; ?>
<div class="controls">
<select name="level" onchange="window.location.href='view_log.php?level=' + this.value + '&search=<?php echo urlencode($search); ?>&limit=<?php echo $limit; ?>'">
<option value="ALL" <?php echo $filterLevel === 'ALL' ? 'selected' : ''; ?>>全部级别</option>
<option value="INFO" <?php echo $filterLevel === 'INFO' ? 'selected' : ''; ?>>INFO</option>
<option value="WARNING" <?php echo $filterLevel === 'WARNING' ? 'selected' : ''; ?>>WARNING</option>
<option value="ERROR" <?php echo $filterLevel === 'ERROR' ? 'selected' : ''; ?>>ERROR</option>
<option value="DEBUG" <?php echo $filterLevel === 'DEBUG' ? 'selected' : ''; ?>>DEBUG</option>
</select>
<form method="GET" style="flex: 1; display: flex; gap: 10px;">
<input type="hidden" name="level" value="<?php echo htmlspecialchars($filterLevel); ?>">
<input type="text" name="search" placeholder="搜索日志..." value="<?php echo htmlspecialchars($search); ?>">
<button type="submit">🔍 搜索</button>
</form>
<select onchange="window.location.href='view_log.php?level=<?php echo urlencode($filterLevel); ?>&search=<?php echo urlencode($search); ?>&limit=' + this.value">
<option value="50" <?php echo $limit === 50 ? 'selected' : ''; ?>>50行</option>
<option value="100" <?php echo $limit === 100 ? 'selected' : ''; ?>>100行</option>
<option value="200" <?php echo $limit === 200 ? 'selected' : ''; ?>>200行</option>
<option value="500" <?php echo $limit === 500 ? 'selected' : ''; ?>>500行</option>
<option value="1000" <?php echo $limit === 1000 ? 'selected' : ''; ?>>1000行</option>
</select>
<form method="POST" action="view_log.php?action=clear" onsubmit="return confirm('确定要清空所有日志吗?此操作不可恢复!');">
<button type="submit" class="danger">🗑️ 清空日志</button>
</form>
<button onclick="location.reload()">🔄 刷新</button>
</div>
<div class="stats">
<div class="stat-item">
<strong>总行数:</strong> <?php echo count($logLines); ?>
</div>
<div class="stat-item">
<strong>显示:</strong> <?php echo count($filteredLines); ?>
</div>
<div class="stat-item">
<strong>文件大小:</strong> <?php echo number_format(filesize($logFile) / 1024, 2); ?> KB
</div>
</div>
<div class="log-container">
<?php if (empty($filteredLines)): ?>
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>
</svg>
<h3>暂无日志记录</h3>
<p>当系统运行时,日志将显示在这里</p>
</div>
<?php else: ?>
<?php foreach ($filteredLines as $line): ?>
<?php
// 解析日志行
preg_match('/\[(.*?)\] \[(.*?)\] \[(.*?)\] (.*)/', $line, $matches);
if ($matches) {
$timestamp = $matches[1];
$level = $matches[2];
$source = $matches[3];
$message = $matches[4];
} else {
$timestamp = '';
$level = 'INFO';
$source = '';
$message = $line;
}
?>
<div class="log-line <?php echo htmlspecialchars($level); ?>">
<?php if ($timestamp): ?>
<span class="timestamp">[<?php echo htmlspecialchars($timestamp); ?>]</span>
<?php endif; ?>
<span class="level <?php echo htmlspecialchars($level); ?>"><?php echo htmlspecialchars($level); ?></span>
<?php if ($source): ?>
<span class="source">[<?php echo htmlspecialchars($source); ?>]</span>
<?php endif; ?>
<span class="message"><?php echo htmlspecialchars($message); ?></span>
</div>
<?php endforeach; ?>
<?php endif; ?>
</div>
</div>
</body>
</html>