index.bak5.html 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>车牌识别监控系统</title>
  7. <style>
  8. * { box-sizing: border-box; margin: 0; padding: 0; }
  9. body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; }
  10. .header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; text-align: center; }
  11. .header h1 { font-size: 24px; margin-bottom: 5px; }
  12. .container { display: flex; gap: 20px; padding: 20px; max-width: 1400px; margin: 0 auto; }
  13. .panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
  14. .report-panel { width: 320px; flex-shrink: 0; }
  15. .list-panel { flex: 1; min-width: 0; }
  16. .panel h2 { font-size: 18px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 10px; }
  17. .metric { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; }
  18. .metric:last-child { border-bottom: none; }
  19. .metric-label { color: #666; font-size: 14px; }
  20. .metric-value { font-weight: bold; color: #333; font-size: 14px; }
  21. .metric-value.success { color: #52c41a; }
  22. .metric-value.warning { color: #faad14; }
  23. .metric-value.error { color: #f5222d; }
  24. .search-box { display: flex; gap: 10px; margin-bottom: 15px; }
  25. .search-box input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; }
  26. .search-box button { padding: 10px 20px; background: #667eea; color: white; border: none; border-radius: 5px; cursor: pointer; font-size: 14px; }
  27. .search-box button:hover { background: #764ba2; }
  28. .table-container { overflow-x: auto; }
  29. table { width: 100%; border-collapse: collapse; font-size: 13px; }
  30. th, td { padding: 10px 8px; text-align: left; border-bottom: 1px solid #eee; }
  31. th { background: #f8f9fa; font-weight: 600; color: #333; white-space: nowrap; }
  32. tr:hover { background: #f8f9fa; }
  33. .status { padding: 3px 8px; border-radius: 3px; font-size: 12px; display: inline-block; }
  34. .status-success { background: #d9f7be; color: #52c41a; }
  35. .status-failed { background: #fff1f0; color: #f5222d; }
  36. .status-retry { background: #fff7e6; color: #fa8c16; }
  37. .status-pending { background: #f0f5ff; color: #1890ff; }
  38. .btn-retry { padding: 3px 8px; background: #1890ff; color: white; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; }
  39. .btn-retry:hover { background: #40a9ff; }
  40. .btn-retry:disabled { background: #d9d9d9; cursor: not-allowed; }
  41. .photo-thumb { width: 60px; height: 40px; object-fit: cover; border-radius: 3px; cursor: pointer; border: 1px solid #ddd; }
  42. .photo-modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 1000; justify-content: center; align-items: center; }
  43. .photo-modal.active { display: flex; }
  44. .photo-modal img { max-width: 90%; max-height: 90%; border-radius: 5px; }
  45. .photo-modal-close { position: absolute; top: 20px; right: 20px; color: white; font-size: 30px; cursor: pointer; }
  46. .pagination { display: flex; justify-content: center; align-items: center; gap: 10px; margin-top: 15px; }
  47. .pagination button { padding: 8px 15px; border: 1px solid #ddd; background: white; border-radius: 5px; cursor: pointer; }
  48. .pagination button:hover { background: #f0f5ff; }
  49. .pagination button:disabled { background: #f5f5f5; color: #d9d9d9; cursor: not-allowed; }
  50. .pagination .info { color: #666; font-size: 14px; }
  51. .refresh-info { text-align: center; margin-top: 10px; color: #999; font-size: 12px; }
  52. .loading { text-align: center; padding: 20px; color: #666; }
  53. /* 用户信息栏 */
  54. .user-bar { background: rgba(255,255,255,0.15); padding: 8px 20px; display: flex; justify-content: space-between; align-items: center; font-size: 13px; }
  55. .user-bar a { color: rgba(255,255,255,0.85); text-decoration: none; margin-left: 12px; padding: 3px 8px; border-radius: 4px; transition: background 0.2s; }
  56. .user-bar a:hover { background: rgba(255,255,255,0.2); color: white; }
  57. .user-bar .user-info { color: white; font-weight: 500; }
  58. .user-bar .role-badge { background: rgba(255,215,0,0.3); color: #ffd700; padding: 2px 8px; border-radius: 10px; font-size: 11px; margin-left: 6px; }
  59. .user-bar .logout-btn { color: rgba(255,255,255,0.7); cursor: pointer; border: 1px solid rgba(255,255,255,0.3); padding: 2px 10px; border-radius: 4px; background: transparent; font-size: 12px; }
  60. .user-bar .logout-btn:hover { background: rgba(255,255,255,0.15); color: white; }
  61. /* 连接状态面板 */
  62. .conn-panel { background: white; border-radius: 10px; padding: 20px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin: 0 20px 0 20px; max-width: 1360px; }
  63. .conn-panel h2 { font-size: 16px; color: #333; margin-bottom: 15px; border-bottom: 2px solid #667eea; padding-bottom: 8px; }
  64. .conn-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
  65. .conn-item { display: flex; align-items: center; gap: 10px; padding: 10px 14px; background: #f8f9fa; border-radius: 8px; border: 1px solid #e9ecef; }
  66. .conn-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
  67. .conn-dot.online { background: #52c41a; box-shadow: 0 0 6px rgba(82,196,26,0.4); }
  68. .conn-dot.offline { background: #f5222d; box-shadow: 0 0 6px rgba(245,34,46,0.4); }
  69. .conn-dot.warn { background: #faad14; box-shadow: 0 0 6px rgba(250,173,20,0.4); }
  70. .conn-dot.disabled { background: #d9d9d9; }
  71. .conn-label { font-size: 13px; color: #333; font-weight: 500; }
  72. .conn-detail { font-size: 11px; color: #999; margin-top: 2px; }
  73. @media (max-width: 900px) {
  74. .container { flex-direction: column; }
  75. .report-panel { width: 100%; }
  76. }
  77. </style>
  78. </head>
  79. <body>
  80. <div class="header">
  81. <h1>车牌识别监控系统</h1>
  82. <p>Plate Recognition Monitoring System v43.2-fix24</p>
  83. <div class="user-bar" id="main-nav">
  84. <div id="nav-links">
  85. <a href="/">🏠 首页</a>
  86. <a href="/video">🎥 视频</a>
  87. <a href="/weight">⚖️ 称重</a>
  88. <a href="/config" id="nav-config" style="display:none;">⚙️ 配置</a>
  89. <a href="/locks" id="nav-locks" style="display:none;">🔒 锁定</a>
  90. <a href="/system" id="nav-system" style="display:none;">📊 监控</a>
  91. <a href="/frpc" id="nav-frpc" style="display:none;">🔗 隧道</a>
  92. <a href="/ssh-keys" id="nav-ssh" style="display:none;">🔑 SSH</a>
  93. <a href="/logs" id="nav-logs" style="display:none;">📋 日志</a>
  94. </div>
  95. <div id="user-info-area">
  96. <span class="user-info" id="user-display">未登录</span>
  97. </div>
  98. </div>
  99. </div>
  100. <!-- 连接状态面板 -->
  101. <div class="conn-panel" style="margin:20px auto;max-width:1360px;padding:0 20px;">
  102. <div style="background:white;border-radius:10px;padding:20px;box-shadow:0 2px 10px rgba(0,0,0,0.1);">
  103. <h2 style="font-size:16px;color:#333;margin-bottom:15px;border-bottom:2px solid #667eea;padding-bottom:8px;">🔌 连接状态</h2>
  104. <div class="conn-grid" id="connections-grid">
  105. <div class="conn-item"><div class="conn-dot disabled"></div><div><div class="conn-label">加载中...</div></div></div>
  106. </div>
  107. </div>
  108. </div>
  109. <div class="container">
  110. <div class="panel report-panel">
  111. <h2>监控报告</h2>
  112. <div id="monitor-report">
  113. <div class="loading">加载中...</div>
  114. </div>
  115. </div>
  116. <div class="panel list-panel">
  117. <h2>实时消息列表</h2>
  118. <div class="search-box">
  119. <input type="text" id="search-input" placeholder="搜索车牌号/联单编号/日期时间">
  120. <button onclick="searchMessages(1)">搜索</button>
  121. </div>
  122. <div class="table-container">
  123. <table>
  124. <thead>
  125. <tr>
  126. <th>编号</th>
  127. <th>日期时间</th>
  128. <th>车牌号</th>
  129. <th>联单编号</th>
  130. <th>类型</th>
  131. <th>低位照片</th>
  132. <th>高位照片</th>
  133. <th>上传状态</th>
  134. <th>飞书状态</th>
  135. <th>操作</th>
  136. </tr>
  137. </thead>
  138. <tbody id="messages-body">
  139. <tr><td colspan="10" class="loading">加载中...</td></tr>
  140. </tbody>
  141. </table>
  142. </div>
  143. <div class="pagination" id="pagination"></div>
  144. <div class="refresh-info">数据自动刷新间隔: 30秒</div>
  145. </div>
  146. </div>
  147. <div class="photo-modal" id="photo-modal" onclick="closePhotoModal()">
  148. <span class="photo-modal-close">&times;</span>
  149. <img id="photo-modal-img" src="" alt="照片">
  150. </div>
  151. <script>
  152. let currentPage = 1;
  153. let currentKeyword = '';
  154. function getCookie(n){let m=document.cookie.match(new RegExp("(^| )"+n+"=([^;]+)"));return m?m[2]:"";}
  155. function loadMonitorReport() {
  156. fetch('/api/monitor')
  157. .then(r => r.json())
  158. .then(data => {
  159. document.getElementById('monitor-report').innerHTML = `
  160. <div class="metric">
  161. <span class="metric-label">createBill成功率</span>
  162. <span class="metric-value ${data.createbill_success_rate >= 90 ? 'success' : data.createbill_success_rate >= 70 ? 'warning' : 'error'}">${data.createbill_success_rate.toFixed(1)}%</span>
  163. </div>
  164. <div class="metric">
  165. <span class="metric-label">createBill总调用</span>
  166. <span class="metric-value">${data.createbill_total} / 成功 ${data.createbill_success}</span>
  167. </div>
  168. <div class="metric">
  169. <span class="metric-label">照片平均耗时</span>
  170. <span class="metric-value">${data.avg_upload_time}ms</span>
  171. </div>
  172. <div class="metric">
  173. <span class="metric-label">TIME_WINDOW命中</span>
  174. <span class="metric-value">${data.time_window_hits}</span>
  175. </div>
  176. <div class="metric">
  177. <span class="metric-label">进站缓存命中率</span>
  178. <span class="metric-value">${data.in_cache_hit_rate.toFixed(1)}%</span>
  179. </div>
  180. <div class="metric">
  181. <span class="metric-label">出站缓存命中率</span>
  182. <span class="metric-value">${data.out_cache_hit_rate.toFixed(1)}%</span>
  183. </div>
  184. <div class="metric">
  185. <span class="metric-label">进站统计</span>
  186. <span class="metric-value">${data.in_station_count}</span>
  187. </div>
  188. <div class="metric">
  189. <span class="metric-label">出站统计</span>
  190. <span class="metric-value">${data.out_station_count}</span>
  191. </div>
  192. <div class="metric">
  193. <span class="metric-label">总记录数</span>
  194. <span class="metric-value">${data.total_records}</span>
  195. </div>
  196. <div class="metric">
  197. <span class="metric-label">失败记录</span>
  198. <span class="metric-value ${data.failed_records > 0 ? 'error' : ''}">${data.failed_records}</span>
  199. </div>
  200. <div class="metric">
  201. <span class="metric-label">飞书成功数</span>
  202. <span class="metric-value">${data.feishu_success_count}</span>
  203. </div>
  204. <div class="metric">
  205. <span class="metric-label">系统运行时间</span>
  206. <span class="metric-value">${data.uptime}</span>
  207. </div>
  208. <div class="metric">
  209. <span class="metric-label">网络超时</span>
  210. <span class="metric-value ${data.network_timeouts > 10 ? 'warning' : ''}">${data.network_timeouts}</span>
  211. </div>
  212. <div class="metric">
  213. <span class="metric-label">服务器错误</span>
  214. <span class="metric-value ${data.server_errors > 5 ? 'warning' : ''}">${data.server_errors}</span>
  215. </div>
  216. <div class="metric">
  217. <span class="metric-label">总上传错误</span>
  218. <span class="metric-value ${data.upload_errors > 0 ? 'error' : ''}">${data.upload_errors}</span>
  219. </div>
  220. <div class="metric">
  221. <span class="metric-label">交替锁定</span>
  222. <span class="metric-value ${data.alternating_merge_enabled ? 'success' : ''}">${data.alternating_merge_enabled ? '启用' : '禁用'}</span>
  223. </div>
  224. <div class="metric">
  225. <span class="metric-label">紧急告警抑制</span>
  226. <span class="metric-value ${data.emergency_alert_suppressed ? 'warning' : 'success'}">${data.emergency_alert_suppressed ? '已抑制' : '正常'}</span>
  227. </div>
  228. <div class="metric">
  229. <span class="metric-label">交替锁定间隔</span>
  230. <span class="metric-value">${data.in_out_interval_sec}秒</span>
  231. </div>
  232. `;
  233. })
  234. .catch(e => console.error('监控报告加载失败:', e));
  235. }
  236. function loadMessages(page, keyword) {
  237. let url = '/api/messages?page=' + page;
  238. if (keyword) url += '&keyword=' + encodeURIComponent(keyword);
  239. fetch(url)
  240. .then(r => r.json())
  241. .then(data => {
  242. let html = '';
  243. if (data.records.length === 0) {
  244. html = '<tr><td colspan="10" style="text-align:center;padding:30px;color:#999;">暂无数据</td></tr>';
  245. } else {
  246. data.records.forEach(rec => {
  247. const stationType = rec.station_type === 1 ? '进站' : '出站';
  248. const uploadStatus = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? '成功' : '失败';
  249. const uploadClass = rec.lo_upload_status === 1 && rec.hi_upload_status === 1 ? 'status-success' : 'status-failed';
  250. const feishuClass = rec.feishu_status === 1 ? 'status-success' : rec.feishu_status === 2 ? 'status-failed' : 'status-pending';
  251. const feishuText = rec.feishu_status === 1 ? '已发送' : rec.feishu_status === 2 ? '失败' : '未发送';
  252. const retryDisabled = rec.retry_count >= 5 ? 'disabled' : '';
  253. html += '<tr>';
  254. html += '<td>' + rec.id + '</td>';
  255. html += '<td>' + rec.create_time + '</td>';
  256. html += '<td><b>' + rec.plate_number + '</b></td>';
  257. html += '<td style="font-family:monospace;font-size:12px;">' + rec.tb_num + '</td>';
  258. html += '<td>' + stationType + '</td>';
  259. html += '<td>' + (rec.lo_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.lo_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
  260. html += '<td>' + (rec.hi_photo_path ? '<img class="photo-thumb" src="/photo?path=' + encodeURIComponent(rec.hi_photo_path) + '" onclick="showPhoto(this.src)">' : '-') + '</td>';
  261. html += '<td><span class="status ' + uploadClass + '">' + uploadStatus + '</span></td>';
  262. html += '<td><span class="status ' + feishuClass + '">' + feishuText + '</span></td>';
  263. html += '<td><button class="btn-retry" onclick="retryRecord(' + rec.id + ')" ' + retryDisabled + '>重试(' + rec.retry_count + '/5)</button></td>';
  264. html += '</tr>';
  265. });
  266. }
  267. document.getElementById('messages-body').innerHTML = html;
  268. // 分页
  269. let pageHtml = '<button onclick="searchMessages(' + (data.page - 1) + ')" ' + (data.page <= 1 ? 'disabled' : '') + '>上一页</button>';
  270. pageHtml += '<span class="info">第 ' + data.page + ' 页</span>';
  271. pageHtml += '<button onclick="searchMessages(' + (data.page + 1) + ')">下一页</button>';
  272. document.getElementById('pagination').innerHTML = pageHtml;
  273. })
  274. .catch(e => {
  275. console.error('消息列表加载失败:', e);
  276. document.getElementById('messages-body').innerHTML = '<tr><td colspan="10" style="text-align:center;color:#f5222d;">加载失败,请刷新页面</td></tr>';
  277. });
  278. }
  279. function searchMessages(page) {
  280. currentPage = page;
  281. currentKeyword = document.getElementById('search-input').value;
  282. loadMessages(currentPage, currentKeyword);
  283. }
  284. function retryRecord(id) {
  285. if (!confirm('确定要重试此记录吗?')) return;
  286. var btn = event.target;
  287. btn.disabled = true;
  288. btn.textContent = '重试中...';
  289. fetch('/api/retry', {
  290. method: 'POST',
  291. body: 'id=' + id,
  292. headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-CSRF-Token': getCookie('csrf_token') }
  293. })
  294. .then(r => r.json())
  295. .then(data => {
  296. btn.disabled = false;
  297. btn.textContent = '重试';
  298. if (data.success) {
  299. var msg = '✅ 重试成功';
  300. if (data.tb_num) msg += '\n联单编号: ' + data.tb_num;
  301. if (data.uploaded_photos > 0) msg += '\n上传照片: ' + data.uploaded_photos + '张';
  302. alert(msg);
  303. } else {
  304. alert('❌ 重试失败: ' + (data.message || '未知错误'));
  305. }
  306. loadMessages(currentPage, currentKeyword);
  307. loadMonitorReport();
  308. })
  309. .catch(e => {
  310. btn.disabled = false;
  311. btn.textContent = '重试';
  312. alert('重试请求失败: ' + e);
  313. });
  314. }
  315. function showPhoto(src) {
  316. document.getElementById('photo-modal-img').src = src;
  317. document.getElementById('photo-modal').classList.add('active');
  318. }
  319. function closePhotoModal() {
  320. document.getElementById('photo-modal').classList.remove('active');
  321. }
  322. // ==================== 用户信息显示 + 导航栏权限控制 ====================
  323. function loadUserInfo() {
  324. fetch('/api/auth/status')
  325. .then(r => r.json())
  326. .then(data => {
  327. const area = document.getElementById('user-info-area');
  328. if (data.authenticated) {
  329. const roles = {0: '普通用户', 1: '管理员', 2: '超级管理员'};
  330. const roleName = roles[data.role] || '未知角色';
  331. const roleClass = data.role === 2 ? 'style="background:rgba(255,215,0,0.3);color:#ffd700;"' :
  332. data.role === 1 ? 'style="background:rgba(64,169,255,0.3);color:#40a9ff;"' : '';
  333. area.innerHTML = '<span class="user-info">👤 ' + data.username + '</span>' +
  334. '<span class="role-badge" ' + roleClass + '>' + roleName + '</span>' +
  335. ' <button class="logout-btn" onclick="logout()">退出</button>';
  336. // 根据角色显示导航栏项目
  337. // role=0: 首页、视频、称重(已默认显示)
  338. // role=1: + 配置、锁定
  339. // role=2: + 监控、隧道、SSH、日志
  340. if (data.role >= 1) {
  341. show('nav-config');
  342. show('nav-locks');
  343. }
  344. if (data.role >= 2) {
  345. show('nav-system');
  346. show('nav-frpc');
  347. show('nav-ssh');
  348. show('nav-logs');
  349. }
  350. } else {
  351. area.innerHTML = '<a href="/login" style="color:white;text-decoration:underline;">登录</a>';
  352. }
  353. })
  354. .catch(e => console.error('用户信息加载失败:', e));
  355. }
  356. function show(id) {
  357. var el = document.getElementById(id);
  358. if (el) el.style.display = '';
  359. }
  360. function logout() {
  361. fetch('/api/auth/logout', {method: 'POST', headers: {'X-CSRF-Token': getCookie('csrf_token')}})
  362. .then(() => { window.location.href = '/login'; })
  363. .catch(() => { window.location.href = '/login'; });
  364. }
  365. // ==================== 连接状态显示 ====================
  366. function loadConnections() {
  367. fetch('/api/connections')
  368. .then(r => r.json())
  369. .then(data => {
  370. let html = '';
  371. // RTSP 摄像头
  372. if (data.rtsp) {
  373. data.rtsp.forEach(cam => {
  374. let dot = 'disabled';
  375. let detail = '未配置';
  376. if (cam.configured && cam.running) { dot = 'online'; detail = '采集中'; }
  377. else if (cam.configured && !cam.running) { dot = 'offline'; detail = '已断开'; }
  378. else { dot = 'disabled'; detail = '未配置'; }
  379. html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
  380. '<div><div class="conn-label">📷 ' + cam.name + '</div>' +
  381. '<div class="conn-detail">' + detail + '</div></div></div>';
  382. });
  383. }
  384. // MQTT
  385. if (data.mqtt) {
  386. let dot = 'disabled', detail = '未启用';
  387. if (data.mqtt.enabled) {
  388. if (data.mqtt.status === 1) { dot = 'online'; detail = data.mqtt.state + ' (' + data.mqtt.host + ')'; }
  389. else if (data.mqtt.status === 2) { dot = 'warn'; detail = data.mqtt.state + '...'; }
  390. else { dot = 'offline'; detail = data.mqtt.state; }
  391. }
  392. html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
  393. '<div><div class="conn-label">📨 MQTT</div>' +
  394. '<div class="conn-detail">' + detail + '</div></div></div>';
  395. }
  396. // 数据库
  397. if (data.database) {
  398. let dot = data.database.connected ? 'online' : 'offline';
  399. let detail = data.database.connected ? 'SQLite 已连接' : '未连接';
  400. html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
  401. '<div><div class="conn-label">🗄️ 数据库</div>' +
  402. '<div class="conn-detail">' + detail + '</div></div></div>';
  403. }
  404. // frpc
  405. if (data.frpc) {
  406. let dot = data.frpc.running ? 'online' : 'offline';
  407. let detail = data.frpc.running ? (data.frpc.tunnels + ' 条隧道活跃') : '未运行';
  408. html += '<div class="conn-item"><div class="conn-dot ' + dot + '"></div>' +
  409. '<div><div class="conn-label">🔗 frpc 隧道</div>' +
  410. '<div class="conn-detail">' + detail + (data.frpc.version ? ' v' + data.frpc.version : '') + '</div></div></div>';
  411. }
  412. // Web 服务器
  413. if (data.web) {
  414. let detail = '端口 ' + data.web.port + (data.web.https ? ' (HTTPS)' : '');
  415. html += '<div class="conn-item"><div class="conn-dot online"></div>' +
  416. '<div><div class="conn-label">🌐 Web 服务</div>' +
  417. '<div class="conn-detail">' + detail + '</div></div></div>';
  418. }
  419. document.getElementById('connections-grid').innerHTML = html;
  420. })
  421. .catch(e => {
  422. console.error('连接状态加载失败:', e);
  423. document.getElementById('connections-grid').innerHTML =
  424. '<div class="conn-item"><div class="conn-dot offline"></div><div><div class="conn-label">加载失败</div></div></div>';
  425. });
  426. }
  427. // 初始化
  428. loadMonitorReport();
  429. loadMessages(1, '');
  430. loadUserInfo();
  431. loadConnections();
  432. // 自动刷新
  433. setInterval(() => {
  434. loadMonitorReport();
  435. if (currentPage === 1 && !currentKeyword) {
  436. loadMessages(1, '');
  437. }
  438. }, 30000);
  439. // 回车搜索
  440. document.getElementById('search-input').addEventListener('keypress', e => {
  441. if (e.key === 'Enter') searchMessages(1);
  442. });
  443. </script>
  444. </body>
  445. </html>