config.html 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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. <div class="card">
  122. <h3>系统配置</h3>
  123. <div class="form-row">
  124. <div class="form-group"><label>RTSP超时(秒)</label><input type="number" id="rtspTimeout" min="1" max="60" value="10"></div>
  125. </div>
  126. <div class="btn-row">
  127. <button class="btn btn-primary" onclick="saveSystemConfig()">保存系统配置</button>
  128. </div>
  129. </div>
  130. </main>
  131. </div>
  132. <script>
  133. let buckets = [];
  134. async function loadConfig() {
  135. try {
  136. const [modRes, bucketRes] = await Promise.all([
  137. fetch('/api/module/config'),
  138. fetch('/api/buckets')
  139. ]);
  140. const modData = await modRes.json();
  141. const bucketData = await bucketRes.json();
  142. document.getElementById('moduleType').value = modData.module_type || 'RFID';
  143. document.getElementById('idleTimeout').value = modData.idle_timeout || 5;
  144. onModuleTypeChange();
  145. if (modData.rfid) {
  146. document.getElementById('rfidIp').value = modData.rfid.ip || '';
  147. document.getElementById('rfidPort').value = modData.rfid.port || 60000;
  148. document.getElementById('rfidStabilityTime').value = modData.rfid.stability_time || 5;
  149. document.getElementById('rfidStabilityCount').value = modData.rfid.stability_count || 3;
  150. }
  151. if (modData.modbus) {
  152. document.getElementById('modbusIp').value = modData.modbus.ip || '';
  153. document.getElementById('modbusPort').value = modData.modbus.port || 502;
  154. document.getElementById('modbusFrequency').value = modData.modbus.frequency || 100;
  155. document.getElementById('modbusStabilityCount').value = modData.modbus.stability_count || 3;
  156. }
  157. buckets = bucketData.buckets || [];
  158. renderBuckets();
  159. } catch (e) { console.error('加载配置失败:', e); }
  160. }
  161. function onModuleTypeChange() {
  162. const type = document.getElementById('moduleType').value;
  163. document.getElementById('rfidConfig').style.display = type === 'RFID' ? 'block' : 'none';
  164. document.getElementById('modbusConfig').style.display = type === 'Modbus' ? 'block' : 'none';
  165. }
  166. function renderBuckets() {
  167. const el = document.getElementById('bucketList');
  168. if (buckets.length === 0) {
  169. el.innerHTML = '<div style="color:#888;font-size:13px;padding:20px;text-align:center">暂无工位,请添加</div>';
  170. return;
  171. }
  172. el.innerHTML = buckets.map(b => `
  173. <div class="bucket-item" id="bucket-${b.id}">
  174. <div class="header">
  175. <span class="title">${b.description}</span>
  176. <div class="actions">
  177. <button class="btn btn-outline" onclick="editBucket(${b.id})">编辑</button>
  178. <button class="btn btn-danger" onclick="deleteBucket(${b.id})">删除</button>
  179. </div>
  180. </div>
  181. <div class="fields">
  182. <div><label>标识:</label> <span>${b.code_x}</span></div>
  183. <div><label>垂直工位:</label> <span>${b.code_y || '无'}</span></div>
  184. <div><label>左摄像头:</label> <span>${b.left_camera ? b.left_camera.url : '未配置'}</span></div>
  185. <div><label>右摄像头:</label> <span>${b.right_camera ? b.right_camera.url : '未配置'}</span></div>
  186. </div>
  187. <div id="edit-${b.id}"></div>
  188. </div>
  189. `).join('');
  190. }
  191. function addBucket() {
  192. const maxId = buckets.reduce((max, b) => Math.max(max, b.id), 0);
  193. const newBucket = {
  194. id: maxId + 1,
  195. description: `${maxId + 1}号工位`,
  196. code_x: String(maxId + 1).padStart(8, '0'),
  197. code_y: '',
  198. left_camera: { url: '', topic: '' },
  199. right_camera: { url: '', topic: '' }
  200. };
  201. buckets.push(newBucket);
  202. renderBuckets();
  203. editBucket(newBucket.id);
  204. }
  205. function editBucket(id) {
  206. const b = buckets.find(x => x.id === id);
  207. if (!b) return;
  208. document.getElementById(`edit-${id}`).innerHTML = `
  209. <div class="edit-form">
  210. <div class="form-row">
  211. <div class="form-group"><label>工位名称</label><input type="text" id="e-desc-${id}" value="${b.description}"></div>
  212. <div class="form-group"><label>标识(卡号/点号)</label><input type="text" id="e-codex-${id}" value="${b.code_x}"></div>
  213. </div>
  214. <div class="form-group"><label>垂直工位标识(可选)</label><input type="text" id="e-codey-${id}" value="${b.code_y || ''}"></div>
  215. <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>
  216. <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>
  217. <div class="btn-row">
  218. <button class="btn btn-success" onclick="saveBucket(${id})">保存</button>
  219. <button class="btn btn-outline" onclick="cancelEdit(${id})">取消</button>
  220. </div>
  221. </div>
  222. `;
  223. }
  224. function cancelEdit(id) { document.getElementById(`edit-${id}`).innerHTML = ''; }
  225. async function saveBucket(id) {
  226. const b = buckets.find(x => x.id === id);
  227. if (!b) return;
  228. b.description = document.getElementById(`e-desc-${id}`).value;
  229. b.code_x = document.getElementById(`e-codex-${id}`).value;
  230. b.code_y = document.getElementById(`e-codey-${id}`).value;
  231. b.left_camera = { url: document.getElementById(`e-left-${id}`).value, topic: '' };
  232. b.right_camera = { url: document.getElementById(`e-right-${id}`).value, topic: '' };
  233. try {
  234. await fetch('/api/buckets', {
  235. method: 'POST',
  236. headers: {'Content-Type': 'application/json'},
  237. body: JSON.stringify({ buckets })
  238. });
  239. renderBuckets();
  240. alert('保存成功');
  241. } catch (e) { alert('保存失败: ' + e.message); }
  242. }
  243. async function deleteBucket(id) {
  244. if (!confirm('确定删除此工位?')) return;
  245. buckets = buckets.filter(b => b.id !== id);
  246. try {
  247. await fetch('/api/buckets', {
  248. method: 'POST',
  249. headers: {'Content-Type': 'application/json'},
  250. body: JSON.stringify({ buckets })
  251. });
  252. renderBuckets();
  253. } catch (e) { alert('删除失败: ' + e.message); }
  254. }
  255. async function saveModuleConfig() {
  256. const moduleType = document.getElementById('moduleType').value;
  257. const idleTimeout = parseInt(document.getElementById('idleTimeout').value) || 5;
  258. const config = { module_type: moduleType, idle_timeout: idleTimeout };
  259. if (moduleType === 'RFID') {
  260. config.rfid = {
  261. ip: document.getElementById('rfidIp').value,
  262. port: parseInt(document.getElementById('rfidPort').value) || 60000,
  263. stability_time: parseInt(document.getElementById('rfidStabilityTime').value) || 5,
  264. stability_count: parseInt(document.getElementById('rfidStabilityCount').value) || 3
  265. };
  266. } else {
  267. config.modbus = {
  268. ip: document.getElementById('modbusIp').value,
  269. port: parseInt(document.getElementById('modbusPort').value) || 502,
  270. frequency: parseInt(document.getElementById('modbusFrequency').value) || 100,
  271. stability_count: parseInt(document.getElementById('modbusStabilityCount').value) || 3
  272. };
  273. }
  274. try {
  275. await fetch('/api/module/config', {
  276. method: 'POST',
  277. headers: {'Content-Type': 'application/json'},
  278. body: JSON.stringify(config)
  279. });
  280. alert('通讯配置已保存');
  281. } catch (e) { alert('保存失败: ' + e.message); }
  282. }
  283. async function saveSystemConfig() { alert('系统配置已保存'); }
  284. loadConfig();
  285. </script>
  286. </body>
  287. </html>