config.html 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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 class="form-group">
  126. <label><input type="checkbox" id="targetEnabled"> 是否启用目标识别</label>
  127. </div>
  128. <div class="form-group">
  129. <label><input type="checkbox" id="poseEnabled"> 是否启用姿态识别</label>
  130. </div>
  131. </div>
  132. <div class="form-row">
  133. <div class="form-group">
  134. <label>输出通道网格到显示器</label>
  135. <select id="gridOutput">
  136. <option value="true">启用</option>
  137. <option value="false">禁用</option>
  138. </select>
  139. </div>
  140. <div class="form-group">
  141. <label>输出钢包位到显示器</label>
  142. <select id="steelOutput">
  143. <option value="true">启用</option>
  144. <option value="false">禁用</option>
  145. </select>
  146. </div>
  147. </div>
  148. <div class="form-group">
  149. <label>Web视频推流(MJPEG) <span style="color:#e94560" id="mjpegState"></span></label>
  150. <select id="mjpegEnabled" onchange="toggleMjpeg()">
  151. <option value="true">启用</option>
  152. <option value="false">临时关闭(省CPU)</option>
  153. </select>
  154. </div>
  155. <div class="btn-row">
  156. <button class="btn btn-primary" onclick="saveSystemConfig()">保存系统配置</button>
  157. </div>
  158. </div>
  159. </main>
  160. </div>
  161. <script>
  162. let buckets = [];
  163. async function loadConfig() {
  164. try {
  165. const [modRes, bucketRes, poseRes, statusRes] = await Promise.all([
  166. fetch('/api/module/config'),
  167. fetch('/api/buckets'),
  168. fetch('/api/pose/status'),
  169. fetch('/api/status')
  170. ]);
  171. const modData = await modRes.json();
  172. const bucketData = await bucketRes.json();
  173. let targetEnabled = true, poseEnabled = true;
  174. try {
  175. const poseData = await poseRes.json();
  176. if (poseData.target_enabled !== undefined) targetEnabled = poseData.target_enabled;
  177. if (poseData.pose_cfg_enabled !== undefined) poseEnabled = poseData.pose_cfg_enabled;
  178. else if (poseData.pose_enabled !== undefined) poseEnabled = poseData.pose_enabled;
  179. } catch (e) {}
  180. let gridOutput = true, steelOutput = true;
  181. try {
  182. const statusData = await statusRes.json();
  183. if (statusData.target_enabled !== undefined) targetEnabled = statusData.target_enabled;
  184. if (statusData.grid_output !== undefined) gridOutput = statusData.grid_output;
  185. if (statusData.steel_output !== undefined) steelOutput = statusData.steel_output;
  186. } catch (e) {}
  187. document.getElementById('moduleType').value = modData.module_type || 'RFID';
  188. document.getElementById('idleTimeout').value = modData.idle_timeout || 5;
  189. document.getElementById('targetEnabled').checked = targetEnabled;
  190. document.getElementById('poseEnabled').checked = poseEnabled;
  191. document.getElementById('gridOutput').value = gridOutput ? 'true' : 'false';
  192. document.getElementById('steelOutput').value = steelOutput ? 'true' : 'false';
  193. onModuleTypeChange();
  194. if (modData.rfid) {
  195. document.getElementById('rfidIp').value = modData.rfid.ip || '';
  196. document.getElementById('rfidPort').value = modData.rfid.port || 60000;
  197. document.getElementById('rfidStabilityTime').value = modData.rfid.stability_time || 5;
  198. document.getElementById('rfidStabilityCount').value = modData.rfid.stability_count || 3;
  199. }
  200. if (modData.modbus) {
  201. document.getElementById('modbusIp').value = modData.modbus.ip || '';
  202. document.getElementById('modbusPort').value = modData.modbus.port || 502;
  203. document.getElementById('modbusFrequency').value = modData.modbus.frequency || 100;
  204. document.getElementById('modbusStabilityCount').value = modData.modbus.stability_count || 3;
  205. }
  206. buckets = bucketData.buckets || [];
  207. renderBuckets();
  208. } catch (e) { console.error('加载配置失败:', e); }
  209. }
  210. function onModuleTypeChange() {
  211. const type = document.getElementById('moduleType').value;
  212. document.getElementById('rfidConfig').style.display = type === 'RFID' ? 'block' : 'none';
  213. document.getElementById('modbusConfig').style.display = type === 'Modbus' ? 'block' : 'none';
  214. }
  215. function renderBuckets() {
  216. const el = document.getElementById('bucketList');
  217. if (buckets.length === 0) {
  218. el.innerHTML = '<div style="color:#888;font-size:13px;padding:20px;text-align:center">暂无工位,请添加</div>';
  219. return;
  220. }
  221. el.innerHTML = buckets.map(b => `
  222. <div class="bucket-item" id="bucket-${b.id}">
  223. <div class="header">
  224. <span class="title">${b.description}</span>
  225. <div class="actions">
  226. <button class="btn btn-outline" onclick="editBucket(${b.id})">编辑</button>
  227. <button class="btn btn-danger" onclick="deleteBucket(${b.id})">删除</button>
  228. </div>
  229. </div>
  230. <div class="fields">
  231. <div><label>标识:</label> <span>${b.code_x}</span></div>
  232. <div><label>垂直工位:</label> <span>${b.code_y || '无'}</span></div>
  233. <div><label>左摄像头:</label> <span>${b.left_camera ? b.left_camera.url : '未配置'}</span></div>
  234. <div><label>右摄像头:</label> <span>${b.right_camera ? b.right_camera.url : '未配置'}</span></div>
  235. </div>
  236. <div id="edit-${b.id}"></div>
  237. </div>
  238. `).join('');
  239. }
  240. function addBucket() {
  241. const maxId = buckets.reduce((max, b) => Math.max(max, b.id), 0);
  242. const newBucket = {
  243. id: maxId + 1,
  244. description: `${maxId + 1}号工位`,
  245. code_x: String(maxId + 1).padStart(8, '0'),
  246. code_y: '',
  247. left_camera: { url: '', topic: '' },
  248. right_camera: { url: '', topic: '' }
  249. };
  250. buckets.push(newBucket);
  251. renderBuckets();
  252. editBucket(newBucket.id);
  253. }
  254. function editBucket(id) {
  255. const b = buckets.find(x => x.id === id);
  256. if (!b) return;
  257. document.getElementById(`edit-${id}`).innerHTML = `
  258. <div class="edit-form">
  259. <div class="form-row">
  260. <div class="form-group"><label>工位名称</label><input type="text" id="e-desc-${id}" value="${b.description}"></div>
  261. <div class="form-group"><label>标识(卡号/点号)</label><input type="text" id="e-codex-${id}" value="${b.code_x}"></div>
  262. </div>
  263. <div class="form-group"><label>垂直工位标识(可选)</label><input type="text" id="e-codey-${id}" value="${b.code_y || ''}"></div>
  264. <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>
  265. <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>
  266. <div class="btn-row">
  267. <button class="btn btn-success" onclick="saveBucket(${id})">保存</button>
  268. <button class="btn btn-outline" onclick="cancelEdit(${id})">取消</button>
  269. </div>
  270. </div>
  271. `;
  272. }
  273. function cancelEdit(id) { document.getElementById(`edit-${id}`).innerHTML = ''; }
  274. async function saveBucket(id) {
  275. const b = buckets.find(x => x.id === id);
  276. if (!b) return;
  277. b.description = document.getElementById(`e-desc-${id}`).value;
  278. b.code_x = document.getElementById(`e-codex-${id}`).value;
  279. b.code_y = document.getElementById(`e-codey-${id}`).value;
  280. b.left_camera = { url: document.getElementById(`e-left-${id}`).value, topic: '' };
  281. b.right_camera = { url: document.getElementById(`e-right-${id}`).value, topic: '' };
  282. try {
  283. await fetch('/api/buckets', {
  284. method: 'POST',
  285. headers: {'Content-Type': 'application/json'},
  286. body: JSON.stringify({ buckets })
  287. });
  288. renderBuckets();
  289. alert('保存成功');
  290. } catch (e) { alert('保存失败: ' + e.message); }
  291. }
  292. async function deleteBucket(id) {
  293. if (!confirm('确定删除此工位?')) return;
  294. buckets = buckets.filter(b => b.id !== id);
  295. try {
  296. await fetch('/api/buckets', {
  297. method: 'POST',
  298. headers: {'Content-Type': 'application/json'},
  299. body: JSON.stringify({ buckets })
  300. });
  301. renderBuckets();
  302. } catch (e) { alert('删除失败: ' + e.message); }
  303. }
  304. async function saveModuleConfig() {
  305. const moduleType = document.getElementById('moduleType').value;
  306. const idleTimeout = parseInt(document.getElementById('idleTimeout').value) || 5;
  307. const config = { module_type: moduleType, idle_timeout: idleTimeout };
  308. if (moduleType === 'RFID') {
  309. config.rfid = {
  310. ip: document.getElementById('rfidIp').value,
  311. port: parseInt(document.getElementById('rfidPort').value) || 60000,
  312. stability_time: parseInt(document.getElementById('rfidStabilityTime').value) || 5,
  313. stability_count: parseInt(document.getElementById('rfidStabilityCount').value) || 3
  314. };
  315. } else {
  316. config.modbus = {
  317. ip: document.getElementById('modbusIp').value,
  318. port: parseInt(document.getElementById('modbusPort').value) || 502,
  319. frequency: parseInt(document.getElementById('modbusFrequency').value) || 100,
  320. stability_count: parseInt(document.getElementById('modbusStabilityCount').value) || 3
  321. };
  322. }
  323. try {
  324. await fetch('/api/module/config', {
  325. method: 'POST',
  326. headers: {'Content-Type': 'application/json'},
  327. body: JSON.stringify(config)
  328. });
  329. alert('通讯配置已保存');
  330. } catch (e) { alert('保存失败: ' + e.message); }
  331. }
  332. async function saveSystemConfig() {
  333. const targetEnabled = document.getElementById('targetEnabled').checked;
  334. const poseEnabled = document.getElementById('poseEnabled').checked;
  335. const gridOutput = document.getElementById('gridOutput').value === 'true';
  336. const steelOutput = document.getElementById('steelOutput').value === 'true';
  337. try {
  338. await fetch('/api/config', {
  339. method: 'POST',
  340. headers: {'Content-Type': 'application/json'},
  341. body: JSON.stringify({ target_enabled: targetEnabled, pose_enabled: poseEnabled, grid_output: gridOutput, steel_output: steelOutput })
  342. });
  343. alert('系统配置已保存' + (poseEnabled ? '' : '(姿态识别需重启后生效)'));
  344. } catch (e) { alert('保存失败: ' + e.message); }
  345. }
  346. loadConfig();
  347. </script>
  348. </body>
  349. </html>