// 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(args.host || '-')}
用户名
${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();
});