点击展开按钮悬浮窗显示详细24小时/7天/30天详细信息

This commit is contained in:
Alex Yang
2025-11-26 15:16:55 +08:00
parent 30ddd53f19
commit 3ee31047e9
6 changed files with 60299 additions and 1608 deletions

59660
dns-server.log Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

1574
index.html

File diff suppressed because it is too large Load Diff

69
main.go
View File

@@ -16,12 +16,26 @@ import (
)
func main() {
// 解析命令行参数
configPath := flag.String("config", "config.json", "配置文件路径")
// 命令行参数解析
var configFile string
var daemonMode bool
flag.StringVar(&configFile, "config", "config.json", "配置文件路径")
flag.BoolVar(&daemonMode, "daemon", false, "以守护进程模式运行")
flag.Parse()
// 如果是守护进程模式,创建守护进程
if daemonMode {
if err := daemonize(); err != nil {
log.Fatalf("创建守护进程失败: %v", err)
}
// 父进程退出
os.Exit(0)
}
// 初始化配置
cfg, err := config.LoadConfig(*configPath)
var cfg *config.Config
var err error
cfg, err = config.LoadConfig(configFile)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
@@ -61,14 +75,51 @@ func main() {
logger.Info(fmt.Sprintf("DNS服务器已启动监听端口: %d", cfg.DNS.Port))
logger.Info(fmt.Sprintf("HTTP控制台已启动监听端口: %d", cfg.HTTP.Port))
// 等待退出信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
// 监听信号
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
logger.Info("正在关闭服务...")
// 清理资源
log.Println("正在关闭服务...")
dnsServer.Stop()
httpServer.Stop()
shieldManager.StopAutoUpdate()
logger.Info("所有服务已关闭")
// 守护进程模式下不需要删除PID文件
log.Println("服务已关闭")
}
// daemonize 创建守护进程
func daemonize() error {
// 使用更简单的方式创建守护进程:直接在当前进程中进行守护化处理
// 1. 重定向标准输入、输出、错误
nullFile, err := os.OpenFile("/dev/null", os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("打开/dev/null失败: %w", err)
}
defer nullFile.Close()
// 重定向文件描述符
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdin.Fd()))
if err != nil {
return fmt.Errorf("重定向stdin失败: %w", err)
}
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stdout.Fd()))
if err != nil {
return fmt.Errorf("重定向stdout失败: %w", err)
}
err = syscall.Dup2(int(nullFile.Fd()), int(os.Stderr.Fd()))
if err != nil {
return fmt.Errorf("重定向stderr失败: %w", err)
}
// 2. 创建新的会话和进程组
_, err = syscall.Setsid()
if err != nil {
return fmt.Errorf("创建新会话失败: %w", err)
}
fmt.Println("守护进程已启动")
return nil
}

View File

@@ -511,38 +511,60 @@
<!-- 图表和数据表格 -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- DNS请求趋势图表 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
<!-- 时间范围切换按钮 -->
<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-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-80">
<canvas id="dns-requests-chart"></canvas>
</div>
</div>
<!-- 解析与屏蔽比例 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<!-- 三个图表在同一行显示 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<h3 class="text-lg font-semibold mb-6">解析与屏蔽比例</h3>
<div class="h-80 flex items-center justify-center">
<div class="h-64 flex items-center justify-center">
<canvas id="ratio-chart"></canvas>
</div>
</div>
<!-- 解析类型统计 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<h3 class="text-lg font-semibold mb-6">解析类型统计</h3>
<div class="h-80 flex items-center justify-center">
<div class="h-64 flex items-center justify-center">
<canvas id="query-type-chart"></canvas>
</div>
</div>
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-1 md:col-span-1">
<div class="flex items-center justify-between mb-6">
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
<!-- 展开按钮 -->
<button id="expand-chart-btn" class="p-2 rounded-full bg-primary/10 text-primary hover:bg-primary/20 transition-colors" title="展开详细图表">
<i class="fa fa-expand"></i>
</button>
</div>
<div class="h-64">
<canvas id="dns-requests-chart"></canvas>
</div>
</div>
</div>
<!-- 详细图表浮窗 -->
<div id="chart-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-lg w-full max-w-5xl max-h-[90vh] overflow-hidden">
<div class="flex items-center justify-between p-6 border-b border-gray-200">
<h3 class="text-xl font-semibold">DNS请求趋势详细图表</h3>
<div class="flex items-center space-x-4">
<!-- 时间范围切换按钮 -->
<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-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>
<!-- 关闭按钮 -->
<button id="close-modal-btn" class="p-2 rounded-full bg-gray-200 text-gray-700 hover:bg-gray-300 transition-colors">
<i class="fa fa-times"></i>
</button>
</div>
</div>
<div class="p-6">
<div class="h-[600px]">
<canvas id="detailed-dns-requests-chart"></canvas>
</div>
</div>
</div>
</div>
<!-- 最近活动表格 -->

