'use strict'; let wallOwner='',wallOrder=[],wallPage=0,wallDragging='',ptzCamera='',ptzGeneration=0; const WALL_LAYOUTS={auto:[0,0],1:[1,1],2:[2,1],4:[2,2],6:[3,2],9:[3,3],16:[4,4]}; function wallStorageKey(){return 'zhaofu-monitor-'+(currentUser?.id||'guest')+'-'+(state.familyId||'')+'-'+(deviceContext?.id||'wall');} function wallSave(){try{localStorage.setItem(wallStorageKey(),JSON.stringify({order:wallOrder,layout:$('#wall-layout').value}));}catch{}} function wallSync(){ if(wallOwner!==wallStorageKey()){ wallOwner=wallStorageKey();wallPage=0;let saved={};try{saved=JSON.parse(localStorage.getItem(wallStorageKey()))||{};}catch{} wallOrder=Array.isArray(saved.order)?saved.order.filter(x=>/^[a-f0-9]{16}$/.test(x)):[]; $('#wall-layout').value=Object.hasOwn(WALL_LAYOUTS,saved.layout)?saved.layout:'auto'; } const cameras=filtered(),ids=cameras.map(c=>c.id); const allIds=new Set(state.cameras.map(c=>c.id));wallOrder=wallOrder.filter(id=>allIds.has(id)); for(const id of ids)if(!wallOrder.includes(id))wallOrder.push(id); const ordered=wallOrder.filter(id=>ids.includes(id)),layout=$('#wall-layout').value; const capacity=Number(layout)||6,pages=Math.max(1,Math.ceil(ordered.length/capacity)); wallPage=Math.min(wallPage,pages-1);const visible=new Set(ordered.slice(wallPage*capacity,(wallPage+1)*capacity)); $('#live-grid').dataset.layout=layout; $('#live-grid').dataset.visibleCount=String(visible.size); for(const card of $$('#live-grid .camera-card')){ const id=card.querySelector('video').dataset.camera; card.style.order=ordered.indexOf(id);card.hidden=!visible.has(id); card.querySelector('[data-wall-before]').disabled=ordered.indexOf(id)===0; card.querySelector('[data-wall-after]').disabled=ordered.indexOf(id)===ordered.length-1; } $('#wall-page').textContent=`${wallPage+1} / ${pages} 页 · ${ordered.length} 个画面`; $('#wall-prev').disabled=wallPage===0;$('#wall-next').disabled=wallPage===pages-1; $('#live-grid').classList.toggle('arranging',$('#wall-arrange').checked); $$('[data-ptz-camera]').forEach(b=>b.hidden=!(isAdmin()||currentUser?.ptzControl)); } function wallMove(id,target){ const from=wallOrder.indexOf(id),to=wallOrder.indexOf(target);if(from<0||to<0||from===to)return; wallOrder.splice(from,1);wallOrder.splice(to,0,id);wallSave();wallSync(); } $('#wall-layout').onchange=()=>{wallPage=0;wallSave();wallSync();}; $('#wall-arrange').onchange=wallSync; $('#wall-prev').onclick=()=>{wallPage--;wallSync();};$('#wall-next').onclick=()=>{wallPage++;wallSync();}; let wallBodyOverflow=''; function syncWallFullscreen(){ const active=document.fullscreenElement===$('#page-live')||$('#page-live').classList.contains('wall-expanded'); $('#wall-fullscreen').textContent=active?'退出全屏':'全屏监控'; $('#wall-fullscreen').setAttribute('aria-pressed',String(active)); document.body.classList.toggle('wall-fullscreen',active); } function closeWallFullscreen(){ const stage=$('#page-live'); if(stage.classList.contains('wall-expanded')){stage.classList.remove('wall-expanded');document.body.style.overflow=wallBodyOverflow;} if(document.fullscreenElement===stage)document.exitFullscreen().catch(()=>{}); syncWallFullscreen(); } $('#wall-fullscreen').onclick=async()=>{ const stage=$('#page-live'); if(document.fullscreenElement===stage||stage.classList.contains('wall-expanded')){closeWallFullscreen();return;} try{await stage.requestFullscreen();} catch{wallBodyOverflow=document.body.style.overflow;stage.classList.add('wall-expanded');document.body.style.overflow='hidden';} syncWallFullscreen(); }; document.addEventListener('fullscreenchange',syncWallFullscreen); document.addEventListener('keydown',e=>{ if(e.key==='Escape')closeWallFullscreen(); }); setInterval(()=>{$('#wall-clock').textContent=new Date().toLocaleString('zh-CN',{hour12:false});},1000); let wallPointer=null; function wallDragEnd(){wallPointer=null;wallDragging='';$$('.wall-drop-target').forEach(c=>c.classList.remove('wall-drop-target'));} $('#live-grid').addEventListener('pointerdown',e=>{ const handle=e.target.closest('[data-wall-drag]');if(!handle||e.button!==0||!$('#wall-arrange').checked)return; e.preventDefault();handle.setPointerCapture(e.pointerId);wallDragging=handle.dataset.wallDrag; wallPointer={x:e.clientX,y:e.clientY,id:e.pointerId,moved:false}; }); $('#live-grid').addEventListener('pointermove',e=>{ if(!wallPointer||wallPointer.id!==e.pointerId)return; wallPointer.moved ||= Math.hypot(e.clientX-wallPointer.x,e.clientY-wallPointer.y)>8; $$('.wall-drop-target').forEach(c=>c.classList.remove('wall-drop-target')); if(wallPointer.moved)document.elementFromPoint(e.clientX,e.clientY)?.closest('.camera-card')?.classList.add('wall-drop-target'); }); $('#live-grid').addEventListener('pointerup',e=>{ if(!wallPointer||wallPointer.id!==e.pointerId)return; const target=document.elementFromPoint(e.clientX,e.clientY)?.closest('.camera-card')?.querySelector('video')?.dataset.camera; if(wallPointer.moved&&target)wallMove(wallDragging,target);wallDragEnd(); }); $('#live-grid').addEventListener('pointercancel',wallDragEnd); document.addEventListener('click',e=>{ const b=e.target.closest('button');if(!b)return; if(b.dataset.wallBefore||b.dataset.wallAfter){const ids=wallOrder.filter(id=>filtered().some(c=>c.id===id)),id=b.dataset.wallBefore||b.dataset.wallAfter;wallMove(id,ids[ids.indexOf(id)+(b.dataset.wallBefore?-1:1)]);} if(b.dataset.ptzCamera)openPtz(b.dataset.ptzCamera); }); let ptzPresets=null,ptzPresetGeneration=0,ptzBusy=false,ptzCommandGeneration=0,ptzReady=false,ptzConfirmation=null; function setPtzBusy(busy){ ptzBusy=busy; $$('[data-ptz-action]').forEach(b=>b.disabled=!ptzReady||(busy&&b.dataset.ptzAction!=='stop')); $$('#ptz-presets button,#ptz-presets input').forEach(b=>b.disabled=busy); if(!busy&&ptzPresets)$('#preset-form button').disabled=ptzPresets.items.length>=ptzPresets.maximum; } function clearPresetConfirmation(){ptzConfirmation=null;$('#preset-confirmation').hidden=true;} function renderPresets(data){ ptzPresets=data;clearPresetConfirmation(); $('#preset-list').innerHTML=data.items.map(p=>`
${esc(p.name)}${isAdmin()?``:''}
`).join('')||'

还没有预置位。把云台调到需要的位置后保存。

'; $('#preset-count').textContent=`${data.items.length} / ${data.maximum} 个位置`; $('#preset-form').hidden=!isAdmin();$('#preset-form button').disabled=data.items.length>=data.maximum; } async function loadPresets(){ if(!ptzCamera||ptzBusy)return;const id=ptzCamera,generation=++ptzPresetGeneration; ptzPresets=null;clearPresetConfirmation();$('#preset-list').replaceChildren();$('#preset-form').hidden=true; $('#preset-status').textContent='正在读取设备预置位…';$('#preset-reload').disabled=true; try{const data=await api('/api/ptz/presets?camera='+encodeURIComponent(id));if(generation!==ptzPresetGeneration||id!==ptzCamera||!loggedIn)return;renderPresets(data);$('#preset-status').textContent=data.note;} catch(e){if(!e.stale&&generation===ptzPresetGeneration)$('#preset-status').textContent=e.message;} finally{if(generation===ptzPresetGeneration)$('#preset-reload').disabled=false;} } async function openPtz(id){ if(ptzCamera&&ptzCamera!==id)closePtz(); const generation=++ptzGeneration;ptzCamera=id;ptzReady=false;$('#ptz-panel').hidden=false; $('#ptz-title').textContent=(state.cameras.find(c=>c.id===id)?.name||'摄像头')+' · 云台与预置位'; $('#ptz-status').textContent='正在读取云台能力…';setPtzBusy(false);loadPresets(); try{const caps=await api('/api/ptz?camera='+id);if(generation!==ptzGeneration||!loggedIn)return;ptzReady=true;$('#ptz-status').textContent=caps.note;setPtzBusy(ptzBusy);} catch(e){if(!e.stale&&generation===ptzGeneration)$('#ptz-status').textContent=e.message;} } async function ptzCommand(action){ if(!ptzCamera||(!ptzReady&&action!=='stop')||(ptzBusy&&action!=='stop'))return; const id=ptzCamera,generation=ptzGeneration,command=++ptzCommandGeneration; setPtzBusy(true);$('#ptz-status').textContent=action==='stop'?'正在停止…':'正在微调…'; try{await api('/api/ptz',{camera:id,action});if(generation===ptzGeneration&&command===ptzCommandGeneration)$('#ptz-status').textContent='停止指令已确认,可继续微调';} catch(e){if(!e.stale&&generation===ptzGeneration&&command===ptzCommandGeneration)$('#ptz-status').textContent=e.message;} finally{if(generation===ptzGeneration&&command===ptzCommandGeneration)setPtzBusy(false);} } function confirmPreset(action,token){ if(!isAdmin()||ptzBusy||!ptzPresets)return; const preset=ptzPresets.items.find(p=>p.token===token);if(!preset)return; ptzConfirmation={action,token,name:preset.name}; $('#preset-confirm-text').textContent=`删除“${preset.name}”?设备中的这个预置位会被移除。`; $('#preset-confirmation').hidden=false; } async function presetCommand(action,token,name){ if(ptzBusy||!ptzCamera||!ptzPresets)return; const id=ptzCamera,generation=ptzGeneration,command=++ptzCommandGeneration,revision=ptzPresets.revision; clearPresetConfirmation();setPtzBusy(true);$('#preset-status').textContent=action==='goto'?'正在转到预置位,可随时点击停止…':'正在更新设备预置位…'; try{const result=await api('/api/ptz/presets',{camera:id,action,token,name,revision}); if(generation!==ptzGeneration||command!==ptzCommandGeneration||!loggedIn)return; if(result.presets){renderPresets(result.presets);$('#preset-form').reset();} $('#preset-status').textContent=result.message; }catch(e){if(!e.stale&&generation===ptzGeneration&&command===ptzCommandGeneration){ptzPresets=null;$('#preset-list').replaceChildren();$('#preset-form').hidden=true;$('#preset-status').textContent=e.message+'。请重新读取后核对。';}} finally{if(generation===ptzGeneration&&command===ptzCommandGeneration)setPtzBusy(false);} } function closePtz(){ const id=ptzCamera;if(id&&ptzBusy&&loggedIn)api('/api/ptz',{camera:id,action:'stop'}).catch(()=>{}); ptzGeneration++;ptzPresetGeneration++;ptzCommandGeneration++;ptzCamera='';ptzPresets=null;ptzBusy=false;ptzReady=false; $('#ptz-panel').hidden=true;$('#preset-list').replaceChildren();$('#preset-form').reset();clearPresetConfirmation(); } $('#ptz-close').onclick=closePtz; $$('[data-ptz-action]').forEach(b=>b.onclick=()=>ptzCommand(b.dataset.ptzAction)); $('#preset-reload').onclick=loadPresets; $('#preset-form').onsubmit=e=>{e.preventDefault();presetCommand('create',undefined,$('#preset-name').value);}; $('#preset-confirm-cancel').onclick=clearPresetConfirmation; $('#preset-confirm-yes').onclick=()=>{if(ptzConfirmation){const p=ptzConfirmation;presetCommand(p.action,p.token,p.name);}}; $('#preset-list').onclick=e=>{const b=e.target.closest('button');if(!b)return;if(b.dataset.presetGoto)presetCommand('goto',b.dataset.presetGoto);if(b.dataset.presetRemove)confirmPreset('remove',b.dataset.presetRemove);}; document.addEventListener('visibilitychange',()=>{if(document.hidden&&ptzCamera&&ptzBusy)ptzCommand('stop');}); window.addEventListener('pagehide',()=>{if(ptzCamera&&ptzBusy)fetch('/api/ptz',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({camera:ptzCamera,action:'stop'})}).catch(()=>{});});