config.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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. <link rel="icon" href="/logo.png" type="image/png">
  8. <style>
  9. *{margin:0;padding:0;box-sizing:border-box;border-radius:0 !important}
  10. body{font-family:system-ui,sans-serif;background:#12192e;color:#eee;overflow:hidden}
  11. .app{display:flex;height:100vh}
  12. .sidebar{width:200px;background:#0e1426;border-right:1px solid #0f3460;display:flex;flex-direction:column;flex-shrink:0}
  13. .logo{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #0f3460}
  14. .logo img{height:28px;flex-shrink:0}
  15. .logo .t{font-size:14px;color:#e94560;font-weight:bold;white-space:nowrap}
  16. .nav{padding-top:8px;flex:1}
  17. .nav-item{display:flex;align-items:center;gap:12px;padding:13px 18px;color:#8892a8;cursor:pointer;font-size:14px;border-left:3px solid transparent;white-space:nowrap;user-select:none}
  18. .nav-item:hover{background:#182242;color:#ccc}
  19. .nav-item.active{color:#e94560;border-left-color:#e94560;background:#182242;font-weight:bold}
  20. .nav-item .ico{width:20px;text-align:center;flex-shrink:0;font-weight:bold}
  21. .nav-label{transition:none}
  22. .side-foot{border-top:1px solid #0f3460;padding:12px 14px;display:flex;flex-direction:column;gap:10px}
  23. .collapse-btn{background:none;border:1px solid #0f3460;color:#8892a8;padding:5px;cursor:pointer;font-size:12px;width:100%}
  24. .collapse-btn:hover{color:#e94560;border-color:#e94560}
  25. .content{flex:1;overflow:auto;padding:16px}
  26. .card{background:#141d36;border:1px solid #0f3460;padding:16px;margin-bottom:16px}
  27. .card h3{color:#e94560;margin-bottom:12px;font-size:15px}
  28. .form-group{margin-bottom:12px}
  29. .form-group label{display:block;font-size:13px;color:#aaa;margin-bottom:4px}
  30. .form-group input,.form-group select{width:100%;padding:8px;border:1px solid #0f3460;background:#0e1426;color:#eee;font-size:13px}
  31. .form-row{display:flex;gap:12px}
  32. .form-row .form-group{flex:1}
  33. .btn{padding:8px 16px;border:none;cursor:pointer;font-size:13px;font-weight:bold}
  34. .btn-primary{background:#e94560;color:#fff}
  35. .btn-primary:hover{background:#c0392b}
  36. .btn-success{background:#27ae60;color:#fff}
  37. .btn-danger{background:#e74c3c;color:#fff}
  38. .btn-outline{background:transparent;border:1px solid #555;color:#aaa}
  39. .btn-outline:hover{border-color:#e94560;color:#e94560}
  40. .btn-row{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
  41. .bucket-item{background:#0e1426;border:1px solid #0f3460;padding:12px;margin-bottom:10px}
  42. .bucket-item .header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px}
  43. .bucket-item .title{font-weight:bold;color:#3498db;font-size:14px}
  44. .bucket-item .actions{display:flex;gap:6px}
  45. .bucket-item .actions button{padding:4px 10px;font-size:12px}
  46. .bucket-item .fields{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-size:12px;color:#888}
  47. .bucket-item .fields span{word-break:break-all}
  48. .edit-form{background:#182242;border:1px solid #3498db;padding:12px;margin-top:10px}
  49. .edit-form .form-group{margin-bottom:8px}
  50. body.rail .sidebar{width:60px}
  51. body.rail .logo .t,body.rail .nav-label,body.rail .side-foot .nav-label{display:none}
  52. </style>
  53. </head>
  54. <body>
  55. <div class="app">
  56. <aside class="sidebar" id="sidebar">
  57. <div class="logo"><img src="/logo.png" alt=""><span class="t">恒达视觉监控平台</span></div>
  58. <nav class="nav">
  59. <a class="nav-item" href="/" style="text-decoration:none"><span class="ico">监</span><span class="nav-label">实时监控</span></a>
  60. <a class="nav-item" href="/#fence" style="text-decoration:none"><span class="ico">围</span><span class="nav-label">电子围栏</span></a>
  61. <a class="nav-item" href="/#alarm" style="text-decoration:none"><span class="ico">警</span><span class="nav-label">报警历史</span></a>
  62. <a class="nav-item" href="/#action" style="text-decoration:none"><span class="ico">动</span><span class="nav-label">动作记录</span></a>
  63. <a class="nav-item" href="/#relay" style="text-decoration:none"><span class="ico">继</span><span class="nav-label">继电器控制</span></a>
  64. <a class="nav-item" href="/#channel" style="text-decoration:none"><span class="ico">通</span><span class="nav-label">通道管理</span></a>
  65. <a class="nav-item" href="/#system" style="text-decoration:none"><span class="ico">系</span><span class="nav-label">系统设置</span></a>
  66. <div style="border-top:1px solid #0f3460;margin:8px 12px"></div>
  67. <a class="nav-item" href="/preview.html" style="text-decoration:none"><span class="ico">工</span><span class="nav-label">工位预览</span></a>
  68. <a class="nav-item active" href="/config.html" style="text-decoration:none"><span class="ico">设</span><span class="nav-label">工位配置</span></a>
  69. </nav>
  70. <div class="side-foot">
  71. <button class="collapse-btn" onclick="document.body.classList.toggle('rail')">« 收起菜单</button>
  72. </div>
  73. </aside>
  74. <main class="content">
  75. <div class="card">
  76. <h3>通讯配置</h3>
  77. <div class="form-row">
  78. <div class="form-group">
  79. <label>模块类型</label>
  80. <select id="moduleType" onchange="onModuleTypeChange()">
  81. <option value="RFID">RFID读卡器</option>
  82. <option value="Modbus">Modbus TCP (HD3200)</option>
  83. </select>
  84. </div>
  85. <div class="form-group">
  86. <label>空闲超时(秒)</label>
  87. <input type="number" id="idleTimeout" min="1" max="300" value="5">
  88. </div>
  89. </div>
  90. <div id="rfidConfig">
  91. <div class="form-row">
  92. <div class="form-group"><label>RFID IP</label><input type="text" id="rfidIp" placeholder="192.168.100.113"></div>
  93. <div class="form-group"><label>端口</label><input type="number" id="rfidPort" min="1" max="65535" value="60000"></div>
  94. </div>
  95. <div class="form-row">
  96. <div class="form-group"><label>稳定时间(秒)</label><input type="number" id="rfidStabilityTime" min="1" max="60" value="5"></div>
  97. <div class="form-group"><label>稳定次数</label><input type="number" id="rfidStabilityCount" min="1" max="10" value="3"></div>
  98. </div>
  99. </div>
  100. <div id="modbusConfig" style="display:none">
  101. <div class="form-row">
  102. <div class="form-group"><label>Modbus IP</label><input type="text" id="modbusIp" placeholder="192.168.1.200"></div>
  103. <div class="form-group"><label>端口</label><input type="number" id="modbusPort" min="1" max="65535" value="502"></div>
  104. </div>
  105. <div class="form-row">
  106. <div class="form-group"><label>轮询间隔(ms)</label><input type="number" id="modbusFrequency" min="50" max="5000" value="100"></div>
  107. <div class="form-group"><label>稳定次数</label><input type="number" id="modbusStabilityCount" min="1" max="10" value="3"></div>
  108. </div>
  109. </div>
  110. <div class="btn-row">
  111. <button class="btn btn-primary" onclick="saveModuleConfig()">保存通讯配置</button>
  112. </div>
  113. </div>
  114. <div class="card">
  115. <h3>工位管理</h3>
  116. <div id="bucketList"></div>
  117. <div class="btn-row">
  118. <button class="btn btn-primary" onclick="addBucket()">+ 添加工位</button>
  119. </div>
  120. </div>
  121. </main>
  122. </div>
  123. <script>
  124. let buckets = [];
  125. async function loadConfig() {
  126. try {
  127. const [modRes, bucketRes] = await Promise.all([
  128. fetch('/api/module/config'),
  129. fetch('/api/buckets')
  130. ]);
  131. const modData = await modRes.json();
  132. const bucketData = await bucketRes.json();
  133. document.getElementById('moduleType').value = modData.module_type || 'RFID';
  134. document.getElementById('idleTimeout').value = modData.idle_timeout || 5;
  135. onModuleTypeChange();
  136. if (modData.rfid) {
  137. document.getElementById('rfidIp').value = modData.rfid.ip || '';
  138. document.getElementById('rfidPort').value = modData.rfid.port || 60000;
  139. document.getElementById('rfidStabilityTime').value = modData.rfid.stability_time || 5;
  140. document.getElementById('rfidStabilityCount').value = modData.rfid.stability_count || 3;
  141. }
  142. if (modData.modbus) {
  143. document.getElementById('modbusIp').value = modData.modbus.ip || '';
  144. document.getElementById('modbusPort').value = modData.modbus.port || 502;
  145. document.getElementById('modbusFrequency').value = modData.modbus.frequency || 100;
  146. document.getElementById('modbusStabilityCount').value = modData.modbus.stability_count || 3;
  147. }
  148. buckets = bucketData.buckets || [];
  149. renderBuckets();
  150. } catch (e) { console.error('加载配置失败:', e); }
  151. }
  152. function onModuleTypeChange() {
  153. const type = document.getElementById('moduleType').value;
  154. document.getElementById('rfidConfig').style.display = type === 'RFID' ? 'block' : 'none';
  155. document.getElementById('modbusConfig').style.display = type === 'Modbus' ? 'block' : 'none';
  156. }
  157. function renderBuckets() {
  158. const el = document.getElementById('bucketList');
  159. if (buckets.length === 0) {
  160. el.innerHTML = '<div style="color:#888;font-size:13px;padding:20px;text-align:center">暂无工位,请添加</div>';
  161. return;
  162. }
  163. el.innerHTML = buckets.map(b => `
  164. <div class="bucket-item" id="bucket-${b.id}">
  165. <div class="header">
  166. <span class="title">${b.description}</span>
  167. <div class="actions">
  168. <button class="btn btn-outline" onclick="editBucket(${b.id})">编辑</button>
  169. <button class="btn btn-danger" onclick="deleteBucket(${b.id})">删除</button>
  170. </div>
  171. </div>
  172. <div class="fields">
  173. <div><label>标识:</label> <span>${b.code_x}</span></div>
  174. <div><label>垂直工位:</label> <span>${b.code_y || '无'}</span></div>
  175. <div><label>左摄像头:</label> <span>${b.left_camera ? b.left_camera.url : '未配置'}</span></div>
  176. <div><label>右摄像头:</label> <span>${b.right_camera ? b.right_camera.url : '未配置'}</span></div>
  177. </div>
  178. <div id="edit-${b.id}"></div>
  179. </div>
  180. `).join('');
  181. }
  182. function addBucket() {
  183. const maxId = buckets.reduce((max, b) => Math.max(max, b.id), 0);
  184. const newBucket = {
  185. id: maxId + 1,
  186. description: `${maxId + 1}号工位`,
  187. code_x: String(maxId + 1).padStart(8, '0'),
  188. code_y: '',
  189. left_camera: { url: '', topic: '' },
  190. right_camera: { url: '', topic: '' }
  191. };
  192. buckets.push(newBucket);
  193. renderBuckets();
  194. editBucket(newBucket.id);
  195. }
  196. function editBucket(id) {
  197. const b = buckets.find(x => x.id === id);
  198. if (!b) return;
  199. document.getElementById(`edit-${id}`).innerHTML = `
  200. <div class="edit-form">
  201. <div class="form-row">
  202. <div class="form-group"><label>工位名称</label><input type="text" id="e-desc-${id}" value="${b.description}"></div>
  203. <div class="form-group"><label>标识(卡号/点号)</label><input type="text" id="e-codex-${id}" value="${b.code_x}"></div>
  204. </div>
  205. <div class="form-group"><label>垂直工位标识(可选)</label><input type="text" id="e-codey-${id}" value="${b.code_y || ''}"></div>
  206. <div class="form-group"><label>左摄像头RTSP</label><input type="text" id="e-left-${id}" value="${b.left_camera ? b.left_camera.url : ''}" placeholder="rtsp://user:pass@ip:554/stream"></div>
  207. <div class="form-group"><label>右摄像头RTSP</label><input type="text" id="e-right-${id}" value="${b.right_camera ? b.right_camera.url : ''}" placeholder="rtsp://user:pass@ip:554/stream"></div>
  208. <div class="btn-row">
  209. <button class="btn btn-success" onclick="saveBucket(${id})">保存</button>
  210. <button class="btn btn-outline" onclick="cancelEdit(${id})">取消</button>
  211. </div>
  212. </div>
  213. `;
  214. }
  215. function cancelEdit(id) { document.getElementById(`edit-${id}`).innerHTML = ''; }
  216. async function saveBucket(id) {
  217. const b = buckets.find(x => x.id === id);
  218. if (!b) return;
  219. b.description = document.getElementById(`e-desc-${id}`).value;
  220. b.code_x = document.getElementById(`e-codex-${id}`).value;
  221. b.code_y = document.getElementById(`e-codey-${id}`).value;
  222. b.left_camera = { url: document.getElementById(`e-left-${id}`).value, topic: '' };
  223. b.right_camera = { url: document.getElementById(`e-right-${id}`).value, topic: '' };
  224. try {
  225. await fetch('/api/buckets', {
  226. method: 'POST',
  227. headers: {'Content-Type': 'application/json'},
  228. body: JSON.stringify({ buckets })
  229. });
  230. renderBuckets();
  231. alert('保存成功');
  232. } catch (e) { alert('保存失败: ' + e.message); }
  233. }
  234. async function deleteBucket(id) {
  235. if (!confirm('确定删除此工位?')) return;
  236. buckets = buckets.filter(b => b.id !== id);
  237. try {
  238. await fetch('/api/buckets', {
  239. method: 'POST',
  240. headers: {'Content-Type': 'application/json'},
  241. body: JSON.stringify({ buckets })
  242. });
  243. renderBuckets();
  244. } catch (e) { alert('删除失败: ' + e.message); }
  245. }
  246. async function saveModuleConfig() {
  247. const moduleType = document.getElementById('moduleType').value;
  248. const idleTimeout = parseInt(document.getElementById('idleTimeout').value) || 5;
  249. const config = { module_type: moduleType, idle_timeout: idleTimeout };
  250. if (moduleType === 'RFID') {
  251. config.rfid = {
  252. ip: document.getElementById('rfidIp').value,
  253. port: parseInt(document.getElementById('rfidPort').value) || 60000,
  254. stability_time: parseInt(document.getElementById('rfidStabilityTime').value) || 5,
  255. stability_count: parseInt(document.getElementById('rfidStabilityCount').value) || 3
  256. };
  257. } else {
  258. config.modbus = {
  259. ip: document.getElementById('modbusIp').value,
  260. port: parseInt(document.getElementById('modbusPort').value) || 502,
  261. frequency: parseInt(document.getElementById('modbusFrequency').value) || 100,
  262. stability_count: parseInt(document.getElementById('modbusStabilityCount').value) || 3
  263. };
  264. }
  265. try {
  266. await fetch('/api/module/config', {
  267. method: 'POST',
  268. headers: {'Content-Type': 'application/json'},
  269. body: JSON.stringify(config)
  270. });
  271. alert('通讯配置已保存');
  272. } catch (e) { alert('保存失败: ' + e.message); }
  273. }
  274. loadConfig();
  275. </script>
  276. </body>
  277. </html>