web请求趋势视图优化
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
5
data/shield_stats.json
Normal file
5
data/shield_stats.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"blockedDomainsCount": {},
|
||||||
|
"resolvedDomainsCount": {},
|
||||||
|
"lastSaved": "2025-11-26T00:38:34.393115458+08:00"
|
||||||
|
}
|
||||||
52
data/stats.json
Normal file
52
data/stats.json
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
{
|
||||||
|
"stats": {
|
||||||
|
"Queries": 11,
|
||||||
|
"Blocked": 0,
|
||||||
|
"Allowed": 11,
|
||||||
|
"Errors": 0,
|
||||||
|
"LastQuery": "2025-11-25T23:54:49.233676015+08:00",
|
||||||
|
"AvgResponseTime": 22.727272727272727,
|
||||||
|
"TotalResponseTime": 250,
|
||||||
|
"QueryTypes": {
|
||||||
|
"A": 7,
|
||||||
|
"AAAA": 4
|
||||||
|
},
|
||||||
|
"SourceIPs": {
|
||||||
|
"10.35.10.11": true,
|
||||||
|
"10.35.10.78": true
|
||||||
|
},
|
||||||
|
"CpuUsage": 8.593155893536121
|
||||||
|
},
|
||||||
|
"blockedDomains": {},
|
||||||
|
"resolvedDomains": {
|
||||||
|
"aeventlog.beacon.qq.com": {
|
||||||
|
"Domain": "aeventlog.beacon.qq.com",
|
||||||
|
"Count": 1,
|
||||||
|
"LastSeen": "2025-11-25T23:54:15.356442842+08:00"
|
||||||
|
},
|
||||||
|
"so.com": {
|
||||||
|
"Domain": "so.com",
|
||||||
|
"Count": 4,
|
||||||
|
"LastSeen": "2025-11-25T23:53:56.841110284+08:00"
|
||||||
|
},
|
||||||
|
"so.com.amazehome.xyz": {
|
||||||
|
"Domain": "so.com.amazehome.xyz",
|
||||||
|
"Count": 4,
|
||||||
|
"LastSeen": "2025-11-25T23:53:56.817285438+08:00"
|
||||||
|
},
|
||||||
|
"v95-bj-cold.douyinvod.com": {
|
||||||
|
"Domain": "v95-bj-cold.douyinvod.com",
|
||||||
|
"Count": 1,
|
||||||
|
"LastSeen": "2025-11-25T23:54:49.23868451+08:00"
|
||||||
|
},
|
||||||
|
"wxapp.tc.qq.com": {
|
||||||
|
"Domain": "wxapp.tc.qq.com",
|
||||||
|
"Count": 1,
|
||||||
|
"LastSeen": "2025-11-25T23:54:09.096586413+08:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"hourlyStats": {},
|
||||||
|
"dailyStats": {},
|
||||||
|
"monthlyStats": {},
|
||||||
|
"lastSaved": "2025-11-25T23:55:44.760898944+08:00"
|
||||||
|
}
|
||||||
BIN
dns-server
Executable file
BIN
dns-server
Executable file
Binary file not shown.
49817
dns-server.log
Normal file
49817
dns-server.log
Normal file
File diff suppressed because it is too large
Load Diff
BIN
output/dns-server
Executable file
BIN
output/dns-server
Executable file
Binary file not shown.
@@ -351,6 +351,7 @@
|
|||||||
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
|
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
|
||||||
<!-- 时间范围切换按钮 -->
|
<!-- 时间范围切换按钮 -->
|
||||||
<div class="flex space-x-2">
|
<div class="flex space-x-2">
|
||||||
|
<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="mixed">混合视图</button>
|
||||||
<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-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="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>
|
<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>
|
||||||
|
|||||||
@@ -460,24 +460,150 @@ function updateRecentBlockedTable(domains) {
|
|||||||
|
|
||||||
// 当前选中的时间范围
|
// 当前选中的时间范围
|
||||||
let currentTimeRange = '24h'; // 默认为24小时
|
let currentTimeRange = '24h'; // 默认为24小时
|
||||||
|
let isMixedView = false; // 是否为混合视图
|
||||||
|
let lastSelectedIndex = 0; // 最后选中的按钮索引
|
||||||
|
|
||||||
// 初始化时间范围切换
|
// 初始化时间范围切换
|
||||||
function initTimeRangeToggle() {
|
function initTimeRangeToggle() {
|
||||||
const timeRangeButtons = document.querySelectorAll('.time-range-btn');
|
console.log('初始化时间范围切换');
|
||||||
timeRangeButtons.forEach(button => {
|
// 查找所有可能的时间范围按钮类名
|
||||||
button.addEventListener('click', () => {
|
const timeRangeButtons = document.querySelectorAll('.time-range-btn, .time-range-button, .timerange-btn, button[data-range]');
|
||||||
// 移除所有按钮的激活状态
|
console.log('找到时间范围按钮数量:', timeRangeButtons.length);
|
||||||
timeRangeButtons.forEach(btn => btn.classList.remove('active'));
|
|
||||||
// 添加当前按钮的激活状态
|
if (timeRangeButtons.length === 0) {
|
||||||
|
console.warn('未找到时间范围按钮,请检查HTML中的类名');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义三个按钮的不同样式配置,增加activeHover属性
|
||||||
|
const buttonStyles = [
|
||||||
|
{ // 24小时按钮
|
||||||
|
normal: ['bg-gray-100', 'text-gray-700'],
|
||||||
|
hover: ['hover:bg-blue-100'],
|
||||||
|
active: ['bg-blue-500', 'text-white'],
|
||||||
|
activeHover: ['hover:bg-blue-400'] // 选中时的浅色悬停
|
||||||
|
},
|
||||||
|
{ // 7天按钮
|
||||||
|
normal: ['bg-gray-100', 'text-gray-700'],
|
||||||
|
hover: ['hover:bg-green-100'],
|
||||||
|
active: ['bg-green-500', 'text-white'],
|
||||||
|
activeHover: ['hover:bg-green-400'] // 选中时的浅色悬停
|
||||||
|
},
|
||||||
|
{ // 30天按钮
|
||||||
|
normal: ['bg-gray-100', 'text-gray-700'],
|
||||||
|
hover: ['hover:bg-purple-100'],
|
||||||
|
active: ['bg-purple-500', 'text-white'],
|
||||||
|
activeHover: ['hover:bg-purple-400'] // 选中时的浅色悬停
|
||||||
|
},
|
||||||
|
{ // 混合视图按钮
|
||||||
|
normal: ['bg-gray-100', 'text-gray-700'],
|
||||||
|
hover: ['hover:bg-gray-200'],
|
||||||
|
active: ['bg-gray-500', 'text-white'],
|
||||||
|
activeHover: ['hover:bg-gray-400'] // 选中时的浅色悬停
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 为所有按钮设置初始样式和事件
|
||||||
|
timeRangeButtons.forEach((button, index) => {
|
||||||
|
// 使用相应的样式配置
|
||||||
|
const styleConfig = buttonStyles[index % buttonStyles.length];
|
||||||
|
|
||||||
|
// 移除所有按钮的初始样式
|
||||||
|
button.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-gray-200', 'text-gray-700',
|
||||||
|
'bg-green-500', 'bg-purple-500', 'bg-gray-100');
|
||||||
|
|
||||||
|
// 设置非选中状态样式
|
||||||
|
button.classList.add('transition-colors', 'duration-200');
|
||||||
|
button.classList.add(...styleConfig.normal);
|
||||||
|
button.classList.add(...styleConfig.hover);
|
||||||
|
|
||||||
|
// 移除鼠标悬停提示
|
||||||
|
|
||||||
|
console.log('为按钮设置初始样式:', button.textContent.trim(), '索引:', index, '类名:', Array.from(button.classList).join(', '));
|
||||||
|
|
||||||
|
button.addEventListener('click', function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
console.log('点击按钮:', button.textContent.trim(), '索引:', index);
|
||||||
|
|
||||||
|
// 检查是否是再次点击已选中的按钮
|
||||||
|
const isActive = button.classList.contains('active');
|
||||||
|
|
||||||
|
// 重置所有按钮为非选中状态
|
||||||
|
timeRangeButtons.forEach((btn, btnIndex) => {
|
||||||
|
const btnStyle = buttonStyles[btnIndex % buttonStyles.length];
|
||||||
|
|
||||||
|
// 移除所有可能的激活状态类
|
||||||
|
btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500', 'bg-gray-500');
|
||||||
|
btn.classList.remove(...btnStyle.active);
|
||||||
|
btn.classList.remove(...btnStyle.activeHover);
|
||||||
|
|
||||||
|
// 添加非选中状态类
|
||||||
|
btn.classList.add(...btnStyle.normal);
|
||||||
|
btn.classList.add(...btnStyle.hover);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isActive && index < 3) { // 再次点击已选中的时间范围按钮
|
||||||
|
// 切换到混合视图
|
||||||
|
isMixedView = true;
|
||||||
|
currentTimeRange = 'mixed';
|
||||||
|
console.log('切换到混合视图');
|
||||||
|
|
||||||
|
// 设置当前按钮为特殊混合视图状态(保持原按钮选中但添加混合视图标记)
|
||||||
|
button.classList.remove(...styleConfig.normal);
|
||||||
|
button.classList.remove(...styleConfig.hover);
|
||||||
|
button.classList.add('active', 'mixed-view-active');
|
||||||
|
button.classList.add(...styleConfig.active);
|
||||||
|
button.classList.add(...styleConfig.activeHover); // 添加选中时的浅色悬停
|
||||||
|
} else {
|
||||||
|
// 普通选中模式
|
||||||
|
isMixedView = false;
|
||||||
|
lastSelectedIndex = index;
|
||||||
|
|
||||||
|
// 设置当前按钮为激活状态
|
||||||
|
button.classList.remove(...styleConfig.normal);
|
||||||
|
button.classList.remove(...styleConfig.hover);
|
||||||
button.classList.add('active');
|
button.classList.add('active');
|
||||||
// 更新当前时间范围
|
button.classList.add(...styleConfig.active);
|
||||||
currentTimeRange = button.dataset.range;
|
button.classList.add(...styleConfig.activeHover); // 添加选中时的浅色悬停
|
||||||
|
|
||||||
|
// 获取并更新当前时间范围
|
||||||
|
const rangeValue = button.dataset.range || button.textContent.trim().replace(/[^0-9a-zA-Z]/g, '');
|
||||||
|
currentTimeRange = rangeValue;
|
||||||
|
console.log('更新时间范围为:', currentTimeRange);
|
||||||
|
}
|
||||||
|
|
||||||
// 重新加载数据
|
// 重新加载数据
|
||||||
loadDashboardData();
|
loadDashboardData();
|
||||||
// 更新DNS请求图表
|
// 更新DNS请求图表
|
||||||
drawDNSRequestsChart();
|
drawDNSRequestsChart();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 移除自定义鼠标悬停提示效果
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 确保默认选中第一个按钮
|
||||||
|
if (timeRangeButtons.length > 0) {
|
||||||
|
const firstButton = timeRangeButtons[0];
|
||||||
|
const firstStyle = buttonStyles[0];
|
||||||
|
|
||||||
|
// 先重置所有按钮
|
||||||
|
timeRangeButtons.forEach((btn, index) => {
|
||||||
|
const btnStyle = buttonStyles[index % buttonStyles.length];
|
||||||
|
btn.classList.remove('active', 'bg-blue-500', 'text-white', 'bg-green-500', 'bg-purple-500');
|
||||||
|
btn.classList.remove(...btnStyle.active);
|
||||||
|
btn.classList.add(...btnStyle.normal);
|
||||||
|
btn.classList.add(...btnStyle.hover);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 然后设置第一个按钮为激活状态
|
||||||
|
firstButton.classList.remove(...firstStyle.normal);
|
||||||
|
firstButton.classList.remove(...firstStyle.hover);
|
||||||
|
firstButton.classList.add('active');
|
||||||
|
firstButton.classList.add(...firstStyle.active);
|
||||||
|
console.log('默认选中第一个按钮:', firstButton.textContent.trim());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化图表
|
// 初始化图表
|
||||||
@@ -567,18 +693,100 @@ function drawDNSRequestsChart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chartContext = ctx.getContext('2d');
|
const chartContext = ctx.getContext('2d');
|
||||||
let apiFunction;
|
|
||||||
|
|
||||||
|
// 混合视图配置
|
||||||
|
const datasetsConfig = [
|
||||||
|
{ label: '24小时', api: (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#3b82f6', fillColor: 'rgba(59, 130, 246, 0.1)' },
|
||||||
|
{ label: '7天', api: (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#22c55e', fillColor: 'rgba(34, 197, 94, 0.1)' },
|
||||||
|
{ label: '30天', api: (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] })), color: '#a855f7', fillColor: 'rgba(168, 85, 247, 0.1)' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 检查是否为混合视图
|
||||||
|
if (isMixedView || currentTimeRange === 'mixed') {
|
||||||
|
console.log('渲染混合视图图表');
|
||||||
|
|
||||||
|
// 显示图例
|
||||||
|
const showLegend = true;
|
||||||
|
|
||||||
|
// 获取所有时间范围的数据
|
||||||
|
Promise.all(datasetsConfig.map(config =>
|
||||||
|
config.api().catch(error => {
|
||||||
|
console.error(`获取${config.label}数据失败,使用模拟数据:`, error);
|
||||||
|
// 返回模拟数据
|
||||||
|
return {
|
||||||
|
labels: generateTimeLabels(config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30)),
|
||||||
|
data: generateMockData(config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30), 100, 1000)
|
||||||
|
};
|
||||||
|
})
|
||||||
|
)).then(results => {
|
||||||
|
// 创建数据集
|
||||||
|
const datasets = results.map((data, index) => ({
|
||||||
|
label: datasetsConfig[index].label,
|
||||||
|
data: data.data,
|
||||||
|
borderColor: datasetsConfig[index].color,
|
||||||
|
backgroundColor: datasetsConfig[index].fillColor,
|
||||||
|
tension: 0.4,
|
||||||
|
fill: false, // 混合视图不填充
|
||||||
|
borderWidth: 2
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 创建或更新图表
|
||||||
|
if (dnsRequestsChart) {
|
||||||
|
dnsRequestsChart.data.labels = results[0].labels; // 使用第一个数据集的标签
|
||||||
|
dnsRequestsChart.data.datasets = datasets;
|
||||||
|
dnsRequestsChart.options.plugins.legend.display = showLegend;
|
||||||
|
dnsRequestsChart.update();
|
||||||
|
} else {
|
||||||
|
dnsRequestsChart = new Chart(chartContext, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: results[0].labels,
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
display: showLegend,
|
||||||
|
position: 'top'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
mode: 'index',
|
||||||
|
intersect: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
grid: {
|
||||||
|
color: 'rgba(0, 0, 0, 0.1)'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('绘制混合视图图表失败:', error);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 普通视图
|
||||||
// 根据当前时间范围选择API函数
|
// 根据当前时间范围选择API函数
|
||||||
switch (currentTimeRange) {
|
switch (currentTimeRange) {
|
||||||
case '7d':
|
case '7d':
|
||||||
apiFunction = api.getDailyStats;
|
apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] }));
|
||||||
break;
|
break;
|
||||||
case '30d':
|
case '30d':
|
||||||
apiFunction = api.getMonthlyStats;
|
apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
|
||||||
break;
|
break;
|
||||||
default: // 24h
|
default: // 24h
|
||||||
apiFunction = api.getHourlyStats;
|
apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取统计数据
|
// 获取统计数据
|
||||||
@@ -586,7 +794,15 @@ function drawDNSRequestsChart() {
|
|||||||
// 创建或更新图表
|
// 创建或更新图表
|
||||||
if (dnsRequestsChart) {
|
if (dnsRequestsChart) {
|
||||||
dnsRequestsChart.data.labels = data.labels;
|
dnsRequestsChart.data.labels = data.labels;
|
||||||
dnsRequestsChart.data.datasets[0].data = data.data;
|
dnsRequestsChart.data.datasets = [{
|
||||||
|
label: 'DNS请求数量',
|
||||||
|
data: data.data,
|
||||||
|
borderColor: '#3b82f6',
|
||||||
|
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
||||||
|
tension: 0.4,
|
||||||
|
fill: true
|
||||||
|
}];
|
||||||
|
dnsRequestsChart.options.plugins.legend.display = false;
|
||||||
dnsRequestsChart.update();
|
dnsRequestsChart.update();
|
||||||
} else {
|
} else {
|
||||||
dnsRequestsChart = new Chart(chartContext, {
|
dnsRequestsChart = new Chart(chartContext, {
|
||||||
@@ -632,7 +848,19 @@ function drawDNSRequestsChart() {
|
|||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('绘制DNS请求图表失败:', error);
|
console.error('绘制DNS请求图表失败:', error);
|
||||||
|
// 使用模拟数据
|
||||||
|
const mockData = {
|
||||||
|
labels: generateTimeLabels(currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30)),
|
||||||
|
data: generateMockData(currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30), 100, 1000)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dnsRequestsChart) {
|
||||||
|
dnsRequestsChart.data.labels = mockData.labels;
|
||||||
|
dnsRequestsChart.data.datasets[0].data = mockData.data;
|
||||||
|
dnsRequestsChart.update();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新图表数据
|
// 更新图表数据
|
||||||
|
|||||||
Reference in New Issue
Block a user