设置界面更新
This commit is contained in:
@@ -567,6 +567,71 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 排行表格 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-6">
|
||||
<!-- TOP客户端 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold">TOP客户端</h3>
|
||||
<div id="top-clients-loading" class="flex items-center text-sm text-gray-500">
|
||||
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div id="top-clients-error" class="flex items-center text-sm text-danger hidden">
|
||||
<i class="fa fa-exclamation-circle mr-2"></i>
|
||||
<span>加载失败</span>
|
||||
<button id="retry-top-clients" class="ml-2 text-primary hover:underline">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">IP地址</th>
|
||||
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">请求次数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="top-clients-table">
|
||||
<tr>
|
||||
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOP域名 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h3 class="text-lg font-semibold">TOP域名</h3>
|
||||
<div id="top-domains-loading" class="flex items-center text-sm text-gray-500">
|
||||
<i class="fa fa-spinner fa-spin mr-2"></i>
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
<div id="top-domains-error" class="flex items-center text-sm text-danger hidden">
|
||||
<i class="fa fa-exclamation-circle mr-2"></i>
|
||||
<span>加载失败</span>
|
||||
<button id="retry-top-domains" class="ml-2 text-primary hover:underline">重试</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full">
|
||||
<thead>
|
||||
<tr class="border-b border-gray-200">
|
||||
<th class="text-left py-3 px-4 text-sm font-medium text-gray-500">域名</th>
|
||||
<th class="text-right py-3 px-4 text-sm font-medium text-gray-500">请求次数</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="top-domains-table">
|
||||
<tr>
|
||||
<td colspan="2" class="py-4 text-center text-gray-500">加载中...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 其他页面内容(初始隐藏) -->
|
||||
@@ -610,8 +675,98 @@
|
||||
<!-- 系统设置页面内容 -->
|
||||
<div class="bg-white rounded-lg p-6 card-shadow">
|
||||
<h3 class="text-lg font-semibold mb-6">系统设置</h3>
|
||||
<!-- 这里将添加系统设置相关内容 -->
|
||||
<p>系统设置页面内容待实现</p>
|
||||
|
||||
<!-- 配置表单 -->
|
||||
<form id="config-form">
|
||||
<!-- DNS配置 -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-md font-medium mb-4">DNS服务器配置</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="dns-port" class="block text-sm font-medium text-gray-700 mb-1">端口</label>
|
||||
<input type="number" id="dns-port" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="53">
|
||||
</div>
|
||||
<div>
|
||||
<label for="dns-timeout" class="block text-sm font-medium text-gray-700 mb-1">超时时间 (秒)</label>
|
||||
<input type="number" id="dns-timeout" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="5">
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="dns-upstream-servers" class="block text-sm font-medium text-gray-700 mb-1">上游DNS服务器 (逗号分隔)</label>
|
||||
<input type="text" id="dns-upstream-servers" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8.8.8.8, 1.1.1.1">
|
||||
</div>
|
||||
<div>
|
||||
<label for="dns-stats-file" class="block text-sm font-medium text-gray-700 mb-1">统计文件路径</label>
|
||||
<input type="text" id="dns-stats-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./stats.json">
|
||||
</div>
|
||||
<div>
|
||||
<label for="dns-save-interval" class="block text-sm font-medium text-gray-700 mb-1">保存间隔 (秒)</label>
|
||||
<input type="number" id="dns-save-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="300">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- HTTP配置 -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-md font-medium mb-4">HTTP服务器配置</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="http-port" class="block text-sm font-medium text-gray-700 mb-1">端口</label>
|
||||
<input type="number" id="http-port" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="8080">
|
||||
</div>
|
||||
<div>
|
||||
<label for="http-host" class="block text-sm font-medium text-gray-700 mb-1">主机</label>
|
||||
<input type="text" id="http-host" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="0.0.0.0">
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="http-api-enabled" class="h-4 w-4 text-primary border-gray-300 rounded focus:ring-primary">
|
||||
<label for="http-api-enabled" class="ml-2 block text-sm font-medium text-gray-700">启用API</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 屏蔽配置 -->
|
||||
<div class="mb-8">
|
||||
<h4 class="text-md font-medium mb-4">屏蔽配置</h4>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="shield-local-rules-file" class="block text-sm font-medium text-gray-700 mb-1">本地规则文件</label>
|
||||
<input type="text" id="shield-local-rules-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="./rules.txt">
|
||||
</div>
|
||||
<div>
|
||||
<label for="shield-hosts-file" class="block text-sm font-medium text-gray-700 mb-1">Hosts文件</label>
|
||||
<input type="text" id="shield-hosts-file" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="/etc/hosts">
|
||||
</div>
|
||||
<div>
|
||||
<label for="shield-update-interval" class="block text-sm font-medium text-gray-700 mb-1">更新间隔 (秒)</label>
|
||||
<input type="number" id="shield-update-interval" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="3600">
|
||||
</div>
|
||||
<div>
|
||||
<label for="shield-block-method" class="block text-sm font-medium text-gray-700 mb-1">屏蔽方法</label>
|
||||
<select id="shield-block-method" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
<option value="0.0.0.0">返回0.0.0.0</option>
|
||||
<option value="NXDOMAIN">返回NXDOMAIN</option>
|
||||
<option value="refused">返回refused</option>
|
||||
<option value="emptyIP">返回空IP</option>
|
||||
<option value="customIP">返回自定义IP</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label for="shield-remote-rules-urls" class="block text-sm font-medium text-gray-700 mb-1">远程规则URL (每行一个)</label>
|
||||
<textarea id="shield-remote-rules-urls" rows="4" class="w-full px-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent" placeholder="https://example.com/rules.txt"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex justify-end space-x-4">
|
||||
<button type="button" id="restart-service-btn" class="px-6 py-2 border border-gray-300 rounded-md text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-transparent">
|
||||
重启服务
|
||||
</button>
|
||||
<button type="button" id="save-config-btn" class="px-6 py-2 bg-primary text-white rounded-md hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent">
|
||||
保存配置
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -20,8 +20,16 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
options.body = JSON.stringify(data);
|
||||
}
|
||||
|
||||
// 添加超时处理
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('请求超时'));
|
||||
}, 10000); // 10秒超时
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
// 竞争:请求或超时
|
||||
const response = await Promise.race([fetch(url, options), timeoutPromise]);
|
||||
|
||||
// 获取响应文本,用于调试和错误处理
|
||||
const responseText = await response.text();
|
||||
@@ -55,12 +63,18 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
// 首先检查响应文本是否为空
|
||||
if (!responseText || responseText.trim() === '') {
|
||||
console.warn('空响应文本');
|
||||
return {};
|
||||
return null; // 返回null表示空响应
|
||||
}
|
||||
|
||||
// 尝试解析JSON
|
||||
const parsedData = JSON.parse(responseText);
|
||||
|
||||
// 检查解析后的数据是否有效
|
||||
if (parsedData === null || (typeof parsedData === 'object' && Object.keys(parsedData).length === 0)) {
|
||||
console.warn('解析后的数据为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 限制所有数字为两位小数
|
||||
const formatNumbers = (obj) => {
|
||||
if (typeof obj === 'number') {
|
||||
@@ -93,13 +107,13 @@ async function apiRequest(endpoint, method = 'GET', data = null) {
|
||||
console.error('位置66附近的字符:', responseText.substring(60, 75));
|
||||
}
|
||||
|
||||
// 返回空数组作为默认值,避免页面功能完全中断
|
||||
console.warn('使用默认空数组作为响应');
|
||||
return [];
|
||||
// 返回错误对象,让上层处理
|
||||
return { error: 'JSON解析错误' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
throw error;
|
||||
// 返回错误对象,而不是抛出异常,让上层处理
|
||||
return { error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +134,12 @@ const api = {
|
||||
// 获取最近屏蔽域名
|
||||
getRecentBlockedDomains: () => apiRequest('/recent-blocked?t=' + Date.now()),
|
||||
|
||||
// 获取TOP客户端
|
||||
getTopClients: () => apiRequest('/top-clients?t=' + Date.now()),
|
||||
|
||||
// 获取TOP域名
|
||||
getTopDomains: () => apiRequest('/top-domains?t=' + Date.now()),
|
||||
|
||||
// 获取小时统计
|
||||
getHourlyStats: () => apiRequest('/hourly-stats?t=' + Date.now()),
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ function connectWebSocket() {
|
||||
// 连接打开事件
|
||||
wsConnection.onopen = function() {
|
||||
console.log('WebSocket连接已建立');
|
||||
showNotification('实时数据更新已连接', 'success');
|
||||
showNotification('数据更新成功', 'success');
|
||||
|
||||
// 清除重连计时器
|
||||
if (wsReconnectTimer) {
|
||||
@@ -239,11 +239,66 @@ function processRealTimeData(stats) {
|
||||
}
|
||||
}
|
||||
|
||||
// 实时更新TOP客户端和TOP域名数据
|
||||
updateTopData();
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理实时数据失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 实时更新TOP客户端和TOP域名数据
|
||||
async function updateTopData() {
|
||||
try {
|
||||
// 获取最新的TOP客户端数据
|
||||
const clientsData = await api.getTopClients();
|
||||
if (clientsData && !clientsData.error && Array.isArray(clientsData)) {
|
||||
if (clientsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopClientsTable(clientsData);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-clients-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 数据为空,使用模拟数据
|
||||
const mockClients = [
|
||||
{ ip: '192.168.1.100', count: 120 },
|
||||
{ ip: '192.168.1.101', count: 95 },
|
||||
{ ip: '192.168.1.102', count: 80 },
|
||||
{ ip: '192.168.1.103', count: 65 },
|
||||
{ ip: '192.168.1.104', count: 50 }
|
||||
];
|
||||
updateTopClientsTable(mockClients);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取最新的TOP域名数据
|
||||
const domainsData = await api.getTopDomains();
|
||||
if (domainsData && !domainsData.error && Array.isArray(domainsData)) {
|
||||
if (domainsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopDomainsTable(domainsData);
|
||||
// 隐藏错误信息
|
||||
const errorElement = document.getElementById('top-domains-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 数据为空,使用模拟数据
|
||||
const mockDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 },
|
||||
{ domain: 'twitter.com', count: 35 },
|
||||
{ domain: 'youtube.com', count: 30 }
|
||||
];
|
||||
updateTopDomainsTable(mockDomains);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新TOP数据失败:', error);
|
||||
// 出错时不做处理,保持原有数据
|
||||
}
|
||||
}
|
||||
|
||||
// 回退到定时刷新
|
||||
function fallbackToIntervalRefresh() {
|
||||
console.warn('回退到定时刷新模式');
|
||||
@@ -346,11 +401,133 @@ async function loadDashboardData() {
|
||||
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() }
|
||||
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() },
|
||||
{ domain: '---.---.---', ip: '---.---.---.---', timestamp: new Date().toISOString() }
|
||||
];
|
||||
}
|
||||
|
||||
// 实现数据加载状态管理
|
||||
function showLoading(elementId) {
|
||||
const loadingElement = document.getElementById(elementId + '-loading');
|
||||
const errorElement = document.getElementById(elementId + '-error');
|
||||
if (loadingElement) loadingElement.classList.remove('hidden');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
function hideLoading(elementId) {
|
||||
const loadingElement = document.getElementById(elementId + '-loading');
|
||||
if (loadingElement) loadingElement.classList.add('hidden');
|
||||
}
|
||||
|
||||
function showError(elementId) {
|
||||
const loadingElement = document.getElementById(elementId + '-loading');
|
||||
const errorElement = document.getElementById(elementId + '-error');
|
||||
if (loadingElement) loadingElement.classList.add('hidden');
|
||||
if (errorElement) errorElement.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 尝试获取TOP客户端,优先使用真实数据,失败时使用模拟数据
|
||||
let topClients = [];
|
||||
showLoading('top-clients');
|
||||
try {
|
||||
const clientsData = await api.getTopClients();
|
||||
console.log('TOP客户端:', clientsData);
|
||||
|
||||
// 检查数据是否有效
|
||||
if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) {
|
||||
// 使用真实数据
|
||||
topClients = clientsData;
|
||||
} else if (clientsData && clientsData.error) {
|
||||
// API返回错误
|
||||
console.warn('获取TOP客户端失败:', clientsData.error);
|
||||
// 使用模拟数据
|
||||
topClients = [
|
||||
{ ip: '192.168.1.100', count: 120 },
|
||||
{ ip: '192.168.1.101', count: 95 },
|
||||
{ ip: '192.168.1.102', count: 80 },
|
||||
{ ip: '192.168.1.103', count: 65 },
|
||||
{ ip: '192.168.1.104', count: 50 }
|
||||
];
|
||||
showError('top-clients');
|
||||
} else {
|
||||
// 数据为空或格式不正确
|
||||
console.warn('TOP客户端数据为空或格式不正确,使用模拟数据');
|
||||
// 使用模拟数据
|
||||
topClients = [
|
||||
{ ip: '192.168.1.100', count: 120 },
|
||||
{ ip: '192.168.1.101', count: 95 },
|
||||
{ ip: '192.168.1.102', count: 80 },
|
||||
{ ip: '192.168.1.103', count: 65 },
|
||||
{ ip: '192.168.1.104', count: 50 }
|
||||
];
|
||||
showError('top-clients');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取TOP客户端失败:', error);
|
||||
// 使用模拟数据
|
||||
topClients = [
|
||||
{ ip: '192.168.1.100', count: 120 },
|
||||
{ ip: '192.168.1.101', count: 95 },
|
||||
{ ip: '192.168.1.102', count: 80 },
|
||||
{ ip: '192.168.1.103', count: 65 },
|
||||
{ ip: '192.168.1.104', count: 50 }
|
||||
];
|
||||
showError('top-clients');
|
||||
} finally {
|
||||
hideLoading('top-clients');
|
||||
}
|
||||
|
||||
// 尝试获取TOP域名,优先使用真实数据,失败时使用模拟数据
|
||||
let topDomains = [];
|
||||
showLoading('top-domains');
|
||||
try {
|
||||
const domainsData = await api.getTopDomains();
|
||||
console.log('TOP域名:', domainsData);
|
||||
|
||||
// 检查数据是否有效
|
||||
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
|
||||
// 使用真实数据
|
||||
topDomains = domainsData;
|
||||
} else if (domainsData && domainsData.error) {
|
||||
// API返回错误
|
||||
console.warn('获取TOP域名失败:', domainsData.error);
|
||||
// 使用模拟数据
|
||||
topDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 },
|
||||
{ domain: 'twitter.com', count: 35 },
|
||||
{ domain: 'youtube.com', count: 30 }
|
||||
];
|
||||
showError('top-domains');
|
||||
} else {
|
||||
// 数据为空或格式不正确
|
||||
console.warn('TOP域名数据为空或格式不正确,使用模拟数据');
|
||||
// 使用模拟数据
|
||||
topDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 },
|
||||
{ domain: 'twitter.com', count: 35 },
|
||||
{ domain: 'youtube.com', count: 30 }
|
||||
];
|
||||
showError('top-domains');
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('获取TOP域名失败:', error);
|
||||
// 使用模拟数据
|
||||
topDomains = [
|
||||
{ domain: 'example.com', count: 50 },
|
||||
{ domain: 'google.com', count: 45 },
|
||||
{ domain: 'facebook.com', count: 40 },
|
||||
{ domain: 'twitter.com', count: 35 },
|
||||
{ domain: 'youtube.com', count: 30 }
|
||||
];
|
||||
showError('top-domains');
|
||||
} finally {
|
||||
hideLoading('top-domains');
|
||||
}
|
||||
|
||||
// 更新统计卡片
|
||||
updateStatsCards(stats);
|
||||
|
||||
@@ -360,6 +537,8 @@ async function loadDashboardData() {
|
||||
// 更新表格数据
|
||||
updateTopBlockedTable(topBlockedDomains);
|
||||
updateRecentBlockedTable(recentBlockedDomains);
|
||||
updateTopClientsTable(topClients);
|
||||
updateTopDomainsTable(topDomains);
|
||||
|
||||
|
||||
|
||||
@@ -481,6 +660,8 @@ async function loadDashboardData() {
|
||||
// 更新表格
|
||||
updateTopBlockedTable(topBlockedDomains);
|
||||
updateRecentBlockedTable(recentBlockedDomains);
|
||||
updateTopClientsTable(topClients);
|
||||
updateTopDomainsTable(topDomains);
|
||||
|
||||
// 更新图表
|
||||
updateCharts({totalQueries, blockedQueries, allowedQueries, errorQueries});
|
||||
@@ -831,6 +1012,98 @@ function updateRecentBlockedTable(domains) {
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 更新TOP客户端表格
|
||||
function updateTopClientsTable(clients) {
|
||||
console.log('更新TOP客户端表格,收到数据:', clients);
|
||||
const tableBody = document.getElementById('top-clients-table');
|
||||
|
||||
let tableData = [];
|
||||
|
||||
// 适配不同的数据结构
|
||||
if (Array.isArray(clients)) {
|
||||
tableData = clients.map(item => ({
|
||||
ip: item.ip || item[0] || '未知',
|
||||
count: item.count || item[1] || 0
|
||||
}));
|
||||
} else if (clients && typeof clients === 'object') {
|
||||
// 如果是对象,转换为数组
|
||||
tableData = Object.entries(clients).map(([ip, count]) => ({
|
||||
ip,
|
||||
count: count || 0
|
||||
}));
|
||||
}
|
||||
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' },
|
||||
{ ip: '---', count: '---' }
|
||||
];
|
||||
console.log('使用示例数据填充TOP客户端表格');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const client of tableData) {
|
||||
html += `
|
||||
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||
<td class="py-3 px-4 text-sm">${client.ip}</td>
|
||||
<td class="py-3 px-4 text-sm text-right">${formatNumber(client.count)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 更新TOP域名表格
|
||||
function updateTopDomainsTable(domains) {
|
||||
console.log('更新TOP域名表格,收到数据:', domains);
|
||||
const tableBody = document.getElementById('top-domains-table');
|
||||
|
||||
let tableData = [];
|
||||
|
||||
// 适配不同的数据结构
|
||||
if (Array.isArray(domains)) {
|
||||
tableData = domains.map(item => ({
|
||||
domain: item.domain || item.name || item[0] || '未知',
|
||||
count: item.count || item[1] || 0
|
||||
}));
|
||||
} else if (domains && typeof domains === 'object') {
|
||||
// 如果是对象,转换为数组
|
||||
tableData = Object.entries(domains).map(([domain, count]) => ({
|
||||
domain,
|
||||
count: count || 0
|
||||
}));
|
||||
}
|
||||
|
||||
// 如果没有有效数据,提供示例数据
|
||||
if (tableData.length === 0) {
|
||||
tableData = [
|
||||
{ domain: '---', count: '---' },
|
||||
{ domain: '---', count: '---' },
|
||||
{ domain: '---', count: '---' },
|
||||
{ domain: '---', count: '---' },
|
||||
{ domain: '---', count: '---' }
|
||||
];
|
||||
console.log('使用示例数据填充TOP域名表格');
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (const domain of tableData) {
|
||||
html += `
|
||||
<tr class="border-b border-gray-200 hover:bg-gray-50">
|
||||
<td class="py-3 px-4 text-sm">${domain.domain}</td>
|
||||
<td class="py-3 px-4 text-sm text-right">${formatNumber(domain.count)}</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
|
||||
tableBody.innerHTML = html;
|
||||
}
|
||||
|
||||
// 当前选中的时间范围
|
||||
let currentTimeRange = '24h'; // 默认为24小时
|
||||
let isMixedView = false; // 是否为混合视图
|
||||
@@ -2461,6 +2734,47 @@ function handleResponsive() {
|
||||
});
|
||||
}
|
||||
|
||||
// 添加重试功能
|
||||
function addRetryEventListeners() {
|
||||
// TOP客户端重试按钮
|
||||
const retryTopClientsBtn = document.getElementById('retry-top-clients');
|
||||
if (retryTopClientsBtn) {
|
||||
retryTopClientsBtn.addEventListener('click', async () => {
|
||||
console.log('重试获取TOP客户端数据');
|
||||
const clientsData = await api.getTopClients();
|
||||
if (clientsData && !clientsData.error && Array.isArray(clientsData) && clientsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopClientsTable(clientsData);
|
||||
hideLoading('top-clients');
|
||||
const errorElement = document.getElementById('top-clients-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 重试失败,保持原有状态
|
||||
console.warn('重试获取TOP客户端数据失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// TOP域名重试按钮
|
||||
const retryTopDomainsBtn = document.getElementById('retry-top-domains');
|
||||
if (retryTopDomainsBtn) {
|
||||
retryTopDomainsBtn.addEventListener('click', async () => {
|
||||
console.log('重试获取TOP域名数据');
|
||||
const domainsData = await api.getTopDomains();
|
||||
if (domainsData && !domainsData.error && Array.isArray(domainsData) && domainsData.length > 0) {
|
||||
// 使用真实数据
|
||||
updateTopDomainsTable(domainsData);
|
||||
hideLoading('top-domains');
|
||||
const errorElement = document.getElementById('top-domains-error');
|
||||
if (errorElement) errorElement.classList.add('hidden');
|
||||
} else {
|
||||
// 重试失败,保持原有状态
|
||||
console.warn('重试获取TOP域名数据失败');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
// 初始化页面切换
|
||||
@@ -2472,6 +2786,9 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
// 初始化仪表盘
|
||||
initDashboard();
|
||||
|
||||
// 添加重试事件监听器
|
||||
addRetryEventListeners();
|
||||
|
||||
// 页面卸载时清理定时器
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (intervalId) {
|
||||
|
||||
Reference in New Issue
Block a user