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}
|
- 设备 |
- 型号 |
- STF |
- 任务状态 |
- 前台App |
- 进度 |
- 当前任务 |
- 当前动作 |
- 最近错误 |
+ 设备 |
+ 型号 |
+ STF |
+ 任务状态 |
+ 前台App |
+ 进度 |
+ 当前任务 |
+ 当前动作 |
+ 最近错误 |
操作 |
@@ -369,7 +369,7 @@ select.form-control{cursor:pointer}
- | 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}