瀏覽代碼

测速结果页面样式调整

Taio_O 1 周之前
父節點
當前提交
f8ad4ac8a8
共有 7 個文件被更改,包括 428 次插入100 次删除
  1. 98 0
      go-speed-test/internal/core/scheduler.go
  2. 11 0
      go-speed-test/internal/database/database.go
  3. 116 51
      public/app.js
  4. 8 8
      public/index.html
  5. 143 21
      public/styles.css
  6. 15 6
      src/api/routes.js
  7. 37 14
      src/core/scheduler.js

+ 98 - 0
go-speed-test/internal/core/scheduler.go

@@ -117,6 +117,9 @@ func (s *Scheduler) runSpeedTest() {
 	// 执行测速
 	results := s.speedTester.TestNodes(nodes)
 
+	// 处理测试结果并更新节点状态
+	s.processTestResults(nodes, results)
+
 	// 统计结果
 	successCount := 0
 	failCount := 0
@@ -135,6 +138,101 @@ func (s *Scheduler) runSpeedTest() {
 	})
 }
 
+// 处理测试结果并更新节点状态
+func (s *Scheduler) processTestResults(nodes []database.Node, results []*database.TestResult) {
+	for _, node := range nodes {
+		// 查找对应的测试结果
+		var testResult *database.TestResult
+		for _, result := range results {
+			if result.NodeID == node.ID {
+				testResult = result
+				break
+			}
+		}
+
+		if testResult == nil {
+			continue
+		}
+
+		previousStatus := node.Status
+		previousFailureCount := node.FailureCount
+
+		// 更新节点状态
+		if testResult.IsSuccess {
+			// 检查延迟是否超时(超过2000ms)
+			isTimeout := testResult.Latency != nil && *testResult.Latency > 2000
+
+			if isTimeout {
+				// 延迟超时,标记为故障节点
+				newFailureCount := previousFailureCount + 1
+				
+				updateData := map[string]interface{}{
+					"status":         "offline",
+					"last_test_time": testResult.TestTime,
+					"last_test_result": false,
+					"failure_count":  newFailureCount,
+				}
+				
+				if testResult.Latency != nil {
+					updateData["average_latency"] = *testResult.Latency
+				}
+
+				if err := database.UpdateNode(node.ID, updateData); err != nil {
+					logger.Error("更新节点状态失败", map[string]interface{}{
+						"node_id": node.ID,
+						"error":   err.Error(),
+					})
+				}
+
+				logger.Warn("节点延迟超时,标记为故障", map[string]interface{}{
+					"node_name": node.Name,
+					"latency":   *testResult.Latency,
+				})
+			} else {
+				// 测试成功且延迟正常
+				updateData := map[string]interface{}{
+					"status":         "online",
+					"last_test_time": testResult.TestTime,
+					"last_test_result": true,
+					"failure_count":  0,
+				}
+
+				if testResult.Latency != nil {
+					updateData["average_latency"] = *testResult.Latency
+				}
+
+				if testResult.DownloadSpeed != nil {
+					updateData["average_speed"] = *testResult.DownloadSpeed
+				}
+
+				if err := database.UpdateNode(node.ID, updateData); err != nil {
+					logger.Error("更新节点状态失败", map[string]interface{}{
+						"node_id": node.ID,
+						"error":   err.Error(),
+					})
+				}
+			}
+		} else {
+			// 测试失败
+			newFailureCount := previousFailureCount + 1
+			
+			updateData := map[string]interface{}{
+				"status":         "offline",
+				"last_test_time": testResult.TestTime,
+				"last_test_result": false,
+				"failure_count":  newFailureCount,
+			}
+
+			if err := database.UpdateNode(node.ID, updateData); err != nil {
+				logger.Error("更新节点状态失败", map[string]interface{}{
+					"node_id": node.ID,
+					"error":   err.Error(),
+				})
+			}
+		}
+	}
+}
+
 // 获取调度器状态
 func (s *Scheduler) IsRunning() bool {
 	s.mu.RLock()

+ 11 - 0
go-speed-test/internal/database/database.go

@@ -34,6 +34,12 @@ type Node struct {
 	Obfs        string    `gorm:"size:50" json:"obfs"`
 	Group       string    `gorm:"size:255" json:"group"`
 	IsActive    bool      `gorm:"default:true" json:"is_active"`
+	Status      string    `gorm:"size:20;default:'offline'" json:"status"`
+	LastTestTime *time.Time `json:"last_test_time"`
+	LastTestResult *bool   `json:"last_test_result"`
+	AverageLatency *int    `json:"average_latency"`
+	AverageSpeed   *float64 `json:"average_speed"`
+	FailureCount   int     `gorm:"default:0" json:"failure_count"`
 	CreatedAt   time.Time `json:"created_at"`
 	UpdatedAt   time.Time `json:"updated_at"`
 }
@@ -133,4 +139,9 @@ func GetNodeTestHistory(nodeID uint, limit int) ([]TestResult, error) {
 	var results []TestResult
 	err := DB.Where("node_id = ?", nodeID).Order("test_time DESC").Limit(limit).Find(&results).Error
 	return results, err
+}
+
+// 更新节点信息
+func UpdateNode(nodeID uint, updateData map[string]interface{}) error {
+	return DB.Model(&Node{}).Where("id = ?", nodeID).Updates(updateData).Error
 } 

+ 116 - 51
public/app.js

@@ -24,6 +24,9 @@ async function initializeApp() {
         // 设置定时刷新
         setInterval(loadDashboard, 30000); // 每30秒刷新一次
         
+        // 设置实时搜索
+        setupRealTimeSearch();
+        
         console.log('应用初始化完成');
     } catch (error) {
         console.error('应用初始化失败:', error);
@@ -135,26 +138,36 @@ function updateRecentResults(results) {
     
     const html = results.map(result => `
         <div class="result-item ${result.isSuccess ? 'result-success' : 'result-failure'}">
-            <div class="result-header">
-                <div class="result-node">${result.node ? result.node.name : '未知节点'}</div>
-                <div class="result-time">${formatTime(result.testTime)}</div>
-            </div>
-            <div class="result-details">
-                <div class="result-detail">
-                    <i class="bi ${result.isSuccess ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
-                    状态: ${result.isSuccess ? '成功' : '失败'}
-                </div>
-                ${result.isSuccess ? `
-                    <div class="result-detail">
-                        <i class="bi bi-speedometer2"></i>
-                        延迟: ${result.latency}ms
-                    </div>
-                ` : `
-                    <div class="result-detail">
-                        <i class="bi bi-exclamation-triangle"></i>
-                        错误: ${result.error || '未知错误'}
+            <div class="result-content">
+                <div class="result-left">
+                    <div class="result-node">${result.node ? result.node.name : '未知节点'}</div>
+                    <div class="result-info">
+                        <div class="result-status">
+                            <i class="bi ${result.isSuccess ? 'bi-check-circle' : 'bi-x-circle'}"></i>
+                            ${result.isSuccess ? '成功' : '失败'}
+                        </div>
+                        ${result.isSuccess ? `
+                            <div class="result-latency">
+                                <i class="bi bi-speedometer2"></i>
+                                ${(() => {
+                                    const avg = result.node && result.node.averageLatency != null ? result.node.averageLatency : null;
+                                    const latency = result.latency;
+                                    if (avg != null) {
+                                        return avg > 2000 ? '超时' : avg + 'ms';
+                                    } else {
+                                        return latency > 2000 ? '超时' : latency + 'ms';
+                                    }
+                                })()}
+                            </div>
+                        ` : `
+                            <div class="result-error">
+                                <i class="bi bi-exclamation-triangle"></i>
+                                ${result.error || '未知错误'}
+                            </div>
+                        `}
                     </div>
-                `}
+                </div>
+                <div class="result-time">${formatTime(result.testTime)}</div>
             </div>
         </div>
     `).join('');
@@ -201,9 +214,19 @@ function updateNodesList(data) {
         return;
     }
     
-    const html = '<div class="row">' + nodes.map((node, index) => `
+    // 对节点进行排序:故障节点(offline)排在前面
+    const sortedNodes = [...nodes].sort((a, b) => {
+        // 首先按状态排序:offline在前,online在后
+        if (a.status === 'offline' && b.status === 'online') return -1;
+        if (a.status === 'online' && b.status === 'offline') return 1;
+        
+        // 如果状态相同,按名称排序
+        return a.name.localeCompare(b.name);
+    });
+    
+    const html = '<div class="row">' + sortedNodes.map((node, index) => `
         <div class="col-md-6 mb-3">
-            <div class="node-item">
+            <div class="node-item ${node.status === 'offline' ? 'node-offline' : ''}">
                 <div class="node-header">
                     <div class="node-name">${node.name}</div>
                     <div class="node-status ${node.status}">${node.status === 'online' ? '在线' : '离线'}</div>
@@ -213,7 +236,7 @@ function updateNodesList(data) {
                         <i class="bi bi-hdd-network"></i>
                         类型: ${node.type}
                     </div>
-                    <div class="node-info-item">
+                    <div class="node-info-item server-info">
                         <i class="bi bi-geo-alt"></i>
                         服务器: ${node.server}
                     </div>
@@ -229,7 +252,7 @@ function updateNodesList(data) {
                             ${node.testResults[0].isSuccess ? `
                                 <span class="latency-separator">|</span>
                                 <i class="bi bi-speedometer2"></i>
-                                延迟: <span class="latency-value">${node.testResults[0].latency}ms</span>
+                                延迟: <span class="latency-value ${node.testResults[0].latency > 2000 ? 'text-danger' : ''}">${node.testResults[0].latency > 2000 ? '超时' : node.testResults[0].latency + 'ms'}</span>
                             ` : ''}
                         </div>
                     ` : ''}
@@ -249,19 +272,33 @@ function updateNodesList(data) {
     container.innerHTML = html;
 }
 
+// 更新节点统计信息
+function updateNodesStats(nodes) {
+    const totalNodes = nodes.length;
+    const onlineNodes = nodes.filter(node => node.status === 'online').length;
+    const offlineNodes = nodes.filter(node => node.status === 'offline').length;
+    const onlineRate = totalNodes > 0 ? Math.round((onlineNodes / totalNodes) * 100) : 0;
+    
+    document.getElementById('total-nodes-count').textContent = totalNodes;
+    document.getElementById('online-nodes-count').textContent = onlineNodes;
+    document.getElementById('offline-nodes-count').textContent = offlineNodes;
+    document.getElementById('online-rate').textContent = onlineRate + '%';
+}
+
 // 加载测速结果
-async function loadTestResults() {
+async function loadTestResults(page = 1) {
     try {
         showLoading();
         
-        const nodeId = document.getElementById('result-node-filter')?.value || '';
+        const nodeName = document.getElementById('result-node-filter')?.value || '';
         const isSuccess = document.getElementById('result-status-filter')?.value || '';
         const startDate = document.getElementById('start-date')?.value || '';
         const endDate = document.getElementById('end-date')?.value || '';
         
         const params = new URLSearchParams({
-            limit: 1000, // 设置一个很大的限制,获取所有结果
-            ...(nodeId && { nodeId }),
+            page: page,
+            limit: 20, // 每页显示20条记录
+            ...(nodeName && { nodeName }),
             ...(isSuccess && { isSuccess }),
             ...(startDate && { startDate }),
             ...(endDate && { endDate })
@@ -272,6 +309,7 @@ async function loadTestResults() {
         
         if (data.success) {
             updateTestResults(data.data);
+            currentResultsPage = page;
         }
         
         hideLoading();
@@ -288,39 +326,50 @@ function updateTestResults(data) {
     const { results, pagination } = data;
     
     if (results.length === 0) {
-        container.innerHTML = '<div class="row"><div class="col-12"><div class="text-center text-muted">暂无测速结果</div></div></div>';
+        container.innerHTML = '<div class="text-center text-muted">暂无测速结果</div>';
         return;
     }
     
-    const html = '<div class="row">' + results.map((result, index) => `
-        <div class="col-md-6 mb-3">
-            <div class="result-item ${result.isSuccess ? 'result-success' : 'result-failure'}">
-                <div class="result-header">
+    const html = results.map((result, index) => `
+        <div class="result-item ${result.isSuccess ? 'result-success' : 'result-failure'}">
+            <div class="result-content">
+                <div class="result-left">
                     <div class="result-node">${result.node ? result.node.name : '未知节点'}</div>
-                    <div class="result-time">${formatTime(result.testTime)}</div>
-                </div>
-                <div class="result-details">
-                    <div class="result-detail">
-                        <i class="bi ${result.isSuccess ? 'bi-check-circle text-success' : 'bi-x-circle text-danger'}"></i>
-                        状态: ${result.isSuccess ? '成功' : '失败'}
-                    </div>
-                    ${result.isSuccess ? `
-                        <div class="result-detail">
-                            <i class="bi bi-speedometer2"></i>
-                            延迟: ${result.latency}ms
-                        </div>
-                    ` : `
-                        <div class="result-detail">
-                            <i class="bi bi-exclamation-triangle"></i>
-                            错误: ${result.error || '未知错误'}
+                    <div class="result-info">
+                        <div class="result-status">
+                            <i class="bi ${result.isSuccess ? 'bi-check-circle' : 'bi-x-circle'}"></i>
+                            ${result.isSuccess ? '成功' : '失败'}
                         </div>
-                    `}
+                        ${result.isSuccess ? `
+                            <div class="result-latency">
+                                <i class="bi bi-speedometer2"></i>
+                                ${(() => {
+                                    const avg = result.node && result.node.averageLatency != null ? result.node.averageLatency : null;
+                                    const latency = result.latency;
+                                    if (avg != null) {
+                                        return avg > 2000 ? '超时' : avg + 'ms';
+                                    } else {
+                                        return latency > 2000 ? '超时' : latency + 'ms';
+                                    }
+                                })()}
+                            </div>
+                        ` : `
+                            <div class="result-error">
+                                <i class="bi bi-exclamation-triangle"></i>
+                                ${result.error || '未知错误'}
+                            </div>
+                        `}
+                    </div>
                 </div>
+                <div class="result-time">${formatTime(result.testTime)}</div>
             </div>
         </div>
-    `).join('') + '</div>';
+    `).join('');
     
     container.innerHTML = html;
+    
+    // 更新分页
+    updatePagination('test-results-pagination', pagination, loadTestResults);
 }
 
 // 加载通知记录
@@ -558,8 +607,24 @@ function searchNodes() {
 }
 
 // 搜索结果
+// 搜索防抖函数
+let searchTimeout;
+
 function searchResults() {
-    loadTestResults();
+    loadTestResults(1);
+}
+
+// 实时搜索功能
+function setupRealTimeSearch() {
+    const nodeFilter = document.getElementById('result-node-filter');
+    if (nodeFilter) {
+        nodeFilter.addEventListener('input', function() {
+            clearTimeout(searchTimeout);
+            searchTimeout = setTimeout(() => {
+                loadTestResults(1);
+            }, 500); // 500ms 防抖延迟
+        });
+    }
 }
 
 // 导入节点

+ 8 - 8
public/index.html

@@ -227,6 +227,8 @@
                 </div>
             </div>
 
+
+
             <!-- 节点列表 -->
             <div class="row">
                 <div class="col-12">
@@ -265,9 +267,7 @@
                         <div class="card-body">
                             <div class="row">
                                 <div class="col-md-3">
-                                    <select class="form-select" id="result-node-filter">
-                                        <option value="">所有节点</option>
-                                    </select>
+                                    <input type="text" class="form-control" id="result-node-filter" placeholder="输入节点名称搜索...">
                                 </div>
                                 <div class="col-md-2">
                                     <select class="form-select" id="result-status-filter">
@@ -302,13 +302,13 @@
                         </div>
                         <div class="card-body">
                             <div id="test-results">
-                                <div class="row">
-                                    <div class="text-center text-muted">
-                                        <i class="bi bi-hourglass-split"></i> 加载中...
-                                    </div>
+                                <div class="text-center text-muted">
+                                    <i class="bi bi-hourglass-split"></i> 加载中...
                                 </div>
                             </div>
-
+                            <!-- 分页 -->
+                            <nav aria-label="测速结果分页" id="test-results-pagination">
+                            </nav>
                         </div>
                     </div>
                 </div>

+ 143 - 21
public/styles.css

@@ -127,9 +127,19 @@ body {
     color: #721c24;
 }
 
+/* 故障节点特殊样式 */
+.node-offline {
+    border-color: #dc3545;
+    background-color: #fff5f5;
+}
+
+.node-offline .node-name {
+    color: #dc3545;
+}
+
 .node-info {
     display: grid;
-    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+    grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
     gap: 10px;
     font-size: 0.85rem;
     color: #666;
@@ -159,9 +169,22 @@ body {
 .node-info-item {
     display: flex;
     align-items: center;
-    white-space: nowrap;
-    overflow: hidden;
-    text-overflow: ellipsis;
+    word-break: break-all;
+    overflow-wrap: break-word;
+    hyphens: auto;
+}
+
+.node-info-item.server-info {
+    grid-column: 1 / -1;
+    word-break: break-all;
+    overflow-wrap: break-word;
+    min-height: auto;
+    line-height: 1.4;
+}
+
+.node-info-item.server-info i {
+    margin-top: 2px;
+    align-self: flex-start;
 }
 
 .node-info-item i {
@@ -177,6 +200,12 @@ body {
     padding: 15px;
     margin-bottom: 10px;
     background: white;
+    transition: all 0.2s ease;
+}
+
+.result-item:hover {
+    box-shadow: 0 4px 12px rgba(0,0,0,0.1);
+    transform: translateY(-1px);
 }
 
 .result-success {
@@ -187,38 +216,92 @@ body {
     border-left: 4px solid #dc3545;
 }
 
-.result-header {
+.result-content {
     display: flex;
-    justify-content: between;
+    justify-content: space-between;
     align-items: center;
-    margin-bottom: 10px;
+    gap: 20px;
+}
+
+.result-left {
+    display: flex;
+    align-items: center;
+    gap: 20px;
+    flex: 1;
 }
 
 .result-node {
     font-weight: 600;
     color: #333;
+    min-width: 150px;
+    font-size: 0.95rem;
 }
 
-.result-time {
+.result-info {
+    display: flex;
+    align-items: center;
+    gap: 15px;
+}
+
+.result-status {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 0.85rem;
+    padding: 2px 8px;
+    border-radius: 12px;
+    background-color: #f8f9fa;
     color: #666;
-    font-size: 0.9rem;
 }
 
-.result-details {
-    display: grid;
-    grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
-    gap: 10px;
-    font-size: 0.9rem;
+.result-success .result-status {
+    background-color: #d4edda;
+    color: #155724;
 }
 
-.result-detail {
+.result-failure .result-status {
+    background-color: #f8d7da;
+    color: #721c24;
+}
+
+.result-latency {
+    display: flex;
+    align-items: center;
+    gap: 4px;
+    font-size: 0.85rem;
+    color: #007bff;
+    font-weight: 600;
+    padding: 2px 8px;
+    border-radius: 12px;
+    background-color: #e7f3ff;
+}
+
+.result-error {
     display: flex;
     align-items: center;
+    gap: 4px;
+    font-size: 0.85rem;
+    color: #dc3545;
+    padding: 2px 8px;
+    border-radius: 12px;
+    background-color: #f8d7da;
+}
+
+.result-time {
+    color: #666;
+    font-size: 0.85rem;
+    white-space: nowrap;
+    padding: 2px 8px;
+    border-radius: 12px;
+    background-color: #f8f9fa;
 }
 
-.result-detail i {
-    margin-right: 5px;
-    width: 16px;
+.result-status i,
+.result-latency i,
+.result-error i {
+    margin-right: 4px;
+    width: 14px;
+    flex-shrink: 0;
 }
 
 /* 通知样式 */
@@ -295,6 +378,11 @@ body {
 .pagination {
     justify-content: center;
     margin-top: 20px;
+    margin-bottom: 10px;
+}
+
+#test-results-pagination {
+    margin-top: 15px;
 }
 
 .page-link {
@@ -335,12 +423,40 @@ body {
 @media (max-width: 768px) {
     .node-info {
         grid-template-columns: 1fr;
-        gap: 6px;
+        gap: 8px;
         font-size: 0.8rem;
     }
     
-    .result-details {
-        grid-template-columns: 1fr;
+    .result-content {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 10px;
+    }
+    
+    .result-left {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 8px;
+        width: 100%;
+    }
+    
+    .result-info {
+        flex-direction: column;
+        align-items: flex-start;
+        gap: 6px;
+    }
+    
+    .result-node {
+        min-width: auto;
+        font-size: 0.9rem;
+    }
+    
+    .result-status,
+    .result-latency,
+    .result-error,
+    .result-time {
+        font-size: 0.8rem;
+        padding: 3px 8px;
     }
     
     .stat-card {
@@ -349,6 +465,12 @@ body {
     
     .node-info-item {
         white-space: normal;
+        word-break: break-all;
+    }
+    
+    .node-info-item.server-info {
+        font-size: 0.75rem;
+        line-height: 1.3;
     }
 }
 

+ 15 - 6
src/api/routes.js

@@ -287,7 +287,7 @@ router.post('/test/ping/batch', async (req, res) => {
 
 router.get('/test/results', async (req, res) => {
   try {
-    const { page = 1, limit = 50, nodeId, isSuccess, startDate, endDate } = req.query;
+    const { page = 1, limit = 50, nodeId, nodeName, isSuccess, startDate, endDate } = req.query;
     const offset = (page - 1) * limit;
     
     const where = {};
@@ -299,16 +299,25 @@ router.get('/test/results', async (req, res) => {
       if (endDate) where.testTime[require('sequelize').Op.lte] = new Date(endDate);
     }
 
+    const includeOptions = [{
+      model: Node,
+      as: 'node',
+      attributes: ['name', 'type', 'group', 'averageLatency']
+    }];
+
+    // 如果提供了节点名称,添加节点名称过滤条件
+    if (nodeName) {
+      includeOptions[0].where = {
+        name: { [require('sequelize').Op.like]: `%${nodeName}%` }
+      };
+    }
+
     const { count, rows } = await TestResult.findAndCountAll({
       where,
       limit: parseInt(limit),
       offset: parseInt(offset),
       order: [['testTime', 'DESC']],
-      include: [{
-        model: Node,
-        as: 'node',
-        attributes: ['name', 'type', 'group']
-      }]
+      include: includeOptions
     });
 
     res.json({

+ 37 - 14
src/core/scheduler.js

@@ -138,22 +138,45 @@ class Scheduler {
 
       // 更新节点状态
       if (testResult.isSuccess) {
-        // 测试成功
-        if (node.status === 'offline') {
-          // 节点从离线恢复
-          if (previousFailureCount >= this.recoveryThreshold) {
-            await this.notifier.sendRecoveryNotification(node, testResult);
+        // 检查延迟是否超时(超过2000ms)
+        const isTimeout = testResult.latency && testResult.latency > 2000;
+        
+        if (isTimeout) {
+          // 延迟超时,标记为故障节点
+          const newFailureCount = previousFailureCount + 1;
+          
+          await node.update({
+            status: 'offline',
+            lastTestTime: testResult.testTime,
+            lastTestResult: false,
+            failureCount: newFailureCount,
+            averageLatency: testResult.latency
+          });
+
+          // 检查是否需要发送故障通知
+          if (newFailureCount === this.failureThreshold && previousStatus === 'online') {
+            await this.notifier.sendFailureNotification(node, testResult);
+          }
+          
+          logger.warn(`节点延迟超时,标记为故障: ${node.name} - 延迟: ${testResult.latency}ms`);
+        } else {
+          // 测试成功且延迟正常
+          if (node.status === 'offline') {
+            // 节点从离线恢复
+            if (previousFailureCount >= this.recoveryThreshold) {
+              await this.notifier.sendRecoveryNotification(node, testResult);
+            }
           }
-        }
 
-        await node.update({
-          status: 'online',
-          lastTestTime: testResult.testTime,
-          lastTestResult: true,
-          failureCount: 0,
-          averageLatency: testResult.latency,
-          averageSpeed: testResult.downloadSpeed
-        });
+          await node.update({
+            status: 'online',
+            lastTestTime: testResult.testTime,
+            lastTestResult: true,
+            failureCount: 0,
+            averageLatency: testResult.latency,
+            averageSpeed: testResult.downloadSpeed
+          });
+        }
 
       } else {
         // 测试失败