diff --git a/templates/admin/monitor.html b/templates/admin/monitor.html index 71495c8..ac302a1 100644 --- a/templates/admin/monitor.html +++ b/templates/admin/monitor.html @@ -54,6 +54,11 @@ body{background:var(--bg);font-family:-apple-system,"Segoe UI",Roboto,"PingFang .btn-sm{padding:4px 12px;font-size:12px} .btn-xs{padding:3px 10px;font-size:12px} +/* 通用列表分页栏 */ +.list-pager{display:flex;align-items:center;gap:6px;flex-wrap:wrap;margin:8px 0 20px} +.list-pager .lp-total{font-size:12px;color:var(--text-light);margin-right:auto} +.list-search{width:240px} + /* 区块标题 */ .section-title{font-size:15px;font-weight:600;color:var(--text);margin:0 0 12px;padding-bottom:8px;border-bottom:2px solid var(--primary);display:inline-block} @@ -202,6 +207,8 @@ select.form-control{cursor:pointer} .step-card.group-block .sc-ico{background:#8b5cf6} .step-card.selected{border-color:var(--primary);box-shadow:0 0 0 2px rgba(59,130,246,.2)} .step-children{margin-left:20px;margin-top:6px;border-left:2px dashed var(--border);padding-left:8px} +.step-children.drag-over{border-left-color:var(--primary);background:rgba(59,130,246,.06);border-radius:6px} +#se-canvas.drag-over{outline:2px dashed var(--primary);outline-offset:2px;border-radius:8px} .sel-picker{display:flex;gap:4px;align-items:center} .sel-picker input{flex:1} .sel-picker .btn-pick{padding:5px 8px;font-size:11px;white-space:nowrap} @@ -287,6 +294,7 @@ select.form-control{cursor:pointer} + · 每 5 秒自动刷新 · · 未选中 @@ -305,6 +313,7 @@ select.form-control{cursor:pointer}
+
任务运行概况
@@ -317,6 +326,7 @@ select.form-control{cursor:pointer}
+
@@ -324,6 +334,23 @@ select.form-control{cursor:pointer}
+
+ + +
+
自定义动作管理
+
把常用步骤打包成可复用的动作,拖入任务步骤编辑器使用;也可在任务编辑器里"☑选中步骤→打包"生成
+
+ + + +
+ + + +
图标动作名步骤数创建时间操作
+
+
@@ -336,6 +363,7 @@ select.form-control{cursor:pointer} + @@ -344,6 +372,32 @@ select.form-control{cursor:pointer}
+
+ + +
+
设备已装应用
+
查看某台设备上安装的所有应用(含版本号)
+
+ + + + +
+
+ + + + + + + +
应用版本号versionCodeAPK 路径
选择设备后点击"查看应用"
+
+
+
@@ -353,6 +407,7 @@ select.form-control{cursor:pointer}
+
@@ -360,6 +415,7 @@ select.form-control{cursor:pointer}
+
@@ -393,6 +449,7 @@ select.form-control{cursor:pointer}
+
@@ -400,6 +457,7 @@ select.form-control{cursor:pointer}
+
@@ -416,6 +474,18 @@ select.form-control{cursor:pointer} + + + '; - // 初始化步骤编辑器(generic_steps 时显示,其他隐藏) - _stepEditor.init(j.params&&j.params.steps?j.params.steps:[],j.params&&j.params.max_duration?j.params.max_duration:0); + // 初始化步骤编辑器(generic_steps 时显示,其他隐藏);jobId 作为草稿 key + _stepEditor.init(j.params&&j.params.steps?j.params.steps:[],j.params&&j.params.max_duration?j.params.max_duration:0,jobId||''); document.getElementById('modal-footer').innerHTML= ''+ @@ -1021,7 +1167,7 @@ var STEP_LIB=[ {type:'click',label:'点击元素',icon:'✦',params:{selector_type:'xpath',selector_value:'',wait_timeout:2}}, {type:'input_text',label:'输入文字',icon:'⌨',params:{mode:'random',texts:'你好\n有趣\n支持',fixed_text:''}}, {type:'wait',label:'等待',icon:'⏱',params:{min:1.0,max:3.0}}, - {type:'loop',label:'循环块',icon:'↻',params:{max_iterations:10,children:[]}}, + {type:'loop',label:'循环块',icon:'↻',params:{loop_mode:'rounds',max_iterations:10,loop_duration:600,children:[]}}, {type:'group',label:'动作组',icon:'📦',params:{children:[]}} ]; var _customActions=[]; // 后端加载的自定义动作 @@ -1041,15 +1187,39 @@ var _stepEditor={ _steps:[], _selected:[], _dragSuppressClick:false, - init:function(steps,maxDuration){ - this._steps=steps.length?JSON.parse(JSON.stringify(steps)):[]; + init:function(steps,maxDuration,draftKey,canvasId,libId){ + this._draftKey=draftKey||'new'; this._selected=[]; this._dragSuppressClick=false; + // 可指定编辑容器(任务编辑器 vs 动作编辑器),默认任务编辑器 + this._canvas=document.getElementById(canvasId||'se-canvas'); + this._lib=document.getElementById(libId||'se-lib'); + // 嵌套结构数组注册表:arr 唯一 id → 数组引用(修复多层级循环内移动错位) + this._arrCounter=0; + this._arrById={}; + this._arrIdMap=new WeakMap(); + // 恢复上次未保存的草稿(实时保存);没有则用传入的步骤 + var draft=this._loadDraft(); + if(draft&&Array.isArray(draft)){ + this._steps=draft; + }else{ + this._steps=steps.length?JSON.parse(JSON.stringify(steps)):[]; + } this._ensureIds(this._steps); this._renderLib(); this._renderCanvas(); _loadCustomActions(); - // max_duration 输入框(放在 se-head 旁边) + // 实时同步:画布内任何输入(含下拉/勾选)都同步回 _steps 并自动保存 + var canvas=this._canvas; + if(canvas&&!canvas.dataset.seBound){ + canvas.dataset.seBound='1'; + var self=this; + canvas.addEventListener('input',function(){self._syncFromDom();}); + } + if(draft&&Array.isArray(draft)){ + showToast('已恢复上次未保存的步骤编辑','success'); + } + // max_duration 输入框(只在任务编辑器注入,动作编辑不需要) var head=document.querySelector('#step-editor .se-head'); if(head&&!document.getElementById('f-max_duration')){ var md=document.createElement('span'); @@ -1068,7 +1238,8 @@ var _stepEditor={ }); }, _renderLib:function(){ - var lib=document.getElementById('se-lib'); + var lib=this._lib; + if(!lib)return; // 未初始化编辑器(如仅加载动作列表时)跳过 var items=STEP_LIB.map(function(s){ return '
'+ ''+s.icon+''+s.label+'
'; @@ -1115,12 +1286,14 @@ var _stepEditor={ }); }, _renderCanvas:function(){ - this._renderStepList(document.getElementById('se-canvas'),this._steps); + if(!this._canvas)return; + this._renderStepList(this._canvas,this._steps); }, _renderStepList:function(container,steps){ var self=this; if(!steps.length){ container.innerHTML='
拖拽左侧操作到这里
或点击操作库直接添加
'; + this._bindDropZone(container,steps); // 空时也要能接收拖入 return; } container.innerHTML=''; @@ -1148,10 +1321,10 @@ var _stepEditor={ ''+ ''+ ''+ - ''+ - ''+ - ''+ - ''+ + ''+ + ''+ + ''+ + ''+ ''; head.addEventListener('click',function(e){ if(e.target.tagName==='BUTTON')return; @@ -1165,7 +1338,7 @@ var _stepEditor={ card.appendChild(body); // 拖拽排序 card.addEventListener('dragstart',function(e){ - e.dataTransfer.setData('text/plain','move:'+idx+':'+self._path(parentArr)); + e.dataTransfer.setData('text/plain','move:'+idx+':'+self._arrId(parentArr)); e.dataTransfer.effectAllowed='move'; card.classList.add('dragging'); }); @@ -1177,6 +1350,7 @@ var _stepEditor={ card.addEventListener('dragleave',function(){card.classList.remove('drag-over');}); card.addEventListener('drop',function(e){ e.preventDefault(); + e.stopPropagation(); // 关键:阻止冒泡到外层卡片/容器,避免重复添加 card.classList.remove('drag-over'); self._handleDrop(e,parentArr,idx); }); @@ -1222,41 +1396,66 @@ var _stepEditor={ h+=''; h+='
需先用"点击元素"步骤定位到输入框,本步骤只负责输入文字
'; }else if(step.type==='loop'){ - h+='
'; - h+='
将下方子步骤(拖拽到本卡片内)循环执行指定次数
'; + h+='
'+ + '
'+ + '
'; + h+='
将下方子步骤(拖拽到本卡片内)循环执行指定轮次或时长;按时间时配合"运行时长上限"看预计结束时间
'; }else if(step.type==='group'){ h+='
动作组:按顺序执行下方子步骤一次。把多个步骤打包成一个可复用的整体。
'; h+='
把本组步骤保存到左侧自定义动作库,方便复用
'; } + // 共享:触发概率(所有步骤通用,0~100) + h+='
0~100,<100 表示按概率执行(如 30=隔几次才触发一次,适合"偶尔点赞")
'; // 可编辑 label - h+='
'; + h+='
'; return h; }, _bindDropZone:function(container,arr){ + // 常驻容器(#se-canvas)多次重渲染会累积 drop 监听 → 拖一次添加多个。 + // 用 dataset 标记只绑定一次;数组引用用 WeakMap 每次更新。 + if(!this._dropArrs)this._dropArrs=new WeakMap(); + this._dropArrs.set(container,arr); + if(container.dataset.dropBound)return; + container.dataset.dropBound='1'; var self=this; - // 画布空白处也能接收 drop - container.addEventListener('dragover',function(e){e.preventDefault();}); + // 画布空白处也能接收 drop,拖入时高亮提示 + container.addEventListener('dragover',function(e){ + e.preventDefault(); + container.classList.add('drag-over'); + }); + container.addEventListener('dragleave',function(e){ + if(!container.contains(e.relatedTarget))container.classList.remove('drag-over'); + }); container.addEventListener('drop',function(e){ e.preventDefault(); + e.stopPropagation(); // 阻止冒泡到外层卡片/容器,避免重复添加 + container.classList.remove('drag-over'); if(e.target===container||e.target.classList.contains('se-empty')){ - self._handleDrop(e,arr,-1); + self._handleDrop(e,self._dropArrs.get(container),-1); } }); }, _handleDrop:function(e,arr,idx){ + var self=this; var data=e.dataTransfer.getData('text/plain'); if(!data)return; + // drop 处理中同步重建 DOM 会打断拖拽事件,可能被浏览器重发 drop 导致重复添加。 + // 延迟到下一帧渲染,保证一次拖入只加一次。 + function rerender(){ setTimeout(function(){self._renderCanvas();},0); } if(data.startsWith('move:')){ // 内部排序 var parts=data.split(':'); var fromIdx=parseInt(parts[1]); var fromPath=parts[2]; - if(fromPath===this._path(arr)&&fromIdx!==idx){ + if(fromPath===this._arrId(arr)&&fromIdx!==idx){ + this._syncFromDom(); var moved=arr.splice(fromIdx,1)[0]; if(idx<0)arr.push(moved); else if(fromIdx=arr.length)arr.push(ns); else arr.splice(idx,0,ns); - this._renderCanvas(); + rerender(); } }else if(data.startsWith('type:')){ // 从系统操作库拖入 @@ -1279,7 +1478,7 @@ var _stepEditor={ var ns2=this._makeStep(lib); if(idx<0||idx>=arr.length)arr.push(ns2); else arr.splice(idx,0,ns2); - this._renderCanvas(); + rerender(); } } }, @@ -1289,12 +1488,14 @@ var _stepEditor={ addStep:function(type){ var lib=_findLib(type); if(!lib)return; + this._syncFromDom(); this._steps.push(this._makeStep(lib)); this._renderCanvas(); }, addCustomAction:function(actionId){ var action=_customActions.find(function(a){return a.id===actionId;}); if(!action||!action.steps||!action.steps.length)return; + this._syncFromDom(); var ns=this._makeStep({type:'group',label:action.name,icon:action.icon||'📦',params:{children:[]}}); ns.label=action.name; ns.params.children=JSON.parse(JSON.stringify(action.steps)); @@ -1310,6 +1511,7 @@ var _stepEditor={ }); }, toggleSelect:function(stepId){ + this._syncFromDom(); if(!this._selected)this._selected=[]; var i=this._selected.indexOf(stepId); if(i>=0)this._selected.splice(i,1); @@ -1368,8 +1570,32 @@ var _stepEditor={ g.style.display=g.dataset.mode===mode?'block':'none'; }); }, + _toggleLoopMode:function(sel){ + var body=sel.closest('.sc-body'); + var mode=sel.value; + body.querySelectorAll('[data-lmode]').forEach(function(g){ + g.style.display=g.dataset.lmode===mode?'block':'none'; + }); + }, + // 给数组分配唯一 id,用于 move/duplicate/remove/拖拽定位到正确的父数组 + _arrId:function(arr){ + if(arr===this._steps)return 'root'; + if(!this._arrIdMap)this._arrIdMap=new WeakMap(); + if(!this._arrById)this._arrById={}; + if(!this._arrIdMap.has(arr)){ + var id='arr_'+(++this._arrCounter); + this._arrIdMap.set(arr,id); + this._arrById[id]=arr; + } + return this._arrIdMap.get(arr); + }, + _resolveArr:function(path){ + if(path==='root')return this._steps; + return this._arrById&&this._arrById[path]; + }, move:function(idx,dir,path){ - var arr=path==='root'?this._steps:this._findLoopChildren(); + this._syncFromDom(); // 先同步 DOM 编辑,避免重渲染丢内容 + var arr=this._resolveArr(path); if(!arr)return; var ni=idx+dir; if(ni<0||ni>=arr.length)return; @@ -1377,7 +1603,8 @@ var _stepEditor={ this._renderCanvas(); }, duplicate:function(idx,path){ - var arr=path==='root'?this._steps:this._findLoopChildren(); + this._syncFromDom(); + var arr=this._resolveArr(path); if(!arr)return; var copy=JSON.parse(JSON.stringify(arr[idx])); copy.id=_newStepId(); @@ -1386,24 +1613,16 @@ var _stepEditor={ this._renderCanvas(); }, remove:function(idx,path){ - var arr=path==='root'?this._steps:this._findLoopChildren(); + this._syncFromDom(); + var arr=this._resolveArr(path); if(!arr)return; arr.splice(idx,1); this._renderCanvas(); }, - _findLoopChildren:function(){ - // 简化:找到第一个 loop 的 children(当前只支持单层 loop 编辑) - for(var i=0;i90%屏幕,没有选择价值,还会盖住小元素 + if((x2-x1)*(y2-y1) > imgW*imgH*0.9)return; var ov=document.createElement('div'); ov.className='el-shot-overlay'; + ov.setAttribute('data-idx',i); ov.style.left=(imgOffsetX+x1*scaleX)+'px'; ov.style.top=(imgOffsetY+y1*scaleY)+'px'; ov.style.width=w+'px'; ov.style.height=h+'px'; ov.title=el.name||el.class||''; - ov.onclick=function(e){ - e.stopPropagation(); - self._highlightEl(i); - }; container.appendChild(ov); }); + // 点击截图:按 bounds 命中测试,选中最小的(最深的)元素, + // 而不是被 DOM 层叠中的大容器/大兄弟挡住 + container.onclick=function(e){ + var im=container.querySelector('img'); + if(!im)return; + var r=im.getBoundingClientRect(); + var px=(e.clientX-r.left)/scaleX; // 换算回原始分辨率坐标 + var py=(e.clientY-r.top)/scaleY; + var best=-1,bestArea=Infinity; + self._elData.forEach(function(el,i){ + var mb=el.bounds&&el.bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/); + if(!mb)return; + var a=parseInt(mb[1]),b=parseInt(mb[2]),c=parseInt(mb[3]),dd=parseInt(mb[4]); + if(px>=a&&px<=c&&py>=b&&py<=dd){ + var area=(c-a)*(dd-b); + if(area=0)self._highlightEl(best); + }; } if(img.complete&&img.naturalWidth>0)draw(); else img.onload=draw; @@ -1606,9 +1863,9 @@ var _stepEditor={ var picker=document.getElementById('el-picker-overlay'); picker.querySelectorAll('.el-shot-overlay.active').forEach(function(o){o.classList.remove('active');}); picker.querySelectorAll('.el-picker-item.active').forEach(function(o){o.classList.remove('active');}); - // 高亮当前(overlays 基于 _elData 原始索引) - var overlays=picker.querySelectorAll('.el-shot-overlay'); - if(overlays[i])overlays[i].classList.add('active'); + // 高亮当前(overlay 用 data-idx 查找,渲染时跳过了全屏容器,索引不再一一对应) + var overlay=picker.querySelector('.el-shot-overlay[data-idx="'+i+'"]'); + if(overlay)overlay.classList.add('active'); // 列表项用 data-idx 查找(过滤后顺序可能不同) var item=picker.querySelector('.el-picker-item[data-idx="'+i+'"]'); if(item){ @@ -1651,6 +1908,8 @@ var _stepEditor={ this._elTargetInput.value=el.suggested.value; var sel=this._elTargetInput.closest('.sc-body').querySelector('[data-key="selector_type"]'); if(sel)sel.value=el.suggested.type; + // 程序赋值不会触发 input 事件,手动同步回 _steps 防丢失 + this._syncFromDom(); // 只关闭抓取元素弹窗,不影响任务编辑窗口 closeElPicker(); showToast('已填入: '+el.suggested.type+'='+el.suggested.value,'success'); @@ -1706,10 +1965,10 @@ async function saveTask(jobId){ if(jobId){ const r=await apiPut('/api/jobs/'+jobId,data); - handleResult(r,()=>{closeModal();loadTasks();}); + handleResult(r,()=>{_stepEditor.clearDraft();closeModal();loadTasks();}); }else{ const r=await apiPost('/api/jobs',data); - handleResult(r,()=>{closeModal();loadTasks();}); + handleResult(r,()=>{_stepEditor.clearDraft();closeModal();loadTasks();}); } } @@ -1719,6 +1978,82 @@ async function deleteTask(jobId){ handleResult(r,()=>loadTasks()); } +// ================== 自定义动作管理 ================== +async function loadCustomActions(){ + const r=await apiGet('/api/custom_actions'); if(!r||!r.ok)return; + _customActions=r.actions||[]; + setListPager('actions',{ + data:()=>_customActions, + filter:(a,kw)=>((a.name||'').toLowerCase().includes(kw)), + render:renderActionRow, + tbody:'tb-actions',pager:'pager-actions',pageSize:10, + empty:'暂无自定义动作,可点击"新建动作"或用任务编辑器☑选中步骤打包' + }); + applyListPager('actions'); + initListSearch('actions','search-actions'); + _stepEditor._renderLib(); // 刷新步骤编辑器左侧动作库 +} +function renderActionRow(a){ + const n=(a.steps||[]).length; + return ''+ + ''+esc(a.icon||'📦')+''+ + ''+esc(a.name)+''+ + ''+n+''+ + ''+esc(a.created_at||'-')+''+ + ''+ + ' '+ + ''+ + ''+ + ''; +} +function openActionModal(actionId){ + const action=actionId?_customActions.find(a=>a.id===actionId):null; + const isEdit=!!action; + document.getElementById('action-modal-title').textContent=isEdit?'编辑动作':'新建动作'; + document.getElementById('action-modal-body').innerHTML= + '
'+ + '
'+ + '
'+ + '
'+ + '
用下方步骤编辑器编排动作内容,保存后可拖入任务步骤编辑器复用
'+ + '
'+ + '
动作步骤支持循环/分组/概率触发/拖拽排序
'+ + '
'+ + '
操作库
'+ + '
'+ + '
'+ + '
'; + document.getElementById('action-modal-footer').innerHTML= + ''+ + ''; + // 用独立画布初始化步骤编辑器(不与任务编辑器冲突) + _stepEditor.init(action&&action.steps?action.steps:[], 0, 'action:'+(actionId||'new'), 'action-canvas', 'action-lib'); + document.getElementById('action-modal-overlay').classList.add('show'); +} +async function saveAction(actionId){ + const name=document.getElementById('a-name').value.trim(); + if(!name){showToast('动作名不能为空','error');return;} + const icon=document.getElementById('a-icon').value.trim()||'📦'; + const steps=_stepEditor.collect(); + if(!steps.length){showToast('至少需要1个步骤','error');return;} + const data={name:name,icon:icon,steps:steps}; + if(actionId){ + const r=await apiPut('/api/custom_actions/'+actionId,data); + handleResult(r,()=>{_stepEditor.clearDraft();closeActionModal();loadCustomActions();}); + }else{ + const r=await apiPost('/api/custom_actions',data); + handleResult(r,()=>{_stepEditor.clearDraft();closeActionModal();loadCustomActions();}); + } +} +async function deleteAction(id){ + if(!confirm('确认删除此动作?'))return; + const r=await apiDelete('/api/custom_actions/'+id); + handleResult(r,()=>loadCustomActions()); +} +function closeActionModal(){ + document.getElementById('action-modal-overlay').classList.remove('show'); +} + // ================== Tab 3: 分组 ================== async function loadGroups(){ const r=await apiGet('/api/groups');if(!r)return; @@ -1730,19 +2065,28 @@ async function loadGroups(){ function renderGroupTable(){ const groups=_groupsList||[]; - document.getElementById('tb-groups').innerHTML=groups.length?groups.map(g=>{ - const serials=g.serials||[]; - return ''+ - ''+esc(g.name)+''+ - ''+esc(g.description||'-')+''+ - ''+serials.length+''+ - ''+(serials.length?serials.map(esc).join(', '):'-')+''+ - ''+ - ''+ - ''+ - ''+ - ''; - }).join(''):'暂无分组,点击"新建分组"创建'; + setListPager('groups',{ + data:()=>groups, + filter:(g,kw)=>((g.name||'').toLowerCase().includes(kw)||(g.description||'').toLowerCase().includes(kw)||(g.serials||[]).join(' ').toLowerCase().includes(kw)), + render:renderGroupRow, + tbody:'tb-groups',pager:'pager-groups',pageSize:10, + empty:'暂无分组,点击"新建分组"创建' + }); + applyListPager('groups'); + initListSearch('groups','search-groups'); +} +function renderGroupRow(g){ + const serials=g.serials||[]; + return ''+ + ''+esc(g.name)+''+ + ''+esc(g.description||'-')+''+ + ''+serials.length+''+ + ''+(serials.length?serials.map(esc).join(', '):'-')+''+ + ''+ + ''+ + ''+ + ''+ + ''; } function openGroupModal(name){ @@ -1823,18 +2167,27 @@ function toggleLogAuto(){ async function loadUsers(){ const r=await apiGet('/api/users');if(!r)return; const users=r.users||[]; - document.getElementById('tb-users').innerHTML=users.length?users.map(u=>{ - const admin=u.is_admin?'管理员':'普通'; - return ''+ - ''+u.id+''+ - ''+esc(u.username)+''+ - ''+admin+''+ - ''+ - ''+ - (u.username==='admin'?'':'')+ - ''+ - ''; - }).join(''):'暂无用户'; + setListPager('users',{ + data:()=>users, + filter:(u,kw)=>(u.username||'').toLowerCase().includes(kw), + render:renderUserRow, + tbody:'tb-users',pager:'pager-users',pageSize:10, + empty:'暂无用户' + }); + applyListPager('users'); + initListSearch('users','search-users'); +} +function renderUserRow(u){ + const admin=u.is_admin?'管理员':'普通'; + return ''+ + ''+u.id+''+ + ''+esc(u.username)+''+ + ''+admin+''+ + ''+ + ''+ + (u.username==='admin'?'':'')+ + ''+ + ''; } function openUserModal(uid){ @@ -1888,6 +2241,7 @@ async function deleteUser(uid){ // ================== Tab: 应用 ================== let _apksCache=[]; let _installTimer=null; +let _devAppsData=[]; function fmtSize(b){ if(!b)return '0 B'; @@ -1901,23 +2255,80 @@ async function loadApks(){ const apks=r.apks||[]; _apksCache=apks; document.getElementById('apk-hint').textContent='共 '+apks.length+' 个应用'; - if(!apks.length){ - document.getElementById('tb-apks').innerHTML='暂无APK,点击"上传APK"上传'; - return; - } - document.getElementById('tb-apks').innerHTML=apks.map(a=> - ''+ - ''+esc(a.display_name||'(未命名)')+''+ - ''+esc(a.package_name||'-')+''+ - ''+(a.version_name?esc(a.version_name):'-')+''+ - ''+fmtSize(a.size)+''+ - ''+esc(a.upload_time)+''+ - ''+ - ' '+ - ''+ - ''+ - '' + setListPager('apks',{ + data:()=>apks, + 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, + empty:'暂无APK,点击"上传APK"上传' + }); + applyListPager('apks'); + initListSearch('apks','search-apks'); + await fillAppsDeviceList(); +} +function renderApkRow(a){ + return ''+ + ''+esc(a.display_name||'(未命名)')+''+ + ''+esc(a.package_name||'-')+''+ + ''+(a.version_name?esc(a.version_name):'-')+''+ + ''+fmtSize(a.size)+''+ + ''+esc(a.upload_time)+''+ + ''+ + ' '+ + ''+ + ''+ + ''; +} + +// 填充"设备已装应用"的设备下拉(保留当前选择) +async function fillAppsDeviceList(){ + const sr=await apiGet('/api/status');if(!sr||!sr.ok)return; + const sel=document.getElementById('apps-device'); + if(!sel)return; + const cur=sel.value; + const devices=(sr.devices||[]).filter(d=>d.present); + sel.innerHTML=''+devices.map(d=> + '' ).join(''); + if(cur)sel.value=cur; +} + +// 查看指定设备已安装应用 +async function loadDeviceApps(){ + const sel=document.getElementById('apps-device'); + const serial=sel.value; + if(!serial){showToast('请先选择设备','error');return;} + const hint=document.getElementById('apps-hint'); + const tb=document.getElementById('tb-device-apps'); + hint.textContent='加载中...'; + const r=await apiGet('/api/devices/'+encodeURIComponent(serial)+'/apps'); + hint.textContent=''; + if(!r||!r.ok){ + tb.innerHTML=''+(r&&r.error?esc(r.error):'获取失败')+''; + showToast('获取失败','error');return; + } + const apps=r.apps||[]; + if(!apps.length){tb.innerHTML='无应用';return;} + const userApps=apps.filter(a=>/^\/data\//.test(a.path||'')).length; + hint.textContent='共 '+apps.length+' 个应用(用户安装 '+userApps+')'; + _devAppsData=apps; + setListPager('devapps',{ + data:()=>_devAppsData||[], + filter:(a,kw)=>((a.label||a.package||'').toLowerCase().includes(kw)||(a.package||'').toLowerCase().includes(kw)||(a.path||'').toLowerCase().includes(kw)), + render:renderDevAppRow, + tbody:'tb-device-apps',pager:'pager-devapps',pageSize:20, + empty:'无应用' + }); + applyListPager('devapps'); + initListSearch('devapps','search-devapps'); +} +function renderDevAppRow(a){ + return ''+ + ''+esc(a.label||a.package)+''+(a.label?' '+esc(a.package)+'':'')+''+ + ''+(a.version_name?esc(a.version_name):'-')+''+ + ''+(a.version_code!=null?esc(a.version_code):'-')+''+ + ''+esc(a.path||'-')+''+ + ''; } async function uploadApk(input){ @@ -1994,7 +2405,8 @@ async function openInstallModal(apkId){ online.map(d=>{ const running=d.worker_status==='running'||d.worker_status==='connecting'; const st=''+(running?'运行中':'空闲')+''; - const label=d.device_name||d.model||d.serial; + // serial 唯一,显示 IP + 型号便于区分同型号设备 + const label=(d.serial||'').replace(/:5555$/,'')+' · '+(d.device_name||d.model||'设备'); return ''; }).join('')+ '';