Files
openwrt_packages/luci-app-bandix/htdocs/luci-static/resources/view/bandix/dns.js
2025-11-10 00:09:51 +08:00

1525 lines
62 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';
const translations = {
'zh-cn': {
'Bandix DNS 监控': 'Bandix DNS 监控',
'正在加载数据...': '正在加载数据...',
'无法获取数据': '无法获取数据',
'DNS 监控': 'DNS 监控',
'DNS 查询记录': 'DNS 查询记录',
'DNS 统计信息': 'DNS 统计信息',
'DNS监控未启用': 'DNS监控未启用',
'请在设置中启用DNS监控功能': '请在设置中启用DNS监控功能',
'前往设置': '前往设置',
'无数据': '无数据',
'时间': '时间',
'域名': '域名',
'查询类型': '查询类型',
'类型': '类型',
'响应码': '响应码',
'响应时间': '响应时间',
'源IP': '源IP',
'目标IP': '目标IP',
'设备': '设备',
'响应IP': '响应IP',
'响应结果': '响应结果',
'DNS服务器': 'DNS服务器',
'查询': '查询',
'响应': '响应',
'过滤': '过滤',
'域名过滤': '域名过滤',
'设备过滤': '设备过滤',
'DNS服务器过滤': 'DNS服务器过滤',
'类型过滤': '类型过滤',
'全部': '全部',
'仅查询': '仅查询',
'仅响应': '仅响应',
'搜索': '搜索',
'搜索域名': '搜索域名',
'搜索设备': '搜索设备',
'搜索DNS服务器': '搜索DNS服务器',
'清除': '清除',
'上一页': '上一页',
'下一页': '下一页',
'第': '第',
'页,共': '页,共',
'共': '共',
'条记录': '条记录',
'每页显示': '每页显示',
'条': '条',
'总查询数': '总查询数',
'总响应数': '总响应数',
'有响应查询': '有响应查询',
'无响应查询': '无响应查询',
'平均响应时间': '平均响应时间',
'最快响应时间': '最快响应时间',
'最慢响应时间': '最慢响应时间',
'响应时间': '响应时间',
'成功率': '成功率',
'成功': '成功',
'失败': '失败',
'最常查询域名': '最常查询域名',
'最常用查询类型': '最常用查询类型',
'最活跃设备': '最活跃设备',
'最常用DNS服务器': '最常用DNS服务器',
'唯一设备数': '唯一设备数',
'时间范围': '时间范围',
'毫秒': '毫秒',
'分钟': '分钟',
'刷新': '刷新',
'未知设备': '未知设备',
'成功': '成功',
'域名未找到': '域名未找到',
'服务器错误': '服务器错误',
'格式错误': '格式错误',
'拒绝': '拒绝',
'其他': '其他'
},
'zh-tw': {
'Bandix DNS 监控': 'Bandix DNS 監控',
'正在加载数据...': '正在載入資料...',
'无法获取数据': '無法獲取資料',
'DNS 监控': 'DNS 監控',
'DNS 查询记录': 'DNS 查詢記錄',
'DNS 统计信息': 'DNS 統計資訊',
'DNS监控未启用': 'DNS監控未啟用',
'请在设置中启用DNS监控功能': '請在設置中啟用DNS監控功能',
'前往设置': '前往設置',
'无数据': '無數據',
'时间': '時間',
'域名': '域名',
'查询类型': '查詢類型',
'类型': '類型',
'响应码': '響應碼',
'响应时间': '響應時間',
'源IP': '源IP',
'目标IP': '目標IP',
'设备': '設備',
'响应IP': '響應IP',
'响应结果': '響應結果',
'DNS服务器': 'DNS伺服器',
'查询': '查詢',
'响应': '響應',
'过滤': '過濾',
'域名过滤': '域名過濾',
'设备过滤': '設備過濾',
'DNS服务器过滤': 'DNS伺服器過濾',
'类型过滤': '類型過濾',
'全部': '全部',
'仅查询': '僅查詢',
'仅响应': '僅響應',
'搜索': '搜尋',
'搜索域名': '搜尋域名',
'搜索设备': '搜尋設備',
'搜索DNS服务器': '搜尋DNS伺服器',
'清除': '清除',
'上一页': '上一頁',
'下一页': '下一頁',
'第': '第',
'页,共': '頁,共',
'共': '共',
'条记录': '條記錄',
'每页显示': '每頁顯示',
'条': '條',
'总查询数': '總查詢數',
'总响应数': '總響應數',
'有响应查询': '有響應查詢',
'无响应查询': '無響應查詢',
'平均响应时间': '平均響應時間',
'最快响应时间': '最快響應時間',
'最慢响应时间': '最慢響應時間',
'响应时间': '響應時間',
'成功率': '成功率',
'成功': '成功',
'失败': '失敗',
'最常查询域名': '最常查詢域名',
'最常用查询类型': '最常用查詢類型',
'最活跃设备': '最活躍設備',
'最常用DNS服务器': '最常用DNS伺服器',
'唯一设备数': '唯一設備數',
'时间范围': '時間範圍',
'毫秒': '毫秒',
'分钟': '分鐘',
'刷新': '重新整理',
'未知设备': '未知設備',
'成功': '成功',
'域名未找到': '域名未找到',
'服务器错误': '伺服器錯誤',
'格式错误': '格式錯誤',
'拒绝': '拒絕',
'其他': '其他'
},
'en': {
'Bandix DNS 监控': 'Bandix DNS Monitor',
'正在加载数据...': 'Loading data...',
'无法获取数据': 'Unable to fetch data',
'DNS 监控': 'DNS Monitor',
'DNS 查询记录': 'DNS Query Records',
'DNS 统计信息': 'DNS Statistics',
'DNS监控未启用': 'DNS Monitoring Disabled',
'请在设置中启用DNS监控功能': 'Please enable DNS monitoring in settings',
'前往设置': 'Go to Settings',
'无数据': 'No Data',
'时间': 'Time',
'域名': 'Domain',
'查询类型': 'Query Type',
'类型': 'Type',
'响应码': 'Response Code',
'响应时间': 'Response Time',
'源IP': 'Source IP',
'目标IP': 'Destination IP',
'设备': 'Device',
'响应IP': 'Response IPs',
'响应结果': 'Response Result',
'DNS服务器': 'DNS Server',
'查询': 'Query',
'响应': 'Response',
'过滤': 'Filter',
'域名过滤': 'Domain Filter',
'设备过滤': 'Device Filter',
'DNS服务器过滤': 'DNS Server Filter',
'类型过滤': 'Type Filter',
'全部': 'All',
'仅查询': 'Queries Only',
'仅响应': 'Responses Only',
'搜索': 'Search',
'搜索域名': 'Search Domain',
'搜索设备': 'Search Device',
'搜索DNS服务器': 'Search DNS Server',
'清除': 'Clear',
'上一页': 'Previous',
'下一页': 'Next',
'第': 'Page',
'页,共': 'of',
'共': 'Total',
'条记录': 'records',
'每页显示': 'Per Page',
'条': '',
'总查询数': 'Total Queries',
'总响应数': 'Total Responses',
'有响应查询': 'Queries with Response',
'无响应查询': 'Queries without Response',
'平均响应时间': 'Avg Response Time',
'最快响应时间': 'Min Response Time',
'最慢响应时间': 'Max Response Time',
'响应时间': 'Response Time',
'成功率': 'Success Rate',
'成功': 'Success',
'失败': 'Failure',
'最常查询域名': 'Top Domains',
'最常用查询类型': 'Top Query Types',
'最活跃设备': 'Top Devices',
'最常用DNS服务器': 'Top DNS Servers',
'唯一设备数': 'Unique Devices',
'时间范围': 'Time Range',
'毫秒': 'ms',
'分钟': 'minutes',
'刷新': 'Refresh',
'未知设备': 'Unknown Device',
'成功': 'Success',
'域名未找到': 'Domain not found',
'服务器错误': 'Server error',
'格式错误': 'Format error',
'拒绝': 'Refused',
'其他': 'Other'
},
'fr': {
'Bandix DNS 监控': 'Bandix Surveillance DNS',
'正在加载数据...': 'Chargement des données...',
'无法获取数据': 'Impossible de récupérer les données',
'DNS 监控': 'Surveillance DNS',
'DNS 查询记录': 'Enregistrements de Requêtes DNS',
'DNS 统计信息': 'Statistiques DNS',
'DNS监控未启用': 'Surveillance DNS désactivée',
'请在设置中启用DNS监控功能': 'Veuillez activer la surveillance DNS dans les paramètres',
'前往设置': 'Aller aux Paramètres',
'无数据': 'Aucune Donnée',
'时间': 'Heure',
'域名': 'Domaine',
'查询类型': 'Type de Requête',
'类型': 'Type',
'响应码': 'Code de Réponse',
'响应时间': 'Temps de Réponse',
'源IP': 'IP Source',
'目标IP': 'IP de Destination',
'设备': 'Appareil',
'响应IP': 'IPs de Réponse',
'响应结果': 'Résultat de Réponse',
'DNS服务器': 'Serveur DNS',
'查询': 'Requête',
'响应': 'Réponse',
'过滤': 'Filtre',
'域名过滤': 'Filtre de Domaine',
'设备过滤': 'Filtre d\'Appareil',
'DNS服务器过滤': 'Filtre de Serveur DNS',
'类型过滤': 'Filtre de Type',
'全部': 'Tous',
'仅查询': 'Requêtes Seulement',
'仅响应': 'Réponses Seulement',
'搜索': 'Rechercher',
'搜索域名': 'Rechercher un Domaine',
'搜索设备': 'Rechercher un Appareil',
'搜索DNS服务器': 'Rechercher un Serveur DNS',
'清除': 'Effacer',
'上一页': 'Précédent',
'下一页': 'Suivant',
'第': 'Page',
'页,共': 'sur',
'共': 'Total',
'条记录': 'enregistrements',
'每页显示': 'Par Page',
'条': '',
'总查询数': 'Total des Requêtes',
'总响应数': 'Total des Réponses',
'有响应查询': 'Requêtes avec Réponse',
'无响应查询': 'Requêtes sans Réponse',
'平均响应时间': 'Temps de Réponse Moyen',
'最快响应时间': 'Temps de Réponse Minimum',
'最慢响应时间': 'Temps de Réponse Maximum',
'响应时间': 'Temps de Réponse',
'成功率': 'Taux de Réussite',
'成功': 'Succès',
'失败': 'Échec',
'最常查询域名': 'Domaines les Plus Consultés',
'最常用查询类型': 'Types de Requêtes les Plus Utilisés',
'最活跃设备': 'Appareils les Plus Actifs',
'最常用DNS服务器': 'Serveurs DNS les Plus Utilisés',
'唯一设备数': 'Appareils Uniques',
'时间范围': 'Plage de Temps',
'毫秒': 'ms',
'分钟': 'minutes',
'刷新': 'Actualiser',
'未知设备': 'Appareil Inconnu',
'成功': 'Succès',
'域名未找到': 'Domaine introuvable',
'服务器错误': 'Erreur serveur',
'格式错误': 'Erreur de format',
'拒绝': 'Refusé',
'其他': 'Autre'
},
'ja': {
'Bandix DNS 监控': 'Bandix DNS監視',
'正在加载数据...': 'データを読み込み中...',
'无法获取数据': 'データを取得できません',
'DNS 监控': 'DNS監視',
'DNS 查询记录': 'DNSクエリ記録',
'DNS 统计信息': 'DNS統計情報',
'DNS监控未启用': 'DNS監視が無効です',
'请在设置中启用DNS监控功能': '設定でDNS監視機能を有効にしてください',
'前往设置': '設定へ',
'无数据': 'データなし',
'时间': '時刻',
'域名': 'ドメイン',
'查询类型': 'クエリタイプ',
'类型': 'タイプ',
'响应码': '応答コード',
'响应时间': '応答時間',
'源IP': '送信元IP',
'目标IP': '宛先IP',
'设备': 'デバイス',
'响应IP': '応答IP',
'响应结果': '応答結果',
'DNS服务器': 'DNSサーバー',
'查询': 'クエリ',
'响应': '応答',
'过滤': 'フィルター',
'域名过滤': 'ドメインフィルター',
'设备过滤': 'デバイスフィルター',
'DNS服务器过滤': 'DNSサーバーフィルター',
'类型过滤': 'タイプフィルター',
'全部': 'すべて',
'仅查询': 'クエリのみ',
'仅响应': '応答のみ',
'搜索': '検索',
'搜索域名': 'ドメインを検索',
'搜索设备': 'デバイスを検索',
'搜索DNS服务器': 'DNSサーバーを検索',
'清除': 'クリア',
'上一页': '前へ',
'下一页': '次へ',
'第': 'ページ',
'页,共': '/',
'共': '合計',
'条记录': '件の記録',
'每页显示': 'ページあたり',
'条': '',
'总查询数': '総クエリ数',
'总响应数': '総応答数',
'有响应查询': '応答ありのクエリ',
'无响应查询': '応答なしのクエリ',
'平均响应时间': '平均応答時間',
'最快响应时间': '最小応答時間',
'最慢响应时间': '最大応答時間',
'响应时间': '応答時間',
'成功率': '成功率',
'成功': '成功',
'失败': '失敗',
'最常查询域名': '最も頻繁にクエリされるドメイン',
'最常用查询类型': '最も使用されるクエリタイプ',
'最活跃设备': '最もアクティブなデバイス',
'最常用DNS服务器': '最も使用されるDNSサーバー',
'唯一设备数': 'ユニークデバイス数',
'时间范围': '時間範囲',
'毫秒': 'ミリ秒',
'分钟': '分',
'刷新': '更新',
'未知设备': '不明なデバイス',
'成功': '成功',
'域名未找到': 'ドメインが見つかりません',
'服务器错误': 'サーバーエラー',
'格式错误': 'フォーマットエラー',
'拒绝': '拒否',
'其他': 'その他'
},
'ru': {
'Bandix DNS 监控': 'Bandix Мониторинг DNS',
'正在加载数据...': 'Загрузка данных...',
'无法获取数据': 'Не удалось получить данные',
'DNS 监控': 'Мониторинг DNS',
'DNS 查询记录': 'Записи DNS-запросов',
'DNS 统计信息': 'Статистика DNS',
'DNS监控未启用': 'Мониторинг DNS отключен',
'请在设置中启用DNS监控功能': 'Пожалуйста, включите мониторинг DNS в настройках',
'前往设置': 'Перейти в Настройки',
'无数据': 'Нет Данных',
'时间': 'Время',
'域名': 'Домен',
'查询类型': 'Тип Запроса',
'类型': 'Тип',
'响应码': 'Код Ответа',
'响应时间': 'Время Ответа',
'源IP': 'Исходный IP',
'目标IP': 'IP Назначения',
'设备': 'Устройство',
'响应IP': 'IP Ответов',
'响应结果': 'Результат Ответа',
'DNS服务器': 'DNS Сервер',
'查询': 'Запрос',
'响应': 'Ответ',
'过滤': 'Фильтр',
'域名过滤': 'Фильтр Домена',
'设备过滤': 'Фильтр Устройства',
'DNS服务器过滤': 'Фильтр DNS Сервера',
'类型过滤': 'Фильтр Типа',
'全部': 'Все',
'仅查询': 'Только Запросы',
'仅响应': 'Только Ответы',
'搜索': 'Поиск',
'搜索域名': 'Поиск Домена',
'搜索设备': 'Поиск Устройства',
'搜索DNS服务器': 'Поиск DNS Сервера',
'清除': 'Очистить',
'上一页': 'Предыдущая',
'下一页': 'Следующая',
'第': 'Страница',
'页,共': 'из',
'共': 'Всего',
'条记录': 'записей',
'每页显示': 'На Странице',
'条': '',
'总查询数': 'Всего Запросов',
'总响应数': 'Всего Ответов',
'有响应查询': 'Запросы с Ответом',
'无响应查询': 'Запросы без Ответа',
'平均响应时间': 'Среднее Время Ответа',
'最快响应时间': 'Минимальное Время Ответа',
'最慢响应时间': 'Максимальное Время Ответа',
'响应时间': 'Время Ответа',
'成功率': 'Процент Успеха',
'成功': 'Успех',
'失败': 'Неудача',
'最常查询域名': 'Наиболее Запрашиваемые Домены',
'最常用查询类型': 'Наиболее Используемые Типы Запросов',
'最活跃设备': 'Наиболее Активные Устройства',
'最常用DNS服务器': 'Наиболее Используемые DNS Серверы',
'唯一设备数': 'Уникальных Устройств',
'时间范围': 'Временной Диапазон',
'毫秒': 'мс',
'分钟': 'минут',
'刷新': 'Обновить',
'未知设备': 'Неизвестное Устройство',
'成功': 'Успех',
'域名未找到': 'Домен не найден',
'服务器错误': 'Ошибка сервера',
'格式错误': 'Ошибка формата',
'拒绝': 'Отклонено',
'其他': 'Другое'
}
};
function getTranslation(key, language) {
return translations[language]?.[key] || key;
}
function getSystemLanguage() {
var luciLang = uci.get('luci', 'main', 'lang');
if (luciLang && translations[luciLang]) {
return luciLang;
}
var systemLang = document.documentElement.lang || 'en';
if (translations[systemLang]) {
return systemLang;
}
return 'en';
}
function isDarkMode() {
var userTheme = uci.get('bandix', 'general', 'theme');
if (userTheme) {
if (userTheme === 'dark') {
return true;
} else if (userTheme === 'light') {
return false;
}
}
var mediaUrlBase = uci.get('luci', 'main', 'mediaurlbase');
if (mediaUrlBase && mediaUrlBase.toLowerCase().includes('dark')) {
return true;
}
if (mediaUrlBase && mediaUrlBase.toLowerCase().includes('argon')) {
var argonMode = uci.get('argon', '@global[0]', 'mode');
if (argonMode) {
if (argonMode.toLowerCase() === 'dark') {
return true;
} else if (argonMode.toLowerCase() === 'light') {
return false;
}
if (argonMode.toLowerCase() === 'normal' || argonMode.toLowerCase() === 'auto') {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
}
return false;
}
}
}
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return true;
}
return false;
}
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) {
var language = getSystemLanguage();
if (code === 'Success') return getTranslation('成功', language);
if (code === 'Domain not found') return getTranslation('域名未找到', language);
if (code === 'Server error') return getTranslation('服务器错误', language);
if (code === 'Format error') return getTranslation('格式错误', language);
if (code === 'Refused') return getTranslation('拒绝', language);
return code || getTranslation('其他', language);
}
function formatDeviceName(device) {
var language = getSystemLanguage();
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 getTranslation('未知设备', language);
}
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: [] };
// 显示响应IPresponse_ips它是一个字符串数组
if (query.response_ips && Array.isArray(query.response_ips) && query.response_ips.length > 0) {
var maxDisplay = 5; // 最多显示5条
var displayRecords = query.response_ips.slice(0, maxDisplay);
var fullRecords = query.response_ips;
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 language = uci.get('bandix', 'general', 'language');
if (!language || language === 'auto') {
language = getSystemLanguage();
}
var darkMode = isDarkMode();
var dnsEnabled = uci.get('bandix', 'dns', 'enabled') === '1';
var style = E('style', {}, `
.bandix-dns-container {
margin: 0;
padding: 16px;
background-color: ${darkMode ? '#1a1a1a' : '#f8fafc'};
min-height: calc(100vh - 100px);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
color: ${darkMode ? '#e2e8f0' : '#1f2937'};
border-radius: 8px;
}
.bandix-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
}
.bandix-title {
font-size: 1.5rem;
font-weight: 600;
color: ${darkMode ? '#f1f5f9' : '#1f2937'};
margin: 0;
}
.bandix-alert {
background-color: ${darkMode ? '#2a2a2a' : '#eff6ff'};
border-left: 3px solid ${darkMode ? '#3b82f6' : '#2563eb'};
border-radius: 4px;
padding: 10px 12px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 10px;
color: ${darkMode ? '#d0d0d0' : '#1e293b'};
font-size: 0.875rem;
}
.bandix-alert-icon {
color: ${darkMode ? '#60a5fa' : '#2563eb'};
font-size: 0.875rem;
font-weight: 700;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid currentColor;
border-radius: 50%;
flex-shrink: 0;
}
.bandix-card {
background-color: ${darkMode ? '#2a2a2a' : 'white'};
border-radius: 8px;
border: 1px solid ${darkMode ? '#444444' : '#e2e8f0'};
box-shadow: 0 2px 8px rgba(0, 0, 0, ${darkMode ? '0.3' : '0.08'});
margin-bottom: 24px;
overflow: hidden;
}
.bandix-card-header {
padding: 16px;
border-bottom: 1px solid ${darkMode ? '#444444' : '#e2e8f0'};
background-color: ${darkMode ? '#2a2a2a' : '#f8fafc'};
}
.bandix-card-title {
font-size: 1.125rem;
font-weight: 600;
color: ${darkMode ? '#f1f5f9' : '#1f2937'};
margin: 0;
display: flex;
align-items: center;
gap: 8px;
}
.bandix-card-body {
padding: 16px;
}
.filter-section {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 16px;
align-items: center;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-label {
font-size: 0.875rem;
font-weight: 500;
color: ${darkMode ? '#94a3b8' : '#64748b'};
white-space: nowrap;
}
.filter-input {
padding: 6px 12px;
border: 1px solid ${darkMode ? '#444444' : '#cbd5e1'};
border-radius: 4px;
background-color: ${darkMode ? '#1a1a1a' : 'white'};
color: ${darkMode ? '#e2e8f0' : '#1f2937'};
font-size: 0.875rem;
min-width: 150px;
}
.filter-select {
padding: 6px 12px;
border: 1px solid ${darkMode ? '#444444' : '#cbd5e1'};
border-radius: 4px;
background-color: ${darkMode ? '#1a1a1a' : 'white'};
color: ${darkMode ? '#e2e8f0' : '#1f2937'};
font-size: 0.875rem;
cursor: pointer;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 4px;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-primary {
background-color: #3b82f6;
color: white;
}
.btn-primary:hover {
background-color: #2563eb;
}
.btn-secondary {
background-color: ${darkMode ? '#3a3a3a' : '#e5e7eb'};
color: ${darkMode ? '#d0d0d0' : '#374151'};
}
.btn-secondary:hover {
background-color: ${darkMode ? '#4a4a4a' : '#d1d5db'};
}
.bandix-table {
width: 100%;
border-collapse: collapse;
background-color: transparent;
font-size: 0.875rem;
}
.bandix-table th {
background-color: ${darkMode ? '#2a2a2a' : '#f8fafc'};
padding: 10px 12px;
text-align: left;
font-weight: 600;
color: ${darkMode ? '#d0d0d0' : '#475569'};
border-bottom: 2px solid ${darkMode ? '#444444' : '#e2e8f0'};
white-space: nowrap;
}
.bandix-table td {
padding: 10px 12px;
border-bottom: 1px solid ${darkMode ? '#333333' : '#f1f5f9'};
color: ${darkMode ? '#d0d0d0' : '#334155'};
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;
margin-top: 16px;
flex-wrap: wrap;
gap: 12px;
}
.pagination-info {
color: ${darkMode ? '#94a3b8' : '#64748b'};
font-size: 0.875rem;
}
.pagination-controls {
display: flex;
align-items: center;
gap: 8px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-bottom: 24px;
}
.stats-card {
background-color: ${darkMode ? '#2a2a2a' : 'white'};
border-radius: 8px;
padding: 16px;
border: 1px solid ${darkMode ? '#444444' : '#e2e8f0'};
box-shadow: 0 2px 8px rgba(0, 0, 0, ${darkMode ? '0.3' : '0.08'});
}
.stats-card-title {
font-size: 0.75rem;
font-weight: 600;
color: ${darkMode ? '#94a3b8' : '#64748b'};
margin: 0 0 8px 0;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.stats-card-value {
font-size: 1.5rem;
font-weight: 700;
color: ${darkMode ? '#f1f5f9' : '#1f2937'};
margin: 0;
}
.stats-card-unit {
font-size: 0.875rem;
color: ${darkMode ? '#94a3b8' : '#64748b'};
margin-left: 4px;
}
.stats-card-details {
margin-top: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
.stats-detail-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.875rem;
}
.stats-detail-label {
color: ${darkMode ? '#9ca3af' : '#6b7280'};
font-weight: 500;
}
.stats-detail-value {
font-weight: 600;
color: ${darkMode ? '#e2e8f0' : '#374151'};
}
.top-list {
list-style: none;
padding: 0;
margin: 0;
}
.top-list-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
border-bottom: 1px solid ${darkMode ? '#333333' : '#f1f5f9'};
}
.top-list-item:last-child {
border-bottom: none;
}
.top-list-name {
flex: 1;
color: ${darkMode ? '#e2e8f0' : '#334155'};
font-size: 0.875rem;
word-break: break-word;
}
.top-list-count {
font-weight: 600;
color: ${darkMode ? '#f1f5f9' : '#1f2937'};
font-size: 0.875rem;
margin-left: 12px;
}
.loading-state {
text-align: center;
padding: 40px;
color: ${darkMode ? '#94a3b8' : '#6b7280'};
font-style: italic;
}
.error-state {
text-align: center;
padding: 40px;
color: ${darkMode ? '#f87171' : '#ef4444'};
}
.response-ips {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.response-ip-badge {
display: inline-block;
padding: 2px 6px;
background-color: ${darkMode ? '#3a3a3a' : '#e5e7eb'};
border-radius: 4px;
font-size: 0.75rem;
color: ${darkMode ? '#d0d0d0' : '#374151'};
}
`);
document.head.appendChild(style);
var container = E('div', { 'class': 'bandix-dns-container' });
var header = E('div', { 'class': 'bandix-header' }, [
E('h1', { 'class': 'bandix-title' }, getTranslation('Bandix DNS 监控', language))
]);
container.appendChild(header);
if (!dnsEnabled) {
var alertDiv = E('div', { 'class': 'bandix-alert' }, [
E('span', { 'class': 'bandix-alert-icon' }, '!'),
E('div', {}, [
E('strong', {}, getTranslation('DNS监控未启用', language)),
E('p', { 'style': 'margin: 4px 0 0 0;' },
getTranslation('请在设置中启用DNS监控功能', language))
])
]);
container.appendChild(alertDiv);
var settingsCard = E('div', { 'class': 'bandix-card' }, [
E('div', { 'class': 'bandix-card-body', 'style': 'text-align: center;' }, [
E('a', {
'href': '/cgi-bin/luci/admin/network/bandix/settings',
'class': 'btn btn-primary'
}, getTranslation('前往设置', language))
])
]);
container.appendChild(settingsCard);
return container;
}
// DNS 统计信息卡片
var statsCard = E('div', { 'class': 'bandix-card' }, [
E('div', { 'class': 'bandix-card-body' }, [
E('div', { 'id': 'dns-stats-container' }, [
E('div', { 'class': 'loading-state' }, getTranslation('正在加载数据...', language))
])
])
]);
container.appendChild(statsCard);
// DNS 查询记录卡片
var queriesCard = E('div', { 'class': 'bandix-card' }, [
E('div', { 'class': 'bandix-card-header' }, [
E('h2', { 'class': 'bandix-card-title' }, getTranslation('DNS 查询记录', language))
]),
E('div', { 'class': 'bandix-card-body' }, [
E('div', { 'class': 'filter-section' }, [
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, getTranslation('类型过滤', language) + ':'),
E('select', { 'class': 'filter-select', 'id': 'type-filter' }, [
E('option', { 'value': '' }, getTranslation('全部', language)),
E('option', { 'value': 'true' }, getTranslation('仅查询', language)),
E('option', { 'value': 'false' }, getTranslation('仅响应', language))
])
]),
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, getTranslation('域名过滤', language) + ':'),
E('input', {
'type': 'text',
'class': 'filter-input',
'id': 'domain-filter',
'placeholder': getTranslation('搜索域名', language)
})
]),
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, getTranslation('设备过滤', language) + ':'),
E('input', {
'type': 'text',
'class': 'filter-input',
'id': 'device-filter',
'placeholder': getTranslation('搜索设备', language)
})
]),
E('div', { 'class': 'filter-group' }, [
E('label', { 'class': 'filter-label' }, getTranslation('DNS服务器过滤', language) + ':'),
E('input', {
'type': 'text',
'class': 'filter-input',
'id': 'dns-server-filter',
'placeholder': getTranslation('搜索DNS服务器', language)
})
]),
E('div', { 'class': 'filter-group', 'style': 'margin-left: auto;' }, [
E('button', {
'class': 'btn btn-primary',
'id': 'refresh-queries-btn'
}, getTranslation('刷新', language))
])
]),
E('div', { 'id': 'dns-queries-container' }, [
E('div', { 'class': 'loading-state' }, getTranslation('正在加载数据...', language))
])
])
]);
container.appendChild(queriesCard);
// 状态变量
var currentPage = 1;
var pageSize = 20;
var currentFilters = {
domain: '',
device: '',
is_query: '',
dns_server: ''
};
// 更新统计信息
var statsInitialized = false;
function updateStats() {
callGetDnsStats().then(function (result) {
var container = document.getElementById('dns-stats-container');
if (!container) return;
if (!result || result.status !== 'success' || !result.data || !result.data.stats) {
if (!statsInitialized) {
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'error-state' },
getTranslation('无法获取数据', language)));
}
return;
}
var stats = result.data.stats;
// 如果还没有初始化,创建完整的 UI 结构
if (!statsInitialized) {
var statsHtml = E('div', {}, [
E('div', { 'class': 'stats-grid' }, [
E('div', { 'class': 'stats-card' }, [
E('div', { 'class': 'stats-card-title' }, getTranslation('总查询数', language)),
E('div', { 'class': 'stats-card-value', 'id': 'stat-total-queries' }, stats.total_queries || 0)
]),
E('div', { 'class': 'stats-card' }, [
E('div', { 'class': 'stats-card-title' }, getTranslation('响应时间', language)),
E('div', { 'class': 'stats-card-value', 'id': 'stat-avg-response-time', 'style': 'margin-bottom: 12px;' }, [
E('span', {}, (stats.avg_response_time_ms || 0).toFixed(1)),
E('span', { 'class': 'stats-card-unit' }, ' ' + getTranslation('毫秒', language))
]),
E('div', { 'class': 'stats-card-details', 'id': 'stat-response-time-details' }, [
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, getTranslation('最快响应时间', language) + ':'),
E('span', { 'class': 'stats-detail-value', 'id': 'stat-min-response-time' },
(stats.min_response_time_ms || 0) + ' ' + getTranslation('毫秒', language))
]),
E('div', { 'class': 'stats-detail-row' }, [
E('span', { 'class': 'stats-detail-label' }, getTranslation('最慢响应时间', language) + ':'),
E('span', { 'class': 'stats-detail-value', 'id': 'stat-max-response-time' },
(stats.max_response_time_ms || 0) + ' ' + getTranslation('毫秒', language))
])
])
]),
E('div', { 'class': 'stats-card' }, [
E('div', { 'class': 'stats-card-title' }, getTranslation('成功率', language)),
E('div', { 'class': 'stats-card-value', 'id': 'stat-success-rate' }, [
E('span', {}, ((stats.success_rate || 0) * 100).toFixed(1)),
E('span', { 'class': 'stats-card-unit' }, '%')
])
])
]),
E('div', { 'style': 'display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 16px; margin-top: 24px;' }, [
E('div', { 'class': 'stats-card', 'id': 'top-domains-card' }),
E('div', { 'class': 'stats-card', 'id': 'top-query-types-card' }),
E('div', { 'class': 'stats-card', 'id': 'top-devices-card' }),
E('div', { 'class': 'stats-card', 'id': 'top-dns-servers-card' })
])
]);
container.innerHTML = '';
container.appendChild(statsHtml);
statsInitialized = true;
}
// 只更新数字内容
var totalQueriesEl = document.getElementById('stat-total-queries');
if (totalQueriesEl) {
totalQueriesEl.textContent = stats.total_queries || 0;
}
var avgResponseTimeEl = document.getElementById('stat-avg-response-time');
if (avgResponseTimeEl && avgResponseTimeEl.firstChild) {
avgResponseTimeEl.firstChild.textContent = (stats.avg_response_time_ms || 0).toFixed(1);
}
var minResponseTimeEl = document.getElementById('stat-min-response-time');
if (minResponseTimeEl) {
minResponseTimeEl.textContent = (stats.min_response_time_ms || 0) + ' ' + getTranslation('毫秒', language);
}
var maxResponseTimeEl = document.getElementById('stat-max-response-time');
if (maxResponseTimeEl) {
maxResponseTimeEl.textContent = (stats.max_response_time_ms || 0) + ' ' + getTranslation('毫秒', language);
}
var successRateEl = document.getElementById('stat-success-rate');
if (successRateEl && successRateEl.firstChild) {
successRateEl.firstChild.textContent = ((stats.success_rate || 0) * 100).toFixed(1);
}
// 更新 Top 列表
function updateTopList(cardId, titleKey, items, maxItems) {
var card = document.getElementById(cardId);
if (!card) return;
if (!items || items.length === 0) {
card.style.display = 'none';
return;
}
card.style.display = '';
var list = card.querySelector('.top-list');
if (!list) {
// 创建列表
card.innerHTML = '';
card.appendChild(E('div', { 'class': 'stats-card-title' }, getTranslation(titleKey, language)));
list = E('ul', { 'class': 'top-list' });
card.appendChild(list);
}
var itemsToShow = items.slice(0, maxItems || 10);
var listItems = list.querySelectorAll('.top-list-item');
// 如果列表项数量不匹配,重新创建列表
if (listItems.length !== itemsToShow.length) {
list.innerHTML = '';
itemsToShow.forEach(function (item) {
list.appendChild(E('li', { 'class': 'top-list-item' }, [
E('span', { 'class': 'top-list-name' }, item.name || '-'),
E('span', { 'class': 'top-list-count' }, item.count || 0)
]));
});
} else {
// 只更新文本内容
itemsToShow.forEach(function (item, index) {
var listItem = listItems[index];
if (listItem) {
var nameEl = listItem.querySelector('.top-list-name');
var countEl = listItem.querySelector('.top-list-count');
if (nameEl) nameEl.textContent = item.name || '-';
if (countEl) countEl.textContent = item.count || 0;
}
});
}
}
updateTopList('top-domains-card', '最常查询域名', stats.top_domains, 10);
updateTopList('top-query-types-card', '最常用查询类型', stats.top_query_types);
updateTopList('top-devices-card', '最活跃设备', stats.top_devices, 10);
updateTopList('top-dns-servers-card', '最常用DNS服务器', stats.top_dns_servers, 5);
}).catch(function (error) {
console.error('Failed to load DNS stats:', error);
var container = document.getElementById('dns-stats-container');
if (!container) return;
if (!statsInitialized) {
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'error-state' },
getTranslation('无法获取数据', language)));
}
});
}
// 更新查询记录
function updateQueries() {
var container = document.getElementById('dns-queries-container');
if (!container) return;
// 检查是否有现有内容
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) {
var overlayBg = darkMode ? 'rgba(42, 42, 42, 0.9)' : 'rgba(255, 255, 255, 0.9)';
loadingDiv = E('div', {
'class': 'loading-overlay',
'style': 'position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: ' + overlayBg + '; display: flex; align-items: center; justify-content: center; z-index: 10; color: ' + (darkMode ? '#e2e8f0' : '#1f2937') + ';'
}, getTranslation('正在加载数据...', language));
container.style.position = 'relative';
container.appendChild(loadingDiv);
} else {
loadingDiv.style.display = 'flex';
}
} else {
// 如果没有内容,使用简单的加载状态
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'loading-state' },
getTranslation('正在加载数据...', language)));
}
callGetDnsQueries(
currentFilters.domain,
currentFilters.device,
currentFilters.is_query,
currentFilters.dns_server,
currentPage,
pageSize
).then(function (result) {
// 隐藏或移除加载状态
if (loadingDiv) {
loadingDiv.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' },
getTranslation('无法获取数据', language)));
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' },
getTranslation('无数据', language)));
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;' }, getTranslation('时间', language)),
E('th', { 'style': 'width: 200px;' }, getTranslation('域名', language)),
E('th', { 'style': 'width: 100px;' }, getTranslation('查询类型', language)),
E('th', { 'style': 'width: 100px;' }, getTranslation('类型', language)),
E('th', { 'style': 'width: 100px;' }, getTranslation('响应时间', language)),
E('th', { 'style': 'width: 200px;' }, getTranslation('设备', language)),
E('th', { 'style': 'width: 140px;' }, getTranslation('DNS服务器', language)),
E('th', { 'style': 'width: 200px;' }, getTranslation('响应结果', language))
])
]),
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 ? getTranslation('查询', language) : getTranslation('响应', language))
]),
E('td', {}, query.response_time_ms ? query.response_time_ms + ' ' + getTranslation('毫秒', language) : '-'),
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' },
getTranslation('第', language) + ' ' + currentPage + ' ' + getTranslation('页,共', language) + ' ' + totalPages + '' + getTranslation('共', language) + ' ' + total + ' ' + getTranslation('条记录', language)
),
E('div', { 'class': 'pagination-controls' }, [
E('select', {
'class': 'filter-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': 'btn btn-secondary',
'id': 'prev-page-btn',
'disabled': currentPage <= 1 ? 'disabled' : null
}, getTranslation('上一页', language)),
E('button', {
'class': 'btn btn-secondary',
'id': 'next-page-btn',
'disabled': currentPage >= totalPages ? 'disabled' : null
}, getTranslation('下一页', language))
])
]);
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;
container.innerHTML = '';
container.appendChild(E('div', { 'class': 'error-state' },
getTranslation('无法获取数据', language)));
});
}
// 初始化数据加载 - 延迟执行确保 DOM 元素已添加
setTimeout(function () {
updateStats();
updateQueries();
// 实时搜索功能(带防抖)
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 () {
updateQueries();
});
}
}
}, 100);
// 轮询更新统计信息每1秒查询记录不自动刷新
poll.add(updateStats, 1);
return container;
}
});