342 lines
17 KiB
HTML
342 lines
17 KiB
HTML
{% 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, "'")})'
|
||
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 %} |