View File

@@ -3,6 +3,7 @@
// 全局变量
let ratioChart = null;
let dnsRequestsChart = null;
let detailedDnsRequestsChart = null; // 详细DNS请求趋势图表(浮窗)
let queryTypeChart = null; // 解析类型统计饼图
let intervalId = null;
let wsConnection = null;
@@ -939,6 +940,219 @@ function initTimeRangeToggle() {
}
}
// 初始化展开按钮功能
function initExpandButton() {
const expandBtn = document.getElementById('expand-chart-btn');
const chartModal = document.getElementById('chart-modal');
const closeBtn = document.getElementById('close-modal-btn');
if (!expandBtn || !chartModal || !closeBtn) {
console.error('未找到展开按钮或浮窗元素');
return;
}
// 展开按钮点击事件
expandBtn.addEventListener('click', () => {
console.log('展开按钮被点击');
chartModal.classList.remove('hidden');
// 初始化详细图表
drawDetailedDNSRequestsChart();
});
// 关闭按钮点击事件
closeBtn.addEventListener('click', () => {
console.log('关闭浮窗');
chartModal.classList.add('hidden');
});
// 点击浮窗外部关闭
chartModal.addEventListener('click', (event) => {
if (event.target === chartModal) {
chartModal.classList.add('hidden');
}
});
// 初始化详细图表的时间范围切换
initDetailedTimeRangeToggle();
}
// 初始化详细图表的时间范围切换
function initDetailedTimeRangeToggle() {
const timeRangeBtns = document.querySelectorAll('.time-range-btn[data-range]');
if (!timeRangeBtns.length) {
console.warn('未找到详细图表的时间范围按钮');
return;
}
// 详细图表的当前时间范围
let detailedCurrentTimeRange = '24h';
let detailedIsMixedView = false;
// 为所有时间范围按钮添加点击事件
timeRangeBtns.forEach(btn => {
btn.addEventListener('click', () => {
const range = btn.getAttribute('data-range');
if (range === 'mixed') {
detailedIsMixedView = !detailedIsMixedView;
} else {
detailedCurrentTimeRange = range;
detailedIsMixedView = false;
}
// 更新按钮样式
updateTimeRangeButtonStyles(timeRangeBtns, range, detailedIsMixedView);
// 重新绘制详细图表
drawDetailedDNSRequestsChart(detailedCurrentTimeRange, detailedIsMixedView);
});
});
}
// 更新时间范围按钮样式
function updateTimeRangeButtonStyles(buttons, activeRange, isMixedView) {
buttons.forEach(btn => {
const btnRange = btn.getAttribute('data-range');
if (btnRange === activeRange && (btnRange !== 'mixed' || isMixedView)) {
btn.classList.add('bg-primary', 'text-white');
btn.classList.remove('bg-gray-200', 'text-gray-700');
} else {
btn.classList.remove('bg-primary', 'text-white');
btn.classList.add('bg-gray-200', 'text-gray-700');
}
});
}
// 绘制详细DNS请求图表
function drawDetailedDNSRequestsChart(timeRange = '24h', isMixedView = false) {
const chartElement = document.getElementById('detailed-dns-requests-chart');
if (!chartElement) {
console.error('未找到详细DNS请求图表元素');
return;
}
const chartContext = chartElement.getContext('2d');
// 这里可以复用或修改现有的drawDNSRequestsChart函数的逻辑
// 为详细图表使用与原始图表相同的数据获取逻辑
let apiFunction;
let count;
if (isMixedView) {
// 混合视图 - 不同时间范围的数据
apiFunction = () => Promise.resolve({
labels: ['0h', '6h', '12h', '18h', '24h', '48h', '72h', '96h', '120h', '144h', '168h'],
data: generateMockData(11, 1000, 5000)
});
} else {
// 普通视图 - 基于时间范围
if (timeRange === '24h') {
count = 24;
apiFunction = () => Promise.resolve({
labels: generateTimeLabels(count),
data: generateMockData(count, 100, 500)
});
} else if (timeRange === '7d') {
count = 7;
apiFunction = () => Promise.resolve({
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
data: generateMockData(count, 1000, 5000)
});
} else if (timeRange === '30d') {
count = 30;
apiFunction = () => Promise.resolve({
labels: Array(count).fill(''),
data: generateMockData(count, 10000, 50000)
});
}
}
// 获取数据并绘制图表
if (apiFunction) {
apiFunction().then(data => {
// 创建或更新图表
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = data.labels;
detailedDnsRequestsChart.data.datasets = [{
label: 'DNS请求数量',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}];
detailedDnsRequestsChart.options.plugins.legend.display = false;
// 使用平滑过渡动画更新图表
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
detailedDnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'DNS请求数量',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: false
},
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('绘制详细DNS请求图表失败:', error);
// 错误处理:使用空数据
const count = timeRange === '24h' ? 24 : (timeRange === '7d' ? 7 : 30);
const emptyData = {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = emptyData.labels;
detailedDnsRequestsChart.data.datasets[0].data = emptyData.data;
detailedDnsRequestsChart.update();
}
});
}
}
// 初始化图表
function initCharts() {
// 初始化比例图表
@@ -969,9 +1183,27 @@ function initCharts() {
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12, // 减小图例框的宽度
font: {
size: 11 // 减小字体大小
},
padding: 10 // 减小内边距
}
},
tooltip: {
callbacks: {
label: function(context) {
const label = context.label || '';
const value = context.raw || 0;
const total = context.dataset.data.reduce((acc, val) => acc + (typeof val === 'number' ? val : 0), 0);
const percentage = total > 0 ? Math.round((value / total) * 100) : 0;
return `${label}: ${value} (${percentage}%)`;
}
}
}
},
cutout: '70%'
cutout: '75%' // 增加中心空白区域比例,使环形更适合小容器
}
});
@@ -1003,6 +1235,13 @@ function initCharts() {
plugins: {
legend: {
position: 'bottom',
labels: {
boxWidth: 12, // 减小图例框的宽度
font: {
size: 11 // 减小字体大小
},
padding: 10 // 减小内边距
}
},
tooltip: {
callbacks: {
@@ -1016,7 +1255,7 @@ function initCharts() {
}
}
},
cutout: '70%'
cutout: '75%' // 增加中心空白区域比例,使环形更适合小容器
}
});
} else {
@@ -1025,6 +1264,299 @@ function initCharts() {
// 初始化DNS请求统计图表
drawDNSRequestsChart();
// 初始化展开按钮功能
initExpandButton();
}
// 初始化展开按钮事件
function initExpandButton() {
const expandBtn = document.getElementById('expand-chart-btn');
const chartModal = document.getElementById('chart-modal');
const closeModalBtn = document.getElementById('close-modal-btn'); // 修复ID匹配
// 添加调试日志
console.log('初始化展开按钮功能:', { expandBtn, chartModal, closeModalBtn });
if (expandBtn && chartModal && closeModalBtn) {
// 展开按钮点击事件
expandBtn.addEventListener('click', () => {
console.log('展开按钮被点击');
// 显示浮窗
chartModal.classList.remove('hidden');
// 初始化或更新详细图表
drawDetailedDNSRequestsChart();
// 初始化浮窗中的时间范围切换
initDetailedTimeRangeToggle();
});
// 关闭按钮点击事件
closeModalBtn.addEventListener('click', () => {
console.log('关闭按钮被点击');
chartModal.classList.add('hidden');
});
// 点击遮罩层关闭浮窗使用chartModal作为遮罩层
chartModal.addEventListener('click', (e) => {
// 检查点击目标是否是遮罩层本身即最外层div
if (e.target === chartModal) {
console.log('点击遮罩层关闭');
chartModal.classList.add('hidden');
}
});
// ESC键关闭浮窗
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !chartModal.classList.contains('hidden')) {
console.log('ESC键关闭浮窗');
chartModal.classList.add('hidden');
}
});
} else {
console.error('无法找到必要的DOM元素');
}
}
// 初始化详细图表的时间范围切换
function initDetailedTimeRangeToggle() {
const detailedTimeRangeButtons = document.querySelectorAll('.time-range-btn');
console.log('初始化详细图表时间范围切换,找到按钮数量:', detailedTimeRangeButtons.length);
detailedTimeRangeButtons.forEach((button) => {
button.addEventListener('click', () => {
// 设置当前时间范围
const timeRange = button.dataset.range;
const isMixedMode = timeRange === 'mixed';
console.log('时间范围按钮被点击:', { timeRange, isMixedMode });
// 更新按钮状态
detailedTimeRangeButtons.forEach((btn) => {
if (btn === button) {
btn.classList.add('bg-blue-500', 'text-white');
btn.classList.remove('bg-gray-200', 'text-gray-700');
} else {
btn.classList.remove('bg-blue-500', 'text-white');
btn.classList.add('bg-gray-200', 'text-gray-700');
}
});
// 更新详细图表
currentTimeRange = timeRange;
isMixedView = isMixedMode;
drawDetailedDNSRequestsChart();
});
});
}
// 绘制详细的DNS请求趋势图表
function drawDetailedDNSRequestsChart() {
console.log('绘制详细DNS请求趋势图表时间范围:', currentTimeRange, '混合视图:', isMixedView);
const ctx = document.getElementById('detailed-dns-requests-chart');
if (!ctx) {
console.error('未找到详细DNS请求图表元素');
return;
}
const chartContext = ctx.getContext('2d');
// 混合视图配置
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);
// 返回空数据
const count = config.label === '24小时' ? 24 : (config.label === '7天' ? 7 : 30);
return {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
})
)).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 (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = results[0].labels;
detailedDnsRequestsChart.data.datasets = datasets;
detailedDnsRequestsChart.options.plugins.legend.display = showLegend;
// 使用平滑过渡动画更新图表
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
detailedDnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: results[0].labels,
datasets: datasets
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
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函数
let apiFunction;
switch (currentTimeRange) {
case '7d':
apiFunction = (api && api.getDailyStats) || (() => Promise.resolve({ labels: [], data: [] }));
break;
case '30d':
apiFunction = (api && api.getMonthlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
break;
default: // 24h
apiFunction = (api && api.getHourlyStats) || (() => Promise.resolve({ labels: [], data: [] }));
}
// 获取统计数据
apiFunction().then(data => {
// 创建或更新图表
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = data.labels;
detailedDnsRequestsChart.data.datasets = [{
label: 'DNS请求数量',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}];
detailedDnsRequestsChart.options.plugins.legend.display = false;
// 使用平滑过渡动画更新图表
detailedDnsRequestsChart.update({
duration: 800,
easing: 'easeInOutQuart'
});
} else {
detailedDnsRequestsChart = new Chart(chartContext, {
type: 'line',
data: {
labels: data.labels,
datasets: [{
label: 'DNS请求数量',
data: data.data,
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: {
duration: 800,
easing: 'easeInOutQuart'
},
plugins: {
legend: {
display: false
},
title: {
display: true,
text: 'DNS请求趋势',
font: {
size: 14
}
},
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('绘制详细DNS请求图表失败:', error);
// 错误处理:使用空数据
const count = currentTimeRange === '24h' ? 24 : (currentTimeRange === '7d' ? 7 : 30);
const emptyData = {
labels: Array(count).fill(''),
data: Array(count).fill(0)
};
if (detailedDnsRequestsChart) {
detailedDnsRequestsChart.data.labels = emptyData.labels;
detailedDnsRequestsChart.data.datasets[0].data = emptyData.data;
detailedDnsRequestsChart.update();
}
});
}
}
// 绘制DNS请求统计图表