config.html 17 KB

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