Fix DHCP client unable to get IP and config not persisting
- Fixed verifyAssignment being too strict for new clients - Fixed parseRequestedIP string conversion bug - Fixed response sent to 0.0.0.0 instead of broadcast address - Added SO_BROADCAST support for UDP socket - Fixed session persistence after page refresh (localStorage) - Added in-memory session store for auth middleware - Added config reloader so DHCP server picks up web UI changes dynamically
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
nav {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
section {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
/* Login Form */
|
||||
#loginForm {
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #3498db;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
/* Stats */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
color: white;
|
||||
margin-bottom: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stat-card p {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Status Grid */
|
||||
.status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.status-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.status-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #27ae60;
|
||||
}
|
||||
|
||||
/* Info Grid */
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Panels */
|
||||
.panel {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Form Styles */
|
||||
.form-row {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.form-row label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #34495e;
|
||||
}
|
||||
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="number"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-row input[type="checkbox"] {
|
||||
width: auto;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Pool Stats */
|
||||
.pool-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
padding: 15px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Pool Bar */
|
||||
.pool-bar {
|
||||
height: 24px;
|
||||
background: #e9ecef;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.pool-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #27ae60, #2ecc71);
|
||||
border-radius: 12px;
|
||||
transition: width 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
/* Client Status */
|
||||
.status-active {
|
||||
color: #27ae60;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.status-expired {
|
||||
color: #e74c3c;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.stats {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.status-grid,
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
nav {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
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', () => {
|
||||
sessionId = null;
|
||||
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 = '<tr><td colspan="6" style="text-align:center;color:#999;">暂无客户端</td></tr>';
|
||||
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 ? '<span class="status-active">● 在线</span>' : '<span class="status-expired">● 已过期</span>';
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>DHCP & DNS 管理器</title>
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>🌐 DHCP & DNS 管理器</h1>
|
||||
<nav>
|
||||
<a href="#dashboard">仪表盘</a>
|
||||
<a href="#clients">DHCP 客户端</a>
|
||||
<a href="#dhcp">DHCP 配置</a>
|
||||
<a href="#dns">DNS 配置</a>
|
||||
<a href="#settings">系统设置</a>
|
||||
<button id="logoutBtn" style="display:none;">退出</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Login Section -->
|
||||
<section id="loginSection">
|
||||
<h2>登录</h2>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="用户名" required>
|
||||
<input type="password" id="password" placeholder="密码" required>
|
||||
<button type="submit">登录</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<!-- Dashboard -->
|
||||
<section id="dashboard" style="display:none;">
|
||||
<h2>仪表盘</h2>
|
||||
<div class="stats">
|
||||
<div class="stat-card" onclick="document.querySelector('a[href=\'#clients\']').click()" style="cursor:pointer;">
|
||||
<h3>活跃租约</h3>
|
||||
<p id="activeLeases">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>静态绑定</h3>
|
||||
<p id="staticBindings">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>DNS 记录</h3>
|
||||
<p id="dnsRecords">0</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<h3>在线设备</h3>
|
||||
<p id="onlineDevices">0</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>系统状态</h3>
|
||||
<div class="status-grid">
|
||||
<div class="status-item">
|
||||
<span class="status-label">DHCP 服务</span>
|
||||
<span class="status-value" id="dhcpStatus">运行中</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">DNS 服务</span>
|
||||
<span class="status-value" id="dnsStatus">运行中</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span class="status-label">Web 服务</span>
|
||||
<span class="status-value" id="webStatus">运行中</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DHCP Configuration -->
|
||||
<section id="dhcp" style="display:none;">
|
||||
<h2>DHCP 配置</h2>
|
||||
|
||||
<div class="panel">
|
||||
<h3>基础配置</h3>
|
||||
<form id="dhcpBasicForm">
|
||||
<div class="form-row">
|
||||
<label>启用 DHCP</label>
|
||||
<input type="checkbox" id="dhcpEnabled" checked>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>网络接口</label>
|
||||
<input type="text" id="dhcpInterface" placeholder="eth0" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>网段地址</label>
|
||||
<input type="text" id="dhcpNetwork" placeholder="192.168.1.0" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>子网掩码</label>
|
||||
<input type="text" id="dhcpNetmask" placeholder="255.255.255.0" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>网关地址</label>
|
||||
<input type="text" id="dhcpGateway" placeholder="192.168.1.1" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>域名</label>
|
||||
<input type="text" id="dhcpDomain" placeholder="local">
|
||||
</div>
|
||||
<button type="submit">保存基础配置</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>IP 地址池</h3>
|
||||
<form id="dhcpPoolForm">
|
||||
<div class="form-row">
|
||||
<label>起始 IP</label>
|
||||
<input type="text" id="dhcpPoolStart" placeholder="192.168.1.100" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>结束 IP</label>
|
||||
<input type="text" id="dhcpPoolEnd" placeholder="192.168.1.200" required>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>租约时间(秒)</label>
|
||||
<input type="number" id="dhcpLeaseTime" placeholder="86400" value="86400">
|
||||
</div>
|
||||
<button type="submit">保存地址池配置</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>DNS 服务器</h3>
|
||||
<form id="dhcpDnsForm">
|
||||
<div class="form-row">
|
||||
<label>DNS 服务器列表(逗号分隔)</label>
|
||||
<input type="text" id="dhcpDnsServers" placeholder="192.168.1.1,114.114.114.114,8.8.8.8">
|
||||
</div>
|
||||
<button type="submit">保存 DNS 配置</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>排除 IP 列表</h3>
|
||||
<form id="dhcpExcludedForm">
|
||||
<div class="form-row">
|
||||
<label>排除的 IP(逗号分隔)</label>
|
||||
<input type="text" id="dhcpExcludedIps" placeholder="192.168.1.1,192.168.1.2,192.168.1.3">
|
||||
</div>
|
||||
<button type="submit">保存排除列表</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>静态 IP 绑定</h3>
|
||||
<button onclick="showAddBindingForm()">+ 新增绑定</button>
|
||||
<table id="bindingsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC 地址</th>
|
||||
<th>IP 地址</th>
|
||||
<th>主机名</th>
|
||||
<th>描述</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DHCP Clients -->
|
||||
<section id="clients" style="display:none;">
|
||||
<h2>DHCP 客户端列表</h2>
|
||||
|
||||
<div class="panel">
|
||||
<h3>已分配 IP 的客户端</h3>
|
||||
<button onclick="loadClients()">🔄 刷新</button>
|
||||
<button onclick="toggleAutoRefresh()" id="autoRefreshBtn">⏸️ 自动刷新: 关</button>
|
||||
<table id="clientsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MAC 地址</th>
|
||||
<th>IP 地址</th>
|
||||
<th>主机名</th>
|
||||
<th>租约剩余</th>
|
||||
<th>过期时间</th>
|
||||
<th>状态</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;color:#999;">暂无客户端</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>IP 地址池使用情况</h3>
|
||||
<div class="pool-stats">
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">地址池范围</span>
|
||||
<span class="stat-value" id="poolRange">--</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">已分配</span>
|
||||
<span class="stat-value" id="poolUsed">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">可用</span>
|
||||
<span class="stat-value" id="poolAvailable">0</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-label">使用率</span>
|
||||
<span class="stat-value" id="poolUsage">0%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pool-bar">
|
||||
<div class="pool-bar-fill" id="poolBarFill" style="width: 0%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- DNS Configuration -->
|
||||
<section id="dns" style="display:none;">
|
||||
<h2>DNS 配置</h2>
|
||||
|
||||
<div class="panel">
|
||||
<h3>基础配置</h3>
|
||||
<form id="dnsBasicForm">
|
||||
<div class="form-row">
|
||||
<label>启用 DNS</label>
|
||||
<input type="checkbox" id="dnsEnabled" checked>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>监听地址</label>
|
||||
<input type="text" id="dnsListenAddr" placeholder="0.0.0.0" value="0.0.0.0">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>监听端口</label>
|
||||
<input type="number" id="dnsListenPort" placeholder="53" value="53">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>启用递归查询</label>
|
||||
<input type="checkbox" id="dnsRecursion" checked>
|
||||
</div>
|
||||
<button type="submit">保存基础配置</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>上游 DNS 服务器</h3>
|
||||
<form id="dnsUpstreamForm">
|
||||
<div class="form-row">
|
||||
<label>上游 DNS(逗号分隔)</label>
|
||||
<input type="text" id="dnsUpstream" placeholder="8.8.8.8,1.1.1.1,114.114.114.114">
|
||||
</div>
|
||||
<button type="submit">保存上游 DNS</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>DNS 区域 (Zone)</h3>
|
||||
<button onclick="showAddZoneForm()">+ 新增区域</button>
|
||||
<table id="zonesTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>区域名称</th>
|
||||
<th>类型</th>
|
||||
<th>记录数</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>DNS 记录</h3>
|
||||
<button onclick="showAddRecordForm()">+ 新增记录</button>
|
||||
<table id="recordsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>域名</th>
|
||||
<th>类型</th>
|
||||
<th>值</th>
|
||||
<th>TTL</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>查询日志</h3>
|
||||
<button onclick="loadLogs()">刷新</button>
|
||||
<table id="logsTable">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>客户端 IP</th>
|
||||
<th>查询域名</th>
|
||||
<th>类型</th>
|
||||
<th>响应</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Settings -->
|
||||
<section id="settings" style="display:none;">
|
||||
<h2>系统设置</h2>
|
||||
|
||||
<div class="panel">
|
||||
<h3>Web 设置</h3>
|
||||
<form id="webSettingsForm">
|
||||
<div class="form-row">
|
||||
<label>监听地址</label>
|
||||
<input type="text" id="webHost" placeholder="0.0.0.0" value="0.0.0.0">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label>监听端口</label>
|
||||
<input type="number" id="webPort" placeholder="8080" value="8080">
|
||||
</div>
|
||||
<button type="submit">保存 Web 设置</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>配置管理</h3>
|
||||
<button onclick="exportConfig()">导出配置</button>
|
||||
<button onclick="importConfig()">导入配置</button>
|
||||
<button onclick="restartService()">重启服务</button>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>系统信息</h3>
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="info-label">版本</span>
|
||||
<span class="info-value">v0.1.1</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">运行时间</span>
|
||||
<span class="info-value" id="uptime">--</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="info-label">数据库大小</span>
|
||||
<span class="info-value" id="dbSize">--</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user