Files
openwrt_packages/luci-app-bandix/htdocs/luci-static/resources/view/bandix/dns.js
2025-11-21 00:13:07 +08:00

1130 lines
45 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
'require view';
'require ui';
'require uci';
'require rpc';
'require poll';
// 暗色模式检测已改为使用 CSS 媒体查询 @media (prefers-color-scheme: dark)
function formatTimestamp(timestamp) {
if (!timestamp) return '-';
var date = new Date(timestamp);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
fractionalSecondDigits: 3
});
}
function formatResponseCode(code) {
if (code === 'Success' || code === 'NOERROR') return _('Success');
if (code === 'Domain not found' || code === 'NXDomain' || code === 'NXDOMAIN') return _('Domain not found');
if (code === 'Server error' || code === 'ServFail' || code === 'SERVFAIL') return _('Server error');
if (code === 'Format error' || code === 'FormErr' || code === 'FORMERR') return _('Format error');
if (code === 'Refused' || code === 'Refused' || code === 'REFUSED') return _('Refused');
return code || _('Other');
}
function formatDeviceName(device) {
var parts = [];
if (device && device.device_name && device.device_name !== '') {
parts.push(device.device_name);
}
// 显示IP地址
// 查询时使用source_ip查询设备的IP
// 响应时使用destination_ip目标IP即查询设备的IP而不是source_ipDNS服务器的IP
var ip = null;
if (device && device.is_query) {
// 查询记录使用source_ip
ip = device.source_ip;
} else {
// 响应记录使用destination_ip目标IP
ip = device.destination_ip;
}
if (ip) {
parts.push(ip);
}
if (parts.length === 0) {
return _('Unknown Device');
}
return parts.join(' / ');
}
function formatDnsServer(query) {
if (!query) return '-';
// 查询时显示目标IPdestination_ip响应时显示源IPsource_ip
if (query.is_query) {
return query.destination_ip || '-';
} else {
return query.source_ip || '-';
}
}
function formatResponseResult(query) {
if (!query) return { display: [], full: [] };
// 显示响应记录response_records它是一个字符串数组
if (query.response_records && Array.isArray(query.response_records) && query.response_records.length > 0) {
var maxDisplay = 5; // 最多显示5条
var displayRecords = query.response_records.slice(0, maxDisplay);
var fullRecords = query.response_records;
return {
display: displayRecords,
full: fullRecords,
hasMore: fullRecords.length > maxDisplay
};
}
return { display: [], full: [] };
}
var callGetDnsQueries = rpc.declare({
object: 'luci.bandix',
method: 'getDnsQueries',
params: ['domain', 'device', 'is_query', 'dns_server', 'page', 'page_size'],
expect: {}
});
var callGetDnsStats = rpc.declare({
object: 'luci.bandix',
method: 'getDnsStats',
expect: {}
});
return view.extend({
load: function () {
return Promise.all([
uci.load('bandix'),
uci.load('luci'),
uci.load('argon').catch(function () {
return null;
})
]);
},
render: function (data) {
var dnsEnabled = uci.get('bandix', 'dns', 'enabled') === '1';
var style = E('style', {}, `
.bandix-dns-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.bandix-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.bandix-title {
font-size: 1.5rem;
font-weight: 600;
margin: 0;
}
.bandix-alert {
border-radius: 4px;
padding: 10px 12px;
display: flex;
align-items: center;
gap: 10px;
font-size: 0.875rem;
}
.bandix-alert-icon {
font-size: 0.875rem;
font-weight: 700;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
flex-shrink: 0;
}
.filter-section {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 16px;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;
}
.filter-input {
padding: 6px 12px;
border-radius: 4px;
font-size: 0.875rem;
min-width: 150px;
opacity: 1;
}
.bandix-table {
width: 100%;
font-size: 0.875rem;
}
.bandix-table th {
padding: 10px 12px;
text-align: left;
font-weight: 600;
opacity: 1;
white-space: nowrap;
}
.bandix-table td {
padding: 10px 12px;
word-break: break-word;
}
.query-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.query-badge.query {
background-color: #3b82f6;
color: white;
}
.query-badge.response {
background-color: #10b981;
color: white;
}
.response-code {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 600;
}
.response-code.success {
background-color: #10b981;
color: white;
}
.response-code.error {
background-color: #ef4444;
color: white;
}
.response-code.warning {
background-color: #f59e0b;
color: white;
}
.pagination {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px;
flex-wrap: wrap;
gap: 12px;
}
.pagination-info {
font-size: 0.875rem;
opacity: 0.7;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.loading-state {
text-align: center;
padding: 40px;
opacity: 0.7;
font-style: italic;
}
.error-state {
text-align: center;
padding: 40px;
}
.refresh-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
.response-ips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.response-ip-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 0.75rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) {
.stats-grid {
grid-template-columns: 1fr;
}
}
.stats-card-title {
font-size: 0.875rem;
font-weight: 600;
opacity: 0.7;
margin: 0 0 12px 0;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.stats-grid .cbi-section {
padding: 16px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
transition: box-shadow 0.2s ease, transform 0.2s ease, border-color 0.2s ease;
}
.stats-grid .cbi-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
@media (prefers-color-scheme: dark) {
.stats-grid .cbi-section {
border-color: rgba(255, 255, 255, 0.15);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.stats-grid .cbi-section:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
}
.stats-card-main-value {
font-size: 2.25rem;
font-weight: 700;
margin: 0 0 8px 0;
line-height: 1;
}
.stats-card-sub-value {
font-size: 0.875rem;
opacity: 0.7;
margin: 0;
}
.stats-card-details {
margin-top: 16px;
display: flex;
flex-direction: column;
gap: 8px;
}
.stats-detail-row {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
}
.stats-detail-label {
opacity: 0.7;
font-weight: 500;
}
.stats-detail-value {
font-weight: 600;
}
.stats-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
font-size: 0.875rem;
}
.stats-list-name {
opacity: 0.8;
}
.stats-list-count {
font-weight: 600;
opacity: 0.9;
}
`);
document.head.appendChild(style);
var container = E('div', { 'class': 'bandix-dns-container' });
var header = E('div', { 'class': 'bandix-header' }, [
E('h1', { 'class': 'bandix-title' }, _('Bandix DNS Monitor'))
]);
container.appendChild(header);
if (!dnsEnabled) {
var alertDiv = E('div', { 'class': 'bandix-alert' }, [
E('div', {}, [
E('strong', {}, _('DNS Monitoring Disabled')),
E('p', { 'style': 'margin: 4px 0 0 0;' },
_('Please enable DNS monitoring in settings'))
])
]);
container.appendChild(alertDiv);
var settingsCard = E('div', { 'class': 'cbi-section' }, [
E('div', { 'style': 'text-align: center; padding: 16px;' }, [
E('a', {
'href': '/cgi-bin/luci/admin/network/bandix/settings',
'class': 'btn btn-primary'
}, _('Go to Settings'))
])
]);
container.appendChild(settingsCard);
return container;
}
// 添加提示信息
var infoAlert = E('div', { 'class': 'bandix-alert' }, [
E('span', {}, _('Does not include DoH and DoT'))
]);
container.appendChild(infoAlert);
// DNS 统计信息卡片
var statsGrid = E('div', { 'class': 'stats-grid', 'id': 'dns-stats-grid' });
container.appendChild(statsGrid);
// DNS 查询记录
var queriesSection = E('div', { 'class': 'cbi-section' }, [
E('h3', {}, _('DNS Query Records')),
E('div', {}, [
E('div', { 'class': 'filter-section' }, [
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, _('Type Filter') + ':'),
E('select', { 'class': 'cbi-select', 'id': 'type-filter' }, [
E('option', { 'value': '' }, _('All')),
E('option', { 'value': 'true' }, _('Queries Only')),
E('option', { 'value': 'false' }, _('Responses Only'))
])
]),
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, _('Domain Filter') + ':'),
E('input', {
'type': 'text',
'class': 'filter-input',
'id': 'domain-filter',
'placeholder': _('Search Domain')
})
]),
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, _('Device Filter') + ':'),
E('input', {
'type': 'text',
'class': 'filter-input',
'id': 'device-filter',
'placeholder': _('Search Device')
})
]),
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, _('DNS Server Filter') + ':'),
E('input', {
'type': 'text',
'class': 'filter-input',
'id': 'dns-server-filter',
'placeholder': _('Search DNS Server')
})
]),
E('div', { 'class': 'filter-group', 'style': 'margin-left: auto;' }, [
E('button', {
'class': 'cbi-button cbi-button-action',
'id': 'refresh-queries-btn'
}, _('Refresh'))
])
]),
E('div', { 'id': 'dns-queries-container' }, [
E('div', { 'class': 'loading-state' }, _('Loading data...'))
])
])
]);
container.appendChild(queriesSection);
// 状态变量
var currentPage = 1;
var pageSize = 20;
var currentFilters = {
domain: '',
device: '',
is_query: '',
dns_server: ''
};
// 更新查询记录
function updateQueries() {
var container = document.getElementById('dns-queries-container');
if (!container) return Promise.resolve();
// 检查是否有现有内容
var hasContent = container.querySelector('.bandix-table') || container.querySelector('.loading-state') || container.querySelector('.error-state');
// 保存当前容器高度,避免跳动(只在有内容时)
var currentHeight = container.offsetHeight;
if (currentHeight > 0 && hasContent) {
container.style.minHeight = currentHeight + 'px';
}
// 显示加载状态,但保持容器结构(只在有内容时显示遮罩层)
var loadingDiv = container.querySelector('.loading-overlay');
if (hasContent) {
if (!loadingDiv) {
loadingDiv = E('div', {
'class': 'loading-overlay',
'style': 'position: absolute; top: 0; left: 0; right: 0; bottom: 0; display: flex; align-items: center; justify-content: center; z-index: 10;'
}, _('Loading data...'));
container.style.position = 'relative';
container.appendChild(loadingDiv);
// 应用主题背景色
setTimeout(function() {
applyThemeColors();
}, 50);
} else {
loadingDiv.style.display = 'flex';
}
} else {
// 如果没有内容,使用简单的加载状态
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'loading-state' },
_('Loading data...')));
// 应用主题背景色
setTimeout(function() {
applyThemeColors();
}, 50);
}
return callGetDnsQueries(
currentFilters.domain,
currentFilters.device,
currentFilters.is_query,
currentFilters.dns_server,
currentPage,
pageSize
).then(function (result) {
// 隐藏或移除加载状态
if (loadingDiv) {
loadingDiv.remove();
}
// 移除刷新蒙版
var refreshOverlay = container.querySelector('.refresh-overlay');
if (refreshOverlay) {
refreshOverlay.remove();
}
// 恢复最小高度和定位
container.style.minHeight = '';
if (!hasContent) {
container.style.position = '';
}
if (!result || result.status !== 'success' || !result.data) {
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'error-state' },
_('Unable to fetch data')));
// 应用主题背景色
setTimeout(function() {
applyThemeColors();
}, 50);
return;
}
var queries = result.data.queries || [];
var total = result.data.total || 0;
var totalPages = result.data.total_pages || 1;
if (queries.length === 0) {
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'loading-state' },
_('No Data')));
// 应用主题背景色
setTimeout(function() {
applyThemeColors();
}, 50);
return;
}
// 移除旧的表格和分页
var oldTable = container.querySelector('.bandix-table');
var oldPagination = container.querySelector('.pagination');
var oldLoadingState = container.querySelector('.loading-state');
if (oldTable) oldTable.remove();
if (oldPagination) oldPagination.remove();
if (oldLoadingState) oldLoadingState.remove();
// 确保容器是相对定位(用于遮罩层)
container.style.position = 'relative';
var table = E('table', { 'class': 'bandix-table' }, [
E('thead', {}, [
E('tr', {}, [
E('th', { 'style': 'width: 180px;' }, _('Time')),
E('th', { 'style': 'width: 200px;' }, _('Domain')),
E('th', { 'style': 'width: 100px;' }, _('Query Type')),
E('th', { 'style': 'width: 100px;' }, _('Type')),
E('th', { 'style': 'width: 100px;' }, _('Response Time')),
E('th', { 'style': 'width: 200px;' }, _('Device')),
E('th', { 'style': 'width: 140px;' }, _('DNS Server')),
E('th', { 'style': 'width: 200px;' }, _('Response Result'))
])
]),
E('tbody', {}, queries.map(function (query) {
return E('tr', {}, [
E('td', {}, formatTimestamp(query.timestamp)),
E('td', {}, query.domain || '-'),
E('td', {}, query.query_type || '-'),
E('td', {}, [
E('span', {
'class': 'query-badge ' + (query.is_query ? 'query' : 'response')
}, query.is_query ? _('Query') : _('Response'))
]),
E('td', {}, query.response_time_ms ? query.response_time_ms + ' ' + _('ms') : '-'),
E('td', {}, formatDeviceName(query)),
E('td', {}, formatDnsServer(query)),
E('td', {}, [
E('div', {
'class': 'response-ips',
'title': (function() {
var result = formatResponseResult(query);
if (result.full.length === 0) {
return '';
}
return result.full.join('\n');
})()
}, (function() {
var result = formatResponseResult(query);
if (result.display.length === 0) {
return [E('span', { 'class': 'response-ip-badge' }, '-')];
}
var badges = result.display.map(function (item) {
return E('span', { 'class': 'response-ip-badge' }, item);
});
if (result.hasMore) {
badges.push(E('span', { 'class': 'response-ip-badge', 'style': 'opacity: 0.7;' }, '...'));
}
return badges;
})())
])
]);
}))
]);
var pagination = E('div', { 'class': 'pagination' }, [
E('div', { 'class': 'pagination-info' },
_('Page') + ' ' + currentPage + ' ' + _('of') + ' ' + totalPages + '' + _('Total') + ' ' + total + ' ' + _('records')
),
E('div', { 'class': 'pagination-controls' }, [
E('select', {
'class': 'cbi-select',
'id': 'page-size-select',
'style': 'margin-right: 8px;'
}, [
E('option', { 'value': '10', 'selected': pageSize === 10 }, '10'),
E('option', { 'value': '20', 'selected': pageSize === 20 }, '20'),
E('option', { 'value': '50', 'selected': pageSize === 50 }, '50'),
E('option', { 'value': '100', 'selected': pageSize === 100 }, '100')
]),
E('button', {
'class': 'cbi-button cbi-button-action',
'id': 'prev-page-btn',
'disabled': currentPage <= 1 ? 'disabled' : null
}, _('Previous')),
E('button', {
'class': 'cbi-button cbi-button-action',
'id': 'next-page-btn',
'disabled': currentPage >= totalPages ? 'disabled' : null
}, _('Next'))
])
]);
container.appendChild(table);
container.appendChild(pagination);
// 绑定分页事件
var prevBtn = document.getElementById('prev-page-btn');
var nextBtn = document.getElementById('next-page-btn');
var pageSizeSelect = document.getElementById('page-size-select');
// 设置按钮的 disabled 状态
if (prevBtn) {
if (currentPage <= 1) {
prevBtn.setAttribute('disabled', 'disabled');
} else {
prevBtn.removeAttribute('disabled');
}
prevBtn.onclick = function (e) {
e.preventDefault();
e.stopPropagation();
if (currentPage > 1) {
currentPage--;
updateQueries();
}
};
}
if (nextBtn) {
if (currentPage >= totalPages) {
nextBtn.setAttribute('disabled', 'disabled');
} else {
nextBtn.removeAttribute('disabled');
}
nextBtn.onclick = function (e) {
e.preventDefault();
e.stopPropagation();
if (currentPage < totalPages) {
currentPage++;
updateQueries();
}
};
}
if (pageSizeSelect) {
pageSizeSelect.value = pageSize.toString();
pageSizeSelect.onchange = function (e) {
e.preventDefault();
e.stopPropagation();
pageSize = parseInt(this.value);
currentPage = 1;
updateQueries();
};
}
}).catch(function (error) {
console.error('Failed to load DNS queries:', error);
var container = document.getElementById('dns-queries-container');
if (!container) return;
// 移除刷新蒙版
var refreshOverlay = container.querySelector('.refresh-overlay');
if (refreshOverlay) {
refreshOverlay.remove();
}
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'error-state' },
_('Unable to fetch data')));
// 应用主题背景色
setTimeout(function() {
applyThemeColors();
}, 50);
});
}
// 更新统计信息卡片
function updateStats() {
var statsGrid = document.getElementById('dns-stats-grid');
if (!statsGrid) return Promise.resolve();
return callGetDnsStats().then(function (result) {
if (!result || result.status !== 'success' || !result.data || !result.data.stats) {
statsGrid.innerHTML = '';
return;
}
var stats = result.data.stats;
statsGrid.innerHTML = '';
// 格式化时间范围
function formatTimeRange(start, end, durationMinutes) {
if (!start || !end) return '-';
var startDate = new Date(start);
var endDate = new Date(end);
var startStr = startDate.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
var endStr = endDate.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return startStr + ' - ' + endStr + ' (' + durationMinutes + ' ' + _('minutes') + ')';
}
// 格式化百分比
function formatPercent(value) {
if (typeof value !== 'number') return '-';
return (value * 100).toFixed(2) + '%';
}
// 查询和响应数量卡片
// 使用后端返回的 total_queries 和 total_responses
var queryCount = stats.total_queries || 0;
var responseCount = stats.total_responses || 0;
var totalQueriesCard = E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'stats-card-title' }, _('Query & Response')),
E('div', { 'class': 'stats-card-main-value' }, (queryCount + responseCount || 0).toLocaleString()),
E('div', { 'class': 'stats-card-details' }, [
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, _('Queries') + ':'),
E('span', { 'class': 'stats-detail-value' }, queryCount.toLocaleString())
]),
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, _('Responses') + ':'),
E('span', { 'class': 'stats-detail-value' }, responseCount.toLocaleString())
])
])
]);
statsGrid.appendChild(totalQueriesCard);
// 响应时间卡片
statsGrid.appendChild(E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'stats-card-title' }, _('Response Time')),
E('div', { 'class': 'stats-card-main-value' }, Math.round(stats.latest_response_time_ms || 0) + ' ' + _('ms')),
E('div', { 'class': 'stats-card-details' }, [
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, _('Average Response Time') + ':'),
E('span', { 'class': 'stats-detail-value' }, (stats.avg_response_time_ms || 0).toFixed(2) + ' ' + _('ms'))
]),
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, _('Min Response Time') + ':'),
E('span', { 'class': 'stats-detail-value' }, (stats.min_response_time_ms || 0) + ' ' + _('ms'))
]),
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, _('Max Response Time') + ':'),
E('span', { 'class': 'stats-detail-value' }, (stats.max_response_time_ms || 0) + ' ' + _('ms'))
])
])
]));
// 最常用查询类型卡片
if (stats.top_query_types && stats.top_query_types.length > 0) {
var queryTypesList = stats.top_query_types.map(function(item) {
return E('div', { 'class': 'stats-list-item' }, [
E('span', { 'class': 'stats-list-name' }, item.name || '-'),
E('span', { 'class': 'stats-list-count' }, (item.count || 0).toLocaleString())
]);
});
var queryTypesCard = E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'stats-card-title' }, _('Top Query Types')),
E('div', { 'class': 'stats-card-details' })
]);
queryTypesList.forEach(function(item) {
queryTypesCard.querySelector('.stats-card-details').appendChild(item);
});
statsGrid.appendChild(queryTypesCard);
}
// 最常查询域名卡片
if (stats.top_domains && stats.top_domains.length > 0) {
var domainsList = stats.top_domains.map(function(item) {
return E('div', { 'class': 'stats-list-item' }, [
E('span', { 'class': 'stats-list-name' }, item.name || '-'),
E('span', { 'class': 'stats-list-count' }, (item.count || 0).toLocaleString())
]);
});
statsGrid.appendChild(E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'stats-card-title' }, _('Top Domains')),
E('div', { 'class': 'stats-card-details' }, domainsList)
]));
}
// 最活跃设备卡片
if (stats.top_devices && stats.top_devices.length > 0) {
var devicesList = stats.top_devices.map(function(item) {
return E('div', { 'class': 'stats-list-item' }, [
E('span', { 'class': 'stats-list-name' }, item.name || '-'),
E('span', { 'class': 'stats-list-count' }, (item.count || 0).toLocaleString())
]);
});
statsGrid.appendChild(E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'stats-card-title' }, _('Top Devices')),
E('div', { 'class': 'stats-card-details' }, devicesList)
]));
}
// 最常用DNS服务器卡片
if (stats.top_dns_servers && stats.top_dns_servers.length > 0) {
var serversList = stats.top_dns_servers.map(function(item) {
return E('div', { 'class': 'stats-list-item' }, [
E('span', { 'class': 'stats-list-name' }, item.name || '-'),
E('span', { 'class': 'stats-list-count' }, (item.count || 0).toLocaleString())
]);
});
statsGrid.appendChild(E('div', { 'class': 'cbi-section' }, [
E('div', { 'class': 'stats-card-title' }, _('Top DNS Servers')),
E('div', { 'class': 'stats-card-details' }, serversList)
]));
}
// 应用主题颜色
setTimeout(function() {
applyThemeColors();
}, 50);
}).catch(function (error) {
console.error('Failed to load DNS stats:', error);
var statsGrid = document.getElementById('dns-stats-grid');
if (statsGrid) {
statsGrid.innerHTML = '';
}
});
}
// 初始化数据加载 - 延迟执行确保 DOM 元素已添加
setTimeout(function () {
updateStats();
updateQueries();
// 轮询更新统计数据每5秒
poll.add(function() {
return updateStats();
}, 1);
// 实时搜索功能(带防抖)
var domainFilter = document.getElementById('domain-filter');
var deviceFilter = document.getElementById('device-filter');
var dnsServerFilter = document.getElementById('dns-server-filter');
var typeFilter = document.getElementById('type-filter');
var refreshBtn = document.getElementById('refresh-queries-btn');
if (domainFilter && deviceFilter && dnsServerFilter && typeFilter) {
var searchTimer = null;
function performSearch() {
currentFilters.domain = domainFilter.value.trim();
currentFilters.device = deviceFilter.value.trim();
currentFilters.dns_server = dnsServerFilter.value.trim();
currentFilters.is_query = typeFilter.value;
currentPage = 1;
updateQueries();
}
// 防抖函数
function debounceSearch() {
if (searchTimer) {
clearTimeout(searchTimer);
}
searchTimer = setTimeout(performSearch, 300);
}
// 输入框实时搜索(带防抖)
domainFilter.addEventListener('input', debounceSearch);
deviceFilter.addEventListener('input', debounceSearch);
dnsServerFilter.addEventListener('input', debounceSearch);
// 下拉框立即搜索(不需要防抖)
typeFilter.addEventListener('change', performSearch);
// 刷新按钮
if (refreshBtn) {
refreshBtn.addEventListener('click', function () {
// 同时刷新统计数据和查询记录
updateStats();
var container = document.getElementById('dns-queries-container');
if (!container) {
updateQueries();
return;
}
// 确保容器是相对定位
container.style.position = 'relative';
// 移除旧的蒙版
var oldOverlay = container.querySelector('.refresh-overlay');
if (oldOverlay) {
oldOverlay.remove();
}
// 创建新的刷新蒙版
var overlay = E('div', {
'class': 'refresh-overlay'
});
container.appendChild(overlay);
// 应用主题背景色到蒙版
setTimeout(function() {
applyThemeColors();
}, 50);
// 刷新数据(蒙版会在 updateQueries 中自动移除)
updateQueries();
});
}
}
}, 10);
// 自动适应主题背景色和文字颜色的函数
function applyThemeColors() {
try {
var mainElement = document.querySelector('.main') || document.body;
var computedStyle = window.getComputedStyle(mainElement);
var bgColor = computedStyle.backgroundColor;
// 如果父元素有背景色,应用到容器和卡片
if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent') {
var containerEl = document.querySelector('.bandix-dns-container');
if (containerEl) {
containerEl.style.backgroundColor = bgColor;
}
// 应用到表格表头
var tableHeaders = document.querySelectorAll('.bandix-table th');
tableHeaders.forEach(function(th) {
th.style.backgroundColor = bgColor;
});
// 应用到 Response Result 字段的 badge
var badges = document.querySelectorAll('.response-ip-badge');
badges.forEach(function(badge) {
badge.style.backgroundColor = bgColor;
});
// 应用到搜索框(不包括 cbi-select因为它使用官方样式
var inputs = document.querySelectorAll('.filter-input');
inputs.forEach(function(input) {
input.style.backgroundColor = bgColor;
});
// 应用到统计卡片
var statsCards = document.querySelectorAll('.stats-grid .cbi-section');
statsCards.forEach(function(card) {
card.style.backgroundColor = bgColor;
});
// 应用到加载状态和错误状态
var loadingStates = document.querySelectorAll('.loading-state');
loadingStates.forEach(function(el) {
el.style.backgroundColor = bgColor;
});
var errorStates = document.querySelectorAll('.error-state');
errorStates.forEach(function(el) {
el.style.backgroundColor = bgColor;
});
// 应用到加载遮罩层(使用半透明背景)
var loadingOverlays = document.querySelectorAll('.loading-overlay');
loadingOverlays.forEach(function(el) {
// 将背景色转换为 rgba并添加透明度
var rgb = bgColor.match(/\d+/g);
if (rgb && rgb.length >= 3) {
el.style.backgroundColor = 'rgba(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ', 0.8)';
}
});
// 应用到刷新蒙版(使用半透明背景和模糊效果)
var refreshOverlays = document.querySelectorAll('.refresh-overlay');
refreshOverlays.forEach(function(el) {
// 将背景色转换为 rgba并添加透明度
var rgb = bgColor.match(/\d+/g);
if (rgb && rgb.length >= 3) {
el.style.backgroundColor = 'rgba(' + rgb[0] + ', ' + rgb[1] + ', ' + rgb[2] + ', 0.6)';
}
});
}
// 检测文字颜色并应用
var textColor = computedStyle.color;
if (textColor && textColor !== 'rgba(0, 0, 0, 0)') {
var containerEl = document.querySelector('.bandix-dns-container');
if (containerEl) {
containerEl.style.color = textColor;
}
// 应用到搜索框的文字颜色(不包括 cbi-select
var inputs = document.querySelectorAll('.filter-input');
inputs.forEach(function(input) {
input.style.color = textColor;
});
}
} catch (e) {
// 如果检测失败,使用默认值
console.log('Theme adaptation:', e);
}
}
// 初始应用主题颜色
setTimeout(applyThemeColors, 100);
// 监听 DOM 变化,自动应用到新创建的元素
if (typeof MutationObserver !== 'undefined') {
var observer = new MutationObserver(function(mutations) {
applyThemeColors();
});
setTimeout(function() {
var container = document.querySelector('.bandix-dns-container');
if (container) {
observer.observe(container, {
childList: true,
subtree: true
});
}
}, 200);
}
return container;
}
});