2026-01-12 00:53:31 +08:00
{% extends "base.html" %}
2026-01-16 22:24:14 +08:00
{% block title %}系统审计日志 - AI 视界{% endblock %}
2026-01-12 00:53:31 +08:00
{% block content %}
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / div >
< div >
< h1 class = "text-3xl font-black text-slate-900 tracking-tight" > 系统审计日志< / h1 >
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / div >
< / div >
2026-01-16 22:24:14 +08:00
<!-- 增强版筛选工具 -->
< 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()">
2026-01-12 00:53:31 +08:00
< / div >
2026-01-16 22:24:14 +08:00
< 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()">
2026-01-12 00:53:31 +08:00
< option value = "" > 全部级别< / option >
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / select >
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / button >
< / div >
< / div >
2026-01-16 22:24:14 +08:00
<!-- 数据表格容器 -->
< 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" >
2026-01-12 00:53:31 +08:00
< thead >
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / tr >
< / thead >
2026-01-16 22:24:14 +08:00
< tbody id = "logTableBody" class = "text-sm" >
<!-- 骨架屏加载状态 -->
2026-01-12 00:53:31 +08:00
< tr >
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / tr >
< / tbody >
< / table >
< / div >
2026-01-16 22:24:14 +08:00
<!-- 分页控制栏 -->
< 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 >
2026-01-12 00:53:31 +08:00
< / div >
< / div >
< / div >
{% endblock %}
{% block scripts %}
< script >
2026-01-16 22:24:14 +08:00
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();
}
2026-01-12 00:53:31 +08:00
async function loadLogs() {
2026-01-16 22:24:14 +08:00
if (is_loading) return;
is_loading = true;
const refreshIcon = document.getElementById('refreshIcon');
refreshIcon?.classList.add('animate-spin');
2026-01-12 00:53:31 +08:00
const search = document.getElementById('logSearch')?.value || '';
const level = document.getElementById('logLevel')?.value || '';
2026-01-16 22:24:14 +08:00
const url = `/api/auth/logs?search=${encodeURIComponent(search)}&level=${level}&page=${currentPage}&per_page=15`;
2026-01-12 00:53:31 +08:00
try {
const r = await fetch(url);
const d = await r.json();
const body = document.getElementById('logTableBody');
2026-01-16 22:24:14 +08:00
2026-01-12 00:53:31 +08:00
if (d.error) {
2026-01-16 22:24:14 +08:00
body.innerHTML = `< tr > < td colspan = "5" class = "px-8 py-32 text-center text-rose-500 font-bold" > ${d.error}< / td > < / tr > `;
2026-01-12 00:53:31 +08:00
return;
}
2026-01-16 22:24:14 +08:00
// 更新页信息
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);
2026-01-12 00:53:31 +08:00
if (d.logs.length === 0) {
2026-01-16 22:24:14 +08:00
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();
2026-01-12 00:53:31 +08:00
return;
}
body.innerHTML = d.logs.map(log => `
2026-01-16 22:24:14 +08:00
< 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 = "px-2.5 py-1 rounded-lg text- [ 10px ] font-black uppercase tracking-tight $ { 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 >
2026-01-12 00:53:31 +08:00
< / td >
2026-01-16 22:24:14 +08:00
< 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 >
2026-01-12 00:53:31 +08:00
< / td >
< / tr >
`).join('');
2026-01-16 22:24:14 +08:00
lucide.createIcons();
2026-01-12 00:53:31 +08:00
} catch (e) {
console.error(e);
2026-01-16 22:24:14 +08:00
} finally {
is_loading = false;
refreshIcon?.classList.remove('animate-spin');
2026-01-12 00:53:31 +08:00
}
}
2026-01-16 22:24:14 +08:00
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();
}
2026-01-12 00:53:31 +08:00
// 初始化加载
loadLogs();
2026-01-16 22:24:14 +08:00
// 自动刷新逻辑: 如果搜索框为空且在第一页, 则每10秒刷新一次
2026-01-12 00:53:31 +08:00
setInterval(() => {
const search = document.getElementById('logSearch')?.value || '';
2026-01-16 22:24:14 +08:00
if (!search & & currentPage === 1) loadLogs();
}, 10000);
2026-01-12 00:53:31 +08:00
< / script >
{% endblock %}