app.js 27 KB

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