实现解析日志查询功能
This commit is contained in:
@@ -59,6 +59,12 @@
|
||||
<span>DNS屏蔽查询</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#logs" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||
<i class="fa fa-file-text-o mr-3 text-lg"></i>
|
||||
<span>查询日志</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#config" class="flex items-center px-4 py-3 text-gray-700 hover:bg-gray-100 rounded-md transition-all">
|
||||
<i class="fa fa-cog mr-3 text-lg"></i>
|
||||
@@ -762,6 +768,168 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 查询日志页面 -->
|
||||
<div id="logs-content" class="hidden space-y-6">
|
||||
<!-- 日志统计概览 -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-4 gap-6">
|
||||
<!-- 总查询数 -->
|
||||
<div class="bg-blue-50 rounded-lg p-4 card-shadow relative overflow-hidden">
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-primary opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">总查询数</h3>
|
||||
<div class="p-2 rounded-full bg-primary/10 text-primary">
|
||||
<i class="fa fa-refresh"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-total-queries">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 平均响应时间 -->
|
||||
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-info opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">平均响应时间</h3>
|
||||
<div class="p-2 rounded-full bg-info/10 text-info">
|
||||
<i class="fa fa-clock-o"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-avg-response-time">0ms</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活跃来源IP -->
|
||||
<div class="bg-white rounded-lg p-4 card-shadow relative overflow-hidden">
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-success opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">活跃来源IP</h3>
|
||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
||||
<i class="fa fa-globe"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-active-ips">0</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 屏蔽率 -->
|
||||
<div class="bg-red-50 rounded-lg p-4 card-shadow relative overflow-hidden">
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-danger opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">屏蔽率</h3>
|
||||
<div class="p-2 rounded-full bg-danger/10 text-danger">
|
||||
<i class="fa fa-ban"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="logs-block-rate">0%</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志搜索和过滤 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex flex-col md:flex-row space-y-4 md:space-y-0 md:space-x-4">
|
||||
<div class="flex-1">
|
||||
<input type="text" id="logs-search" placeholder="搜索域名或客户端IP" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
</div>
|
||||
<div class="w-32">
|
||||
<select id="logs-result-filter" class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<option value="">全部结果</option>
|
||||
<option value="allowed">允许</option>
|
||||
<option value="blocked">屏蔽</option>
|
||||
<option value="error">错误</option>
|
||||
</select>
|
||||
</div>
|
||||
<button id="logs-search-btn" class="px-6 py-3 bg-primary text-white rounded-md hover:bg-primary/90 transition-colors">
|
||||
<i class="fa fa-search mr-2"></i>搜索
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志趋势图表 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold">查询趋势</h3>
|
||||
<div class="flex space-x-2">
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-primary text-white" data-range="24h">24小时</button>
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="7d">7天</button>
|
||||
<button class="time-range-btn px-4 py-2 rounded-md bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors" data-range="30d">30天</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-64">
|
||||
<canvas id="logs-trend-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情表格 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold">查询日志详情</h3>
|
||||
<div id="logs-loading" class="flex items-center text-sm text-gray-500 hidden">
|
||||
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">时间</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">客户端IP</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">查询类型</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">结果</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">响应时间</th>
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">屏蔽规则</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="logs-table-body">
|
||||
<tr>
|
||||
<td colspan="7" class="py-8 text-center text-gray-500">
|
||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||
<div>暂无查询日志</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="flex items-center justify-between mt-6">
|
||||
<div class="text-sm text-gray-500">
|
||||
显示 <span id="logs-current-page">1</span> / <span id="logs-total-pages">1</span> 页
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button id="logs-prev-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
||||
<i class="fa fa-chevron-left"></i>
|
||||
</button>
|
||||
<button id="logs-next-page" class="px-4 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent" disabled>
|
||||
<i class="fa fa-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="config-content" class="hidden">
|
||||
<!-- 系统设置页面内容 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
@@ -860,6 +1028,7 @@
|
||||
<script src="js/shield.js"></script>
|
||||
<script src="js/hosts.js"></script>
|
||||
<script src="js/query.js"></script>
|
||||
<script src="js/logs.js"></script>
|
||||
<script src="js/config.js"></script>
|
||||
|
||||
<!-- 直接渲染滚动列表的静态HTML内容 -->
|
||||
|
||||
359
static/js/logs.js
Normal file
359
static/js/logs.js
Normal file
@@ -0,0 +1,359 @@
|
||||
// logs.js - 查询日志页面功能
|
||||
|
||||
// 全局变量
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let logsPerPage = 20;
|
||||
let currentFilter = '';
|
||||
let currentSearch = '';
|
||||
let logsChart = null;
|
||||
|
||||
// 初始化查询日志页面
|
||||
function initLogsPage() {
|
||||
console.log('初始化查询日志页面');
|
||||
|
||||
// 加载日志统计数据
|
||||
loadLogsStats();
|
||||
|
||||
// 加载日志详情
|
||||
loadLogs();
|
||||
|
||||
// 初始化图表
|
||||
initLogsChart();
|
||||
|
||||
// 绑定事件
|
||||
bindLogsEvents();
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
function bindLogsEvents() {
|
||||
// 搜索按钮
|
||||
const searchBtn = document.getElementById('logs-search-btn');
|
||||
if (searchBtn) {
|
||||
searchBtn.addEventListener('click', () => {
|
||||
currentSearch = document.getElementById('logs-search').value;
|
||||
currentPage = 1;
|
||||
loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索框回车事件
|
||||
const searchInput = document.getElementById('logs-search');
|
||||
if (searchInput) {
|
||||
searchInput.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
currentSearch = searchInput.value;
|
||||
currentPage = 1;
|
||||
loadLogs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 结果过滤
|
||||
const resultFilter = document.getElementById('logs-result-filter');
|
||||
if (resultFilter) {
|
||||
resultFilter.addEventListener('change', () => {
|
||||
currentFilter = resultFilter.value;
|
||||
currentPage = 1;
|
||||
loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
// 分页按钮
|
||||
const prevBtn = document.getElementById('logs-prev-page');
|
||||
const nextBtn = document.getElementById('logs-next-page');
|
||||
|
||||
if (prevBtn) {
|
||||
prevBtn.addEventListener('click', () => {
|
||||
if (currentPage > 1) {
|
||||
currentPage--;
|
||||
loadLogs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (nextBtn) {
|
||||
nextBtn.addEventListener('click', () => {
|
||||
if (currentPage < totalPages) {
|
||||
currentPage++;
|
||||
loadLogs();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 时间范围切换
|
||||
const timeRangeBtns = document.querySelectorAll('.time-range-btn');
|
||||
timeRangeBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
// 更新按钮样式
|
||||
timeRangeBtns.forEach(b => {
|
||||
b.classList.remove('bg-primary', 'text-white');
|
||||
b.classList.add('bg-gray-200', 'text-gray-700');
|
||||
});
|
||||
btn.classList.remove('bg-gray-200', 'text-gray-700');
|
||||
btn.classList.add('bg-primary', 'text-white');
|
||||
|
||||
// 更新图表
|
||||
const range = btn.getAttribute('data-range');
|
||||
updateLogsChart(range);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 加载日志统计数据
|
||||
function loadLogsStats() {
|
||||
fetch('/api/logs/stats')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 更新统计卡片
|
||||
document.getElementById('logs-total-queries').textContent = data.totalQueries;
|
||||
document.getElementById('logs-avg-response-time').textContent = data.avgResponseTime.toFixed(2) + 'ms';
|
||||
document.getElementById('logs-active-ips').textContent = data.activeIPs;
|
||||
|
||||
// 计算屏蔽率
|
||||
const blockRate = data.totalQueries > 0 ? (data.blockedQueries / data.totalQueries * 100).toFixed(1) : '0';
|
||||
document.getElementById('logs-block-rate').textContent = blockRate + '%';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载日志统计数据失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 加载日志详情
|
||||
function loadLogs() {
|
||||
// 显示加载状态
|
||||
const loadingEl = document.getElementById('logs-loading');
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 构建请求URL
|
||||
let url = `/api/logs/query?limit=${logsPerPage}&offset=${(currentPage - 1) * logsPerPage}`;
|
||||
|
||||
// 添加过滤条件
|
||||
if (currentFilter) {
|
||||
url += `&result=${currentFilter}`;
|
||||
}
|
||||
|
||||
// 添加搜索条件
|
||||
if (currentSearch) {
|
||||
url += `&search=${encodeURIComponent(currentSearch)}`;
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 加载日志总数
|
||||
return fetch('/api/logs/count').then(response => response.json()).then(countData => {
|
||||
return { logs: data, count: countData.count };
|
||||
});
|
||||
})
|
||||
.then(result => {
|
||||
const logs = result.logs;
|
||||
const totalLogs = result.count;
|
||||
|
||||
// 计算总页数
|
||||
totalPages = Math.ceil(totalLogs / logsPerPage);
|
||||
|
||||
// 更新日志表格
|
||||
updateLogsTable(logs);
|
||||
|
||||
// 更新分页信息
|
||||
updateLogsPagination();
|
||||
|
||||
// 隐藏加载状态
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('加载日志详情失败:', error);
|
||||
|
||||
// 隐藏加载状态
|
||||
if (loadingEl) {
|
||||
loadingEl.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 更新日志表格
|
||||
function updateLogsTable(logs) {
|
||||
const tableBody = document.getElementById('logs-table-body');
|
||||
if (!tableBody) return;
|
||||
|
||||
// 清空表格
|
||||
tableBody.innerHTML = '';
|
||||
|
||||
if (logs.length === 0) {
|
||||
// 显示空状态
|
||||
const emptyRow = document.createElement('tr');
|
||||
emptyRow.innerHTML = `
|
||||
<td colspan="7" class="py-8 text-center text-gray-500">
|
||||
<i class="fa fa-file-text-o text-4xl mb-2 text-gray-300"></i>
|
||||
<div>暂无查询日志</div>
|
||||
</td>
|
||||
`;
|
||||
tableBody.appendChild(emptyRow);
|
||||
return;
|
||||
}
|
||||
|
||||
// 填充表格
|
||||
logs.forEach(log => {
|
||||
const row = document.createElement('tr');
|
||||
row.className = 'border-b border-gray-100 hover:bg-gray-50 transition-colors';
|
||||
|
||||
// 格式化时间
|
||||
const time = new Date(log.Timestamp);
|
||||
const formattedTime = time.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
|
||||
// 结果样式
|
||||
let resultClass = '';
|
||||
let resultText = '';
|
||||
switch (log.Result) {
|
||||
case 'allowed':
|
||||
resultClass = 'text-success';
|
||||
resultText = '允许';
|
||||
break;
|
||||
case 'blocked':
|
||||
resultClass = 'text-danger';
|
||||
resultText = '屏蔽';
|
||||
break;
|
||||
case 'error':
|
||||
resultClass = 'text-warning';
|
||||
resultText = '错误';
|
||||
break;
|
||||
}
|
||||
|
||||
// 构建行内容
|
||||
row.innerHTML = `
|
||||
<td class="py-3 px-4 text-sm">${formattedTime}</td>
|
||||
<td class="py-3 px-4 text-sm">${log.ClientIP}</td>
|
||||
<td class="py-3 px-4 text-sm font-medium">${log.Domain}</td>
|
||||
<td class="py-3 px-4 text-sm">${log.QueryType}</td>
|
||||
<td class="py-3 px-4 text-sm"><span class="${resultClass}">${resultText}</span></td>
|
||||
<td class="py-3 px-4 text-sm">${log.ResponseTime}ms</td>
|
||||
<td class="py-3 px-4 text-sm text-gray-500">${log.BlockRule || '-'}</td>
|
||||
`;
|
||||
|
||||
tableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新分页信息
|
||||
function updateLogsPagination() {
|
||||
// 更新页码显示
|
||||
document.getElementById('logs-current-page').textContent = currentPage;
|
||||
document.getElementById('logs-total-pages').textContent = totalPages;
|
||||
|
||||
// 更新按钮状态
|
||||
const prevBtn = document.getElementById('logs-prev-page');
|
||||
const nextBtn = document.getElementById('logs-next-page');
|
||||
|
||||
if (prevBtn) {
|
||||
prevBtn.disabled = currentPage === 1;
|
||||
}
|
||||
|
||||
if (nextBtn) {
|
||||
nextBtn.disabled = currentPage === totalPages;
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化日志图表
|
||||
function initLogsChart() {
|
||||
const ctx = document.getElementById('logs-trend-chart');
|
||||
if (!ctx) return;
|
||||
|
||||
// 获取24小时统计数据
|
||||
fetch('/api/hourly-stats')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 创建图表
|
||||
logsChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: data.labels,
|
||||
datasets: [{
|
||||
label: '查询数',
|
||||
data: data.data,
|
||||
borderColor: '#3b82f6',
|
||||
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
position: 'top'
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
ticks: {
|
||||
precision: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('初始化日志图表失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新日志图表
|
||||
function updateLogsChart(range) {
|
||||
if (!logsChart) return;
|
||||
|
||||
let url = '';
|
||||
switch (range) {
|
||||
case '24h':
|
||||
url = '/api/hourly-stats';
|
||||
break;
|
||||
case '7d':
|
||||
url = '/api/daily-stats';
|
||||
break;
|
||||
case '30d':
|
||||
url = '/api/monthly-stats';
|
||||
break;
|
||||
default:
|
||||
url = '/api/hourly-stats';
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 更新图表数据
|
||||
logsChart.data.labels = data.labels;
|
||||
logsChart.data.datasets[0].data = data.data;
|
||||
logsChart.update();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('更新日志图表失败:', error);
|
||||
});
|
||||
}
|
||||
|
||||
// 定期更新日志数据
|
||||
setInterval(() => {
|
||||
// 只有在查询日志页面时才更新
|
||||
if (window.location.hash === '#logs') {
|
||||
loadLogsStats();
|
||||
loadLogs();
|
||||
}
|
||||
}, 30000); // 每30秒更新一次
|
||||
@@ -9,6 +9,7 @@ function setupNavigation() {
|
||||
document.getElementById('shield-content'),
|
||||
document.getElementById('hosts-content'),
|
||||
document.getElementById('query-content'),
|
||||
document.getElementById('logs-content'),
|
||||
document.getElementById('config-content')
|
||||
];
|
||||
const pageTitle = document.getElementById('page-title');
|
||||
@@ -28,6 +29,8 @@ function setupNavigation() {
|
||||
initShieldPage();
|
||||
} else if (target === 'hosts' && typeof initHostsPage === 'function') {
|
||||
initHostsPage();
|
||||
} else if (target === 'logs' && typeof initLogsPage === 'function') {
|
||||
initLogsPage();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user