1525 lines
62 KiB
JavaScript
1525 lines
62 KiB
JavaScript
'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_ip(DNS服务器的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 '-';
|
||
// 查询时显示目标IP(destination_ip),响应时显示源IP(source_ip)
|
||
if (query.is_query) {
|
||
return query.destination_ip || '-';
|
||
} else {
|
||
return query.source_ip || '-';
|
||
}
|
||
}
|
||
|
||
function formatResponseResult(query) {
|
||
if (!query) return { display: [], full: [] };
|
||
|
||
// 显示响应IP(response_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;
|
||
}
|
||
});
|
||
|