// MosDNS 管理面板 JavaScript 应用 class MosDNSAdmin { constructor() { this.apiBase = '/api'; this.currentTab = 'dashboard'; this.refreshInterval = null; this.init(); } init() { this.setupEventListeners(); this.loadInitialData(); this.startAutoRefresh(); } setupEventListeners() { // 导航栏切换 document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', (e) => { const tab = e.target.dataset.tab; this.switchTab(tab); }); }); // 页面可见性变化时处理自动刷新 document.addEventListener('visibilitychange', () => { if (document.hidden) { this.stopAutoRefresh(); } else { this.startAutoRefresh(); } }); } switchTab(tab) { // 更新导航栏状态 document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); document.querySelector(`[data-tab="${tab}"]`).classList.add('active'); // 更新内容区域 document.querySelectorAll('.tab-content').forEach(content => { content.classList.remove('active'); }); document.getElementById(tab).classList.add('active'); this.currentTab = tab; this.loadTabData(tab); } async loadInitialData() { try { await this.loadServerInfo(); await this.loadTabData(this.currentTab); } catch (error) { this.showMessage('加载初始数据失败: ' + error.message, 'error'); } } async loadServerInfo() { try { const response = await this.apiCall('/server/info'); if (response.success) { const info = response.data; document.getElementById('version').textContent = info.version || 'v5.0.0'; document.getElementById('service-status').textContent = info.status; // 使用秒数来格式化运行时间,修复 NaN 问题 if (info.uptime_seconds !== undefined) { document.getElementById('uptime').textContent = this.formatUptimeFromSeconds(info.uptime_seconds); } else { document.getElementById('uptime').textContent = info.uptime || '-'; } // 显示 DNS 端口 if (info.dns_ports && info.dns_ports.length > 0) { document.getElementById('dns-ports').textContent = info.dns_ports.join(', '); } else { document.getElementById('dns-ports').textContent = '未检测到'; } // 显示 API 地址 if (info.api_address) { document.getElementById('api-address').textContent = info.api_address; } } } catch (error) { console.error('Failed to load server info:', error); } } formatUptimeFromSeconds(seconds) { if (!seconds || seconds < 0) { return '0分钟'; } const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); let parts = []; if (days > 0) parts.push(`${days}天`); if (hours > 0) parts.push(`${hours}小时`); if (minutes > 0 || parts.length === 0) parts.push(`${minutes}分钟`); return parts.join(' '); } async loadTabData(tab) { switch (tab) { case 'dashboard': await this.loadDashboardData(); break; case 'mikrotik': await this.loadMikroTikTab(); break; case 'domains': await this.loadDomainFiles(); break; case 'logs': await this.loadLogs(); break; case 'stats': await this.loadDetailedStats(); break; } } async loadDashboardData() { try { // 加载详细统计 const statsResponse = await this.apiCall('/stats/detailed'); if (statsResponse.success) { const stats = statsResponse.data; document.getElementById('total-queries').textContent = stats.totalQueries?.toLocaleString() || '-'; document.getElementById('cache-hits').textContent = stats.cacheHits?.toLocaleString() || '-'; document.getElementById('avg-response').textContent = stats.avgResponseTime ? `${stats.avgResponseTime}ms` : '-'; } } catch (error) { console.error('Failed to load dashboard data:', error); } } // MikroTik 标签页加载 async loadMikroTikTab() { try { // 直接加载 MikroTik 配置列表(不再需要加载域名文件下拉框) await this.loadMikrotikList(); } catch (error) { console.error('Failed to load MikroTik tab:', error); } } // 加载 MikroTik 配置列表 async loadMikrotikList() { const listDiv = document.getElementById('mikrotik-list'); try { console.log('开始加载 MikroTik 配置列表...'); listDiv.innerHTML = '
加载中...
'; const response = await this.apiCall('/mikrotik/list'); console.log('MikroTik API 响应:', response); if (!response) { throw new Error('API 响应为空'); } if (!response.success) { throw new Error(response.message || '加载失败'); } const configs = response.data || []; if (configs.length === 0) { listDiv.innerHTML = '
暂无 MikroTik 配置
'; return; } let html = ''; configs.forEach(config => { const args = config.args || {}; const domainFiles = args.domain_files || []; const domainFilesStr = Array.isArray(domainFiles) ? domainFiles.join(', ') : domainFiles; html += `
${this.escapeHtml(config.tag || '')}
主机地址
${this.escapeHtml(args.host || '-')}
端口
${args.port || '-'}
用户名
${this.escapeHtml(args.username || '-')}
地址列表
${this.escapeHtml(args.address_list4 || '-')}
域名文件
${this.escapeHtml(domainFilesStr || '-')}
`; }); listDiv.innerHTML = html; console.log(`成功加载 ${configs.length} 个 MikroTik 配置`); } catch (error) { console.error('加载 MikroTik 列表失败:', error); listDiv.innerHTML = `

❌ 加载失败

${this.escapeHtml(error.message)}

