ai_v/templates/logs.html

342 lines
17 KiB
HTML
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.

{% extends "base.html" %}
{% block title %}系统审计日志 - AI 视界{% endblock %}
{% block content %}
<div class="w-full h-full overflow-y-auto p-6 lg:p-10 custom-scrollbar bg-slate-50/50">
<div class="max-w-7xl mx-auto space-y-6">
<!-- 头部导航与操作 -->
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6">
<div class="flex items-center gap-5">
<div
class="w-14 h-14 bg-gradient-to-br from-slate-800 to-slate-900 text-white rounded-2xl flex items-center justify-center shadow-2xl ring-4 ring-white">
<i data-lucide="shield-check" class="w-8 h-8"></i>
</div>
<div>
<h1 class="text-3xl font-black text-slate-900 tracking-tight">系统审计日志</h1>
<div class="flex items-center gap-2 mt-1">
<span
class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-bold bg-green-100 text-green-600">
<span class="w-1.5 h-1.5 rounded-full bg-green-500 mr-1.5 animate-pulse"></span>
系统监控中
</span>
<p class="text-slate-400 text-xs font-medium">记录用户关键动作与系统安全审计</p>
</div>
</div>
</div>
<!-- 增强版筛选工具 -->
<div class="flex flex-wrap items-center gap-3">
<div class="relative group">
<i data-lucide="search"
class="w-4 h-4 text-slate-400 absolute left-4 top-1/2 -translate-y-1/2 transition-colors group-focus-within:text-slate-900"></i>
<input type="text" id="logSearch" placeholder="搜索动作、手机号..."
class="bg-white border border-slate-200 rounded-2xl pl-11 pr-4 py-3 text-sm w-64 focus:ring-4 focus:ring-slate-100 focus:border-slate-400 transition-all outline-none shadow-sm"
oninput="debounceLoad()">
</div>
<select id="logLevel"
class="bg-white border border-slate-200 rounded-2xl px-5 py-3 text-sm font-bold text-slate-700 outline-none focus:ring-4 focus:ring-slate-100 shadow-sm appearance-none cursor-pointer"
onchange="resetAndLoad()">
<option value="">全部级别</option>
<option value="INFO" class="text-indigo-600">INFO (常规动作)</option>
<option value="WARNING" class="text-amber-600">WARNING (安全警告)</option>
<option value="ERROR" class="text-rose-600">ERROR (异常拦截)</option>
</select>
<button onclick="loadLogs()"
class="bg-white border border-slate-200 p-3 rounded-2xl hover:bg-slate-50 hover:shadow-md transition-all active:scale-95 shadow-sm">
<i data-lucide="refresh-cw" id="refreshIcon" class="w-5 h-5 text-slate-600"></i>
</button>
</div>
</div>
<!-- 数据表格容器 -->
<div
class="bg-white rounded-[2.5rem] shadow-2xl shadow-slate-200/50 border border-slate-100 overflow-hidden min-h-[500px] flex flex-col">
<div class="flex-grow overflow-x-auto">
<table class="w-full text-left border-separate border-spacing-0">
<thead>
<tr class="bg-slate-50/80 backdrop-blur-md">
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100">
时间</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100">
级别</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100">
操作人</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100">
动作详情</th>
<th
class="px-8 py-5 text-[10px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-100 text-right">
操作</th>
</tr>
</thead>
<tbody id="logTableBody" class="text-sm">
<!-- 骨架屏加载状态 -->
<tr>
<td colspan="5" class="px-8 py-32 text-center">
<div class="flex flex-col items-center gap-4">
<div
class="w-10 h-10 border-4 border-slate-200 border-t-slate-900 rounded-full animate-spin">
</div>
<p class="text-slate-400 font-bold tracking-tight">正在调取审计数据...</p>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页控制栏 -->
<div class="px-8 py-6 bg-slate-50/50 border-t border-slate-100 flex items-center justify-between">
<div class="text-xs font-bold text-slate-400">
<span id="totalCount" class="text-slate-900">0</span> 条记录
</div>
<div class="flex items-center gap-2">
<button id="prevBtn" onclick="changePage(-1)"
class="p-2 rounded-xl border border-slate-200 bg-white hover:bg-slate-50 disabled:opacity-30 disabled:cursor-not-allowed transition-all">
<i data-lucide="chevron-left" class="w-5 h-5"></i>
</button>
<div id="pageNumbers" class="flex items-center gap-1">
<!-- 页码 -->
</div>
<button id="nextBtn" onclick="changePage(1)"
class="p-2 rounded-xl border border-slate-200 bg-white hover:bg-slate-50 disabled:opacity-30 disabled:cursor-not-allowed transition-all">
<i data-lucide="chevron-right" class="w-5 h-5"></i>
</button>
</div>
</div>
</div>
</div>
</div>
<!-- 详情模态框 -->
<div id="logModal"
class="fixed inset-0 z-[100] hidden flex items-center justify-center p-6 backdrop-blur-sm bg-slate-900/20">
<div
class="bg-white rounded-[3rem] shadow-3xl max-w-2xl w-full max-h-[80vh] overflow-hidden flex flex-col animate-in fade-in zoom-in duration-300">
<div class="px-10 py-8 border-b border-slate-100 flex items-center justify-between bg-slate-50/30">
<div class="flex items-center gap-4">
<div id="modalLevelIcon" class="w-10 h-10 rounded-xl flex items-center justify-center"></div>
<div>
<h3 class="text-xl font-black text-slate-900">日志详细参数</h3>
<p id="modalTime" class="text-slate-400 text-xs font-mono"></p>
</div>
</div>
<button onclick="closeModal()"
class="w-10 h-10 rounded-full hover:bg-slate-200/50 flex items-center justify-center transition-colors">
<i data-lucide="x" class="w-6 h-6 text-slate-400"></i>
</button>
</div>
<div class="p-10 overflow-y-auto custom-scrollbar flex-grow">
<div id="modalMessage" class="text-lg font-bold text-slate-800 mb-8 border-l-4 border-slate-900 pl-6 py-2">
</div>
<!-- 更多请求详情 -->
<div class="grid grid-cols-2 gap-4 mb-8">
<div class="bg-slate-50 p-4 rounded-2xl">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">操作账户</p>
<p id="modalUser" class="text-sm font-bold text-slate-700"></p>
</div>
<div class="bg-slate-50 p-4 rounded-2xl">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">访问 IP</p>
<p id="modalIP" class="text-sm font-mono text-slate-700"></p>
</div>
<div class="bg-slate-50 p-4 rounded-2xl">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">接口路径</p>
<p id="modalPath" class="text-sm font-mono text-slate-700"></p>
</div>
<div class="bg-slate-50 p-4 rounded-2xl">
<p class="text-[10px] font-black text-slate-400 uppercase tracking-widest mb-1">操作模块</p>
<p id="modalModule" class="text-sm font-bold text-slate-700"></p>
</div>
</div>
<div class="space-y-4">
<h4 class="text-[10px] font-black text-slate-400 uppercase tracking-widest">关键上下文 Data</h4>
<div id="modalExtra"
class="bg-slate-900 rounded-3xl p-8 font-mono text-sm text-indigo-300 break-all whitespace-pre-wrap leading-relaxed shadow-inner">
<!-- JSON 详情 -->
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
let currentPage = 1;
let totalPages = 1;
let is_loading = false;
let debounceTimer;
function debounceLoad() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => resetAndLoad(), 500);
}
function resetAndLoad() {
currentPage = 1;
loadLogs();
}
function changePage(delta) {
if (is_loading) return;
const targetPage = currentPage + delta;
if (targetPage >= 1 && targetPage <= totalPages) {
currentPage = targetPage;
loadLogs();
}
}
function goToPage(p) {
if (currentPage === p || is_loading) return;
currentPage = p;
loadLogs();
}
async function loadLogs() {
if (is_loading) return;
is_loading = true;
const refreshIcon = document.getElementById('refreshIcon');
refreshIcon?.classList.add('animate-spin');
const search = document.getElementById('logSearch')?.value || '';
const level = document.getElementById('logLevel')?.value || '';
const url = `/api/auth/logs?search=${encodeURIComponent(search)}&level=${level}&page=${currentPage}&per_page=15`;
try {
const r = await fetch(url);
const d = await r.json();
const body = document.getElementById('logTableBody');
if (d.error) {
body.innerHTML = `<tr><td colspan="5" class="px-8 py-32 text-center text-rose-500 font-bold">${d.error}</td></tr>`;
return;
}
// 更新页信息
totalPages = d.total_pages;
document.getElementById('totalCount').innerText = d.total;
document.getElementById('prevBtn').disabled = currentPage <= 1;
document.getElementById('nextBtn').disabled = currentPage >= totalPages;
// 渲染页码按钮
renderPagination(d.page, d.total_pages);
if (d.logs.length === 0) {
body.innerHTML = `<tr><td colspan="5" class="px-8 py-32 text-center"><div class="flex flex-col items-center gap-3 opacity-30"><i data-lucide="inbox" class="w-12 h-12"></i><p class="font-bold">未找到匹配的审计记录</p></div></td></tr>`;
lucide.createIcons();
return;
}
body.innerHTML = d.logs.map(log => `
<tr class="group hover:bg-slate-50/50 transition-all cursor-default">
<td class="px-8 py-6 text-slate-400 font-mono text-[11px] border-b border-slate-50">${log.time}</td>
<td class="px-8 py-6 border-b border-slate-50">
<span class="inline-flex items-center justify-center px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-wider whitespace-nowrap ${log.level === 'INFO' ? 'bg-indigo-50 text-indigo-600' :
log.level === 'WARNING' ? 'bg-amber-50 text-amber-600' : 'bg-rose-50 text-rose-600'
}">${log.level === 'INFO' ? '常规操作' : log.level === 'WARNING' ? '安全警告' : '异常拦截'}</span>
</td>
<td class="px-8 py-6 border-b border-slate-50">
<div class="flex flex-col">
<span class="text-slate-900 font-bold text-xs">${log.user_phone}</span>
<span class="text-slate-400 text-[10px] font-mono">${log.ip || 'Unknown IP'}</span>
</div>
</td>
<td class="px-8 py-6 border-b border-slate-50">
<div class="flex flex-col gap-1">
<span class="text-slate-900 font-black tracking-tight text-sm">${log.message}</span>
<span class="text-slate-400 text-[10px] font-medium opacity-0 group-hover:opacity-100 transition-opacity">
路径: ${log.method} ${log.path} | 模块: ${log.module || 'system'}
</span>
</div>
</td>
<td class="px-8 py-6 text-right border-b border-slate-50">
<button onclick='showDetails(${JSON.stringify(log).replace(/'/g, "&apos;")})'
class="inline-flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-100 text-slate-600 text-[11px] font-black hover:bg-slate-900 hover:text-white transition-all">
查看详情 <i data-lucide="chevron-right" class="w-3.5 h-3.5"></i>
</button>
</td>
</tr>
`).join('');
lucide.createIcons();
} catch (e) {
console.error(e);
} finally {
is_loading = false;
refreshIcon?.classList.remove('animate-spin');
}
}
function renderPagination(current, total) {
const wrapper = document.getElementById('pageNumbers');
let html = '';
// 简单的分页逻辑显示当前页及前后2页
for (let i = Math.max(1, current - 2); i <= Math.min(total, current + 2); i++) {
html += `<button onclick="goToPage(${i})" class="w-10 h-10 rounded-xl font-bold text-xs transition-all ${i === current ? 'bg-slate-900 text-white shadow-lg' : 'bg-white text-slate-400 hover:text-slate-900 border border-slate-200'
}">${i}</button>`;
}
wrapper.innerHTML = html;
}
function showDetails(log) {
const modal = document.getElementById('logModal');
const iconWrap = document.getElementById('modalLevelIcon');
document.getElementById('modalTime').innerText = log.time;
document.getElementById('modalMessage').innerText = log.message;
document.getElementById('modalExtra').innerText = JSON.stringify(log.extra, null, 4);
// 填充新增的详细信息
document.getElementById('modalUser').innerText = log.user_phone || '系统/游客';
document.getElementById('modalIP').innerText = log.ip || 'Unknown';
document.getElementById('modalPath').innerText = (log.method || '') + ' ' + (log.path || '');
document.getElementById('modalModule').innerText = log.module || 'system';
if (log.level === 'INFO') {
iconWrap.className = 'w-10 h-10 rounded-xl flex items-center justify-center bg-indigo-50 text-indigo-600';
iconWrap.innerHTML = '<i data-lucide="info" class="w-6 h-6"></i>';
} else if (log.level === 'WARNING') {
iconWrap.className = 'w-10 h-10 rounded-xl flex items-center justify-center bg-amber-50 text-amber-600';
iconWrap.innerHTML = '<i data-lucide="alert-triangle" class="w-6 h-6"></i>';
} else {
iconWrap.className = 'w-10 h-10 rounded-xl flex items-center justify-center bg-rose-50 text-rose-600';
iconWrap.innerHTML = '<i data-lucide="x-circle" class="w-6 h-6"></i>';
}
modal.classList.remove('hidden');
modal.classList.add('flex');
lucide.createIcons();
document.body.style.overflow = 'hidden';
}
function closeModal() {
const modal = document.getElementById('logModal');
modal.classList.add('hidden');
modal.classList.remove('flex');
document.body.style.overflow = '';
}
// 点击外侧关闭
document.getElementById('logModal').onclick = (e) => {
if (e.target === document.getElementById('logModal')) closeModal();
}
// 初始化加载
loadLogs();
// 自动刷新逻辑如果搜索框为空且在第一页则每10秒刷新一次
setInterval(() => {
const search = document.getElementById('logSearch')?.value || '';
if (!search && currentPage === 1) loadLogs();
}, 10000);
</script>
{% endblock %}