app.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858
  1. let sessionId = localStorage.getItem('session_id') || null;
  2. let autoRefreshInterval = null;
  3. let autoRefreshEnabled = false;
  4. // Restore session on page load
  5. if (sessionId) {
  6. // Verify session is still valid
  7. fetch('/api/session/verify', {
  8. method: 'POST',
  9. headers: { 'Content-Type': 'application/json' },
  10. body: JSON.stringify({ session_id: sessionId })
  11. })
  12. .then(res => res.json())
  13. .then(data => {
  14. if (data.valid) {
  15. document.getElementById('loginSection').style.display = 'none';
  16. document.getElementById('dashboard').style.display = 'block';
  17. document.getElementById('logoutBtn').style.display = 'block';
  18. loadDashboard();
  19. loadDHCPConfig();
  20. loadDNSConfig();
  21. } else {
  22. // Session expired, clear and show login
  23. localStorage.removeItem('session_id');
  24. sessionId = null;
  25. }
  26. })
  27. .catch(err => {
  28. console.error('Session verify error:', err);
  29. // On error, clear and show login
  30. localStorage.removeItem('session_id');
  31. sessionId = null;
  32. });
  33. }
  34. // Auto Refresh
  35. function toggleAutoRefresh() {
  36. autoRefreshEnabled = !autoRefreshEnabled;
  37. const btn = document.getElementById('autoRefreshBtn');
  38. if (autoRefreshEnabled) {
  39. btn.textContent = '▶️ 自动刷新: 开';
  40. autoRefreshInterval = setInterval(loadClients, 10000); // 每10秒刷新
  41. } else {
  42. btn.textContent = '⏸️ 自动刷新: 关';
  43. clearInterval(autoRefreshInterval);
  44. }
  45. }
  46. // Login
  47. document.getElementById('loginForm').addEventListener('submit', async (e) => {
  48. e.preventDefault();
  49. const username = document.getElementById('username').value;
  50. const password = document.getElementById('password').value;
  51. try {
  52. const response = await fetch('/api/login', {
  53. method: 'POST',
  54. headers: { 'Content-Type': 'application/json' },
  55. body: JSON.stringify({ username, password })
  56. });
  57. const data = await response.json();
  58. if (response.ok) {
  59. sessionId = data.session_id;
  60. localStorage.setItem('session_id', sessionId);
  61. document.getElementById('loginSection').style.display = 'none';
  62. document.getElementById('dashboard').style.display = 'block';
  63. document.getElementById('logoutBtn').style.display = 'block';
  64. loadDashboard();
  65. loadDHCPConfig();
  66. loadDNSConfig();
  67. } else {
  68. alert(data.error || '登录失败');
  69. }
  70. } catch (error) {
  71. alert('登录失败:' + error.message);
  72. }
  73. });
  74. // Logout
  75. document.getElementById('logoutBtn').addEventListener('click', () => {
  76. localStorage.removeItem('session_id');
  77. location.reload();
  78. });
  79. // Navigation
  80. document.querySelectorAll('nav a').forEach(link => {
  81. link.addEventListener('click', (e) => {
  82. e.preventDefault();
  83. const target = e.target.getAttribute('href').substring(1);
  84. document.querySelectorAll('section').forEach(section => {
  85. if (section.id !== 'loginSection') {
  86. section.style.display = 'none';
  87. }
  88. });
  89. document.getElementById(target).style.display = 'block';
  90. if (target === 'dashboard') loadDashboard();
  91. if (target === 'clients') loadClients();
  92. if (target === 'dhcp') loadDHCPConfig();
  93. if (target === 'dns') loadDNSConfig();
  94. if (target === 'settings') loadSystemInfo();
  95. });
  96. });
  97. // Load Dashboard
  98. async function loadDashboard() {
  99. try {
  100. const response = await fetch('/api/dashboard', {
  101. headers: { 'X-Session-ID': sessionId }
  102. });
  103. const data = await response.json();
  104. document.getElementById('activeLeases').textContent = data.active_leases || 0;
  105. document.getElementById('staticBindings').textContent = data.static_bindings || 0;
  106. document.getElementById('dnsRecords').textContent = data.dns_records || 0;
  107. document.getElementById('onlineDevices').textContent = data.online_devices || 0;
  108. } catch (error) {
  109. console.error('Failed to load dashboard:', error);
  110. }
  111. }
  112. // Load DHCP Clients
  113. async function loadClients() {
  114. try {
  115. const response = await fetch('/api/dhcp/leases', {
  116. headers: { 'X-Session-ID': sessionId }
  117. });
  118. const data = await response.json();
  119. const tbody = document.querySelector('#clientsTable tbody');
  120. tbody.innerHTML = '';
  121. if (!data.leases || data.leases.length === 0) {
  122. tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:#999;">暂无客户端</td></tr>';
  123. updatePoolStats(0, 0);
  124. return;
  125. }
  126. const now = Math.floor(Date.now() / 1000);
  127. let activeCount = 0;
  128. data.leases.forEach(lease => {
  129. const expiresAt = lease.ExpiresAt || 0;
  130. const remaining = expiresAt - now;
  131. const isActive = remaining > 0;
  132. if (isActive) activeCount++;
  133. const row = document.createElement('tr');
  134. // MAC
  135. const macCell = document.createElement('td');
  136. macCell.textContent = lease.MAC || '-';
  137. row.appendChild(macCell);
  138. // IP
  139. const ipCell = document.createElement('td');
  140. ipCell.textContent = lease.IP || '-';
  141. row.appendChild(ipCell);
  142. // Hostname
  143. const hostCell = document.createElement('td');
  144. hostCell.textContent = lease.Hostname || '-';
  145. row.appendChild(hostCell);
  146. // Remaining time
  147. const remainCell = document.createElement('td');
  148. remainCell.textContent = isActive ? formatTimeRemaining(remaining) : '已过期';
  149. remainCell.style.color = isActive ? '#27ae60' : '#e74c3c';
  150. row.appendChild(remainCell);
  151. // Expiry time
  152. const expireCell = document.createElement('td');
  153. expireCell.textContent = expiresAt > 0 ? new Date(expiresAt * 1000).toLocaleString() : '-';
  154. row.appendChild(expireCell);
  155. // Status
  156. const statusCell = document.createElement('td');
  157. statusCell.innerHTML = isActive ? '<span class="status-active">● 在线</span>' : '<span class="status-expired">● 已过期</span>';
  158. row.appendChild(statusCell);
  159. // Action buttons
  160. const actionCell = document.createElement('td');
  161. if (isActive) {
  162. const evictBtn = document.createElement('button');
  163. evictBtn.textContent = '剔除';
  164. evictBtn.style.cssText = 'background-color: #f39c12; color: white; border: none; padding: 5px 10px; border-radius: 3px; cursor: pointer; margin-right: 5px;';
  165. evictBtn.onclick = () => evictClient(lease.MAC, lease.IP);
  166. actionCell.appendChild(evictBtn);
  167. }
  168. row.appendChild(actionCell);
  169. tbody.appendChild(row);
  170. });
  171. updatePoolStats(activeCount, data.leases.length);
  172. } catch (error) {
  173. console.error('Failed to load clients:', error);
  174. }
  175. }
  176. // Update Pool Stats
  177. async function updatePoolStats(activeCount, totalCount) {
  178. try {
  179. const response = await fetch('/api/dhcp/config', {
  180. headers: { 'X-Session-ID': sessionId }
  181. });
  182. const data = await response.json();
  183. const cfg = data.config;
  184. if (cfg) {
  185. const startIP = cfg.ip_pool_start || '192.168.1.100';
  186. const endIP = cfg.ip_pool_end || '192.168.1.200';
  187. document.getElementById('poolRange').textContent = `${startIP} - ${endIP}`;
  188. document.getElementById('poolUsed').textContent = activeCount;
  189. // Calculate pool size
  190. const startBytes = ipToBytes(startIP);
  191. const endBytes = ipToBytes(endIP);
  192. const poolSize = bytesToIP(endBytes) - bytesToIP(startBytes) + 1;
  193. const available = poolSize - activeCount;
  194. const usage = poolSize > 0 ? Math.round((activeCount / poolSize) * 100) : 0;
  195. document.getElementById('poolAvailable').textContent = available;
  196. document.getElementById('poolUsage').textContent = usage + '%';
  197. const barFill = document.getElementById('poolBarFill');
  198. barFill.style.width = usage + '%';
  199. barFill.textContent = usage + '%';
  200. // Color based on usage
  201. if (usage > 90) {
  202. barFill.style.background = 'linear-gradient(90deg, #e74c3c, #c0392b)';
  203. } else if (usage > 70) {
  204. barFill.style.background = 'linear-gradient(90deg, #f39c12, #e67e22)';
  205. } else {
  206. barFill.style.background = 'linear-gradient(90deg, #27ae60, #2ecc71)';
  207. }
  208. }
  209. } catch (error) {
  210. console.error('Failed to update pool stats:', error);
  211. }
  212. }
  213. // Helper functions
  214. function ipToBytes(ip) {
  215. return ip.split('.').map(Number);
  216. }
  217. function bytesToIP(bytes) {
  218. return (bytes[0] << 24) + (bytes[1] << 16) + (bytes[2] << 8) + bytes[3];
  219. }
  220. function formatTimeRemaining(seconds) {
  221. if (seconds <= 0) return '已过期';
  222. const days = Math.floor(seconds / 86400);
  223. const hours = Math.floor((seconds % 86400) / 3600);
  224. const minutes = Math.floor((seconds % 3600) / 60);
  225. if (days > 0) return `${days}天${hours}小时`;
  226. if (hours > 0) return `${hours}小时${minutes}分钟`;
  227. return `${minutes}分钟`;
  228. }
  229. // Load DHCP Config
  230. async function loadDHCPConfig() {
  231. try {
  232. const response = await fetch('/api/dhcp/config', {
  233. headers: { 'X-Session-ID': sessionId }
  234. });
  235. const data = await response.json();
  236. const cfg = data.config;
  237. if (cfg) {
  238. document.getElementById('dhcpEnabled').checked = cfg.enabled;
  239. document.getElementById('dhcpInterface').value = cfg.interface || '';
  240. document.getElementById('dhcpNetwork').value = cfg.network || '';
  241. document.getElementById('dhcpNetmask').value = cfg.netmask || '';
  242. document.getElementById('dhcpGateway').value = cfg.gateway || '';
  243. document.getElementById('dhcpDomain').value = cfg.domain_name || '';
  244. document.getElementById('dhcpPoolStart').value = cfg.ip_pool_start || '';
  245. document.getElementById('dhcpPoolEnd').value = cfg.ip_pool_end || '';
  246. document.getElementById('dhcpLeaseTime').value = cfg.lease_time || 86400;
  247. document.getElementById('dhcpDnsServers').value = (cfg.dns_servers || []).join(',');
  248. document.getElementById('dhcpExcludedIps').value = (cfg.excluded_ips || []).join(',');
  249. }
  250. loadBindings();
  251. } catch (error) {
  252. console.error('Failed to load DHCP config:', error);
  253. }
  254. }
  255. // Save DHCP Basic Config
  256. document.getElementById('dhcpBasicForm').addEventListener('submit', async (e) => {
  257. e.preventDefault();
  258. const config = {
  259. enabled: document.getElementById('dhcpEnabled').checked,
  260. interface: document.getElementById('dhcpInterface').value,
  261. network: document.getElementById('dhcpNetwork').value,
  262. netmask: document.getElementById('dhcpNetmask').value,
  263. gateway: document.getElementById('dhcpGateway').value,
  264. domain_name: document.getElementById('dhcpDomain').value
  265. };
  266. try {
  267. const response = await fetch('/api/dhcp/config', {
  268. method: 'PUT',
  269. headers: {
  270. 'X-Session-ID': sessionId,
  271. 'Content-Type': 'application/json'
  272. },
  273. body: JSON.stringify(config)
  274. });
  275. const contentType = response.headers.get('content-type');
  276. if (!contentType || !contentType.includes('application/json')) {
  277. const text = await response.text();
  278. console.error('Non-JSON response:', text);
  279. alert('服务器返回了非 JSON 格式响应,请查看控制台');
  280. return;
  281. }
  282. const data = await response.json();
  283. if (response.ok) {
  284. alert('基础配置已保存');
  285. } else {
  286. alert('保存失败:' + (data.error || '未知错误'));
  287. }
  288. } catch (error) {
  289. console.error('Save error:', error);
  290. alert('保存失败:' + error.message);
  291. }
  292. });
  293. // Save DHCP Pool Config
  294. document.getElementById('dhcpPoolForm').addEventListener('submit', async (e) => {
  295. e.preventDefault();
  296. const config = {
  297. ip_pool_start: document.getElementById('dhcpPoolStart').value,
  298. ip_pool_end: document.getElementById('dhcpPoolEnd').value,
  299. lease_time: parseInt(document.getElementById('dhcpLeaseTime').value)
  300. };
  301. try {
  302. const response = await fetch('/api/dhcp/config', {
  303. method: 'PUT',
  304. headers: {
  305. 'X-Session-ID': sessionId,
  306. 'Content-Type': 'application/json'
  307. },
  308. body: JSON.stringify(config)
  309. });
  310. const contentType = response.headers.get('content-type');
  311. if (!contentType || !contentType.includes('application/json')) {
  312. const text = await response.text();
  313. console.error('Non-JSON response:', text);
  314. alert('服务器返回了非 JSON 格式响应,请查看控制台');
  315. return;
  316. }
  317. const data = await response.json();
  318. if (response.ok) {
  319. alert('地址池配置已保存');
  320. } else {
  321. alert('保存失败:' + (data.error || '未知错误'));
  322. }
  323. } catch (error) {
  324. console.error('Save error:', error);
  325. alert('保存失败:' + error.message);
  326. }
  327. });
  328. // Save DHCP DNS Config
  329. document.getElementById('dhcpDnsForm').addEventListener('submit', async (e) => {
  330. e.preventDefault();
  331. const dnsServers = document.getElementById('dhcpDnsServers').value.split(',').map(s => s.trim()).filter(s => s);
  332. try {
  333. const response = await fetch('/api/dhcp/config', {
  334. method: 'PUT',
  335. headers: {
  336. 'X-Session-ID': sessionId,
  337. 'Content-Type': 'application/json'
  338. },
  339. body: JSON.stringify({ dns_servers: dnsServers })
  340. });
  341. const contentType = response.headers.get('content-type');
  342. if (!contentType || !contentType.includes('application/json')) {
  343. const text = await response.text();
  344. console.error('Non-JSON response:', text);
  345. alert('服务器返回了非 JSON 格式响应,请查看控制台');
  346. return;
  347. }
  348. const data = await response.json();
  349. if (response.ok) {
  350. alert('DNS 配置已保存');
  351. } else {
  352. alert('保存失败:' + (data.error || '未知错误'));
  353. }
  354. } catch (error) {
  355. console.error('Save error:', error);
  356. alert('保存失败:' + error.message);
  357. }
  358. });
  359. // Save DHCP Excluded IPs
  360. document.getElementById('dhcpExcludedForm').addEventListener('submit', async (e) => {
  361. e.preventDefault();
  362. const excludedIps = document.getElementById('dhcpExcludedIps').value.split(',').map(s => s.trim()).filter(s => s);
  363. try {
  364. const response = await fetch('/api/dhcp/config', {
  365. method: 'PUT',
  366. headers: {
  367. 'X-Session-ID': sessionId,
  368. 'Content-Type': 'application/json'
  369. },
  370. body: JSON.stringify({ excluded_ips: excludedIps })
  371. });
  372. const contentType = response.headers.get('content-type');
  373. if (!contentType || !contentType.includes('application/json')) {
  374. const text = await response.text();
  375. console.error('Non-JSON response:', text);
  376. alert('服务器返回了非 JSON 格式响应,请查看控制台');
  377. return;
  378. }
  379. const data = await response.json();
  380. if (response.ok) {
  381. alert('排除列表已保存');
  382. } else {
  383. alert('保存失败:' + (data.error || '未知错误'));
  384. }
  385. } catch (error) {
  386. console.error('Save error:', error);
  387. alert('保存失败:' + error.message);
  388. }
  389. });
  390. // Load Bindings
  391. async function loadBindings() {
  392. try {
  393. const response = await fetch('/api/dhcp/bindings', {
  394. headers: { 'X-Session-ID': sessionId }
  395. });
  396. const data = await response.json();
  397. const tbody = document.querySelector('#bindingsTable tbody');
  398. tbody.innerHTML = '';
  399. data.bindings.forEach(binding => {
  400. const row = tbody.insertRow();
  401. row.insertCell(0).textContent = binding.MAC;
  402. row.insertCell(1).textContent = binding.IP;
  403. row.insertCell(2).textContent = binding.Hostname || '-';
  404. row.insertCell(3).textContent = binding.Description || '-';
  405. const actionCell = row.insertCell(4);
  406. const deleteBtn = document.createElement('button');
  407. deleteBtn.textContent = '删除';
  408. deleteBtn.onclick = () => deleteBinding(binding.ID);
  409. actionCell.appendChild(deleteBtn);
  410. });
  411. } catch (error) {
  412. console.error('Failed to load bindings:', error);
  413. }
  414. }
  415. function showAddBindingForm() {
  416. // TODO: Implement add binding form
  417. alert('添加绑定功能开发中...');
  418. }
  419. async function deleteBinding(id) {
  420. if (!confirm('确定要删除这个绑定吗?')) return;
  421. try {
  422. const response = await fetch(`/api/dhcp/bindings/${id}`, {
  423. method: 'DELETE',
  424. headers: { 'X-Session-ID': sessionId }
  425. });
  426. if (response.ok) {
  427. loadBindings();
  428. } else {
  429. alert('删除失败');
  430. }
  431. } catch (error) {
  432. alert('删除失败:' + error.message);
  433. }
  434. }
  435. async function evictClient(mac, ip) {
  436. if (!confirm(`确定要剔除主机 ${mac} (${ip}) 吗?\n\n主机将被迫释放该 IP 并重新获取。`)) return;
  437. try {
  438. const response = await fetch(`/api/dhcp/leases/evict`, {
  439. method: 'POST',
  440. headers: { 'X-Session-ID': sessionId, 'Content-Type': 'application/json' },
  441. body: JSON.stringify({ mac, ip })
  442. });
  443. if (response.ok) {
  444. alert('主机已剔除,将重新获取 IP');
  445. loadClients();
  446. } else {
  447. const data = await response.json();
  448. alert('剔除失败:' + (data.error || '未知错误'));
  449. }
  450. } catch (error) {
  451. alert('剔除失败:' + error.message);
  452. }
  453. }
  454. // Load DNS Config
  455. async function loadDNSConfig() {
  456. try {
  457. const response = await fetch('/api/dns/config', {
  458. headers: { 'X-Session-ID': sessionId }
  459. });
  460. const data = await response.json();
  461. const cfg = data.config;
  462. if (cfg) {
  463. document.getElementById('dnsEnabled').checked = cfg.enabled;
  464. document.getElementById('dnsListenAddr').value = cfg.listen_addr || '0.0.0.0';
  465. document.getElementById('dnsListenPort').value = cfg.listen_port || 53;
  466. document.getElementById('dnsRecursion').checked = cfg.recursion !== false;
  467. document.getElementById('dnsUpstream').value = (cfg.upstream || []).join(',');
  468. }
  469. loadDNSRecords();
  470. loadZones();
  471. loadLogs();
  472. } catch (error) {
  473. console.error('Failed to load DNS config:', error);
  474. }
  475. }
  476. // Save DNS Basic Config
  477. document.getElementById('dnsBasicForm').addEventListener('submit', async (e) => {
  478. e.preventDefault();
  479. const config = {
  480. enabled: document.getElementById('dnsEnabled').checked,
  481. listen_addr: document.getElementById('dnsListenAddr').value,
  482. listen_port: parseInt(document.getElementById('dnsListenPort').value),
  483. recursion: document.getElementById('dnsRecursion').checked
  484. };
  485. try {
  486. const response = await fetch('/api/dns/config', {
  487. method: 'PUT',
  488. headers: {
  489. 'X-Session-ID': sessionId,
  490. 'Content-Type': 'application/json'
  491. },
  492. body: JSON.stringify(config)
  493. });
  494. const contentType = response.headers.get('content-type');
  495. if (!contentType || !contentType.includes('application/json')) {
  496. const text = await response.text();
  497. console.error('Non-JSON response:', text);
  498. alert('服务器返回了非 JSON 格式响应,请查看控制台');
  499. return;
  500. }
  501. const data = await response.json();
  502. if (response.ok) {
  503. alert('DNS 基础配置已保存');
  504. } else {
  505. alert('保存失败:' + (data.error || '未知错误'));
  506. }
  507. } catch (error) {
  508. console.error('Save error:', error);
  509. alert('保存失败:' + error.message);
  510. }
  511. });
  512. // Save DNS Upstream
  513. document.getElementById('dnsUpstreamForm').addEventListener('submit', async (e) => {
  514. e.preventDefault();
  515. const upstream = document.getElementById('dnsUpstream').value.split(',').map(s => s.trim()).filter(s => s);
  516. try {
  517. const response = await fetch('/api/dns/config', {
  518. method: 'PUT',
  519. headers: {
  520. 'X-Session-ID': sessionId,
  521. 'Content-Type': 'application/json'
  522. },
  523. body: JSON.stringify({ upstream })
  524. });
  525. const contentType = response.headers.get('content-type');
  526. if (!contentType || !contentType.includes('application/json')) {
  527. const text = await response.text();
  528. console.error('Non-JSON response:', text);
  529. alert('服务器返回了非 JSON 格式响应,请查看控制台');
  530. return;
  531. }
  532. const data = await response.json();
  533. if (response.ok) {
  534. alert('上游 DNS 已保存');
  535. } else {
  536. alert('保存失败:' + (data.error || '未知错误'));
  537. }
  538. } catch (error) {
  539. console.error('Save error:', error);
  540. alert('保存失败:' + error.message);
  541. }
  542. });
  543. // Load DNS Records
  544. async function loadDNSRecords() {
  545. try {
  546. const response = await fetch('/api/dns/records', {
  547. headers: { 'X-Session-ID': sessionId }
  548. });
  549. const data = await response.json();
  550. const tbody = document.querySelector('#recordsTable tbody');
  551. tbody.innerHTML = '';
  552. data.records.forEach(record => {
  553. const row = tbody.insertRow();
  554. row.insertCell(0).textContent = record.Name;
  555. row.insertCell(1).textContent = record.Type;
  556. row.insertCell(2).textContent = record.Value;
  557. row.insertCell(3).textContent = record.TTL;
  558. const actionCell = row.insertCell(4);
  559. const deleteBtn = document.createElement('button');
  560. deleteBtn.textContent = '删除';
  561. deleteBtn.onclick = () => deleteRecord(record.ID);
  562. actionCell.appendChild(deleteBtn);
  563. });
  564. } catch (error) {
  565. console.error('Failed to load records:', error);
  566. }
  567. }
  568. function showAddRecordForm() {
  569. // TODO: Implement add record form
  570. alert('添加记录功能开发中...');
  571. }
  572. async function deleteRecord(id) {
  573. if (!confirm('确定要删除这条记录吗?')) return;
  574. try {
  575. const response = await fetch(`/api/dns/records/${id}`, {
  576. method: 'DELETE',
  577. headers: { 'X-Session-ID': sessionId }
  578. });
  579. if (response.ok) {
  580. loadDNSRecords();
  581. } else {
  582. alert('删除失败');
  583. }
  584. } catch (error) {
  585. alert('删除失败:' + error.message);
  586. }
  587. }
  588. // Load Zones
  589. async function loadZones() {
  590. try {
  591. const response = await fetch('/api/dns/zones', {
  592. headers: { 'X-Session-ID': sessionId }
  593. });
  594. const data = await response.json();
  595. const tbody = document.querySelector('#zonesTable tbody');
  596. tbody.innerHTML = '';
  597. data.zones.forEach(zone => {
  598. const row = tbody.insertRow();
  599. row.insertCell(0).textContent = zone.name;
  600. row.insertCell(1).textContent = zone.type;
  601. row.insertCell(2).textContent = zone.record_count;
  602. const actionCell = row.insertCell(3);
  603. const deleteBtn = document.createElement('button');
  604. deleteBtn.textContent = '删除';
  605. deleteBtn.onclick = () => deleteZone(zone.ID);
  606. actionCell.appendChild(deleteBtn);
  607. });
  608. } catch (error) {
  609. console.error('Failed to load zones:', error);
  610. }
  611. }
  612. function showAddZoneForm() {
  613. // TODO: Implement add zone form
  614. alert('添加区域功能开发中...');
  615. }
  616. async function deleteZone(id) {
  617. if (!confirm('确定要删除这个区域吗?')) return;
  618. try {
  619. const response = await fetch(`/api/dns/zones/${id}`, {
  620. method: 'DELETE',
  621. headers: { 'X-Session-ID': sessionId }
  622. });
  623. if (response.ok) {
  624. loadZones();
  625. } else {
  626. alert('删除失败');
  627. }
  628. } catch (error) {
  629. alert('删除失败:' + error.message);
  630. }
  631. }
  632. // Load Logs
  633. async function loadLogs() {
  634. try {
  635. const response = await fetch('/api/dns/logs', {
  636. headers: { 'X-Session-ID': sessionId }
  637. });
  638. const data = await response.json();
  639. const tbody = document.querySelector('#logsTable tbody');
  640. tbody.innerHTML = '';
  641. data.logs.forEach(log => {
  642. const row = tbody.insertRow();
  643. row.insertCell(0).textContent = new Date(log.Timestamp * 1000).toLocaleString();
  644. row.insertCell(1).textContent = log.ClientIP;
  645. row.insertCell(2).textContent = log.QueryName;
  646. row.insertCell(3).textContent = log.QueryType;
  647. row.insertCell(4).textContent = log.Response || '-';
  648. });
  649. } catch (error) {
  650. console.error('Failed to load logs:', error);
  651. }
  652. }
  653. // Load System Info
  654. async function loadSystemInfo() {
  655. try {
  656. const response = await fetch('/api/config', {
  657. headers: { 'X-Session-ID': sessionId }
  658. });
  659. const data = await response.json();
  660. // TODO: Update system info display
  661. } catch (error) {
  662. console.error('Failed to load system info:', error);
  663. }
  664. }
  665. // Export Config
  666. async function exportConfig() {
  667. try {
  668. const response = await fetch('/api/config/export', {
  669. headers: { 'X-Session-ID': sessionId }
  670. });
  671. const blob = await response.blob();
  672. const url = window.URL.createObjectURL(blob);
  673. const a = document.createElement('a');
  674. a.href = url;
  675. a.download = 'dhcp-dns-config.json';
  676. a.click();
  677. window.URL.revokeObjectURL(url);
  678. } catch (error) {
  679. alert('导出失败:' + error.message);
  680. }
  681. }
  682. // Import Config
  683. async function importConfig() {
  684. const input = document.createElement('input');
  685. input.type = 'file';
  686. input.accept = '.json';
  687. input.onchange = async (e) => {
  688. const file = e.target.files[0];
  689. if (!file) return;
  690. const formData = new FormData();
  691. formData.append('config', file);
  692. try {
  693. const response = await fetch('/api/config/import', {
  694. method: 'POST',
  695. headers: { 'X-Session-ID': sessionId },
  696. body: formData
  697. });
  698. if (response.ok) {
  699. alert('配置已导入');
  700. } else {
  701. const data = await response.json();
  702. alert('导入失败:' + data.error);
  703. }
  704. } catch (error) {
  705. alert('导入失败:' + error.message);
  706. }
  707. };
  708. input.click();
  709. }
  710. // Restart Service
  711. async function restartService() {
  712. if (!confirm('确定要重启服务吗?服务将短暂中断。')) return;
  713. try {
  714. const response = await fetch('/api/service/restart', {
  715. method: 'POST',
  716. headers: { 'X-Session-ID': sessionId }
  717. });
  718. if (response.ok) {
  719. alert('服务重启请求已发送');
  720. } else {
  721. const data = await response.json();
  722. alert('重启失败:' + data.error);
  723. }
  724. } catch (error) {
  725. alert('重启失败:' + error.message);
  726. }
  727. }