`; } } // HTML 转义 escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } async loadConfigData() { try { const response = await this.apiCall('/config'); if (response.success) { document.getElementById('config-editor').value = response.data.content || ''; } } catch (error) { this.showMessage('加载配置失败: ' + error.message, 'error'); } } async loadDomainFiles() { try { const response = await this.apiCall('/domain-files'); if (response.success) { this.renderDomainFilesList(response.data); } } catch (error) { this.showMessage('加载域名文件失败: ' + error.message, 'error'); } } async loadLogs() { try { const response = await this.apiCall('/logs'); if (response.success) { document.getElementById('logs-content').textContent = response.data.content || '暂无日志内容'; } } catch (error) { this.showMessage('加载日志失败: ' + error.message, 'error'); } } async loadDetailedStats() { try { const response = await this.apiCall('/stats/detailed'); if (response.success) { this.renderDetailedStats(response.data); } } catch (error) { this.showMessage('加载统计信息失败: ' + error.message, 'error'); } } renderPluginsList(plugins) { const container = document.getElementById('plugins-list'); if (!plugins || plugins.length === 0) { container.innerHTML = '
暂无插件信息
'; return; } const html = plugins.map(plugin => `
${plugin.tag}: ${plugin.status || '运行中'}
`).join(''); container.innerHTML = html; } renderDomainFilesList(files) { const container = document.getElementById('domain-files-list'); if (!files || files.length === 0) { container.innerHTML = '
暂无域名文件
'; return; } const html = files.map(file => `
${file.filename}
大小: ${this.formatFileSize(file.size)} | 修改时间: ${new Date(file.modTime).toLocaleString()}
`).join(''); container.innerHTML = html; } renderDetailedStats(stats) { const container = document.getElementById('detailed-stats'); if (!stats) { container.innerHTML = '
暂无统计信息
'; return; } const html = `
DNS 查询总数: ${stats.totalQueries?.toLocaleString() || '-'}
成功响应: ${stats.successfulQueries?.toLocaleString() || '-'}
失败响应: ${stats.failedQueries?.toLocaleString() || '-'}
缓存命中: ${stats.cacheHits?.toLocaleString() || '-'}
缓存未命中: ${stats.cacheMisses?.toLocaleString() || '-'}
平均响应时间: ${stats.avgResponseTime ? stats.avgResponseTime + 'ms' : '-'}
`; container.innerHTML = html; } async apiCall(endpoint, options = {}) { const url = this.apiBase + endpoint; const defaultOptions = { method: 'GET', headers: { 'Content-Type': 'application/json', }, }; const response = await fetch(url, { ...defaultOptions, ...options }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.json(); } startAutoRefresh() { this.stopAutoRefresh(); this.refreshInterval = setInterval(() => { if (this.currentTab === 'dashboard') { this.loadDashboardData(); } }, 30000); // 30秒刷新一次 } stopAutoRefresh() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } } showMessage(message, type = 'success') { const container = document.getElementById('message-container'); const messageEl = document.createElement('div'); messageEl.className = `message ${type}`; messageEl.textContent = message; container.appendChild(messageEl); setTimeout(() => { messageEl.remove(); }, 5000); } formatUptime(seconds) { if (!seconds) return '-'; const days = Math.floor(seconds / 86400); const hours = Math.floor((seconds % 86400) / 3600); const minutes = Math.floor((seconds % 3600) / 60); if (days > 0) return `${days}天 ${hours}小时 ${minutes}分钟`; if (hours > 0) return `${hours}小时 ${minutes}分钟`; return `${minutes}分钟`; } formatFileSize(bytes) { if (!bytes) return '0 B'; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(1024)); return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i]; } // 域名文件操作方法 async viewDomainFile(filename) { try { const response = await this.apiCall(`/domain-files/${filename}`); if (response.success) { alert(`文件内容:\n\n${response.data.content}`); } } catch (error) { this.showMessage('查看文件失败: ' + error.message, 'error'); } } async editDomainFile(filename) { this.showMessage('编辑功能正在开发中', 'warning'); } async deleteDomainFile(filename) { if (!confirm(`确定要删除文件 ${filename} 吗?`)) return; try { const response = await this.apiCall(`/domain-files/${filename}`, { method: 'DELETE' }); if (response.success) { this.showMessage('文件删除成功'); this.loadDomainFiles(); } } catch (error) { this.showMessage('删除文件失败: ' + error.message, 'error'); } } } // 全局函数,供 HTML 中的按钮调用 async function reloadConfig() { try { const response = await app.apiCall('/config/reload', { method: 'POST' }); if (response.success) { app.showMessage('配置重载成功'); } } catch (error) { app.showMessage('配置重载失败: ' + error.message, 'error'); } } async function flushCache() { try { const response = await app.apiCall('/cache/flush', { method: 'POST' }); if (response.success) { app.showMessage('缓存清空成功'); } } catch (error) { app.showMessage('缓存清空失败: ' + error.message, 'error'); } } async function refreshStats() { app.loadDashboardData(); app.showMessage('统计信息已刷新'); } async function restartService() { if (!confirm('确定要重启服务吗?服务将在 3 秒后重启。')) return; try { const response = await app.apiCall('/system/restart', { method: 'POST' }); if (response.success) { app.showMessage('重启请求已发送,服务将在 3 秒后重启', 'success'); } } catch (error) { app.showMessage('重启失败: ' + error.message, 'error'); } } // MikroTik 管理函数 async function saveMikrotikConfig() { const tag = document.getElementById('mikrotik-tag').value.trim(); const host = document.getElementById('mikrotik-host').value.trim(); const port = document.getElementById('mikrotik-port').value.trim(); const username = document.getElementById('mikrotik-username').value.trim(); const password = document.getElementById('mikrotik-password').value; const addresslist = document.getElementById('mikrotik-addresslist').value.trim(); const domainFilePath = document.getElementById('mikrotik-domains').value.trim(); // 验证必填字段 if (!tag) { app.showMessage('请填写配置标签', 'error'); return; } if (!host) { app.showMessage('请填写 MikroTik 地址', 'error'); return; } if (!username) { app.showMessage('请填写用户名', 'error'); return; } if (!password) { app.showMessage('请填写密码', 'error'); return; } if (!addresslist) { app.showMessage('请填写地址列表名', 'error'); return; } if (!domainFilePath) { app.showMessage('请填写域名文件路径', 'error'); return; } // 构建配置对象 const config = { tag: tag, type: 'mikrotik_addresslist', args: { domain_files: [domainFilePath], // 使用用户输入的完整路径 host: host, port: parseInt(port) || 9728, username: username, password: password, use_tls: false, timeout: 3, address_list4: addresslist, mask4: 24, comment: `${addresslist}-AutoAdd`, timeout_addr: 43200, cache_ttl: 3600, verify_add: false, add_all_ips: true, max_ips: 50 } }; try { const response = await app.apiCall('/mikrotik/add', { method: 'POST', body: JSON.stringify(config) }); if (response.success) { app.showMessage(response.message || 'MikroTik 配置已保存', 'success'); // 清空表单 clearMikrotikForm(); // 刷新列表 await app.loadMikrotikList(); } else { app.showMessage(response.message || '保存失败', 'error'); } } catch (error) { app.showMessage('保存失败: ' + error.message, 'error'); } } async function deleteMikrotikConfig(tag) { if (!confirm(`确定要删除 MikroTik 配置 "${tag}" 吗?`)) { return; } try { const response = await app.apiCall(`/mikrotik/${encodeURIComponent(tag)}`, { method: 'DELETE' }); if (response.success) { app.showMessage(response.message || '配置已删除', 'success'); // 刷新列表 await app.loadMikrotikList(); } else { app.showMessage(response.message || '删除失败', 'error'); } } catch (error) { app.showMessage('删除失败: ' + error.message, 'error'); } } function clearMikrotikForm() { document.getElementById('mikrotik-tag').value = ''; document.getElementById('mikrotik-host').value = ''; document.getElementById('mikrotik-port').value = '9728'; document.getElementById('mikrotik-username').value = 'admin'; document.getElementById('mikrotik-password').value = ''; document.getElementById('mikrotik-addresslist').value = ''; document.getElementById('mikrotik-domains').value = ''; app.showMessage('表单已清空', 'info'); } // 重新加载 MikroTik 列表 async function loadMikrotikList() { await app.loadMikrotikList(); } async function saveConfig() { const content = document.getElementById('config-editor').value; try { const response = await app.apiCall('/config', { method: 'PUT', body: JSON.stringify({ content }) }); if (response.success) { app.showMessage('配置保存成功'); } } catch (error) { app.showMessage('配置保存失败: ' + error.message, 'error'); } } async function validateConfig() { const content = document.getElementById('config-editor').value; try { const response = await app.apiCall('/config/validate', { method: 'POST', body: JSON.stringify({ content }) }); if (response.success) { app.showMessage('配置验证通过'); } } catch (error) { app.showMessage('配置验证失败: ' + error.message, 'error'); } } async function backupConfig() { try { const response = await app.apiCall('/config/backup', { method: 'POST' }); if (response.success) { app.showMessage('配置备份成功'); } } catch (error) { app.showMessage('配置备份失败: ' + error.message, 'error'); } } async function addDomainFile() { app.showMessage('添加文件功能正在开发中', 'warning'); } async function refreshDomainFiles() { app.loadDomainFiles(); app.showMessage('域名文件列表已刷新'); } async function clearLogs() { if (!confirm('确定要清空日志吗?')) return; try { const response = await app.apiCall('/logs/clear', { method: 'POST' }); if (response.success) { document.getElementById('logs-content').textContent = ''; app.showMessage('日志清空成功'); } } catch (error) { app.showMessage('日志清空失败: ' + error.message, 'error'); } } async function refreshLogs() { app.loadLogs(); app.showMessage('日志已刷新'); } async function exportStats() { app.showMessage('导出功能正在开发中', 'warning'); } async function refreshDetailedStats() { app.loadDetailedStats(); app.showMessage('统计信息已刷新'); } // 初始化应用 let app; document.addEventListener('DOMContentLoaded', () => { app = new MosDNSAdmin(); });