refactor: web_server 模块化拆分(web/ 蓝图包 auth/monitor/tasks/admin/tools/devices/apks/tailscale + context/common,web_server 1796→193 行仅留装配与启动)+ 大屏优化(网格 5s 轮询、弹窗独占缩略图、流支持 q/fps)+ 三修复(MIUI 屏幕状态检测改 dumpsys power、补 ADB_PATH 导入修复一键亮屏、renderGrid 增量渲染不重载缩略图)

This commit is contained in:
2026-08-19 14:55:29 +08:00
parent e07b339a51
commit ce47a5b83d
16 changed files with 1922 additions and 1780 deletions
+73 -34
View File
@@ -239,50 +239,89 @@ function fmtProgress(p){
}
function shortIp(serial){return String(serial||'').replace(/:5555$/,'');}
function cardHtml(d, i){
const cls=cardCls(d);
const serial=esc(d.serial);
const name=esc(d.device_name||d.model||'设备');
const ip=esc(shortIp(d.serial));
const st=badgeText(d);
const stTag=_screenState[d.serial]==='off'?'<div class="screen-tag off">🌙</div>':'';
const action=d.current_action
?'<div class="action">'+(d.task_job?'<b>'+esc(d.task_job)+'</b> · ':'')+esc(d.current_action)+'</div>'
:(d.task_job?'<div class="action"><b>'+esc(d.task_job)+'</b></div>':'<div class="action"></div>');
return '<div class="card '+cls+'" data-serial="'+serial+'" style="animation-delay:'+(i*30)+'ms" onclick="openCtrl(\''+serial+'\')">'+
'<div class="thumb">'+
'<img data-thumb="'+serial+'" alt="">'+
'<div class="ph"><span class="ico">📱</span><span>'+serial+'</span></div>'+
'<div class="badge"><span class="dot"></span>'+st+'</div>'+stTag+
'</div>'+
'<div class="info">'+
'<div class="name">'+name+'</div>'+
'<div class="ip">'+ip+'</div>'+
'<div class="model">'+esc(d.model||'')+'</div>'+
action+fmtProgress(d.progress)+
'<div class="hint">点击进入设备操作</div>'+
'</div>'+
'</div>';
}
// 增量渲染:已有卡片只更新信息区(不重建 img,缩略图不闪不重载);增删设备才动 DOM
function renderGrid(devs){
const grid=document.getElementById('wall-grid');
if(!devs.length){
grid.innerHTML='<div class="empty-wall">设备池为空,请到「工具 → 设备池管理」添加设备</div>';
return;
}
grid.innerHTML=devs.map((d,i)=>{
const cls=cardCls(d);
const serial=esc(d.serial);
const name=esc(d.device_name||d.model||'设备');
const ip=esc(shortIp(d.serial));
const st=badgeText(d);
const stTag=_screenState[d.serial]==='off'?'<div class="screen-tag off">🌙</div>':'';
const action=d.current_action
?'<div class="action">'+(d.task_job?'<b>'+esc(d.task_job)+'</b> · ':'')+esc(d.current_action)+'</div>'
:(d.task_job?'<div class="action"><b>'+esc(d.task_job)+'</b></div>':'<div class="action"></div>');
return '<div class="card '+cls+'" data-serial="'+serial+'" style="animation-delay:'+(i*30)+'ms" onclick="openCtrl(\''+serial+'\')">'+
'<div class="thumb">'+
'<img data-thumb="'+serial+'" alt="">'+
'<div class="ph"><span class="ico">📱</span><span>'+serial+'</span></div>'+
'<div class="badge"><span class="dot"></span>'+st+'</div>'+stTag+
'</div>'+
'<div class="info">'+
'<div class="name">'+name+'</div>'+
'<div class="ip">'+ip+'</div>'+
'<div class="model">'+esc(d.model||'')+'</div>'+
action+fmtProgress(d.progress)+
'<div class="hint">点击进入设备操作</div>'+
'</div>'+
'</div>';
}).join('');
// 离线卡片显示占位
grid.querySelectorAll('.card').forEach(c=>{
const d=devs.find(x=>x.serial===c.dataset.serial);
const img=c.querySelector('img[data-thumb]');
const ph=c.querySelector('.ph');
if(d&&d.present&&img){img.style.display='block';ph.style.display='none';}
else if(ph){ph.style.display='flex';}
const existing={};
grid.querySelectorAll('.card').forEach(c=>{existing[c.dataset.serial]=c;});
// 1. 移除已不存在的卡片
Object.keys(existing).forEach(s=>{
if(!devs.find(d=>d.serial===s))existing[s].remove();
});
// 2. 更新/新建
devs.forEach((d,i)=>{
const serial=d.serial;
const card=existing[serial];
if(card){
// 已有卡片:只更新状态类信息(badge/class/信息区),不动 img
card.className='card '+cardCls(d);
const badge=card.querySelector('.badge');
const bt=badgeText(d);
if(badge&&badge.textContent!==bt)badge.innerHTML='<span class="dot"></span>'+bt;
// 熄屏角标
const tag=card.querySelector('.screen-tag');
if(_screenState[serial]==='off'&&!tag)card.querySelector('.thumb').insertAdjacentHTML('beforeend','<div class="screen-tag off">🌙</div>');
else if(_screenState[serial]!=='off'&&tag)tag.remove();
// 信息区
card.querySelector('.name').textContent=d.device_name||d.model||'设备';
card.querySelector('.ip').textContent=shortIp(serial);
card.querySelector('.model').textContent=d.model||'';
const action=d.current_action
?(d.task_job?'<b>'+esc(d.task_job)+'</b> · ':'')+esc(d.current_action)
:(d.task_job?'<b>'+esc(d.task_job)+'</b>':'');
card.querySelector('.action').innerHTML=action;
// 进度
const progWrap=card.querySelector('.prog-wrap');
if(progWrap)progWrap.innerHTML=fmtProgress(d.progress);
else if(d.progress&&d.progress.total)card.querySelector('.info').insertAdjacentHTML('beforeend','<div class="prog-wrap">'+fmtProgress(d.progress)+'</div>');
// 离线占位切换
const img=card.querySelector('img[data-thumb]');
const ph=card.querySelector('.ph');
if(d.present){if(img)img.style.display='block';if(ph)ph.style.display='none';}
else{if(img)img.style.display='none';if(ph)ph.style.display='flex';}
}else{
grid.insertAdjacentHTML('beforeend',cardHtml(d,i));
const nc=grid.lastElementChild;
if(!d.present)nc.querySelector('.ph').style.display='flex';
}
});
refreshThumbs();
}
// 缩略图轮询(2.5s):fetch 读取 X-Screen-State 头 → blob 喂 img
async function refreshThumbs(){
// 进入设备操作弹窗后:停止全部缩略图获取,资源全部给当前设备的高清流
if(_ctrlSerial)return;
if(document.visibilityState!=='visible')return;
const now=Date.now();
const imgs=[...document.querySelectorAll('img[data-thumb]')];
@@ -343,7 +382,7 @@ function closeCtrl(){
function startCtrlStream(){
if(!_ctrlSerial)return;
const img=document.getElementById('ctrl-img');
img.src='/api/screen/stream?serial='+encodeURIComponent(_ctrlSerial)+'&t='+Date.now();
img.src='/api/screen/stream?serial='+encodeURIComponent(_ctrlSerial)+'&q=85&fps=10&t='+Date.now(); // 弹窗高清高帧
}
function updateCtrlHead(){
const d=_devices.find(x=>x.serial===_ctrlSerial);
@@ -414,7 +453,7 @@ function ctrlSendText(){
})();
// 定时器
setInterval(()=>{if(document.visibilityState==='visible')refreshThumbs();},2500);
setInterval(()=>{if(document.visibilityState==='visible')refreshThumbs();},5000); // 网格低帧率(省资源)
setInterval(()=>{if(document.visibilityState==='visible')refreshStatus();},5000);
// 初始化