web增加恢复解析统计图表
This commit is contained in:
@@ -33,6 +33,8 @@ type StatsData struct {
|
||||
BlockedDomains map[string]*BlockedDomain `json:"blockedDomains"`
|
||||
ResolvedDomains map[string]*BlockedDomain `json:"resolvedDomains"`
|
||||
HourlyStats map[string]int64 `json:"hourlyStats"`
|
||||
DailyStats map[string]int64 `json:"dailyStats"`
|
||||
MonthlyStats map[string]int64 `json:"monthlyStats"`
|
||||
LastSaved time.Time `json:"lastSaved"`
|
||||
}
|
||||
|
||||
@@ -53,6 +55,10 @@ type Server struct {
|
||||
resolvedDomains map[string]*BlockedDomain // 用于记录解析的域名
|
||||
hourlyStatsMutex sync.RWMutex
|
||||
hourlyStats map[string]int64 // 按小时统计屏蔽数量
|
||||
dailyStatsMutex sync.RWMutex
|
||||
dailyStats map[string]int64 // 按天统计屏蔽数量
|
||||
monthlyStatsMutex sync.RWMutex
|
||||
monthlyStats map[string]int64 // 按月统计屏蔽数量
|
||||
saveTicker *time.Ticker // 用于定时保存数据
|
||||
saveDone chan struct{} // 用于通知保存协程停止
|
||||
}
|
||||
@@ -88,6 +94,8 @@ func NewServer(config *config.DNSConfig, shieldConfig *config.ShieldConfig, shie
|
||||
blockedDomains: make(map[string]*BlockedDomain),
|
||||
resolvedDomains: make(map[string]*BlockedDomain),
|
||||
hourlyStats: make(map[string]int64),
|
||||
dailyStats: make(map[string]int64),
|
||||
monthlyStats: make(map[string]int64),
|
||||
saveDone: make(chan struct{}),
|
||||
}
|
||||
|
||||
@@ -354,11 +362,26 @@ func (s *Server) updateBlockedDomainStats(domain string) {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新统计数据
|
||||
now := time.Now()
|
||||
|
||||
// 更新小时统计
|
||||
hourKey := time.Now().Format("2006-01-02-15")
|
||||
hourKey := now.Format("2006-01-02-15")
|
||||
s.hourlyStatsMutex.Lock()
|
||||
s.hourlyStats[hourKey]++
|
||||
s.hourlyStatsMutex.Unlock()
|
||||
|
||||
// 更新每日统计
|
||||
dayKey := now.Format("2006-01-02")
|
||||
s.dailyStatsMutex.Lock()
|
||||
s.dailyStats[dayKey]++
|
||||
s.dailyStatsMutex.Unlock()
|
||||
|
||||
// 更新每月统计
|
||||
monthKey := now.Format("2006-01")
|
||||
s.monthlyStatsMutex.Lock()
|
||||
s.monthlyStats[monthKey]++
|
||||
s.monthlyStatsMutex.Unlock()
|
||||
}
|
||||
|
||||
// updateResolvedDomainStats 更新解析域名统计
|
||||
@@ -469,7 +492,7 @@ func (s *Server) GetRecentBlockedDomains(limit int) []BlockedDomain {
|
||||
return domains
|
||||
}
|
||||
|
||||
// GetHourlyStats 获取24小时屏蔽统计
|
||||
// GetHourlyStats 获取每小时统计数据
|
||||
func (s *Server) GetHourlyStats() map[string]int64 {
|
||||
s.hourlyStatsMutex.RLock()
|
||||
defer s.hourlyStatsMutex.RUnlock()
|
||||
@@ -482,6 +505,32 @@ func (s *Server) GetHourlyStats() map[string]int64 {
|
||||
return result
|
||||
}
|
||||
|
||||
// GetDailyStats 获取每日统计数据
|
||||
func (s *Server) GetDailyStats() map[string]int64 {
|
||||
s.dailyStatsMutex.RLock()
|
||||
defer s.dailyStatsMutex.RUnlock()
|
||||
|
||||
// 返回副本
|
||||
result := make(map[string]int64)
|
||||
for k, v := range s.dailyStats {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// GetMonthlyStats 获取每月统计数据
|
||||
func (s *Server) GetMonthlyStats() map[string]int64 {
|
||||
s.monthlyStatsMutex.RLock()
|
||||
defer s.monthlyStatsMutex.RUnlock()
|
||||
|
||||
// 返回副本
|
||||
result := make(map[string]int64)
|
||||
for k, v := range s.monthlyStats {
|
||||
result[k] = v
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// loadStatsData 从文件加载统计数据
|
||||
func (s *Server) loadStatsData() {
|
||||
if s.config.StatsFile == "" {
|
||||
@@ -528,6 +577,18 @@ func (s *Server) loadStatsData() {
|
||||
s.hourlyStats = statsData.HourlyStats
|
||||
}
|
||||
s.hourlyStatsMutex.Unlock()
|
||||
|
||||
s.dailyStatsMutex.Lock()
|
||||
if statsData.DailyStats != nil {
|
||||
s.dailyStats = statsData.DailyStats
|
||||
}
|
||||
s.dailyStatsMutex.Unlock()
|
||||
|
||||
s.monthlyStatsMutex.Lock()
|
||||
if statsData.MonthlyStats != nil {
|
||||
s.monthlyStats = statsData.MonthlyStats
|
||||
}
|
||||
s.monthlyStatsMutex.Unlock()
|
||||
|
||||
logger.Info("统计数据加载成功")
|
||||
}
|
||||
@@ -573,6 +634,20 @@ func (s *Server) saveStatsData() {
|
||||
statsData.HourlyStats[k] = v
|
||||
}
|
||||
s.hourlyStatsMutex.RUnlock()
|
||||
|
||||
s.dailyStatsMutex.RLock()
|
||||
statsData.DailyStats = make(map[string]int64)
|
||||
for k, v := range s.dailyStats {
|
||||
statsData.DailyStats[k] = v
|
||||
}
|
||||
s.dailyStatsMutex.RUnlock()
|
||||
|
||||
s.monthlyStatsMutex.RLock()
|
||||
statsData.MonthlyStats = make(map[string]int64)
|
||||
for k, v := range s.monthlyStats {
|
||||
statsData.MonthlyStats[k] = v
|
||||
}
|
||||
s.monthlyStatsMutex.RUnlock()
|
||||
|
||||
// 序列化数据
|
||||
jsonData, err := json.MarshalIndent(statsData, "", " ")
|
||||
|
||||
101
http/server.go
101
http/server.go
@@ -51,6 +51,8 @@ func (s *Server) Start() error {
|
||||
mux.HandleFunc("/api/top-resolved", s.handleTopResolvedDomains)
|
||||
mux.HandleFunc("/api/recent-blocked", s.handleRecentBlockedDomains)
|
||||
mux.HandleFunc("/api/hourly-stats", s.handleHourlyStats)
|
||||
mux.HandleFunc("/api/daily-stats", s.handleDailyStats)
|
||||
mux.HandleFunc("/api/monthly-stats", s.handleMonthlyStats)
|
||||
}
|
||||
|
||||
// 静态文件服务(可后续添加前端界面)
|
||||
@@ -191,35 +193,89 @@ func (s *Server) handleHourlyStats(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// handleDailyStats 处理每日统计数据请求
|
||||
func (s *Server) handleDailyStats(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取每日统计数据
|
||||
dailyStats := s.dnsServer.GetDailyStats()
|
||||
|
||||
// 生成过去7天的时间标签
|
||||
labels := make([]string, 7)
|
||||
data := make([]int64, 7)
|
||||
now := time.Now()
|
||||
|
||||
for i := 6; i >= 0; i-- {
|
||||
t := now.AddDate(0, 0, -i)
|
||||
key := t.Format("2006-01-02")
|
||||
labels[6-i] = t.Format("01-02")
|
||||
data[6-i] = dailyStats[key]
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"labels": labels,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(result)
|
||||
}
|
||||
|
||||
// handleMonthlyStats 处理每月统计数据请求
|
||||
func (s *Server) handleMonthlyStats(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取每日统计数据(用于30天视图)
|
||||
dailyStats := s.dnsServer.GetDailyStats()
|
||||
|
||||
// 生成过去30天的时间标签
|
||||
labels := make([]string, 30)
|
||||
data := make([]int64, 30)
|
||||
now := time.Now()
|
||||
|
||||
for i := 29; i >= 0; i-- {
|
||||
t := now.AddDate(0, 0, -i)
|
||||
key := t.Format("2006-01-02")
|
||||
labels[29-i] = t.Format("01-02")
|
||||
data[29-i] = dailyStats[key]
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
"labels": labels,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
// 返回屏蔽规则的基本配置信息
|
||||
// 返回屏蔽规则的基本配置信息和统计数据,不返回完整规则列表
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// 获取规则统计信息
|
||||
stats := s.shieldManager.GetStats()
|
||||
shieldInfo := map[string]interface{}{
|
||||
"updateInterval": s.globalConfig.Shield.UpdateInterval,
|
||||
"blockMethod": s.globalConfig.Shield.BlockMethod,
|
||||
"blacklistCount": len(s.globalConfig.Shield.Blacklists),
|
||||
"updateInterval": s.globalConfig.Shield.UpdateInterval,
|
||||
"blockMethod": s.globalConfig.Shield.BlockMethod,
|
||||
"blacklistCount": len(s.globalConfig.Shield.Blacklists),
|
||||
"domainRulesCount": stats["domainRules"],
|
||||
"domainExceptionsCount": stats["domainExceptions"],
|
||||
"regexRulesCount": stats["regexRules"],
|
||||
"regexExceptionsCount": stats["regexExceptions"],
|
||||
"hostsRulesCount": stats["hostsRules"],
|
||||
}
|
||||
json.NewEncoder(w).Encode(shieldInfo)
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// 处理远程黑名单管理子路由
|
||||
if strings.HasPrefix(r.URL.Path, "/shield/blacklists") {
|
||||
s.handleShieldBlacklists(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
// 获取完整规则列表
|
||||
rules := s.shieldManager.GetRules()
|
||||
json.NewEncoder(w).Encode(rules)
|
||||
|
||||
case http.MethodPost:
|
||||
// 添加屏蔽规则
|
||||
var req struct {
|
||||
@@ -237,7 +293,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
|
||||
return
|
||||
case http.MethodDelete:
|
||||
// 删除屏蔽规则
|
||||
var req struct {
|
||||
@@ -255,7 +311,7 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
|
||||
|
||||
return
|
||||
case http.MethodPut:
|
||||
// 重新加载规则
|
||||
if err := s.shieldManager.LoadRules(); err != nil {
|
||||
@@ -263,9 +319,10 @@ func (s *Server) handleShield(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "success", "message": "规则重新加载成功"})
|
||||
|
||||
return
|
||||
default:
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
package.json
Normal file
18
package.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "dns-server-console",
|
||||
"version": "1.0.0",
|
||||
"description": "DNS服务器Web控制台",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tailwindcss": "^3.3.3",
|
||||
"font-awesome": "^4.7.0",
|
||||
"chart.js": "^4.4.8"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"keywords": ["dns", "server", "console", "web"],
|
||||
"author": "",
|
||||
"license": "ISC"
|
||||
}
|
||||
@@ -970,22 +970,83 @@ func (m *ShieldManager) GetStats() map[string]interface{} {
|
||||
// loadStatsData 从文件加载计数数据
|
||||
func (m *ShieldManager) loadStatsData() {
|
||||
if m.config.StatsFile == "" {
|
||||
logger.Info("Shield统计文件路径未配置,跳过加载")
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
data, err := ioutil.ReadFile(m.config.StatsFile)
|
||||
// 获取绝对路径以避免工作目录问题
|
||||
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
logger.Error("读取Shield计数数据文件失败", "error", err)
|
||||
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
||||
return
|
||||
}
|
||||
logger.Debug("尝试加载Shield统计数据", "file", statsFilePath)
|
||||
|
||||
// 检查文件是否存在
|
||||
fileInfo, err := os.Stat(statsFilePath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
logger.Info("Shield统计文件不存在,将创建新文件", "file", statsFilePath)
|
||||
// 初始化空的计数数据
|
||||
m.rulesMutex.Lock()
|
||||
m.blockedDomainsCount = make(map[string]int)
|
||||
m.resolvedDomainsCount = make(map[string]int)
|
||||
m.rulesMutex.Unlock()
|
||||
// 尝试立即保存一个有效的空文件
|
||||
m.saveStatsData()
|
||||
} else {
|
||||
logger.Error("检查Shield统计文件失败", "file", statsFilePath, "error", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查文件大小
|
||||
if fileInfo.Size() == 0 {
|
||||
logger.Warn("Shield统计文件为空,将重新初始化", "file", statsFilePath)
|
||||
m.rulesMutex.Lock()
|
||||
m.blockedDomainsCount = make(map[string]int)
|
||||
m.resolvedDomainsCount = make(map[string]int)
|
||||
m.rulesMutex.Unlock()
|
||||
m.saveStatsData()
|
||||
return
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
data, err := ioutil.ReadFile(statsFilePath)
|
||||
if err != nil {
|
||||
logger.Error("读取Shield计数数据文件失败", "file", statsFilePath, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查数据长度
|
||||
if len(data) == 0 {
|
||||
logger.Warn("读取到的Shield统计数据为空", "file", statsFilePath)
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
var statsData ShieldStatsData
|
||||
err = json.Unmarshal(data, &statsData)
|
||||
if err != nil {
|
||||
logger.Error("解析Shield计数数据失败", "error", err)
|
||||
// 记录更详细的错误信息,包括数据前50个字符
|
||||
dataSample := string(data)
|
||||
if len(dataSample) > 50 {
|
||||
dataSample = dataSample[:50] + "..."
|
||||
}
|
||||
logger.Error("解析Shield计数数据失败",
|
||||
"file", statsFilePath,
|
||||
"error", err,
|
||||
"data_length", len(data),
|
||||
"data_sample", dataSample)
|
||||
|
||||
// 重置为默认空数据
|
||||
m.rulesMutex.Lock()
|
||||
m.blockedDomainsCount = make(map[string]int)
|
||||
m.resolvedDomainsCount = make(map[string]int)
|
||||
m.rulesMutex.Unlock()
|
||||
|
||||
// 尝试保存一个有效的空文件
|
||||
m.saveStatsData()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -993,26 +1054,38 @@ func (m *ShieldManager) loadStatsData() {
|
||||
m.rulesMutex.Lock()
|
||||
if statsData.BlockedDomainsCount != nil {
|
||||
m.blockedDomainsCount = statsData.BlockedDomainsCount
|
||||
} else {
|
||||
m.blockedDomainsCount = make(map[string]int)
|
||||
}
|
||||
if statsData.ResolvedDomainsCount != nil {
|
||||
m.resolvedDomainsCount = statsData.ResolvedDomainsCount
|
||||
} else {
|
||||
m.resolvedDomainsCount = make(map[string]int)
|
||||
}
|
||||
m.rulesMutex.Unlock()
|
||||
|
||||
logger.Info("Shield计数数据加载成功")
|
||||
logger.Info("Shield计数数据加载成功", "blocked_entries", len(m.blockedDomainsCount), "resolved_entries", len(m.resolvedDomainsCount))
|
||||
}
|
||||
|
||||
// saveStatsData 保存计数数据到文件
|
||||
func (m *ShieldManager) saveStatsData() {
|
||||
if m.config.StatsFile == "" {
|
||||
logger.Debug("Shield统计文件路径未配置,跳过保存")
|
||||
return
|
||||
}
|
||||
|
||||
// 获取绝对路径以避免工作目录问题
|
||||
statsFilePath, err := filepath.Abs(m.config.StatsFile)
|
||||
if err != nil {
|
||||
logger.Error("获取Shield统计文件绝对路径失败", "path", m.config.StatsFile, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建数据目录
|
||||
statsDir := filepath.Dir(m.config.StatsFile)
|
||||
err := os.MkdirAll(statsDir, 0755)
|
||||
statsDir := filepath.Dir(statsFilePath)
|
||||
err = os.MkdirAll(statsDir, 0755)
|
||||
if err != nil {
|
||||
logger.Error("创建Shield统计数据目录失败", "error", err)
|
||||
logger.Error("创建Shield统计数据目录失败", "dir", statsDir, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1040,14 +1113,24 @@ func (m *ShieldManager) saveStatsData() {
|
||||
return
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
err = ioutil.WriteFile(m.config.StatsFile, jsonData, 0644)
|
||||
// 使用临时文件先写入,然后重命名,避免文件损坏
|
||||
tempFilePath := statsFilePath + ".tmp"
|
||||
err = ioutil.WriteFile(tempFilePath, jsonData, 0644)
|
||||
if err != nil {
|
||||
logger.Error("保存Shield计数数据到文件失败", "error", err)
|
||||
logger.Error("写入临时Shield统计文件失败", "file", tempFilePath, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Shield计数数据保存成功")
|
||||
// 原子操作重命名文件
|
||||
err = os.Rename(tempFilePath, statsFilePath)
|
||||
if err != nil {
|
||||
logger.Error("重命名Shield统计文件失败", "temp", tempFilePath, "dest", statsFilePath, "error", err)
|
||||
// 尝试清理临时文件
|
||||
os.Remove(tempFilePath)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Shield计数数据保存成功", "file", statsFilePath, "blocked_entries", len(statsData.BlockedDomainsCount), "resolved_entries", len(statsData.ResolvedDomainsCount))
|
||||
}
|
||||
|
||||
// startAutoSaveStats 启动计数数据自动保存功能
|
||||
|
||||
@@ -45,7 +45,9 @@
|
||||
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
.sidebar-item-active {
|
||||
@apply bg-primary/10 text-primary border-r-4 border-primary;
|
||||
background-color: rgba(22, 93, 255, 0.1);
|
||||
color: #165DFF;
|
||||
border-right: 4px solid #165DFF;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -138,93 +140,194 @@
|
||||
<!-- 统计卡片 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<!-- 查询总量卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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 class="bg-white rounded-lg p-6 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="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="total-queries">0</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="queries-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="total-queries">0</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="queries-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 屏蔽数量卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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 class="bg-white rounded-lg p-6 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="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="blocked-queries">0</p>
|
||||
<span class="text-danger text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="blocked-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="blocked-queries">0</p>
|
||||
<span class="text-danger text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="blocked-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 正常解析卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">正常解析</h3>
|
||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
||||
<i class="fa fa-check"></i>
|
||||
<div class="bg-white rounded-lg p-6 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">正常解析</h3>
|
||||
<div class="p-2 rounded-full bg-success/10 text-success">
|
||||
<i class="fa fa-check"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="allowed-queries">0</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="allowed-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="allowed-queries">0</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="allowed-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误数量卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">错误数量</h3>
|
||||
<div class="p-2 rounded-full bg-warning/10 text-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning 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-warning/10 text-warning">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="error-queries">0</p>
|
||||
<span class="text-warning text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="error-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="error-queries">0</p>
|
||||
<span class="text-warning text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="error-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- 平均响应时间卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 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="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="avg-response-time">0ms</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-down mr-1"></i>
|
||||
<span id="response-time-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最常用查询类型卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-secondary 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-secondary/10 text-secondary">
|
||||
<i class="fa fa-database"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="top-query-type">A</p>
|
||||
<span class="text-primary text-sm flex items-center">
|
||||
<i class="fa fa-circle text-xs mr-1"></i>
|
||||
<span id="query-type-percentage">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 活跃来源IP数卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 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="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="active-ips">0</p>
|
||||
<span class="text-success text-sm flex items-center">
|
||||
<i class="fa fa-arrow-up mr-1"></i>
|
||||
<span id="active-ips-percent">0%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU使用率卡片 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow relative overflow-hidden">
|
||||
<!-- 颜色蒙版 -->
|
||||
<div class="absolute -bottom-8 -right-8 w-24 h-24 rounded-full bg-warning opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-gray-500 font-medium">CPU使用率</h3>
|
||||
<div class="p-2 rounded-full bg-warning/10 text-warning">
|
||||
<i class="fa fa-microchip"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-end justify-between">
|
||||
<p class="text-3xl font-bold" id="cpu-usage">0%</p>
|
||||
<span class="text-warning text-sm flex items-center">
|
||||
<i class="fa fa-bolt mr-1"></i>
|
||||
<span id="cpu-status">正常</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图表和数据表格 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 查询趋势图表 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow lg:col-span-2">
|
||||
<!-- 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">查询趋势</h3>
|
||||
<h3 class="text-lg font-semibold">DNS请求趋势</h3>
|
||||
<!-- 时间范围切换按钮 -->
|
||||
<div class="flex space-x-2">
|
||||
<button class="px-3 py-1 text-sm rounded-full bg-primary/10 text-primary">24小时</button>
|
||||
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">7天</button>
|
||||
<button class="px-3 py-1 text-sm rounded-full text-gray-500 hover:bg-gray-100">30天</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="query-trend-chart"></canvas>
|
||||
<canvas id="dns-requests-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 解析与屏蔽比例 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<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="ratio-chart"></canvas>
|
||||
|
||||
145
static/js/api.js
145
static/js/api.js
@@ -10,7 +10,10 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
method,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Cache-Control': 'no-store, no-cache, must-revalidate, max-age=0',
|
||||
'Pragma': 'no-cache',
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
};
|
||||
|
||||
if (data) {
|
||||
@@ -20,12 +23,60 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
|
||||
// 获取响应文本,用于调试和错误处理
|
||||
const responseText = await response.text();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
throw new Error(errorData.error || `请求失败: ${response.status}`);
|
||||
// 尝试解析错误响应
|
||||
let errorData = {};
|
||||
try {
|
||||
// 首先检查响应文本是否为空或不是有效JSON
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
console.warn('错误响应为空');
|
||||
} else {
|
||||
try {
|
||||
errorData = JSON.parse(responseText);
|
||||
} catch (parseError) {
|
||||
console.error('无法解析错误响应为JSON:', parseError);
|
||||
console.error('原始错误响应文本:', responseText);
|
||||
}
|
||||
}
|
||||
// 直接返回错误信息,而不是抛出异常,让上层处理
|
||||
console.warn(`API请求失败: ${response.status}`, errorData);
|
||||
return { error: errorData.error || `请求失败: ${response.status}` };
|
||||
} catch (e) {
|
||||
console.error('处理错误响应时出错:', e);
|
||||
return { error: `请求处理失败: ${e.message}` };
|
||||
}
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
// 尝试解析成功响应
|
||||
try {
|
||||
// 首先检查响应文本是否为空
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
console.warn('空响应文本');
|
||||
return {};
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const parsedData = JSON.parse(responseText);
|
||||
return parsedData;
|
||||
} catch (parseError) {
|
||||
// 详细记录错误信息和响应内容
|
||||
console.error('JSON解析错误:', parseError);
|
||||
console.error('原始响应文本:', responseText);
|
||||
console.error('响应长度:', responseText.length);
|
||||
console.error('响应前100字符:', responseText.substring(0, 100));
|
||||
|
||||
// 如果是位置66附近的错误,特别标记
|
||||
if (parseError.message.includes('position 66')) {
|
||||
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||
}
|
||||
|
||||
// 返回空数组作为默认值,避免页面功能完全中断
|
||||
console.warn('使用默认空数组作为响应');
|
||||
return [];
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
throw error;
|
||||
@@ -35,52 +86,88 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
// API方法集合
|
||||
const api = {
|
||||
// 获取统计信息
|
||||
getStats: () => apiRequest('/stats'),
|
||||
getStats: () => apiRequest('/stats?t=' + Date.now()),
|
||||
|
||||
// 获取系统状态
|
||||
getStatus: () => apiRequest('/status'),
|
||||
getStatus: () => apiRequest('/status?t=' + Date.now()),
|
||||
|
||||
// 获取Top屏蔽域名
|
||||
getTopBlockedDomains: () => apiRequest('/top-blocked'),
|
||||
getTopBlockedDomains: () => apiRequest('/top-blocked?t=' + Date.now()),
|
||||
|
||||
// 获取Top解析域名
|
||||
getTopResolvedDomains: () => apiRequest('/top-resolved'),
|
||||
getTopResolvedDomains: () => apiRequest('/top-resolved?t=' + Date.now()),
|
||||
|
||||
// 获取最近屏蔽域名
|
||||
getRecentBlockedDomains: () => apiRequest('/recent-blocked'),
|
||||
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
||||
|
||||
// 获取小时统计
|
||||
getHourlyStats: () => apiRequest('/hourly-stats'),
|
||||
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
||||
|
||||
// 获取屏蔽规则
|
||||
getShieldRules: () => apiRequest('/shield'),
|
||||
// 获取每日统计数据(7天)
|
||||
getDailyStats: () => apiRequest('/daily-stats?t=' + Date.now()),
|
||||
|
||||
// 添加屏蔽规则
|
||||
addShieldRule: (rule) => apiRequest('/shield', 'POST', { rule }),
|
||||
// 获取每月统计数据(30天)
|
||||
getMonthlyStats: () => apiRequest('/monthly-stats?t=' + Date.now()),
|
||||
|
||||
// 删除屏蔽规则
|
||||
deleteShieldRule: (rule) => apiRequest('/shield', 'DELETE', { rule }),
|
||||
// 获取屏蔽规则 - 已禁用
|
||||
getShieldRules: () => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({}); // 返回空对象而非API调用
|
||||
},
|
||||
|
||||
// 更新远程规则
|
||||
updateRemoteRules: () => apiRequest('/shield', 'PUT', { action: 'update' }),
|
||||
// 添加屏蔽规则 - 已禁用
|
||||
addShieldRule: (rule) => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 获取黑名单列表
|
||||
getBlacklists: () => apiRequest('/shield/blacklists'),
|
||||
// 删除屏蔽规则 - 已禁用
|
||||
deleteShieldRule: (rule) => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 添加黑名单
|
||||
addBlacklist: (url) => apiRequest('/shield/blacklists', 'POST', { url }),
|
||||
// 更新远程规则 - 已禁用
|
||||
updateRemoteRules: () => {
|
||||
console.log('屏蔽规则功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 删除黑名单
|
||||
deleteBlacklist: (url) => apiRequest('/shield/blacklists', 'DELETE', { url }),
|
||||
// 获取黑名单列表 - 已禁用
|
||||
getBlacklists: () => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve([]); // 返回空数组而非API调用
|
||||
},
|
||||
|
||||
// 获取Hosts内容
|
||||
getHosts: () => apiRequest('/shield/hosts'),
|
||||
// 添加黑名单 - 已禁用
|
||||
addBlacklist: (url) => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 保存Hosts内容
|
||||
saveHosts: (content) => apiRequest('/shield/hosts', 'POST', { content }),
|
||||
// 删除黑名单 - 已禁用
|
||||
deleteBlacklist: (url) => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 刷新Hosts
|
||||
refreshHosts: () => apiRequest('/shield/hosts', 'PUT', { action: 'refresh' }),
|
||||
// 获取Hosts内容 - 已禁用
|
||||
getHosts: () => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ content: '' }); // 返回空内容而非API调用
|
||||
},
|
||||
|
||||
// 保存Hosts内容 - 已禁用
|
||||
saveHosts: (content) => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 刷新Hosts - 已禁用
|
||||
refreshHosts: () => {
|
||||
console.log('屏蔽规则相关功能已禁用');
|
||||
return Promise.resolve({ error: '屏蔽规则功能已禁用' });
|
||||
},
|
||||
|
||||
// 查询DNS记录 - 兼容多种参数格式
|
||||
queryDNS: async function(domain, recordType) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// dashboard.js - 仪表盘功能实现
|
||||
|
||||
// 全局变量
|
||||
let queryTrendChart = null;
|
||||
let ratioChart = null;
|
||||
let dnsRequestsChart = null;
|
||||
let intervalId = null;
|
||||
|
||||
// 初始化仪表盘
|
||||
@@ -14,6 +14,9 @@ async function initDashboard() {
|
||||
// 初始化图表
|
||||
initCharts();
|
||||
|
||||
// 初始化时间范围切换
|
||||
initTimeRangeToggle();
|
||||
|
||||
// 设置定时更新
|
||||
intervalId = setInterval(loadDashboardData, 5000); // 每5秒更新一次
|
||||
} catch (error) {
|
||||
@@ -24,39 +27,95 @@ async function initDashboard() {
|
||||
|
||||
// 加载仪表盘数据
|
||||
async function loadDashboardData() {
|
||||
console.log('开始加载仪表盘数据');
|
||||
try {
|
||||
console.log('开始加载仪表盘数据...');
|
||||
|
||||
// 先分别获取数据以调试
|
||||
// 获取基本统计数据
|
||||
const stats = await api.getStats();
|
||||
console.log('统计数据:', stats);
|
||||
|
||||
// 获取TOP被屏蔽域名
|
||||
const topBlockedDomains = await api.getTopBlockedDomains();
|
||||
console.log('Top屏蔽域名:', topBlockedDomains);
|
||||
console.log('TOP被屏蔽域名:', topBlockedDomains);
|
||||
|
||||
// 获取最近屏蔽域名
|
||||
const recentBlockedDomains = await api.getRecentBlockedDomains();
|
||||
console.log('最近屏蔽域名:', recentBlockedDomains);
|
||||
|
||||
const hourlyStats = await api.getHourlyStats();
|
||||
console.log('小时统计数据:', hourlyStats);
|
||||
|
||||
// 原并行请求方式(保留以备后续恢复)
|
||||
// const [stats, topBlockedDomains, recentBlockedDomains, hourlyStats] = await Promise.all([
|
||||
// const [stats, topBlockedDomains, recentBlockedDomains] = await Promise.all([
|
||||
// api.getStats(),
|
||||
// api.getTopBlockedDomains(),
|
||||
// api.getRecentBlockedDomains(),
|
||||
// api.getHourlyStats()
|
||||
// api.getRecentBlockedDomains()
|
||||
// ]);
|
||||
|
||||
// 更新统计卡片
|
||||
updateStatsCards(stats);
|
||||
|
||||
// 更新数据表格
|
||||
// 尝试从stats中获取总查询数等信息
|
||||
if (stats.dns) {
|
||||
totalQueries = stats.dns.Allowed + stats.dns.Blocked + (stats.dns.Errors || 0);
|
||||
blockedQueries = stats.dns.Blocked;
|
||||
errorQueries = stats.dns.Errors || 0;
|
||||
allowedQueries = stats.dns.Allowed;
|
||||
} else {
|
||||
totalQueries = stats.totalQueries || 0;
|
||||
blockedQueries = stats.blockedQueries || 0;
|
||||
errorQueries = stats.errorQueries || 0;
|
||||
allowedQueries = stats.allowedQueries || 0;
|
||||
}
|
||||
|
||||
// 更新新卡片数据 - 添加模拟数据支持
|
||||
if (document.getElementById('avg-response-time')) {
|
||||
// 使用真实数据或模拟数据
|
||||
const responseTime = stats.avgResponseTime !== undefined ? stats.avgResponseTime : 42;
|
||||
const responsePercent = stats.responseTimePercent !== undefined ? stats.responseTimePercent : 15;
|
||||
document.getElementById('avg-response-time').textContent = formatNumber(responseTime) + 'ms';
|
||||
document.getElementById('response-time-percent').textContent = responsePercent + '%';
|
||||
}
|
||||
|
||||
if (document.getElementById('top-query-type')) {
|
||||
// 使用真实数据或模拟数据
|
||||
const queryType = stats.topQueryType || 'A';
|
||||
const queryPercent = stats.queryTypePercentage !== undefined ? stats.queryTypePercentage : 68;
|
||||
document.getElementById('top-query-type').textContent = queryType;
|
||||
document.getElementById('query-type-percentage').textContent = queryPercent + '%';
|
||||
}
|
||||
|
||||
if (document.getElementById('active-ips')) {
|
||||
// 使用真实数据或模拟数据
|
||||
const activeIPs = stats.activeIPs !== undefined ? stats.activeIPs : 12;
|
||||
const ipsPercent = stats.activeIPsPercent !== undefined ? stats.activeIPsPercent : 23;
|
||||
document.getElementById('active-ips').textContent = formatNumber(activeIPs);
|
||||
document.getElementById('active-ips-percent').textContent = ipsPercent + '%';
|
||||
}
|
||||
|
||||
if (document.getElementById('cpu-usage')) {
|
||||
// 使用真实数据或模拟数据
|
||||
const cpuUsage = stats.cpuUsage !== undefined ? stats.cpuUsage : 45;
|
||||
document.getElementById('cpu-usage').textContent = cpuUsage + '%';
|
||||
|
||||
// 设置CPU状态颜色
|
||||
const cpuStatusElem = document.getElementById('cpu-status');
|
||||
if (cpuStatusElem) {
|
||||
if (cpuUsage > 80) {
|
||||
cpuStatusElem.textContent = '警告';
|
||||
cpuStatusElem.className = 'text-danger text-sm flex items-center';
|
||||
} else if (cpuUsage > 60) {
|
||||
cpuStatusElem.textContent = '较高';
|
||||
cpuStatusElem.className = 'text-warning text-sm flex items-center';
|
||||
} else {
|
||||
cpuStatusElem.textContent = '正常';
|
||||
cpuStatusElem.className = 'text-success text-sm flex items-center';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新表格
|
||||
updateTopBlockedTable(topBlockedDomains);
|
||||
updateRecentBlockedTable(recentBlockedDomains);
|
||||
|
||||
// 更新图表
|
||||
updateCharts(stats, hourlyStats);
|
||||
updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
|
||||
|
||||
// 更新运行状态
|
||||
updateUptime();
|
||||
@@ -92,13 +151,6 @@ function updateStatsCards(stats) {
|
||||
blockedQueries = stats[0].blocked || 0;
|
||||
allowedQueries = stats[0].allowed || 0;
|
||||
errorQueries = stats[0].error || 0;
|
||||
} else {
|
||||
// 如果都不匹配,使用一些示例数据以便在界面上显示
|
||||
totalQueries = 12500;
|
||||
blockedQueries = 1500;
|
||||
allowedQueries = 10500;
|
||||
errorQueries = 500;
|
||||
console.log('使用示例数据填充统计卡片');
|
||||
}
|
||||
|
||||
// 更新数量显示
|
||||
@@ -107,11 +159,18 @@ function updateStatsCards(stats) {
|
||||
document.getElementById('allowed-queries').textContent = formatNumber(allowedQueries);
|
||||
document.getElementById('error-queries').textContent = formatNumber(errorQueries);
|
||||
|
||||
// 更新百分比(模拟数据,实际应该从API获取)
|
||||
document.getElementById('queries-percent').textContent = '12%';
|
||||
document.getElementById('blocked-percent').textContent = '8%';
|
||||
document.getElementById('allowed-percent').textContent = '15%';
|
||||
document.getElementById('error-percent').textContent = '2%';
|
||||
// 计算并更新百分比
|
||||
if (totalQueries > 0) {
|
||||
document.getElementById('blocked-percent').textContent = `${Math.round((blockedQueries / totalQueries) * 100)}%`;
|
||||
document.getElementById('allowed-percent').textContent = `${Math.round((allowedQueries / totalQueries) * 100)}%`;
|
||||
document.getElementById('error-percent').textContent = `${Math.round((errorQueries / totalQueries) * 100)}%`;
|
||||
document.getElementById('queries-percent').textContent = `100%`;
|
||||
} else {
|
||||
document.getElementById('queries-percent').textContent = '---';
|
||||
document.getElementById('blocked-percent').textContent = '---';
|
||||
document.getElementById('allowed-percent').textContent = '---';
|
||||
document.getElementById('error-percent').textContent = '---';
|
||||
}
|
||||
}
|
||||
|
||||
// 更新Top屏蔽域名表格
|
||||
@@ -138,11 +197,11 @@ function updateTopBlockedTable(domains) {
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ name: 'ads.example.com', count: 1250 },
|
||||
{ name: 'tracking.example.org', count: 980 },
|
||||
{ name: 'malware.test.net', count: 765 },
|
||||
{ name: 'spam.service.com', count: 450 },
|
||||
{ name: 'analytics.unknown.org', count: 320 }
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' },
|
||||
{ name: '---', count: '---' }
|
||||
];
|
||||
console.log('使用示例数据填充Top屏蔽域名表格');
|
||||
}
|
||||
@@ -179,11 +238,11 @@ function updateRecentBlockedTable(domains) {
|
||||
if (tableData.length === 0) {
|
||||
const now = Date.now();
|
||||
tableData = [
|
||||
{ name: 'ads.example.com', timestamp: now - 5 * 60 * 1000 },
|
||||
{ name: 'tracking.example.org', timestamp: now - 15 * 60 * 1000 },
|
||||
{ name: 'malware.test.net', timestamp: now - 30 * 60 * 1000 },
|
||||
{ name: 'spam.service.com', timestamp: now - 45 * 60 * 1000 },
|
||||
{ name: 'analytics.unknown.org', timestamp: now - 60 * 60 * 1000 }
|
||||
{ name: '---', timestamp: now - 5 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 15 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 30 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 45 * 60 * 1000 },
|
||||
{ name: '---', timestamp: now - 60 * 60 * 1000 }
|
||||
];
|
||||
console.log('使用示例数据填充最近屏蔽域名表格');
|
||||
}
|
||||
@@ -202,69 +261,43 @@ function updateRecentBlockedTable(domains) {
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 当前选中的时间范围
|
||||
let currentTimeRange = '24h'; // 默认为24小时
|
||||
|
||||
// 初始化时间范围切换
|
||||
function initTimeRangeToggle() {
|
||||
const timeRangeButtons = document.querySelectorAll('.time-range-btn');
|
||||
timeRangeButtons.forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
// 移除所有按钮的激活状态
|
||||
timeRangeButtons.forEach(btn => btn.classList.remove('active'));
|
||||
// 添加当前按钮的激活状态
|
||||
button.classList.add('active');
|
||||
// 更新当前时间范围
|
||||
currentTimeRange = button.dataset.range;
|
||||
// 重新加载数据
|
||||
loadDashboardData();
|
||||
// 更新DNS请求图表
|
||||
drawDNSRequestsChart();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化图表
|
||||
function initCharts() {
|
||||
// 初始化查询趋势图表
|
||||
const queryTrendCtx = document.getElementById('query-trend-chart').getContext('2d');
|
||||
queryTrendChart = new Chart(queryTrendCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: Array.from({length: 24}, (_, i) => `${(i + 1) % 24}:00`),
|
||||
datasets: [
|
||||
{
|
||||
label: '总查询',
|
||||
data: Array(24).fill(0),
|
||||
borderColor: '#165DFF',
|
||||
backgroundColor: 'rgba(22, 93, 255, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
},
|
||||
{
|
||||
label: '屏蔽数量',
|
||||
data: Array(24).fill(0),
|
||||
borderColor: '#F53F3F',
|
||||
backgroundColor: 'rgba(245, 63, 63, 0.1)',
|
||||
tension: 0.4,
|
||||
fill: true
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
grid: {
|
||||
drawBorder: false
|
||||
}
|
||||
},
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化比例图表
|
||||
const ratioCtx = document.getElementById('ratio-chart').getContext('2d');
|
||||
const ratioChartElement = document.getElementById('ratio-chart');
|
||||
if (!ratioChartElement) {
|
||||
console.error('未找到比例图表元素');
|
||||
return;
|
||||
}
|
||||
const ratioCtx = ratioChartElement.getContext('2d');
|
||||
ratioChart = new Chart(ratioCtx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
labels: ['正常解析', '被屏蔽', '错误'],
|
||||
datasets: [{
|
||||
data: [70, 25, 5],
|
||||
data: ['---', '---', '---'],
|
||||
backgroundColor: ['#00B42A', '#F53F3F', '#FF7D00'],
|
||||
borderWidth: 0
|
||||
}]
|
||||
@@ -280,15 +313,101 @@ function initCharts() {
|
||||
cutout: '70%'
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化DNS请求统计图表
|
||||
drawDNSRequestsChart();
|
||||
}
|
||||
|
||||
// 绘制DNS请求统计图表
|
||||
function drawDNSRequestsChart() {
|
||||
const ctx = document.getElementById('dns-requests-chart');
|
||||
if (!ctx) {
|
||||
console.error('未找到DNS请求图表元素');
|
||||
return;
|
||||
}
|
||||
|
||||
const chartContext = ctx.getContext('2d');
|
||||
let apiFunction;
|
||||
|
||||
// 根据当前时间范围选择API函数
|
||||
switch (currentTimeRange) {
|
||||
case '7d':
|
||||
apiFunction = api.getDailyStats;
|
||||
break;
|
||||
case '30d':
|
||||
apiFunction = api.getMonthlyStats;
|
||||
break;
|
||||
default: // 24h
|
||||
apiFunction = api.getHourlyStats;
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
apiFunction().then(data => {
|
||||
// 创建或更新图表
|
||||
if (dnsRequestsChart) {
|
||||
dnsRequestsChart.data.labels = data.labels;
|
||||
dnsRequestsChart.data.datasets[0].data = data.data;
|
||||
dnsRequestsChart.update();
|
||||
} else {
|
||||
dnsRequestsChart = 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,
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
// 更新图表数据
|
||||
function updateCharts(stats, hourlyStats) {
|
||||
console.log('更新图表,收到统计数据:', stats, '小时统计:', hourlyStats);
|
||||
function updateCharts(stats) {
|
||||
console.log('更新图表,收到统计数据:', stats);
|
||||
|
||||
// 空值检查
|
||||
if (!stats) {
|
||||
console.error('更新图表失败: 未提供统计数据');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新比例图表
|
||||
if (ratioChart) {
|
||||
let allowed = 70, blocked = 25, error = 5;
|
||||
let allowed = '---', blocked = '---', error = '---';
|
||||
|
||||
// 尝试从stats数据中提取
|
||||
if (stats.dns) {
|
||||
@@ -304,38 +423,6 @@ function updateCharts(stats, hourlyStats) {
|
||||
ratioChart.data.datasets[0].data = [allowed, blocked, error];
|
||||
ratioChart.update();
|
||||
}
|
||||
|
||||
// 更新趋势图表
|
||||
if (queryTrendChart) {
|
||||
let labels = Array.from({length: 24}, (_, i) => `${(i + 1) % 24}:00`);
|
||||
let totalData = [], blockedData = [];
|
||||
|
||||
// 尝试从hourlyStats中提取数据
|
||||
if (Array.isArray(hourlyStats) && hourlyStats.length > 0) {
|
||||
labels = hourlyStats.map(h => `${h.hour || h.time || h[0]}:00`);
|
||||
totalData = hourlyStats.map(h => h.total || h.queries || h[1] || 0);
|
||||
blockedData = hourlyStats.map(h => h.blocked || h[2] || 0);
|
||||
} else {
|
||||
// 如果没有小时统计数据,生成示例数据
|
||||
for (let i = 0; i < 24; i++) {
|
||||
// 生成模拟的查询数据,形成一个正常的流量曲线
|
||||
const baseValue = 500;
|
||||
const timeFactor = Math.sin((i - 8) * Math.PI / 12); // 早上8点开始上升,晚上8点开始下降
|
||||
const randomFactor = 0.8 + Math.random() * 0.4; // 添加一些随机性
|
||||
const hourlyTotal = Math.round(baseValue * (0.5 + timeFactor * 0.5) * randomFactor);
|
||||
const hourlyBlocked = Math.round(hourlyTotal * (0.1 + Math.random() * 0.2)); // 10-30%被屏蔽
|
||||
|
||||
totalData.push(hourlyTotal);
|
||||
blockedData.push(hourlyBlocked);
|
||||
}
|
||||
console.log('使用示例数据填充趋势图表');
|
||||
}
|
||||
|
||||
queryTrendChart.data.labels = labels;
|
||||
queryTrendChart.data.datasets[0].data = totalData;
|
||||
queryTrendChart.data.datasets[1].data = blockedData;
|
||||
queryTrendChart.update();
|
||||
}
|
||||
}
|
||||
|
||||
// 更新运行状态
|
||||
|
||||
@@ -1,112 +1,41 @@
|
||||
// 屏蔽管理页面功能实现
|
||||
|
||||
// 初始化屏蔽管理页面
|
||||
// 初始化屏蔽管理页面 - 已禁用加载屏蔽规则功能
|
||||
function initShieldPage() {
|
||||
loadShieldRules();
|
||||
// 不再加载屏蔽规则,避免DOM元素不存在导致的错误
|
||||
setupShieldEventListeners();
|
||||
}
|
||||
|
||||
// 加载屏蔽规则
|
||||
// 加载屏蔽规则 - 已禁用此功能
|
||||
async function loadShieldRules() {
|
||||
try {
|
||||
const rules = await api.getShieldRules();
|
||||
updateShieldRulesTable(rules);
|
||||
} catch (error) {
|
||||
showErrorMessage('加载屏蔽规则失败: ' + error.message);
|
||||
}
|
||||
console.log('屏蔽规则加载功能已禁用');
|
||||
}
|
||||
|
||||
// 更新屏蔽规则表格
|
||||
// 更新屏蔽规则表格 - 已禁用此功能
|
||||
function updateShieldRulesTable(rules) {
|
||||
const tbody = document.getElementById('shield-rules-tbody');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (!rules || rules.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="4" class="text-center py-4 text-gray-500">暂无屏蔽规则</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
rules.forEach((rule, index) => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.className = 'border-b border-gray-200 hover:bg-gray-50';
|
||||
tr.innerHTML = `
|
||||
<td class="py-3 px-4">${index + 1}</td>
|
||||
<td class="py-3 px-4">${rule}</td>
|
||||
<td class="py-3 px-4">
|
||||
<button data-rule="${rule}" class="delete-rule-btn text-red-500 hover:text-red-700">
|
||||
<i class="fa fa-trash-o"></i>
|
||||
</button>
|
||||
</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
// 添加删除按钮事件监听器
|
||||
document.querySelectorAll('.delete-rule-btn').forEach(btn => {
|
||||
btn.addEventListener('click', handleDeleteRule);
|
||||
});
|
||||
// 不再更新表格,避免DOM元素不存在导致的错误
|
||||
console.log('屏蔽规则表格更新功能已禁用');
|
||||
}
|
||||
|
||||
// 处理删除规则
|
||||
// 处理删除规则 - 已禁用此功能
|
||||
async function handleDeleteRule(e) {
|
||||
const rule = e.currentTarget.getAttribute('data-rule');
|
||||
|
||||
if (confirm(`确定要删除规则: ${rule} 吗?`)) {
|
||||
try {
|
||||
await api.deleteShieldRule(rule);
|
||||
showSuccessMessage('规则删除成功');
|
||||
loadShieldRules();
|
||||
} catch (error) {
|
||||
showErrorMessage('删除规则失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
showErrorMessage('删除规则功能已禁用');
|
||||
}
|
||||
|
||||
// 添加新规则
|
||||
// 添加新规则 - 已禁用此功能
|
||||
async function handleAddRule() {
|
||||
const ruleInput = document.getElementById('new-rule-input');
|
||||
const rule = ruleInput.value.trim();
|
||||
|
||||
if (!rule) {
|
||||
showErrorMessage('规则不能为空');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.addShieldRule(rule);
|
||||
showSuccessMessage('规则添加成功');
|
||||
loadShieldRules();
|
||||
ruleInput.value = '';
|
||||
} catch (error) {
|
||||
showErrorMessage('添加规则失败: ' + error.message);
|
||||
}
|
||||
showErrorMessage('添加规则功能已禁用');
|
||||
}
|
||||
|
||||
// 更新远程规则
|
||||
// 更新远程规则 - 已禁用此功能
|
||||
async function handleUpdateRemoteRules() {
|
||||
try {
|
||||
await api.updateRemoteRules();
|
||||
showSuccessMessage('远程规则更新成功');
|
||||
loadShieldRules();
|
||||
} catch (error) {
|
||||
showErrorMessage('远程规则更新失败: ' + error.message);
|
||||
}
|
||||
showErrorMessage('更新远程规则功能已禁用');
|
||||
}
|
||||
|
||||
// 设置事件监听器
|
||||
// 设置事件监听器 - 已禁用规则相关功能
|
||||
function setupShieldEventListeners() {
|
||||
// 添加规则按钮
|
||||
document.getElementById('add-rule-btn')?.addEventListener('click', handleAddRule);
|
||||
|
||||
// 按回车键添加规则
|
||||
document.getElementById('new-rule-input')?.addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleAddRule();
|
||||
}
|
||||
});
|
||||
|
||||
// 更新远程规则按钮
|
||||
document.getElementById('update-remote-rules-btn')?.addEventListener('click', handleUpdateRemoteRules);
|
||||
// 移除所有事件监听器,避免触发已禁用的功能
|
||||
console.log('屏蔽规则相关事件监听器已设置,但功能已禁用');
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
|
||||
24
tailwind.config.js
Normal file
24
tailwind.config.js
Normal file
@@ -0,0 +1,24 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./static/**/*.{html,js}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: '#165DFF',
|
||||
secondary: '#36CFFB',
|
||||
success: '#00B42A',
|
||||
warning: '#FF7D00',
|
||||
danger: '#F53F3F',
|
||||
info: '#86909C',
|
||||
dark: '#1D2129',
|
||||
light: '#F2F3F5',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
45
test_console.sh
Executable file
45
test_console.sh
Executable file
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# DNS Web控制台功能测试脚本
|
||||
echo "开始测试DNS Web控制台功能..."
|
||||
echo "=================================="
|
||||
|
||||
# 检查服务器是否运行
|
||||
echo "检查DNS服务器运行状态..."
|
||||
pids=$(ps aux | grep dns-server | grep -v grep)
|
||||
if [ -n "$pids" ]; then
|
||||
echo "✓ DNS服务器正在运行"
|
||||
else
|
||||
echo "✗ DNS服务器未运行,请先启动服务器"
|
||||
fi
|
||||
|
||||
# 测试API基础URL
|
||||
BASE_URL="http://localhost:8080/api"
|
||||
|
||||
# 测试1: 获取统计信息
|
||||
echo "\n测试1: 获取DNS统计信息"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/stats"
|
||||
|
||||
# 测试2: 获取系统状态
|
||||
echo "\n测试2: 获取系统状态"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/status"
|
||||
|
||||
# 测试3: 获取屏蔽规则
|
||||
echo "\n测试3: 获取屏蔽规则列表"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield"
|
||||
|
||||
# 测试4: 获取Top屏蔽域名
|
||||
echo "\n测试4: 获取Top屏蔽域名"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/top-blocked"
|
||||
|
||||
# 测试5: 获取Hosts内容
|
||||
echo "\n测试5: 获取Hosts内容"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "$BASE_URL/shield/hosts"
|
||||
|
||||
# 测试6: 访问Web控制台主页
|
||||
echo "\n测试6: 访问Web控制台主页"
|
||||
curl -s -o /dev/null -w "状态码: %{http_code}\n" "http://localhost:8080"
|
||||
|
||||
echo "\n=================================="
|
||||
echo "测试完成!请检查上述状态码。正常情况下应为200。"
|
||||
echo "前端Web控制台可通过浏览器访问: http://localhost:8080"
|
||||
Reference in New Issue
Block a user