let sessionId = localStorage.getItem('session_id') || null; let autoRefreshInterval = null; let autoRefreshEnabled = false; // Restore session on page load if (sessionId) { document.getElementById('loginSection').style.display = 'none'; document.getElementById('dashboard').style.display = 'block'; document.getElementById('logoutBtn').style.display = 'block'; loadDashboard(); loadDHCPConfig(); loadDNSConfig(); } // Auto Refresh function toggleAutoRefresh() { autoRefreshEnabled = !autoRefreshEnabled; const btn = document.getElementById('autoRefreshBtn'); if (autoRefreshEnabled) { btn.textContent = '▶️ 自动刷新: 开'; autoRefreshInterval = setInterval(loadClients, 10000); // 每10秒刷新 } else { btn.textContent = '⏸️ 自动刷新: 关'; clearInterval(autoRefreshInterval); } } // Login document.getElementById('loginForm').addEventListener('submit', async (e) => { e.preventDefault(); const username = document.getElementById('username').value; const password = document.getElementById('password').value; try { const response = await fetch('/api/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, password }) }); const data = await response.json(); if (response.ok) { sessionId = data.session_id; localStorage.setItem('session_id', sessionId); document.getElementById('loginSection').style.display = 'none'; document.getElementById('dashboard').style.display = 'block'; document.getElementById('logoutBtn').style.display = 'block'; loadDashboard(); loadDHCPConfig(); loadDNSConfig(); } else { alert(data.error || '登录失败'); } } catch (error) { alert('登录失败:' + error.message); } }); // Logout document.getElementById('logoutBtn').addEventListener('click', () => { localStorage.removeItem('session_id'); location.reload(); }); // Navigation document.querySelectorAll('nav a').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const target = e.target.getAttribute('href').substring(1); document.querySelectorAll('section').forEach(section => { if (section.id !== 'loginSection') { section.style.display = 'none'; } }); document.getElementById(target).style.display = 'block'; if (target === 'dashboard') loadDashboard(); if (target === 'clients') loadClients(); if (target === 'dhcp') loadDHCPConfig(); if (target === 'dns') loadDNSConfig(); if (target === 'settings') loadSystemInfo(); }); }); // Load Dashboard async function loadDashboard() { try { const response = await fetch('/api/dashboard', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); document.getElementById('activeLeases').textContent = data.active_leases || 0; document.getElementById('staticBindings').textContent = data.static_bindings || 0; document.getElementById('dnsRecords').textContent = data.dns_records || 0; document.getElementById('onlineDevices').textContent = data.online_devices || 0; } catch (error) { console.error('Failed to load dashboard:', error); } } // Load DHCP Clients async function loadClients() { try { const response = await fetch('/api/dhcp/leases', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const tbody = document.querySelector('#clientsTable tbody'); tbody.innerHTML = ''; if (!data.leases || data.leases.length === 0) { tbody.innerHTML = '暂无客户端'; updatePoolStats(0, 0); return; } const now = Math.floor(Date.now() / 1000); let activeCount = 0; data.leases.forEach(lease => { const expiresAt = lease.ExpiresAt || 0; const remaining = expiresAt - now; const isActive = remaining > 0; if (isActive) activeCount++; const row = document.createElement('tr'); // MAC const macCell = document.createElement('td'); macCell.textContent = lease.MAC || '-'; row.appendChild(macCell); // IP const ipCell = document.createElement('td'); ipCell.textContent = lease.IP || '-'; row.appendChild(ipCell); // Hostname const hostCell = document.createElement('td'); hostCell.textContent = lease.Hostname || '-'; row.appendChild(hostCell); // Remaining time const remainCell = document.createElement('td'); remainCell.textContent = isActive ? formatTimeRemaining(remaining) : '已过期'; remainCell.style.color = isActive ? '#27ae60' : '#e74c3c'; row.appendChild(remainCell); // Expiry time const expireCell = document.createElement('td'); expireCell.textContent = expiresAt > 0 ? new Date(expiresAt * 1000).toLocaleString() : '-'; row.appendChild(expireCell); // Status const statusCell = document.createElement('td'); statusCell.innerHTML = isActive ? '● 在线' : '● 已过期'; row.appendChild(statusCell); tbody.appendChild(row); }); updatePoolStats(activeCount, data.leases.length); } catch (error) { console.error('Failed to load clients:', error); } } // Update Pool Stats async function updatePoolStats(activeCount, totalCount) { try { const response = await fetch('/api/dhcp/config', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const cfg = data.config; if (cfg) { const startIP = cfg.ip_pool_start || '192.168.1.100'; const endIP = cfg.ip_pool_end || '192.168.1.200'; document.getElementById('poolRange').textContent = `${startIP} - ${endIP}`; document.getElementById('poolUsed').textContent = activeCount; // Calculate pool size const startBytes = ipToBytes(startIP); const endBytes = ipToBytes(endIP); const poolSize = bytesToIP(endBytes) - bytesToIP(startBytes) + 1; const available = poolSize - activeCount; const usage = poolSize > 0 ? Math.round((activeCount / poolSize) * 100) : 0; document.getElementById('poolAvailable').textContent = available; document.getElementById('poolUsage').textContent = usage + '%'; const barFill = document.getElementById('poolBarFill'); barFill.style.width = usage + '%'; barFill.textContent = usage + '%'; // Color based on usage if (usage > 90) { barFill.style.background = 'linear-gradient(90deg, #e74c3c, #c0392b)'; } else if (usage > 70) { barFill.style.background = 'linear-gradient(90deg, #f39c12, #e67e22)'; } else { barFill.style.background = 'linear-gradient(90deg, #27ae60, #2ecc71)'; } } } catch (error) { console.error('Failed to update pool stats:', error); } } // Helper functions function ipToBytes(ip) { return ip.split('.').map(Number); } function bytesToIP(bytes) { return (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3]; } function formatTimeRemaining(seconds) { if (seconds <= 0) 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}小时`; if (hours > 0) return `${hours}小时${minutes}分钟`; return `${minutes}分钟`; } // Load DHCP Config async function loadDHCPConfig() { try { const response = await fetch('/api/dhcp/config', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const cfg = data.config; if (cfg) { document.getElementById('dhcpEnabled').checked = cfg.enabled; document.getElementById('dhcpInterface').value = cfg.interface || ''; document.getElementById('dhcpNetwork').value = cfg.network || ''; document.getElementById('dhcpNetmask').value = cfg.netmask || ''; document.getElementById('dhcpGateway').value = cfg.gateway || ''; document.getElementById('dhcpDomain').value = cfg.domain_name || ''; document.getElementById('dhcpPoolStart').value = cfg.ip_pool_start || ''; document.getElementById('dhcpPoolEnd').value = cfg.ip_pool_end || ''; document.getElementById('dhcpLeaseTime').value = cfg.lease_time || 86400; document.getElementById('dhcpDnsServers').value = (cfg.dns_servers || []).join(','); document.getElementById('dhcpExcludedIps').value = (cfg.excluded_ips || []).join(','); } loadBindings(); } catch (error) { console.error('Failed to load DHCP config:', error); } } // Save DHCP Basic Config document.getElementById('dhcpBasicForm').addEventListener('submit', async (e) => { e.preventDefault(); const config = { enabled: document.getElementById('dhcpEnabled').checked, interface: document.getElementById('dhcpInterface').value, network: document.getElementById('dhcpNetwork').value, netmask: document.getElementById('dhcpNetmask').value, gateway: document.getElementById('dhcpGateway').value, domain_name: document.getElementById('dhcpDomain').value }; try { const response = await fetch('/api/dhcp/config', { method: 'PUT', headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); alert('服务器返回了非 JSON 格式响应,请查看控制台'); return; } const data = await response.json(); if (response.ok) { alert('基础配置已保存'); } else { alert('保存失败:' + (data.error || '未知错误')); } } catch (error) { console.error('Save error:', error); alert('保存失败:' + error.message); } }); // Save DHCP Pool Config document.getElementById('dhcpPoolForm').addEventListener('submit', async (e) => { e.preventDefault(); const config = { ip_pool_start: document.getElementById('dhcpPoolStart').value, ip_pool_end: document.getElementById('dhcpPoolEnd').value, lease_time: parseInt(document.getElementById('dhcpLeaseTime').value) }; try { const response = await fetch('/api/dhcp/config', { method: 'PUT', headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); alert('服务器返回了非 JSON 格式响应,请查看控制台'); return; } const data = await response.json(); if (response.ok) { alert('地址池配置已保存'); } else { alert('保存失败:' + (data.error || '未知错误')); } } catch (error) { console.error('Save error:', error); alert('保存失败:' + error.message); } }); // Save DHCP DNS Config document.getElementById('dhcpDnsForm').addEventListener('submit', async (e) => { e.preventDefault(); const dnsServers = document.getElementById('dhcpDnsServers').value.split(',').map(s => s.trim()).filter(s => s); try { const response = await fetch('/api/dhcp/config', { method: 'PUT', headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' }, body: JSON.stringify({ dns_servers: dnsServers }) }); const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); alert('服务器返回了非 JSON 格式响应,请查看控制台'); return; } const data = await response.json(); if (response.ok) { alert('DNS 配置已保存'); } else { alert('保存失败:' + (data.error || '未知错误')); } } catch (error) { console.error('Save error:', error); alert('保存失败:' + error.message); } }); // Save DHCP Excluded IPs document.getElementById('dhcpExcludedForm').addEventListener('submit', async (e) => { e.preventDefault(); const excludedIps = document.getElementById('dhcpExcludedIps').value.split(',').map(s => s.trim()).filter(s => s); try { const response = await fetch('/api/dhcp/config', { method: 'PUT', headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' }, body: JSON.stringify({ excluded_ips: excludedIps }) }); const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); alert('服务器返回了非 JSON 格式响应,请查看控制台'); return; } const data = await response.json(); if (response.ok) { alert('排除列表已保存'); } else { alert('保存失败:' + (data.error || '未知错误')); } } catch (error) { console.error('Save error:', error); alert('保存失败:' + error.message); } }); // Load Bindings async function loadBindings() { try { const response = await fetch('/api/dhcp/bindings', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const tbody = document.querySelector('#bindingsTable tbody'); tbody.innerHTML = ''; data.bindings.forEach(binding => { const row = tbody.insertRow(); row.insertCell(0).textContent = binding.MAC; row.insertCell(1).textContent = binding.IP; row.insertCell(2).textContent = binding.Hostname || '-'; row.insertCell(3).textContent = binding.Description || '-'; const actionCell = row.insertCell(4); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.onclick = () => deleteBinding(binding.ID); actionCell.appendChild(deleteBtn); }); } catch (error) { console.error('Failed to load bindings:', error); } } function showAddBindingForm() { // TODO: Implement add binding form alert('添加绑定功能开发中...'); } async function deleteBinding(id) { if (!confirm('确定要删除这个绑定吗?')) return; try { const response = await fetch(`/api/dhcp/bindings/${id}`, { method: 'DELETE', headers: { 'X-Session-ID': sessionId } }); if (response.ok) { loadBindings(); } else { alert('删除失败'); } } catch (error) { alert('删除失败:' + error.message); } } // Load DNS Config async function loadDNSConfig() { try { const response = await fetch('/api/dns/config', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const cfg = data.config; if (cfg) { document.getElementById('dnsEnabled').checked = cfg.enabled; document.getElementById('dnsListenAddr').value = cfg.listen_addr || '0.0.0.0'; document.getElementById('dnsListenPort').value = cfg.listen_port || 53; document.getElementById('dnsRecursion').checked = cfg.recursion !== false; document.getElementById('dnsUpstream').value = (cfg.upstream || []).join(','); } loadDNSRecords(); loadZones(); loadLogs(); } catch (error) { console.error('Failed to load DNS config:', error); } } // Save DNS Basic Config document.getElementById('dnsBasicForm').addEventListener('submit', async (e) => { e.preventDefault(); const config = { enabled: document.getElementById('dnsEnabled').checked, listen_addr: document.getElementById('dnsListenAddr').value, listen_port: parseInt(document.getElementById('dnsListenPort').value), recursion: document.getElementById('dnsRecursion').checked }; try { const response = await fetch('/api/dns/config', { method: 'PUT', headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' }, body: JSON.stringify(config) }); const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); alert('服务器返回了非 JSON 格式响应,请查看控制台'); return; } const data = await response.json(); if (response.ok) { alert('DNS 基础配置已保存'); } else { alert('保存失败:' + (data.error || '未知错误')); } } catch (error) { console.error('Save error:', error); alert('保存失败:' + error.message); } }); // Save DNS Upstream document.getElementById('dnsUpstreamForm').addEventListener('submit', async (e) => { e.preventDefault(); const upstream = document.getElementById('dnsUpstream').value.split(',').map(s => s.trim()).filter(s => s); try { const response = await fetch('/api/dns/config', { method: 'PUT', headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' }, body: JSON.stringify({ upstream }) }); const contentType = response.headers.get('content-type'); if (!contentType || !contentType.includes('application/json')) { const text = await response.text(); console.error('Non-JSON response:', text); alert('服务器返回了非 JSON 格式响应,请查看控制台'); return; } const data = await response.json(); if (response.ok) { alert('上游 DNS 已保存'); } else { alert('保存失败:' + (data.error || '未知错误')); } } catch (error) { console.error('Save error:', error); alert('保存失败:' + error.message); } }); // Load DNS Records async function loadDNSRecords() { try { const response = await fetch('/api/dns/records', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const tbody = document.querySelector('#recordsTable tbody'); tbody.innerHTML = ''; data.records.forEach(record => { const row = tbody.insertRow(); row.insertCell(0).textContent = record.Name; row.insertCell(1).textContent = record.Type; row.insertCell(2).textContent = record.Value; row.insertCell(3).textContent = record.TTL; const actionCell = row.insertCell(4); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.onclick = () => deleteRecord(record.ID); actionCell.appendChild(deleteBtn); }); } catch (error) { console.error('Failed to load records:', error); } } function showAddRecordForm() { // TODO: Implement add record form alert('添加记录功能开发中...'); } async function deleteRecord(id) { if (!confirm('确定要删除这条记录吗?')) return; try { const response = await fetch(`/api/dns/records/${id}`, { method: 'DELETE', headers: { 'X-Session-ID': sessionId } }); if (response.ok) { loadDNSRecords(); } else { alert('删除失败'); } } catch (error) { alert('删除失败:' + error.message); } } // Load Zones async function loadZones() { try { const response = await fetch('/api/dns/zones', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const tbody = document.querySelector('#zonesTable tbody'); tbody.innerHTML = ''; data.zones.forEach(zone => { const row = tbody.insertRow(); row.insertCell(0).textContent = zone.name; row.insertCell(1).textContent = zone.type; row.insertCell(2).textContent = zone.record_count; const actionCell = row.insertCell(3); const deleteBtn = document.createElement('button'); deleteBtn.textContent = '删除'; deleteBtn.onclick = () => deleteZone(zone.ID); actionCell.appendChild(deleteBtn); }); } catch (error) { console.error('Failed to load zones:', error); } } function showAddZoneForm() { // TODO: Implement add zone form alert('添加区域功能开发中...'); } async function deleteZone(id) { if (!confirm('确定要删除这个区域吗?')) return; try { const response = await fetch(`/api/dns/zones/${id}`, { method: 'DELETE', headers: { 'X-Session-ID': sessionId } }); if (response.ok) { loadZones(); } else { alert('删除失败'); } } catch (error) { alert('删除失败:' + error.message); } } // Load Logs async function loadLogs() { try { const response = await fetch('/api/dns/logs', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); const tbody = document.querySelector('#logsTable tbody'); tbody.innerHTML = ''; data.logs.forEach(log => { const row = tbody.insertRow(); row.insertCell(0).textContent = new Date(log.Timestamp * 1000).toLocaleString(); row.insertCell(1).textContent = log.ClientIP; row.insertCell(2).textContent = log.QueryName; row.insertCell(3).textContent = log.QueryType; row.insertCell(4).textContent = log.Response || '-'; }); } catch (error) { console.error('Failed to load logs:', error); } } // Load System Info async function loadSystemInfo() { try { const response = await fetch('/api/config', { headers: { 'X-Session-ID': sessionId } }); const data = await response.json(); // TODO: Update system info display } catch (error) { console.error('Failed to load system info:', error); } } // Export Config async function exportConfig() { try { const response = await fetch('/api/config/export', { headers: { 'X-Session-ID': sessionId } }); const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'dhcp-dns-config.json'; a.click(); window.URL.revokeObjectURL(url); } catch (error) { alert('导出失败:' + error.message); } } // Import Config async function importConfig() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = async (e) => { const file = e.target.files[0]; if (!file) return; const formData = new FormData(); formData.append('config', file); try { const response = await fetch('/api/config/import', { method: 'POST', headers: { 'X-Session-ID': sessionId }, body: formData }); if (response.ok) { alert('配置已导入'); } else { const data = await response.json(); alert('导入失败:' + data.error); } } catch (error) { alert('导入失败:' + error.message); } }; input.click(); } // Restart Service async function restartService() { if (!confirm('确定要重启服务吗?服务将短暂中断。')) return; try { const response = await fetch('/api/service/restart', { method: 'POST', headers: { 'X-Session-ID': sessionId } }); if (response.ok) { alert('服务重启请求已发送'); } else { const data = await response.json(); alert('重启失败:' + data.error); } } catch (error) { alert('重启失败:' + error.message); } }