From 136613bc120bdb89f44d1399ecdacd3e23ebb78f Mon Sep 17 00:00:00 2001 From: butubb <1422726308@qq.com> Date: Tue, 11 Aug 2026 14:12:52 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=88=97=E8=A1=A8=E7=BB=9F?= =?UTF-8?q?=E4=B8=80=E4=B8=BA=20setListPager=20=E6=90=9C=E7=B4=A2+?= =?UTF-8?q?=E5=88=86=E9=A1=B5+=E6=8E=92=E5=BA=8F=EF=BC=88=5FlpSort=20?= =?UTF-8?q?=E5=8D=87=E5=BA=8F/=E9=99=8D=E5=BA=8F/=E5=8F=96=E6=B6=88?= =?UTF-8?q?=EF=BC=8C=E4=B8=AD=E6=96=87=E6=95=B0=E5=AD=97=E6=84=9F=E7=9F=A5?= =?UTF-8?q?=EF=BC=8C=E7=A9=BA=E5=80=BC=E6=8E=92=E5=90=8E=EF=BC=89=EF=BC=9B?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=E8=AE=BE=E5=A4=87=E8=A1=A8=E5=B9=B6=E5=85=A5?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E6=8E=92=E5=BA=8F=EF=BC=9BSTF=E8=AE=BE?= =?UTF-8?q?=E5=A4=87=E7=AE=A1=E7=90=86/=E5=BA=94=E7=94=A8=E7=89=88?= =?UTF-8?q?=E6=9C=AC=E7=AE=A1=E7=90=86=E6=8E=A5=E5=85=A5=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/admin/monitor.js | 197 +++++++++++++++++++++-------------- templates/admin/monitor.html | 46 ++++---- 2 files changed, 148 insertions(+), 95 deletions(-) diff --git a/static/admin/monitor.js b/static/admin/monitor.js index d38b362..4593dd0 100644 --- a/static/admin/monitor.js +++ b/static/admin/monitor.js @@ -60,31 +60,9 @@ async function apiPut(url,body){ } // ================== 设备列表排序与多选 ================== -let _sortKey=''; // 当前排序字段 -let _sortDir=''; // '' | 'asc' | 'desc' let _selectedSerials=new Set(); // 已勾选的 serial 集合 -// 点击表头排序 -function sortBy(key){ - if(_sortKey===key){ - // 同一列:切换方向;已是降序则取消 - if(_sortDir==='asc'){_sortDir='desc';} - else if(_sortDir==='desc'){_sortKey='';_sortDir='';} - else{_sortDir='asc';} - }else{ - _sortKey=key;_sortDir='asc'; - } - // 更新表头样式 - document.querySelectorAll('.table thead th.sortable').forEach(th=>{ - th.classList.remove('sort-asc','sort-desc'); - if(th.dataset.key===_sortKey){ - th.classList.add(_sortDir==='asc'?'sort-asc':(_sortDir==='desc'?'sort-desc':'')); - } - }); - renderDevices(); -} - -// 排序比较:从设备对象取排序值 +// 排序比较:从设备对象取排序值(统一排序用,见 setListPager/sortValue) function _sortValue(dev,key){ let v; if(key==='progress'){ @@ -351,6 +329,8 @@ function toggleClipDevice(cb){ } // ================== 工具:应用版本管理 ================== +let _appVerRows = []; // [{serial, installed, version_name, version_code, error}] + async function queryAppVers(){ const pkg = document.getElementById('appver-pkg').value.trim(); if(!pkg){showToast('请输入包名','error');return;} @@ -359,24 +339,41 @@ async function queryAppVers(){ try{ const r = await apiPost('/api/tools/appver', {pkg}); if(!r || !r.ok){showToast('查询失败: '+(r&&r.error||''),'error');return;} - const rows = Object.entries(r.results||{}).map(([serial,v])=>{ - if(v.error)return ''+esc(serial)+''+ - '查询失败'+esc(v.error)+''; - if(v.installed)return ''+esc(serial)+''+ - '已安装'+ - ''+esc(v.version_name||'')+''+esc(v.version_code||'')+''; - return ''+esc(serial)+''+ - '未安装--'; - }).join(''); - document.getElementById('tb-appver').innerHTML = rows || '无可用设备'; + _appVerRows = Object.entries(r.results||{}).map(([serial,v])=>({serial, ...v})); + setListPager('appver',{ + data:()=>_appVerRows, + filter:(x,kw)=>((x.serial||'').toLowerCase().includes(kw)||(x.version_name||'').toLowerCase().includes(kw)||(x.version_code||'').toLowerCase().includes(kw)), + sortValue:(x,k)=>{ + if(k==='state'){ if(x.error)return 2; return x.installed?0:1; } + if(k==='code')return parseInt(x.version_code)||0; + if(k==='version')return x.version_name||''; + return x.serial||''; + }, + render:renderAppVerRow, + tbody:'tb-appver',pager:'pager-appver',pageSize:10, + empty:'无可用设备' + }); + applyListPager('appver'); + initListSearch('appver','search-appver'); document.getElementById('appver-status').textContent = '共 '+(r.total||0)+' 台设备'+(r.fail?','+r.fail+' 台查询失败':''); if(r.fail)showToast(r.fail+' 台设备查询失败,详见表格','error'); } finally { btn.disabled = false; btn.textContent = old; } } +function renderAppVerRow(x){ + if(x.error)return ''+esc(x.serial)+''+ + '查询失败'+esc(x.error)+''; + if(x.installed)return ''+esc(x.serial)+''+ + '已安装'+ + ''+esc(x.version_name||'')+''+esc(x.version_code||'')+''; + return ''+esc(x.serial)+''+ + '未安装--'; +} // ================== 工具:STF 设备管理 ================== +let _stfDevRows = []; // [{ip, inCfg, state}] + async function loadStfDevMgmt(){ const status = document.getElementById('stfdev-status'); const r = await apiGet('/api/stf/devmgmt/status'); @@ -387,26 +384,34 @@ async function loadStfDevMgmt(){ const s = d.serial; connected[s.endsWith(':5555') ? s.slice(0,-5) : s] = d.state; }); - const ips = Array.from(new Set(configured.concat(Object.keys(connected)))); - const tbody = document.getElementById('tb-stfdev'); - if(!ips.length){ - tbody.innerHTML='无设备(脚本未配置、220 adb 也无连接)'; - status.textContent=''; - return; - } - tbody.innerHTML = ips.map(ip=>{ - const inCfg = configured.includes(ip); - const st = connected[ip]; - const stHtml = st==='device' - ? '已连接' - : (st ? ''+esc(st)+'' : '未连接'); - return ''+esc(ip)+''+ - ''+(inCfg?'脚本中':'未配置')+''+ - ''+stHtml+''+ - ''; - }).join(''); + _stfDevRows = Array.from(new Set(configured.concat(Object.keys(connected)))).map(ip=>({ + ip, inCfg: configured.includes(ip), state: connected[ip]||'' + })); + setListPager('stfdev',{ + data:()=>_stfDevRows, + filter:(x,kw)=>x.ip.toLowerCase().includes(kw), + sortValue:(x,k)=>{ + if(k==='cfg')return x.inCfg?1:0; + if(k==='state'){ const o={device:0,offline:1,unauthorized:2,'':9}; return (o[x.state]!==undefined)?o[x.state]:9; } + return x.ip||''; + }, + render:renderStfDevRow, + tbody:'tb-stfdev',pager:'pager-stfdev',pageSize:10, + empty:'无设备(脚本未配置、220 adb 也无连接)' + }); + applyListPager('stfdev'); + initListSearch('stfdev','search-stfdev'); status.textContent = '脚本配置 '+configured.length+' 台,已连接 '+Object.keys(connected).length+' 台'; } +function renderStfDevRow(x){ + const stHtml = x.state==='device' + ? '已连接' + : (x.state ? ''+esc(x.state)+'' : '未连接'); + return ''+esc(x.ip)+''+ + ''+(x.inCfg?'脚本中':'未配置')+''+ + ''+stHtml+''+ + ''; +} async function addStfDev(){ const ip = document.getElementById('stfdev-ip').value.trim(); @@ -449,18 +454,22 @@ async function injectClipboard(){ } } -// ================== 通用列表:搜索 + 分页 ================== +// ================== 通用列表:搜索 + 分页 + 排序 ================== // 用法: // setListPager(key,{data:()=>array, filter:(item,kw)=>bool, render:(item)=>html, -// tbody:'tb-id', pager:'pager-id', pageSize:N}); -// applyListPager(key); // 数据变化后重渲染(含搜索/翻页状态) -// initListSearch(key,'search-id');// 搜索框绑定 +// tbody:'tb-id', pager:'pager-id', pageSize:N, +// sortValue:(item,colKey)=>值, // 可排序列的取值函数 +// empty:'...'}); +// applyListPager(key); // 数据变化后重渲染(含搜索/翻页/排序状态) +// initListSearch(key,'search-id'); // 搜索框绑定 +// _lpSort(key,'colKey'); // 表头排序:th 加 data-sort="colKey" + onclick, +// // 再点切换升降序,第三次取消;排序指示器自动更新 var _LIST_PAGERS={}; -function _lp(key){ return _LIST_PAGERS[key] || (_LIST_PAGERS[key]={page:1,kw:'',cfg:null}); } +function _lp(key){ return _LIST_PAGERS[key] || (_LIST_PAGERS[key]={page:1,kw:'',cfg:null,sort:null}); } function setListPager(key,cfg){ - // 只更新配置,不重置 page/kw —— 监控列表 5 秒自动刷新时保持用户当前页 + // 只更新配置,不重置 page/kw/sort —— 监控列表 5 秒自动刷新时保持用户当前状态 var st=_lp(key); st.cfg=cfg; } function initListSearch(key,inputId){ @@ -471,6 +480,17 @@ function initListSearch(key,inputId){ var st=_lp(key); st.kw=this.value.trim(); st.page=1; applyListPager(key); }); } +function _lpSort(key,colKey){ + var st=_lp(key); + if(st.sort&&st.sort.key===colKey){ + if(st.sort.dir===1)st.sort.dir=-1; // 升序 → 降序 + else if(st.sort.dir===-1)st.sort=null; // 降序 → 取消排序 + else st.sort={key:colKey,dir:1}; + }else{ + st.sort={key:colKey,dir:1}; + } + applyListPager(key); +} function applyListPager(key){ var st=_lp(key); if(!st.cfg)return; @@ -478,6 +498,18 @@ function applyListPager(key){ var all=(cfg.data&&cfg.data())||[]; var kw=st.kw.toLowerCase(); var filtered=kw?all.filter(function(x){return cfg.filter(x,kw);}):all; + // 排序:cfg.sortValue 取列值;空值/缺失恒排最后;字符串用中文数字感知比较(10 排在 9 后) + if(st.sort&&cfg.sortValue){ + var sk=st.sort.key, dir=st.sort.dir; + filtered=filtered.slice().sort(function(a,b){ + var va=cfg.sortValue(a,sk), vb=cfg.sortValue(b,sk); + if(va===vb)return 0; + if(va==null||va==='')return 1; + if(vb==null||vb==='')return -1; + if(typeof va==='number'&&typeof vb==='number')return (va-vb)*dir; + return String(va).localeCompare(String(vb),'zh-Hans-CN',{numeric:true})*dir; + }); + } var pageSize=cfg.pageSize||10; var pages=Math.max(1,Math.ceil(filtered.length/pageSize)); if(st.page>pages)st.page=pages; @@ -485,7 +517,18 @@ function applyListPager(key){ var start=(st.page-1)*pageSize; var slice=filtered.slice(start,start+pageSize); var tb=document.getElementById(cfg.tbody); - if(tb)tb.innerHTML=slice.map(cfg.render).join('')||(cfg.empty||'无数据'); + if(tb){ + tb.innerHTML=slice.map(cfg.render).join('')||(cfg.empty||'无数据'); + // 表头排序指示器(th[data-sort],所有表格统一) + if(cfg.sortValue){ + var table=tb.closest('table'); + if(table)table.querySelectorAll('thead th[data-sort]').forEach(function(th){ + th.classList.add('sortable'); + th.classList.toggle('sort-asc',!!st.sort&&st.sort.key===th.dataset.sort&&st.sort.dir===1); + th.classList.toggle('sort-desc',!!st.sort&&st.sort.key===th.dataset.sort&&st.sort.dir===-1); + }); + } + } var pg=document.getElementById(cfg.pager); if(pg)pg.innerHTML=_lpPagerHtml(key,pages,st.page,filtered.length,pageSize); } @@ -595,33 +638,20 @@ async function loadSummary(){ }catch(e){console.error(e);} } -// 渲染设备表格(带排序+搜索+分页,复选状态保持) -let _sortedDevices=[]; +// 渲染设备表格(统一排序+搜索+分页,复选状态保持) let _devicesSig=''; function renderDevices(){ if(!_statusData||!_statusData.devices){return;} - // 5秒自动刷新时若设备数据/分页/排序都没变化,跳过整表重渲染(避免DOM抖动) + // 5秒自动刷新时若设备数据/分页/搜索/排序都没变化,跳过整表重渲染(避免DOM抖动) const _st=_lp('devices'); - const _sig=JSON.stringify({d:_statusData.devices,p:_st.page,k:_st.kw,s:_sortKey+(_sortDir||'')}); + const _sig=JSON.stringify({d:_statusData.devices,p:_st.page,k:_st.kw,s:_st.sort}); if(_sig===_devicesSig)return; _devicesSig=_sig; - let devices=_statusData.devices.slice(); - // 排序 - if(_sortKey&&_sortDir){ - const dir=_sortDir==='asc'?1:-1; - devices.sort((a,b)=>{ - let va=_sortValue(a,_sortKey),vb=_sortValue(b,_sortKey); - if(typeof va==='string'&&typeof vb==='string'){ - return va.localeCompare(vb)*dir; - } - va=Number(va)||0;vb=Number(vb)||0; - return (va-vb)*dir; - }); - } - _sortedDevices=devices; + // 排序由 applyListPager 统一处理(sortValue + _lpSort) setListPager('devices',{ - data:()=>_sortedDevices, + data:()=>_statusData.devices||[], filter:(dev,kw)=>((dev.serial||'').toLowerCase().includes(kw)||(dev.model||'').toLowerCase().includes(kw)||(dev.foreground_app||'').toLowerCase().includes(kw)||(dev.task_job||'').toLowerCase().includes(kw)||(dev.current_action||'').toLowerCase().includes(kw)), + sortValue:_sortValue, render:renderDeviceRow, tbody:'tb-devices',pager:'pager-devices',pageSize:10, empty:'暂无设备' @@ -804,6 +834,7 @@ function renderTaskTable(){ setListPager('tasks',{ data:()=>jobs, filter:(j,kw)=>((j.name||'').toLowerCase().includes(kw)||(j.task_type||'').toLowerCase().includes(kw)||(j.id||'').toLowerCase().includes(kw)), + sortValue:(j,k)=>{ if(k==='id')return j.id||''; if(k==='enabled')return j.enabled?1:0; if(k==='next_run')return j.next_run||''; return j[k]||''; }, render:renderTaskRow, tbody:'tb-tasks',pager:'pager-tasks',pageSize:10, empty:'暂无任务,点击"新建任务"创建' @@ -2124,6 +2155,7 @@ async function loadCustomActions(){ setListPager('actions',{ data:()=>_customActions, filter:(a,kw)=>((a.name||'').toLowerCase().includes(kw)), + sortValue:(a,k)=>{ if(k==='steps')return (a.steps||[]).length; if(k==='created_at')return a.created_at||''; return a[k]||''; }, render:renderActionRow, tbody:'tb-actions',pager:'pager-actions',pageSize:10, empty:'暂无自定义动作,可点击"新建动作"或用任务编辑器☑选中步骤打包' @@ -2211,6 +2243,7 @@ function renderGroupTable(){ setListPager('groups',{ data:()=>groups, filter:(g,kw)=>((g.name||'').toLowerCase().includes(kw)||(g.description||'').toLowerCase().includes(kw)||(g.serials||[]).join(' ').toLowerCase().includes(kw)), + sortValue:(g,k)=>{ if(k==='count')return (g.serials||[]).length; return g[k]||''; }, render:renderGroupRow, tbody:'tb-groups',pager:'pager-groups',pageSize:10, empty:'暂无分组,点击"新建分组"创建' @@ -2315,6 +2348,7 @@ async function loadUsers(){ setListPager('users',{ data:()=>users, filter:(u,kw)=>(u.username||'').toLowerCase().includes(kw), + sortValue:(u,k)=>{ if(k==='is_admin')return u.is_admin?1:0; return u[k]||''; }, render:renderUserRow, tbody:'tb-users',pager:'pager-users',pageSize:10, empty:'暂无用户' @@ -2515,6 +2549,13 @@ async function loadTailscaleDevices(){ setListPager('tailscale',{ data:()=>_tsDevices, filter:(d,kw)=>((d.hostname||'').toLowerCase().includes(kw)||(d.name||'').toLowerCase().includes(kw)||(d.addresses||[]).join(',').includes(kw)), + sortValue:(d,k)=>{ + if(k==='ip')return (d.addresses||[])[0]||''; + if(k==='online'){ if(d.online===true)return 0; if(d.online===false)return 2; return 1; } // null=最近12h在线 + if(k==='authorized')return d.authorized?1:0; + if(k==='key_expiry')return d.key_expiry_disabled?1:0; + return d[k]||''; + }, render:renderTsRow, tbody:'tb-tailscale',pager:'pager-tailscale',pageSize:10, empty:'tailnet 暂无设备' @@ -2637,6 +2678,7 @@ async function loadApks(){ document.getElementById('apk-hint').textContent='共 '+apks.length+' 个应用'; setListPager('apks',{ data:()=>apks, + sortValue:(a,k)=>{ if(k==='size')return a.size||0; if(k==='upload_time')return a.upload_time||''; if(k==='package_name')return a.package_name||''; if(k==='version_name')return a.version_name||''; return a.display_name||''; }, filter:(a,kw)=>((a.display_name||'').toLowerCase().includes(kw)||(a.package_name||'').toLowerCase().includes(kw)), render:renderApkRow, tbody:'tb-apks',pager:'pager-apks',pageSize:10, @@ -2695,6 +2737,7 @@ async function loadDeviceApps(){ setListPager('devapps',{ data:()=>_devAppsData||[], filter:(a,kw)=>((a.label||a.package||'').toLowerCase().includes(kw)||(a.package||'').toLowerCase().includes(kw)||(a.path||'').toLowerCase().includes(kw)), + sortValue:(a,k)=>{ if(k==='version_code')return parseInt(a.version_code)||0; if(k==='version_name')return a.version_name||''; if(k==='label')return (a.label||a.package||''); return a[k]||''; }, render:renderDevAppRow, tbody:'tb-device-apps',pager:'pager-devapps',pageSize:20, empty:'无应用' diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index fedf88b..660a046 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -321,15 +321,15 @@ select.form-control{cursor:pointer} - - - - - - - - - + + + + + + + + + @@ -369,7 +369,7 @@ select.form-control{cursor:pointer}
设备型号STF任务状态前台App进度当前任务当前动作最近错误设备型号STF任务状态前台App进度当前任务当前动作最近错误 操作
- +
ID任务名类型启用目标调度下次运行重试操作ID任务名类型启用目标调度下次运行重试操作
@@ -385,7 +385,7 @@ select.form-control{cursor:pointer}
把常用步骤打包成可复用的动作,拖入任务步骤编辑器使用;也可在任务编辑器里"☑选中步骤→打包"生成
- +
图标动作名步骤数创建时间操作
图标动作名步骤数创建时间操作
@@ -403,7 +403,7 @@ select.form-control{cursor:pointer} - +
分组名描述设备数设备列表操作分组名描述设备数设备列表操作
@@ -445,7 +445,7 @@ select.form-control{cursor:pointer} - +
ID用户名管理员权限操作ID用户名管理员权限操作
@@ -561,7 +561,7 @@ select.form-control{cursor:pointer} - +
主机名显示名IP系统在线授权密钥过期操作主机名显示名IP系统在线授权密钥过期操作
点击"刷新设备"加载
@@ -582,7 +582,7 @@ select.form-control{cursor:pointer} - +
应用名包名版本大小上传时间操作应用名包名版本大小上传时间操作
@@ -595,14 +595,19 @@ select.form-control{cursor:pointer} + - + + + +
设备状态版本号versionCode设备状态版本号versionCode
输入包名后点击"查询版本"
+
查询所有设备(本地 adb 含 USB + STF 池)上该包名的安装情况与版本号,用于对比各设备 App 版本是否一致;未连接的 STF 设备会自动连接(绝不断开,不影响任务)。
@@ -619,7 +624,7 @@ select.form-control{cursor:pointer}
- + @@ -636,14 +641,19 @@ select.form-control{cursor:pointer} placeholder="设备 IP,如 100.100.10.20" onkeydown="if(event.key==='Enter')addStfDev()"> +
应用版本号versionCodeAPK 路径应用版本号versionCodeAPK 路径
选择设备后点击"查看应用"
- + + + +
设备脚本配置连接状态(220 adb)操作设备脚本配置连接状态(220 adb)操作
点击"刷新"加载
+
管理 220 上 OpenSTF 设备池:添加 = 写入 connect_devices.sh 的 DEVICES 列表并立即 adb connect; 移除 = 从脚本删除并 adb disconnect(设备从 STF 池下线,任务将不再分配到它)。