157 lines
7.2 KiB
HTML
157 lines
7.2 KiB
HTML
<!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/ui-components.css?v=3">
|
|
<link rel="stylesheet" href="/static/css/admin.css">
|
|
</head>
|
|
|
|
<body class="admin-layout">
|
|
<header class="admin-header">
|
|
<div class="header-container">
|
|
<a href="/admin/dashboard" class="brand">
|
|
<span style="font-size: 1.5rem;">⚡</span> JieXi Admin
|
|
</a>
|
|
<nav class="nav-links">
|
|
<a href="/admin/dashboard" class="nav-item">仪表板</a>
|
|
<a href="/admin/users" class="nav-item">用户管理</a>
|
|
<a href="/admin/apis" class="nav-item">接口管理</a>
|
|
<a href="/admin/config" class="nav-item">系统配置</a>
|
|
<a href="/admin/logs" class="nav-item active">日志审计</a>
|
|
</nav>
|
|
<div class="user-actions">
|
|
<a href="/admin/profile" class="ui-btn ui-btn-secondary ui-btn-sm">账号设置</a>
|
|
<button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="logout()">退出登录</button>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<main class="main-container">
|
|
<div class="page-header">
|
|
<h1 class="page-title">日志审计</h1>
|
|
<div class="actions flex gap-2">
|
|
<select id="platformFilter" class="ui-input" style="width: auto; margin-bottom: 0;"
|
|
onchange="loadLogs(1)">
|
|
<option value="">全部平台</option>
|
|
<option value="douyin">抖音</option>
|
|
<option value="tiktok">TikTok</option>
|
|
<option value="bilibili">哔哩哔哩</option>
|
|
</select>
|
|
<select id="statusFilter" class="ui-input" style="width: auto; margin-bottom: 0;"
|
|
onchange="loadLogs(1)">
|
|
<option value="">全部状态</option>
|
|
<option value="success">成功</option>
|
|
<option value="failed">失败</option>
|
|
<option value="queued">排队中</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ui-card">
|
|
<div class="table-container">
|
|
<table id="logsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>时间</th>
|
|
<th>用户ID</th>
|
|
<th>IP地址</th>
|
|
<th>平台</th>
|
|
<th>视频链接</th>
|
|
<th>状态</th>
|
|
<th>响应时间</th>
|
|
<th>错误信息</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr>
|
|
<td colspan="9" style="text-align: center; color: var(--text-muted);">加载中...</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="pagination mt-4 flex justify-between items-center" id="pagination"></div>
|
|
</div>
|
|
</main>
|
|
|
|
<script src="/static/js/ui-components.js"></script>
|
|
<script>
|
|
let currentPage = 1;
|
|
|
|
async function loadLogs(page = 1) {
|
|
const platform = document.getElementById('platformFilter').value;
|
|
const status = document.getElementById('statusFilter').value;
|
|
|
|
let url = `/admin/api/logs?page=${page}&per_page=50`;
|
|
if (platform) url += `&platform=${platform}`;
|
|
if (status) url += `&status=${status}`;
|
|
|
|
try {
|
|
const response = await fetch(url);
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
const tbody = document.querySelector('#logsTable tbody');
|
|
if (result.data.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="9" style="text-align: center; padding: 2rem; color: var(--text-muted);">暂无日志记录</td></tr>';
|
|
} else {
|
|
tbody.innerHTML = result.data.map(log => {
|
|
let badgeClass = 'badge-neutral';
|
|
if (log.status === 'success') badgeClass = 'badge-success';
|
|
else if (log.status === 'failed') badgeClass = 'badge-error';
|
|
else if (log.status === 'queued') badgeClass = 'badge-warning';
|
|
|
|
return `
|
|
<tr>
|
|
<td>#${log.id}</td>
|
|
<td class="text-sm text-muted">${new Date(log.created_at).toLocaleString('zh-CN')}</td>
|
|
<td>${log.user_id || '<span class="text-muted">游客</span>'}</td>
|
|
<td class="text-sm">${log.ip_address}</td>
|
|
<td><span class="font-medium">${log.platform}</span></td>
|
|
<td><button class="ui-btn ui-btn-secondary ui-btn-sm" onclick="copyUrl('${log.video_url}')" title="${log.video_url}">复制链接</button></td>
|
|
<td><span class="badge ${badgeClass}">${log.status}</span></td>
|
|
<td class="text-sm">${log.response_time ? (log.response_time / 1000).toFixed(2) + 's' : '-'}</td>
|
|
<td class="text-sm text-muted" style="max-width: 150px; overflow: hidden; text-overflow: ellipsis;" title="${log.error_message || ''}">${log.error_message || '-'}</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
const pagination = document.getElementById('pagination');
|
|
pagination.innerHTML = `
|
|
<button class="ui-btn ui-btn-secondary ui-btn-sm" ${page === 1 ? 'disabled' : ''} onclick="loadLogs(${page - 1})">上一页</button>
|
|
<span class="text-sm text-muted">第 ${page} / ${result.pagination.pages} 页 (共 ${result.pagination.total} 条)</span>
|
|
<button class="ui-btn ui-btn-secondary ui-btn-sm" ${page === result.pagination.pages ? 'disabled' : ''} onclick="loadLogs(${page + 1})">下一页</button>
|
|
`;
|
|
currentPage = page;
|
|
}
|
|
} catch (error) {
|
|
UI.notify('加载失败: ' + error.message, 'error');
|
|
}
|
|
}
|
|
|
|
function copyUrl(url) {
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
UI.notify('链接已复制', 'success');
|
|
}).catch(() => {
|
|
UI.notify('复制失败', 'error');
|
|
});
|
|
}
|
|
|
|
async function logout() {
|
|
try {
|
|
await fetch('/admin/logout', { method: 'POST' });
|
|
window.location.href = '/admin/login';
|
|
} catch (error) {
|
|
UI.notify('退出失败', 'error');
|
|
}
|
|
}
|
|
|
|
loadLogs();
|
|
</script>
|
|
</body>
|
|
|
|
</html> |