| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>工位预览 - 恒达视觉监控平台</title>
- <link rel="icon" href="/logo.png" type="image/png">
- <style>
- *{margin:0;padding:0;box-sizing:border-box;border-radius:0 !important}
- body{font-family:system-ui,sans-serif;background:#12192e;color:#eee;overflow:hidden}
- .app{display:flex;height:100vh}
- .sidebar{width:200px;background:#0e1426;border-right:1px solid #0f3460;display:flex;flex-direction:column;flex-shrink:0}
- .logo{display:flex;align-items:center;gap:10px;padding:14px 16px;border-bottom:1px solid #0f3460}
- .logo img{height:28px;flex-shrink:0}
- .logo .t{font-size:14px;color:#e94560;font-weight:bold;white-space:nowrap}
- .nav{padding-top:8px;flex:1}
- .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;text-decoration:none}
- .nav-item:hover{background:#182242;color:#ccc}
- .nav-item.active{color:#e94560;border-left-color:#e94560;background:#182242;font-weight:bold}
- .nav-item .ico{width:20px;text-align:center;flex-shrink:0;font-weight:bold}
- .nav-label{transition:none}
- .side-foot{border-top:1px solid #0f3460;padding:12px 14px;display:flex;flex-direction:column;gap:10px}
- .mode-btn{padding:6px 16px;border:none;cursor:pointer;font-size:13px;font-weight:bold;border-radius:4px}
- .mode-btn.auto{background:#27ae60;color:#fff}
- .mode-btn.hand{background:#e94560;color:#fff}
- .mode-btn:hover{opacity:0.9}
- .collapse-btn{background:none;border:1px solid #0f3460;color:#8892a8;padding:5px;cursor:pointer;font-size:12px;width:100%}
- .collapse-btn:hover{color:#e94560;border-color:#e94560}
- .content{flex:1;display:flex;flex-direction:column;overflow:hidden}
- .status-bar{display:flex;align-items:center;justify-content:space-between;padding:10px 20px;background:#0e1426;border-bottom:1px solid #0f3460}
- .status-left{display:flex;gap:20px;align-items:center;flex-wrap:wrap}
- .status-item{font-size:13px;color:#888}
- .status-item .val{color:#3498db;font-weight:bold}
- .status-item.online .val{color:#27ae60}
- .status-item.offline .val{color:#e74c3c}
- .main-body{flex:1;display:flex;overflow:hidden}
- .bucket-panel{width:140px;background:#0e1426;border-right:1px solid #0f3460;padding:12px;overflow-y:auto;flex-shrink:0}
- .bucket-panel h4{color:#e94560;font-size:13px;margin-bottom:12px}
- .bucket-btn{width:100%;padding:10px 6px;margin-bottom:8px;background:#141d36;border:2px solid #0f3460;border-radius:8px;cursor:pointer;text-align:center;transition:all 0.2s}
- .bucket-btn:hover{border-color:#3498db;background:#182242}
- .bucket-btn.active{border-color:#27ae60;background:#1a3a2a}
- .bucket-btn .name{font-size:13px;font-weight:bold;color:#eee}
- .bucket-btn .tag{font-size:11px;color:#888;margin-top:4px}
- .video-area{flex:1;display:flex;padding:12px;gap:12px}
- .video-cell{flex:1;background:#000;border:2px solid #0f3460;border-radius:8px;position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;flex-direction:column}
- .video-cell img{width:100%;height:calc(100% - 32px);object-fit:contain}
- .video-cell .cam-label{position:absolute;top:0;left:0;right:0;height:32px;display:flex;align-items:center;justify-content:center;background:rgba(14,20,38,.9);border-bottom:1px solid #0f3460;color:#ddd;font-size:13px;font-weight:bold;z-index:2}
- .video-cell.offline{border-color:#e74c3c}
- .video-cell.offline img{display:none}
- .video-cell.offline::after{content:"信号离线";position:absolute;inset:0;top:32px;display:flex;align-items:center;justify-content:center;color:#e74c3c;font-size:16px;background:rgba(0,0,0,.55);z-index:1}
- .video-cell .empty{position:absolute;top:32px;left:0;right:0;bottom:0;display:flex;align-items:center;justify-content:center;color:#666;font-size:14px;z-index:1}
- /* Loading Overlay */
- .loading-overlay{position:absolute;top:32px;left:0;right:0;bottom:0;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,.7);z-index:10;flex-direction:column;gap:12px}
- .loading-overlay.show{display:flex}
- .loading-bar-wrap{width:200px;height:4px;background:#0f3460;border-radius:2px;overflow:hidden}
- .loading-bar{height:100%;width:30%;background:linear-gradient(90deg,#3498db,#e94560);border-radius:2px;animation:loadSlide 1.2s ease-in-out infinite}
- @keyframes loadSlide{0%{transform:translateX(-100%)}100%{transform:translateX(400%)}}
- .loading-text{color:#aaa;font-size:12px}
- body.rail .sidebar{width:60px}
- body.rail .logo .t,body.rail .nav-label,body.rail .side-foot .nav-label{display:none}
- </style>
- </head>
- <body>
- <div class="app">
- <aside class="sidebar" id="sidebar">
- <div class="logo"><img src="/logo.png" alt=""><span class="t">恒达视觉监控平台</span></div>
- <nav class="nav">
- <a class="nav-item" href="/" style="text-decoration:none"><span class="ico">监</span><span class="nav-label">实时监控</span></a>
- <a class="nav-item" href="/#fence" style="text-decoration:none"><span class="ico">围</span><span class="nav-label">电子围栏</span></a>
- <a class="nav-item" href="/#alarm" style="text-decoration:none"><span class="ico">警</span><span class="nav-label">报警历史</span></a>
- <a class="nav-item" href="/#action" style="text-decoration:none"><span class="ico">动</span><span class="nav-label">动作记录</span></a>
- <a class="nav-item" href="/#relay" style="text-decoration:none"><span class="ico">继</span><span class="nav-label">继电器控制</span></a>
- <a class="nav-item" href="/#channel" style="text-decoration:none"><span class="ico">通</span><span class="nav-label">通道管理</span></a>
- <a class="nav-item" href="/#system" style="text-decoration:none"><span class="ico">系</span><span class="nav-label">系统设置</span></a>
- <div style="border-top:1px solid #0f3460;margin:8px 12px"></div>
- <a class="nav-item active" href="/preview.html" style="text-decoration:none"><span class="ico">工</span><span class="nav-label">工位预览</span></a>
- <a class="nav-item" href="/config.html" style="text-decoration:none"><span class="ico">设</span><span class="nav-label">工位配置</span></a>
- </nav>
- <div class="side-foot">
- <button class="mode-btn auto" id="modeBtn" onclick="toggleMode()">自动模式</button>
- <button class="collapse-btn" onclick="document.body.classList.toggle('rail')">« 收起菜单</button>
- </div>
- </aside>
- <main class="content">
- <div class="status-bar">
- <div class="status-left">
- <div class="status-item" id="moduleStatus">通讯状态: <span class="val">初始化</span></div>
- <div class="status-item" id="tagStatus">当前卡号: <span class="val">无</span></div>
- <div class="status-item" id="hookState">挂钩状态: <span class="val">空闲</span></div>
- <div class="status-item" id="bucketStatus">当前工位: <span class="val">无</span></div>
- </div>
- </div>
- <div class="main-body">
- <div class="bucket-panel">
- <h4>工位列表</h4>
- <div id="bucketList"></div>
- </div>
- <div class="video-area">
- <div class="video-cell" id="leftVideo">
- <div class="cam-label" id="leftCamLabel">左侧画面</div>
- <img id="leftStream" alt="">
- <div class="empty" id="leftEmpty">请选择工位</div>
- <div class="loading-overlay" id="leftLoading">
- <div class="loading-bar-wrap"><div class="loading-bar"></div></div>
- <div class="loading-text">正在连接摄像头...</div>
- </div>
- </div>
- <div class="video-cell" id="rightVideo">
- <div class="cam-label" id="rightCamLabel">右侧画面</div>
- <img id="rightStream" alt="">
- <div class="empty" id="rightEmpty">请选择工位</div>
- <div class="loading-overlay" id="rightLoading">
- <div class="loading-bar-wrap"><div class="loading-bar"></div></div>
- <div class="loading-text">正在连接摄像头...</div>
- </div>
- </div>
- </div>
- </div>
- </main>
- </div>
- <script>
- let buckets = [];
- let activeId = -1;
- let mode = 'auto';
- let moduleType = 'RFID';
- let lastTag = '';
- let refreshTimer;
- let switching = false;
- async function loadBuckets() {
- try {
- const res = await fetch('/api/buckets');
- const data = await res.json();
- buckets = data.buckets || [];
- activeId = data.active_id;
- mode = data.mode;
- renderBuckets();
- updateModeBtn();
- } catch (e) { console.error('加载工位失败:', e); }
- }
- async function loadStatus() {
- try {
- const res = await fetch('/api/buckets/status');
- const data = await res.json();
- moduleType = data.module_type || 'RFID';
- activeId = data.active_id;
- mode = data.mode;
- const moduleEl = document.getElementById('moduleStatus');
- if (data.rfid) {
- const online = data.rfid.online;
- moduleEl.innerHTML = `通讯状态: <span class="val ${online?'online':'offline'}">${online?'RFID在线':'RFID离线'}</span>`;
- if (data.rfid.last_tag) {
- lastTag = data.rfid.last_tag;
- document.getElementById('tagStatus').innerHTML = `当前卡号: <span class="val">${lastTag}</span>`;
- }
- }
- if (data.modbus) {
- const online = data.modbus.online;
- moduleEl.innerHTML = `通讯状态: <span class="val ${online?'online':'offline'}">${online?'Modbus在线':'Modbus离线'}</span>`;
- }
- if (activeId >= 0) {
- const b = buckets.find(x => x.id === activeId);
- document.getElementById('bucketStatus').innerHTML = `当前工位: <span class="val">${b ? b.description : activeId}</span>`;
- } else {
- document.getElementById('bucketStatus').innerHTML = `当前工位: <span class="val">无</span>`;
- }
- updateModeBtn();
- renderBuckets();
- } catch (e) { console.error('加载状态失败:', e); }
- }
- function renderBuckets() {
- const el = document.getElementById('bucketList');
- el.innerHTML = buckets.map(b => `
- <div class="bucket-btn ${b.id === activeId ? 'active' : ''}"
- onclick="activateBucket(${b.id})">
- <div class="name">${b.description}</div>
- <div class="tag">${b.code_x}</div>
- </div>
- `).join('');
- }
- function showLoading(left, right) {
- document.getElementById('leftLoading').classList.toggle('show', left);
- document.getElementById('rightLoading').classList.toggle('show', right);
- }
- function hideLeftLoading() { document.getElementById('leftLoading').classList.remove('show'); }
- function hideRightLoading() { document.getElementById('rightLoading').classList.remove('show'); }
- async function activateBucket(id) {
- if (switching) return;
- switching = true;
- activeId = id;
- renderBuckets();
- const b = buckets.find(x => x.id === id);
- const bName = b ? b.description : '';
- const leftImg = document.getElementById('leftStream');
- const rightImg = document.getElementById('rightStream');
- const leftLabel = document.getElementById('leftCamLabel');
- const rightLabel = document.getElementById('rightCamLabel');
- const leftCell = document.getElementById('leftVideo');
- const rightCell = document.getElementById('rightVideo');
- const leftEmpty = document.getElementById('leftEmpty');
- const rightEmpty = document.getElementById('rightEmpty');
- // 断开旧流
- leftImg.removeAttribute('src');
- rightImg.removeAttribute('src');
- leftCell.classList.remove('offline');
- rightCell.classList.remove('offline');
- const hasLeft = b && b.left_camera && b.left_camera.url;
- const hasRight = b && b.right_camera && b.right_camera.url;
- // 显示loading
- showLoading(hasLeft, hasRight);
- if (hasLeft) { leftEmpty.style.display = 'none'; leftLabel.textContent = bName + ' - 左'; }
- if (hasRight) { rightEmpty.style.display = 'none'; rightLabel.textContent = bName + ' - 右'; }
- // 通知后端切换
- try {
- await fetch('/api/buckets/activate', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({id})
- });
- } catch (e) { console.error('激活失败:', e); }
- // 等待后端RTSP重连,再请求MJPEG
- await new Promise(r => setTimeout(r, 1500));
- let leftReady = false, rightReady = false;
- if (hasLeft) {
- leftImg.onload = function() {
- if (!leftReady) { leftReady = true; hideLeftLoading(); }
- };
- leftImg.onerror = function() {
- leftCell.classList.add('offline');
- leftLabel.textContent = bName + ' - 左 (离线)';
- hideLeftLoading();
- };
- leftImg.src = '/mjpeg/0?t=' + Date.now();
- } else {
- hideLeftLoading();
- if (b) { leftLabel.textContent = '左侧画面'; leftEmpty.textContent = '未配置左摄像头'; leftEmpty.style.display = 'flex'; }
- }
- if (hasRight) {
- rightImg.onload = function() {
- if (!rightReady) { rightReady = true; hideRightLoading(); }
- };
- rightImg.onerror = function() {
- rightCell.classList.add('offline');
- rightLabel.textContent = bName + ' - 右 (离线)';
- hideRightLoading();
- };
- rightImg.src = '/mjpeg/1?t=' + Date.now();
- } else {
- hideRightLoading();
- if (b) { rightLabel.textContent = '右侧画面'; rightEmpty.textContent = '未配置右摄像头'; rightEmpty.style.display = 'flex'; }
- }
- switching = false;
- }
- async function toggleMode() {
- const newMode = mode === 'auto' ? 'hand' : 'auto';
- try {
- await fetch('/api/buckets/mode', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({mode: newMode})
- });
- mode = newMode;
- updateModeBtn();
- } catch (e) { console.error('切换模式失败:', e); }
- }
- function updateModeBtn() {
- const btn = document.getElementById('modeBtn');
- btn.className = 'mode-btn ' + mode;
- btn.textContent = mode === 'auto' ? '自动模式' : '手动模式';
- }
- async function init() {
- await loadBuckets();
- await loadStatus();
- if (activeId >= 0) {
- activateBucket(activeId);
- }
- refreshTimer = setInterval(loadStatus, 3000);
- }
- init();
- </script>
- </body>
- </html>
|