|
@@ -24,6 +24,9 @@ async function initializeApp() {
|
|
// 设置定时刷新
|
|
// 设置定时刷新
|
|
setInterval(loadDashboard, 30000); // 每30秒刷新一次
|
|
setInterval(loadDashboard, 30000); // 每30秒刷新一次
|
|
|
|
|
|
|
|
+ // 设置实时搜索
|
|
|
|
+ setupRealTimeSearch();
|
|
|
|
+
|
|
console.log('应用初始化完成');
|
|
console.log('应用初始化完成');
|
|
} catch (error) {
|
|
} catch (error) {
|
|
console.error('应用初始化失败:', error);
|
|
console.error('应用初始化失败:', error);
|
|
@@ -135,26 +138,36 @@ function updateRecentResults(results) {
|
|
|
|
|
|
const html = results.map(result => `
|
|
const html = results.map(result => `
|
|
<div class="result-item ${result.isSuccess ? 'result-success' : 'result-failure'}">
|
|
<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>
|
|
|
|
+ <div class="result-time">${formatTime(result.testTime)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
`).join('');
|
|
@@ -201,9 +214,19 @@ function updateNodesList(data) {
|
|
return;
|
|
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="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-header">
|
|
<div class="node-name">${node.name}</div>
|
|
<div class="node-name">${node.name}</div>
|
|
<div class="node-status ${node.status}">${node.status === 'online' ? '在线' : '离线'}</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>
|
|
<i class="bi bi-hdd-network"></i>
|
|
类型: ${node.type}
|
|
类型: ${node.type}
|
|
</div>
|
|
</div>
|
|
- <div class="node-info-item">
|
|
|
|
|
|
+ <div class="node-info-item server-info">
|
|
<i class="bi bi-geo-alt"></i>
|
|
<i class="bi bi-geo-alt"></i>
|
|
服务器: ${node.server}
|
|
服务器: ${node.server}
|
|
</div>
|
|
</div>
|
|
@@ -229,7 +252,7 @@ function updateNodesList(data) {
|
|
${node.testResults[0].isSuccess ? `
|
|
${node.testResults[0].isSuccess ? `
|
|
<span class="latency-separator">|</span>
|
|
<span class="latency-separator">|</span>
|
|
<i class="bi bi-speedometer2"></i>
|
|
<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>
|
|
</div>
|
|
` : ''}
|
|
` : ''}
|
|
@@ -249,19 +272,33 @@ function updateNodesList(data) {
|
|
container.innerHTML = html;
|
|
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 {
|
|
try {
|
|
showLoading();
|
|
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 isSuccess = document.getElementById('result-status-filter')?.value || '';
|
|
const startDate = document.getElementById('start-date')?.value || '';
|
|
const startDate = document.getElementById('start-date')?.value || '';
|
|
const endDate = document.getElementById('end-date')?.value || '';
|
|
const endDate = document.getElementById('end-date')?.value || '';
|
|
|
|
|
|
const params = new URLSearchParams({
|
|
const params = new URLSearchParams({
|
|
- limit: 1000, // 设置一个很大的限制,获取所有结果
|
|
|
|
- ...(nodeId && { nodeId }),
|
|
|
|
|
|
+ page: page,
|
|
|
|
+ limit: 20, // 每页显示20条记录
|
|
|
|
+ ...(nodeName && { nodeName }),
|
|
...(isSuccess && { isSuccess }),
|
|
...(isSuccess && { isSuccess }),
|
|
...(startDate && { startDate }),
|
|
...(startDate && { startDate }),
|
|
...(endDate && { endDate })
|
|
...(endDate && { endDate })
|
|
@@ -272,6 +309,7 @@ async function loadTestResults() {
|
|
|
|
|
|
if (data.success) {
|
|
if (data.success) {
|
|
updateTestResults(data.data);
|
|
updateTestResults(data.data);
|
|
|
|
+ currentResultsPage = page;
|
|
}
|
|
}
|
|
|
|
|
|
hideLoading();
|
|
hideLoading();
|
|
@@ -288,39 +326,50 @@ function updateTestResults(data) {
|
|
const { results, pagination } = data;
|
|
const { results, pagination } = data;
|
|
|
|
|
|
if (results.length === 0) {
|
|
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;
|
|
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-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>
|
|
</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>
|
|
|
|
+ <div class="result-time">${formatTime(result.testTime)}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
- `).join('') + '</div>';
|
|
|
|
|
|
+ `).join('');
|
|
|
|
|
|
container.innerHTML = html;
|
|
container.innerHTML = html;
|
|
|
|
+
|
|
|
|
+ // 更新分页
|
|
|
|
+ updatePagination('test-results-pagination', pagination, loadTestResults);
|
|
}
|
|
}
|
|
|
|
|
|
// 加载通知记录
|
|
// 加载通知记录
|
|
@@ -558,8 +607,24 @@ function searchNodes() {
|
|
}
|
|
}
|
|
|
|
|
|
// 搜索结果
|
|
// 搜索结果
|
|
|
|
+// 搜索防抖函数
|
|
|
|
+let searchTimeout;
|
|
|
|
+
|
|
function searchResults() {
|
|
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 防抖延迟
|
|
|
|
+ });
|
|
|
|
+ }
|
|
}
|
|
}
|
|
|
|
|
|
// 导入节点
|
|
// 导入节点
|