Files
auto_control/templates/admin/wall.html
T

220 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备监控大屏</title>
<style>
/* ========== 监控大屏:深色控制室风格 ========== */
@font-face{font-family:'Bricolage Grotesque';font-style:normal;font-weight:700;font-display:swap;src:url('/static/fonts/bricolage-700.woff2') format('woff2')}
@font-face{font-family:'Bricolage Grotesque';font-style:normal;font-weight:800;font-display:swap;src:url('/static/fonts/bricolage-800.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:400;font-display:swap;src:url('/static/fonts/plexmono-400.woff2') format('woff2')}
@font-face{font-family:'IBM Plex Mono';font-style:normal;font-weight:600;font-display:swap;src:url('/static/fonts/plexmono-600.woff2') format('woff2')}
:root{
--bg:#0d0f13; --card:#14181f; --card-line:#232936; --ink-line:#2a303c;
--text:#e8ecf2; --muted:#8b93a3; --faint:#5c6472;
--teal:#2dd4bf; --green:#34d399; --red:#f87171; --amber:#fbbf24; --indigo:#818cf8;
--display:'Bricolage Grotesque','PingFang SC','Microsoft YaHei',sans-serif;
--mono:'IBM Plex Mono',Menlo,Consolas,monospace;
--body:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%}
body{background:var(--bg);font-family:var(--body);color:var(--text);overflow:hidden}
/* 背景网格纹理 */
body::before{content:"";position:fixed;inset:0;pointer-events:none;
background-image:linear-gradient(rgba(45,212,191,.035) 1px,transparent 1px),
linear-gradient(90deg,rgba(45,212,191,.035) 1px,transparent 1px);
background-size:42px 42px}
/* 顶部栏 */
header{display:flex;align-items:center;gap:28px;padding:16px 26px 12px;border-bottom:1px solid var(--ink-line);background:rgba(13,15,19,.85);backdrop-filter:blur(6px);position:relative;z-index:5}
header .title{font-family:var(--display);font-size:22px;font-weight:800;letter-spacing:1px;color:#fff;display:flex;align-items:center;gap:12px}
header .title::before{content:"";width:10px;height:10px;border-radius:50%;background:var(--teal);box-shadow:0 0 0 0 rgba(45,212,191,.55);animation:pulse 2.2s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(45,212,191,.5)}70%{box-shadow:0 0 0 9px rgba(45,212,191,0)}100%{box-shadow:0 0 0 0 rgba(45,212,191,0)}}
header .stats{display:flex;gap:10px;flex:1}
header .stat{background:var(--card);border:1px solid var(--card-line);border-radius:10px;padding:7px 18px;text-align:center;min-width:86px}
header .stat .n{font-family:var(--mono);font-size:21px;font-weight:600;line-height:1.15}
header .stat .l{font-size:11px;color:var(--faint);margin-top:2px;letter-spacing:.4px}
header .stat.online .n{color:var(--green)}
header .stat.running .n{color:var(--teal)}
header .stat.error .n{color:var(--red)}
header .clock{font-family:var(--mono);font-size:19px;color:var(--muted);text-align:right;line-height:1.2}
header .clock .date{font-size:11px;color:var(--faint)}
/* 设备网格 */
main{position:relative;z-index:2;height:calc(100vh - 86px);overflow-y:auto;padding:18px 24px 30px;display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:16px;align-content:start}
/* 设备卡片 */
.card{background:var(--card);border:1px solid var(--card-line);border-radius:14px;overflow:hidden;position:relative;animation:cardIn .4s ease-out backwards;transition:border-color .2s,box-shadow .2s}
@keyframes cardIn{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
.card.online{border-color:#263142}
.card.running{border-color:rgba(45,212,191,.55);box-shadow:0 0 18px rgba(45,212,191,.12),inset 0 0 0 1px rgba(45,212,191,.2)}
.card.error{border-color:rgba(248,113,113,.5)}
.card.offline{opacity:.62}
.card .thumb{aspect-ratio:9/16;background:#0a0c10;position:relative;overflow:hidden}
.card .thumb img{width:100%;height:100%;object-fit:cover;display:block;transition:opacity .2s}
.card.offline .thumb img{display:none}
.card .thumb .ph{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;color:var(--faint);font-size:12px;background:repeating-linear-gradient(45deg,#0c0e12,#0c0e12 12px,#0f1218 12px,#0f1218 24px)}
.card .thumb .ph .ico{font-size:26px;opacity:.6}
/* 状态角标 */
.card .badge{position:absolute;top:8px;left:8px;display:flex;align-items:center;gap:6px;background:rgba(13,15,19,.78);backdrop-filter:blur(4px);border-radius:99px;padding:3px 10px;font-size:11px;font-weight:600;color:var(--muted);border:1px solid var(--ink-line)}
.card.running .badge{color:var(--teal);border-color:rgba(45,212,191,.4)}
.card.error .badge{color:var(--red)}
.card .badge .dot{width:7px;height:7px;border-radius:50%;background:var(--faint)}
.card.online .badge .dot{background:var(--green);box-shadow:0 0 6px rgba(52,211,153,.7)}
.card.running .badge .dot{background:var(--teal);box-shadow:0 0 7px rgba(45,212,191,.8);animation:pulse 1.6s infinite}
.card.error .badge .dot{background:var(--red);box-shadow:0 0 6px rgba(248,113,113,.7)}
.card.offline .badge .dot{background:var(--faint)}
/* 信息区 */
.card .info{padding:10px 12px 12px}
.card .info .serial{font-family:var(--mono);font-size:13px;font-weight:600;color:#eef2f7;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.card .info .model{font-size:11px;color:var(--faint);margin-top:2px}
.card .info .action{font-size:11.5px;color:var(--muted);margin-top:6px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-height:16px}
.card .info .action b{color:var(--amber);font-weight:600}
.card .info .fg{margin-top:4px}
.card .info .fg .tag{font-size:10.5px;background:#1c2430;color:#9fb3c8;border:1px solid var(--ink-line);border-radius:5px;padding:1px 7px;display:inline-block}
/* 进度条 */
.card .info .prog{margin-top:7px;height:4px;background:#1d242e;border-radius:99px;overflow:hidden}
.card .info .prog i{display:block;height:100%;background:linear-gradient(90deg,var(--teal),#5eead4);border-radius:99px;transition:width .5s}
.card .info .prog-txt{font-size:10px;color:var(--faint);margin-top:3px;font-family:var(--mono)}
/* 空状态 */
.empty-wall{grid-column:1/-1;text-align:center;color:var(--faint);padding:80px 0;font-size:14px}
/* 滚动条 */
::-webkit-scrollbar{width:8px}
::-webkit-scrollbar-thumb{background:#232936;border-radius:4px}
::-webkit-scrollbar-track{background:transparent}
</style>
</head>
<body>
<header>
<div class="title">设备监控大屏</div>
<div class="stats" id="wall-stats"></div>
<div class="clock"><span id="wall-clock">--:--:--</span><div class="date" id="wall-date"></div></div>
</header>
<main id="wall-grid"></main>
<script>
// ================== 监控大屏(深色控制室) ==================
// 状态每 5s 刷新;缩略图每 2.5s 轮询(页面不可见时暂停,省资源)
const STATUS_MAP={idle:'空闲',connecting:'连接中',running:'运行中',done:'完成',error:'异常',released:'已释放',failed:'失败'};
const APP_NAMES={'com.ss.android.ugc.aweme':'抖音','com.ss.android.ugc.aweme.lite':'抖音极速版','com.smile.gifmaker':'快手','com.kuaishou.nebula':'快手极速版','com.tencent.mm':'微信','com.tencent.mobileqq':'QQ','com.miui.home':'桌面','com.android.launcher':'桌面','com.android.settings':'设置','com.android.systemui':'系统'};
let _devices=[];
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
async function apiGet(url){
try{
const r=await fetch(url);
if(r.status===401){window.location='/login';return null;}
return await r.json();
}catch(e){return null;}
}
// 时钟
function tickClock(){
const now=new Date();
document.getElementById('wall-clock').textContent=
String(now.getHours()).padStart(2,'0')+':'+String(now.getMinutes()).padStart(2,'0')+':'+String(now.getSeconds()).padStart(2,'0');
document.getElementById('wall-date').textContent=
now.getFullYear()+'-'+String(now.getMonth()+1).padStart(2,'0')+'-'+String(now.getDate()).padStart(2,'0')+' '+['周日','周一','周二','周三','周四','周五','周六'][now.getDay()];
}
// 状态刷新(5s)
async function refreshStatus(){
const r=await apiGet('/api/status');
if(!r||!r.ok)return;
const devs=r.devices||[];
// 保持卡片顺序稳定:按 serial 排序
devs.sort((a,b)=>a.serial.localeCompare(b.serial));
const changed=JSON.stringify(devs.map(d=>[d.serial,d.present,d.worker_status,d.task_job,d.current_action,d.progress,d.foreground_app]))!==JSON.stringify(_devices.map(d=>[d.serial,d.present,d.worker_status,d.task_job,d.current_action,d.progress,d.foreground_app]));
_devices=devs;
renderStats(devs);
if(changed)renderGrid(devs);
}
function renderStats(devs){
const online=devs.filter(d=>d.present).length;
const running=devs.filter(d=>['running','connecting'].includes(d.worker_status)).length;
const error=devs.filter(d=>['error','failed'].includes(d.worker_status)).length;
document.getElementById('wall-stats').innerHTML=
'<div class="stat"><div class="n">'+devs.length+'</div><div class="l">设备总数</div></div>'+
'<div class="stat online"><div class="n">'+online+'</div><div class="l">在线</div></div>'+
'<div class="stat running"><div class="n">'+running+'</div><div class="l">运行中</div></div>'+
'<div class="stat error"><div class="n">'+error+'</div><div class="l">异常</div></div>';
}
function cardCls(d){
if(['error','failed'].includes(d.worker_status))return 'error';
if(['running','connecting'].includes(d.worker_status))return 'running';
return d.present?'online':'offline';
}
function badgeText(d){
if(['running','connecting'].includes(d.worker_status))return STATUS_MAP[d.worker_status]||d.worker_status;
if(['error','failed'].includes(d.worker_status))return STATUS_MAP[d.worker_status]||d.worker_status;
return d.present?'在线':'离线';
}
function fmtProgress(p){
if(!p||!p.total)return '';
const pct=Math.min(100,Math.round((p.done||0)/p.total*100));
return '<div class="prog"><i style="width:'+pct+'%"></i></div>'+
'<div class="prog-txt">'+(p.done||0)+'/'+p.total+(p.unit?' '+esc(p.unit):'')+' · '+pct+'%</div>';
}
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 fg=d.foreground_app&&d.foreground_app!=='空闲'&&!String(d.foreground_app).startsWith('(')
?'<div class="fg"><span class="tag">'+(APP_NAMES[d.foreground_app]?esc(APP_NAMES[d.foreground_app]):esc(d.foreground_app))+'</span></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*35)+'ms">'+
'<div class="thumb">'+
'<img data-thumb="'+serial+'" alt="">'+
'<div class="ph" style="display:none"><span class="ico">📱</span><span>'+serial+'</span></div>'+
'<div class="badge"><span class="dot"></span>'+badgeText(d)+'</div>'+
'</div>'+
'<div class="info">'+
'<div class="serial">'+serial+'</div>'+
'<div class="model">'+esc(d.model||'')+'</div>'+
action+fg+fmtProgress(d.progress)+
'</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';}
});
refreshThumbs();
}
// 缩略图轮询(2.5s,页面不可见时暂停)
let _thumbTimer=null;
function refreshThumbs(){
if(document.visibilityState!=='visible')return;
const now=Date.now();
document.querySelectorAll('img[data-thumb]').forEach(img=>{
img.src='/api/screen/thumb?serial='+encodeURIComponent(img.dataset.thumb)+'&t='+now;
});
}
setInterval(()=>{if(document.visibilityState==='visible')refreshThumbs();},2500);
setInterval(()=>{if(document.visibilityState==='visible')refreshStatus();},5000);
// 初始化
tickClock();setInterval(tickClock,1000);
refreshStatus();
document.addEventListener('visibilitychange',()=>{
if(document.visibilityState==='visible'){refreshStatus();refreshThumbs();}
});
</script>
</body>
</html>