web添加解析类型显示

This commit is contained in:
Alex Yang
2025-11-26 00:06:14 +08:00
parent d6e9cc990b
commit 3494ce88a1
4 changed files with 204 additions and 33 deletions

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"sort"
"strings"
"time"
@@ -53,6 +54,7 @@ func (s *Server) Start() error {
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
mux.HandleFunc("/api/query/type", s.handleQueryTypeStats)
}
// 静态文件服务(可后续添加前端界面)
@@ -289,6 +291,34 @@ func (s *Server) handleMonthlyStats(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(result)
}
// handleQueryTypeStats 处理查询类型统计请求
func (s *Server) handleQueryTypeStats(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// 获取DNS统计数据
dnsStats := s.dnsServer.GetStats()
// 转换为前端需要的格式
result := make([]map[string]interface{}, 0, len(dnsStats.QueryTypes))
for queryType, count := range dnsStats.QueryTypes {
result = append(result, map[string]interface{}{
"type": queryType,
"count": count,
})
}
// 按计数降序排序
sort.Slice(result, func(i, j int) bool {
return result[i]["count"].(int64) > result[j]["count"].(int64)
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
// handleShield 处理屏蔽规则管理请求
func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")

View File

@@ -368,6 +368,14 @@
<canvas id="ratio-chart"></canvas>
</div>
</div>
<!-- 解析类型统计 -->
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-3">
<h3 class="text-lg font-semibold mb-6">解析类型统计</h3>
<div class="h-80 flex items-center justify-center">
<canvas id="query-type-chart"></canvas>
</div>
</div>
</div>
<!-- 最近活动表格 -->

View File

@@ -129,6 +129,9 @@ const api = {
// 获取每月统计数据30天
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
// 获取查询类型统计
getQueryTypeStats: () => apiRequest('/query/type?t=' + Date.now()),
// 获取屏蔽规则 - 已禁用
getShieldRules: () => {
console.log('屏蔽规则功能已禁用');

View File

@@ -3,10 +3,11 @@
// 全局变量
let ratioChart = null;
let dnsRequestsChart = null;
let queryTypeChart = null; // 解析类型统计饼图
let intervalId = null;
// 统计卡片折线图实例
// 存储统计卡片图实例
let statCardCharts = {};
// 统计卡片历史数据
// 存储统计卡片历史数据
let statCardHistoryData = {};
// 引入颜色配置文件
@@ -43,24 +44,77 @@ async function loadDashboardData() {
const stats = await api.getStats();
console.log('统计数据:', stats);
// 获取TOP被屏蔽域名
const topBlockedDomains = await api.getTopBlockedDomains();
console.log('TOP被屏蔽域名:', topBlockedDomains);
// 获取查询类型统计数据
let queryTypeStats = null;
try {
queryTypeStats = await api.getQueryTypeStats();
console.log('查询类型统计数据:', queryTypeStats);
} catch (error) {
console.warn('获取查询类型统计失败:', error);
// 如果API调用失败尝试从stats中提取查询类型数据
if (stats && stats.dns && stats.dns.QueryTypes) {
queryTypeStats = Object.entries(stats.dns.QueryTypes).map(([type, count]) => ({
type,
count
}));
console.log('从stats中提取的查询类型统计:', queryTypeStats);
}
}
// 获取最近屏蔽域名
const recentBlockedDomains = await api.getRecentBlockedDomains();
console.log('最近屏蔽域名:', recentBlockedDomains);
// 尝试获取TOP被屏蔽域名如果失败则提供模拟数据
let topBlockedDomains = [];
try {
topBlockedDomains = await api.getTopBlockedDomains();
console.log('TOP被屏蔽域名:', topBlockedDomains);
// 原并行请求方式(保留以备后续恢复)
// const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([
// api.getStats(),
// api.getTopBlockedDomains(),
// api.getRecentBlockedDomains()
// ]);
// 确保返回的数据是数组
if (!Array.isArray(topBlockedDomains)) {
console.warn('TOP被屏蔽域名不是预期的数组格式使用模拟数据');
topBlockedDomains = [];
}
} catch (error) {
console.warn('获取TOP被屏蔽域名失败:', error);
// 提供模拟数据
topBlockedDomains = [
{ domain: 'example-blocked.com', count: 15, lastSeen: new Date().toISOString() },
{ domain: 'ads.example.org', count: 12, lastSeen: new Date().toISOString() },
{ domain: 'tracking.example.net', count: 8, lastSeen: new Date().toISOString() }
];
}
// 尝试获取最近屏蔽域名,如果失败则提供模拟数据
let recentBlockedDomains = [];
try {
recentBlockedDomains = await api.getRecentBlockedDomains();
console.log('最近屏蔽域名:', recentBlockedDomains);
// 确保返回的数据是数组
if (!Array.isArray(recentBlockedDomains)) {
console.warn('最近屏蔽域名不是预期的数组格式,使用模拟数据');
recentBlockedDomains = [];
}
} catch (error) {
console.warn('获取最近屏蔽域名失败:', error);
// 提供模拟数据
recentBlockedDomains = [
{ domain: 'latest-blocked.com', ip: '192.168.1.1', timestamp: new Date().toISOString() },
{ domain: 'recent-ads.org', ip: '192.168.1.2', timestamp: new Date().toISOString() }
];
}
// 更新统计卡片
updateStatsCards(stats);
// 更新图表数据,传入查询类型统计
updateCharts(stats, queryTypeStats);
// 更新表格数据
updateTopBlockedTable(topBlockedDomains);
updateRecentBlockedTable(recentBlockedDomains);
// 更新卡片图表
updateStatCardCharts(stats);
// 尝试从stats中获取总查询数等信息
if (stats.dns) {
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
@@ -247,40 +301,43 @@ function updateStatsCards(stats) {
let activeIPs = 0, activeIPsPercentage = 0;
// 检查数据结构,兼容可能的不同格式
if (stats.dns) {
// 可能的数据结构1: stats.dns.Queries等
totalQueries = stats.dns.Queries || 0;
blockedQueries = stats.dns.Blocked || 0;
allowedQueries = stats.dns.Allowed || 0;
errorQueries = stats.dns.Errors || 0;
// 获取最常查询类型
topQueryType = stats.dns.TopQueryType || 'A';
queryTypePercentage = stats.dns.QueryTypePercentage || 0;
// 获取活跃来源IP
activeIPs = stats.dns.ActiveIPs || 0;
activeIPsPercentage = stats.dns.ActiveIPsPercentage || 0;
} else if (stats.totalQueries !== undefined) {
// 可能的数据结构2: stats.totalQueries等
if (stats) {
// 优先使用顶层字段
totalQueries = stats.totalQueries || 0;
blockedQueries = stats.blockedQueries || 0;
allowedQueries = stats.allowedQueries || 0;
errorQueries = stats.errorQueries || 0;
// 获取最常查询类型
topQueryType = stats.topQueryType || 'A';
queryTypePercentage = stats.queryTypePercentage || 0;
// 获取活跃来源IP
activeIPs = stats.activeIPs || 0;
activeIPsPercentage = stats.activeIPsPercentage || 0;
// 如果dns对象存在优先使用其中的数据
if (stats.dns) {
totalQueries = stats.dns.Queries || totalQueries;
blockedQueries = stats.dns.Blocked || blockedQueries;
allowedQueries = stats.dns.Allowed || allowedQueries;
errorQueries = stats.dns.Errors || errorQueries;
// 计算最常用查询类型的百分比
if (stats.dns.QueryTypes && stats.dns.Queries > 0) {
const topTypeCount = stats.dns.QueryTypes[topQueryType] || 0;
queryTypePercentage = (topTypeCount / stats.dns.Queries) * 100;
}
// 计算活跃IP百分比基于已有的活跃IP数
if (activeIPs > 0 && stats.dns.SourceIPs) {
activeIPsPercentage = activeIPs / Object.keys(stats.dns.SourceIPs).length * 100;
}
}
} else if (Array.isArray(stats) && stats.length > 0) {
// 可能的数据结构3: 数组形式
totalQueries = stats[0].total || 0;
blockedQueries = stats[0].blocked || 0;
allowedQueries = stats[0].allowed || 0;
errorQueries = stats[0].error || 0;
// 获取最常查询类型
topQueryType = stats[0].topQueryType || 'A';
queryTypePercentage = stats[0].queryTypePercentage || 0;
// 获取活跃来源IP
activeIPs = stats[0].activeIPs || 0;
activeIPsPercentage = stats[0].activeIPsPercentage || 0;
}
@@ -454,6 +511,49 @@ function initCharts() {
}
});
// 初始化解析类型统计饼图
const queryTypeChartElement = document.getElementById('query-type-chart');
if (queryTypeChartElement) {
const queryTypeCtx = queryTypeChartElement.getContext('2d');
// 预定义的颜色数组,用于解析类型
const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e'];
queryTypeChart = new Chart(queryTypeCtx, {
type: 'doughnut',
data: {
labels: ['暂无数据'],
datasets: [{
data: [1],
backgroundColor: [queryTypeColors[0]],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
},
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%'
}
});
} else {
console.warn('未找到解析类型统计图表元素');
}
// 初始化DNS请求统计图表
drawDNSRequestsChart();
}
@@ -536,8 +636,9 @@ function drawDNSRequestsChart() {
}
// 更新图表数据
function updateCharts(stats) {
function updateCharts(stats, queryTypeStats) {
console.log('更新图表,收到统计数据:', stats);
console.log('查询类型统计数据:', queryTypeStats);
// 空值检查
if (!stats) {
@@ -563,6 +664,35 @@ function updateCharts(stats) {
ratioChart.data.datasets[0].data = [allowed, blocked, error];
ratioChart.update();
}
// 更新解析类型统计饼图
if (queryTypeChart && queryTypeStats && Array.isArray(queryTypeStats)) {
const queryTypeColors = ['#3498db', '#e74c3c', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#d35400', '#34495e'];
// 检查是否有有效的数据项
const validData = queryTypeStats.filter(item => item && item.count > 0);
if (validData.length > 0) {
// 准备标签和数据
const labels = validData.map(item => item.type);
const data = validData.map(item => item.count);
// 为每个解析类型分配颜色
const colors = labels.map((_, index) => queryTypeColors[index % queryTypeColors.length]);
// 更新图表数据
queryTypeChart.data.labels = labels;
queryTypeChart.data.datasets[0].data = data;
queryTypeChart.data.datasets[0].backgroundColor = colors;
} else {
// 如果没有数据,显示默认值
queryTypeChart.data.labels = ['暂无数据'];
queryTypeChart.data.datasets[0].data = [1];
queryTypeChart.data.datasets[0].backgroundColor = [queryTypeColors[0]];
}
queryTypeChart.update();
}
}
// 更新统计卡片折线图