config.html 17 KB

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