devices.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  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" />
  6. <title>设备连接详情</title>
  7. <style>
  8. :root {
  9. --bg: #f6f8fa;
  10. --card: #ffffff;
  11. --border: #e5e7eb;
  12. --text: #111827;
  13. --muted: #6b7280;
  14. --primary: #1976d2;
  15. --secondary: #f9fafb;
  16. }
  17. * { box-sizing: border-box; }
  18. html, body { height: 100%; }
  19. body {
  20. margin: 0;
  21. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Helvetica Neue", Arial, "Noto Sans", "PingFang SC", "Microsoft YaHei", sans-serif;
  22. color: var(--text);
  23. background: var(--bg);
  24. }
  25. .page { max-width: 1100px; margin: 0 auto; padding: 20px; }
  26. .page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
  27. .page-header h1 { font-size: 22px; margin: 0; }
  28. .subtext { color: var(--muted); font-size: 13px; }
  29. .grid { display: grid; grid-template-columns: 1fr; gap: 16px; }
  30. @media (min-width: 768px) { .grid { grid-template-columns: 1fr; } }
  31. .card { background: var(--card); border: 1px solid var(--border); border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,.06); }
  32. .card-header { padding: 14px 16px; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; }
  33. .card-title { font-size: 16px; font-weight: 600; }
  34. .card-body { padding: 14px 16px; }
  35. .toolbar { display: flex; flex-wrap: wrap; gap: 12px; }
  36. .btn { display: inline-flex; align-items: center; gap: 8px; padding: 10px 14px; border-radius: 8px; text-decoration: none; border: 1px solid transparent; transition: background .2s ease, box-shadow .2s ease; }
  37. .btn-primary { background: var(--primary); color: #fff; }
  38. .btn-primary:hover { background: #1561ac; }
  39. .btn-secondary { background: var(--secondary); color: #111827; }
  40. .btn-secondary:hover { background: #e5e7eb; }
  41. .btn-player { background: #3b82f6; color: #fff; }
  42. .btn-player:hover { background: #2563eb; }
  43. .btn-player-no-audio { background: #10b981; color: #fff; }
  44. .btn-player-no-audio:hover { background: #0d9463; }
  45. .table-responsive { width: 100%; overflow-x: auto; }
  46. table { width: 100%; border-collapse: collapse; }
  47. thead th {
  48. background: #f9fafb;
  49. border-bottom: 1px solid var(--border);
  50. color: #374151;
  51. font-weight: 600;
  52. text-align: left;
  53. padding: 12px 14px;
  54. white-space: nowrap;
  55. }
  56. tbody td {
  57. border-top: 1px solid var(--border);
  58. padding: 12px 14px;
  59. line-height: 1.5;
  60. color: #1f2937;
  61. }
  62. tbody tr:nth-child(odd) { background: #fcfcfd; }
  63. tbody tr:hover { background: #eef5ff; }
  64. /* 轻量分隔与模块层次 */
  65. .section-note { margin-top: 4px; color: var(--muted); font-size: 12px; }
  66. /* 响应式优化 */
  67. @media (max-width: 600px) {
  68. .page { padding: 16px; }
  69. .page-header h1 { font-size: 18px; }
  70. thead th, tbody td { padding: 10px 12px; font-size: 14px; }
  71. .btn { width: 100%; justify-content: center; }
  72. }
  73. </style>
  74. </head>
  75. <body>
  76. <div class="page">
  77. <header class="page-header">
  78. <div>
  79. <h1>设备连接详情</h1>
  80. <div class="subtext">实时展示当前连接设备的关键信息</div>
  81. </div>
  82. </header>
  83. <div class="grid">
  84. <!-- 功能入口模块 -->
  85. <section class="card">
  86. <div class="card-header">
  87. <div class="card-title">功能入口</div>
  88. </div>
  89. <div class="card-body">
  90. <div class="toolbar">
  91. <a id="openPlayerBtn" class="btn btn-player" href="/player.html" target="_blank"
  92. rel="noopener">打开 WebSocket 播放器测试页面</a>
  93. <a id="openPlayerNoAudioBtn" class="btn btn-player-no-audio" href="/playerWhthoutAudio.html"
  94. target="_blank" rel="noopener">打开无音频播放器测试页面</a>
  95. <a id="openJessibucaBtn" class="btn btn-secondary" href="/jessibuca/demo.html"
  96. target="_blank" rel="noopener">打开 Jessibuca 播放器测试页面</a>
  97. <a id="openOfflinePlayerBtn" class="btn btn-secondary" href="/offline_player.html"
  98. target="_blank" rel="noopener">打开离线播放器(开发参考)</a>
  99. <a id="openOfflinePlayerNoAudioBtn" class="btn btn-secondary"
  100. href="/offline_player.html?noAudio=true" target="_blank" rel="noopener">打开离线无音频播放器(开发参考)</a>
  101. </div>
  102. <div id="popupTip" class="section-note" style="display:none;">
  103. 浏览器阻止了弹出窗口。请允许弹窗或点击以下备用链接:
  104. <a href="/player.html" target="_blank" rel="noopener">播放器</a> |
  105. <a href="/jessibuca/demo.html" target="_blank" rel="noopener">Jessibuca 播放器</a> |
  106. <a href="/playerWhthoutAudio.html" target="_blank" rel="noopener">无音频播放器</a> |
  107. <a href="/offline_player.html?noAudio=true" target="_blank" rel="noopener">离线无音频播放器</a>
  108. </div>
  109. </div>
  110. </section>
  111. <!-- 设备列表模块 -->
  112. <section class="card">
  113. <div class="card-header">
  114. <div class="card-title">设备列表</div>
  115. </div>
  116. <div class="card-body">
  117. <div class="table-responsive">
  118. <table>
  119. <thead>
  120. <tr>
  121. <th>远程地址</th>
  122. <th>SIM卡号</th>
  123. <th>逻辑通道号</th>
  124. <th>前缀</th>
  125. <th>连接时间</th>
  126. <th>最后活跃时间</th>
  127. <th>操作</th>
  128. </tr>
  129. </thead>
  130. <tbody id="deviceTableBody">
  131. <!-- 数据将通过JavaScript填充 -->
  132. </tbody>
  133. </table>
  134. </div>
  135. </div>
  136. </section>
  137. </div>
  138. </div>
  139. <script>
  140. function loadDevices() {
  141. fetch('/devices')
  142. .then(response => response.json())
  143. .then(devices => {
  144. const tbody = document.getElementById('deviceTableBody');
  145. tbody.innerHTML = '';
  146. devices.forEach(device => {
  147. const row = document.createElement('tr');
  148. const addressCell = document.createElement('td');
  149. addressCell.textContent = device.remoteAddress;
  150. row.appendChild(addressCell);
  151. const simCell = document.createElement('td');
  152. simCell.textContent = device.simCardNumber || 'N/A';
  153. row.appendChild(simCell);
  154. const channelCell = document.createElement('td');
  155. channelCell.textContent = device.logicChannelNumber;
  156. row.appendChild(channelCell);
  157. const prefixCell = document.createElement('td');
  158. prefixCell.textContent = device.prefix || '';
  159. row.appendChild(prefixCell);
  160. const connectTimeCell = document.createElement('td');
  161. connectTimeCell.textContent = new Date(device.connectTime).toLocaleString();
  162. row.appendChild(connectTimeCell);
  163. const activeTimeCell = document.createElement('td');
  164. activeTimeCell.textContent = new Date(device.lastActiveTime).toLocaleString();
  165. row.appendChild(activeTimeCell);
  166. // 添加操作按钮列
  167. const actionCell = document.createElement('td');
  168. actionCell.style.whiteSpace = 'nowrap';
  169. actionCell.style.display = 'flex';
  170. actionCell.style.gap = '6px';
  171. actionCell.style.flexWrap = 'wrap';
  172. // 播放按钮
  173. const playBtn = document.createElement('button');
  174. playBtn.textContent = '播放';
  175. playBtn.className = 'btn btn-player';
  176. playBtn.onclick = function () {
  177. // 构建带参数的URL(包含 prefix)
  178. const url = `/player.html?sim=${encodeURIComponent(device.simCardNumber || '')}&channel=${encodeURIComponent(device.logicChannelNumber)}&prefix=${encodeURIComponent(device.prefix || '')}`;
  179. // 使用bindOpenInPopup函数打开播放器
  180. openPlayerInPopup(url, 'PlayerWindow');
  181. };
  182. actionCell.appendChild(playBtn);
  183. // 无音频播放按钮(偏绿色)
  184. const playNoAudioBtn = document.createElement('button');
  185. playNoAudioBtn.textContent = '无音频播放';
  186. playNoAudioBtn.className = 'btn btn-player-no-audio';
  187. playNoAudioBtn.onclick = function () {
  188. // 构建带参数的URL(包含 prefix)
  189. const url = `/playerWhthoutAudio.html?sim=${encodeURIComponent(device.simCardNumber || '')}&channel=${encodeURIComponent(device.logicChannelNumber)}&prefix=${encodeURIComponent(device.prefix || '')}`;
  190. // 使用bindOpenInPopup函数打开无音频播放器
  191. openPlayerInPopup(url, 'PlayerWindowNoAudio');
  192. };
  193. actionCell.appendChild(playNoAudioBtn);
  194. row.appendChild(actionCell);
  195. tbody.appendChild(row);
  196. });
  197. })
  198. .catch(error => console.error('Error:', error));
  199. }
  200. // 初始加载
  201. loadDevices();
  202. // 每5秒刷新一次
  203. setInterval(loadDevices, 5000);
  204. // 打开播放器(新窗口)通用函数
  205. function openPlayerInPopup(targetUrl, windowName) {
  206. const availW = Math.max(800, (window.screen && window.screen.availWidth) ? window.screen.availWidth : 1200);
  207. const availH = Math.max(600, (window.screen && window.screen.availHeight) ? window.screen.availHeight : 800);
  208. const width = Math.min(1200, Math.floor(availW * 0.9));
  209. const height = Math.min(800, Math.floor(availH * 0.9));
  210. const left = Math.max(0, Math.floor((availW - width) / 2));
  211. const top = Math.max(0, Math.floor((availH - height) / 2));
  212. const features = `popup=yes,width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes`;
  213. const newWin = window.open(targetUrl, windowName, features);
  214. if (newWin && typeof newWin.focus === 'function') {
  215. try { newWin.focus(); } catch (_) { }
  216. } else {
  217. // 如果弹窗被阻止,显示提示并直接跳转
  218. alert('浏览器阻止了弹出窗口,将直接跳转');
  219. window.open(targetUrl, '_blank');
  220. }
  221. }
  222. // 打开播放器(新窗口)通用绑定
  223. function bindOpenInPopup(btnId, targetUrl, windowName, tipId) {
  224. const btn = document.getElementById(btnId);
  225. const tip = tipId ? document.getElementById(tipId) : null;
  226. if (!btn) return;
  227. btn.addEventListener('click', function (e) {
  228. e.preventDefault();
  229. const availW = Math.max(800, (window.screen && window.screen.availWidth) ? window.screen.availWidth : 1200);
  230. const availH = Math.max(600, (window.screen && window.screen.availHeight) ? window.screen.availHeight : 800);
  231. const width = Math.min(1200, Math.floor(availW * 0.9));
  232. const height = Math.min(800, Math.floor(availH * 0.9));
  233. const left = Math.max(0, Math.floor((availW - width) / 2));
  234. const top = Math.max(0, Math.floor((availH - height) / 2));
  235. const features = `popup=yes,width=${width},height=${height},left=${left},top=${top},menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes`;
  236. const newWin = window.open(targetUrl, windowName, features);
  237. if (newWin && typeof newWin.focus === 'function') {
  238. try { newWin.focus(); } catch (_) {}
  239. } else {
  240. if (tip) tip.style.display = 'block';
  241. }
  242. });
  243. }
  244. bindOpenInPopup('openPlayerBtn', '/player.html', 'PlayerWindow', 'popupTip');
  245. bindOpenInPopup('openPlayerNoAudioBtn', '/playerWhthoutAudio.html', 'PlayerWindowNoAudio', 'popupTip');
  246. bindOpenInPopup('openJessibucaBtn', '/jessibuca/demo.html', 'JessibucaPlayerWindow', 'popupTip');
  247. bindOpenInPopup('openOfflinePlayerBtn', '/offline_player.html', 'OfflinePlayerWindow', 'popupTip');
  248. bindOpenInPopup('openOfflinePlayerNoAudioBtn', '/offline_player.html?noAudio=true', 'OfflinePlayerWindowNoAudio', 'popupTip');
  249. </script>
  250. </body>
  251. </html>