// 工具 Tab-应用管理:APK 上传/安装/删除 + 设备已装应用 // ================== Tab: 应用 ================== let _apksCache=[]; let _installTimer=null; let _devAppsData=[]; function fmtSize(b){ if(!b)return '0 B'; if(b<1024)return b+' B'; if(b<1048576)return (b/1024).toFixed(1)+' KB'; return (b/1048576).toFixed(1)+' MB'; } async function loadApks(){ const r=await apiGet('/api/apks');if(!r||!r.ok)return; const apks=r.apks||[]; _apksCache=apks; 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, 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)), 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:'无应用' }); 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){ if(!input.files||!input.files[0])return; const file=input.files[0]; if(!file.name.toLowerCase().endsWith('.apk')){ showToast('请选择 .apk 文件','error');return; } const hint=document.getElementById('apk-hint'); const fd=new FormData(); fd.append('file',file); // 用 XMLHttpRequest 才能拿到上传进度(fetch 不支持上传进度事件) const xhr=new XMLHttpRequest(); xhr.open('POST','/api/apks/upload'); if(_csrfToken)xhr.setRequestHeader('X-CSRF-Token',_csrfToken); xhr.upload.onprogress=function(e){ if(e.lengthComputable){ const pct=Math.round(e.loaded*100/e.total); const loaded=(e.loaded/1048576).toFixed(1); const total=(e.total/1048576).toFixed(1); hint.innerHTML='正在上传 '+esc(file.name)+' '+pct+'% ('+loaded+'/'+total+' MB)'+ '
'; } }; xhr.onload=function(){ if(xhr.status===401){window.location='/login';return;} try{ const data=JSON.parse(xhr.responseText); if(data.ok){ showToast(data.msg||'上传成功','success'); loadApks(); }else{ showToast(data.error||'上传失败','error'); hint.textContent=''; } }catch(e){ showToast('解析响应失败','error'); hint.textContent=''; } }; xhr.onerror=function(){ showToast('上传失败:网络错误','error'); hint.textContent=''; }; xhr.send(fd); input.value=''; } async function deleteApk(id){ if(!confirm('确认删除此APK文件?'))return; const r=await apiDelete('/api/apks/'+id); handleResult(r,loadApks); } async function openInstallModal(apkId){ const apk=_apksCache.find(a=>a.id===apkId); const apkName=apk?(apk.display_name||'APK'):'APK'; const r=await apiGet('/api/apks/install/devices'); const online=(r&&r.ok)?(r.devices||[]):[]; if(!online.length){ showToast('没有可用设备(设备池或本机 adb 均无)','error');return; } const body= '
'+ '安装应用:'+esc(apkName)+''+ ' 运行中设备将自动跳过'+ '
'+ '
'+ ' '+ ' '+ ''+ '
'+ '
'+ online.map(d=>{ const isUsb=d.source==='usb'; const st=''+(isUsb?'本地USB':(d.source==='pool'?'设备池':'adb'))+''; // serial 唯一,显示 IP + 型号便于区分同型号设备 const label=(d.serial||'').replace(/:5555$/,'')+' · '+(d.model||(isUsb?'USB 有线设备':'设备')); return ''; }).join('')+ '
'; document.getElementById('modal-title').textContent='安装应用到设备'; document.getElementById('modal-body').innerHTML=body; document.getElementById('modal-footer').innerHTML= ''+ ''; document.getElementById('modal-overlay').classList.add('show'); } function selectIdleForInstall(){ document.querySelectorAll('#install-dev-list label').forEach(lbl=>{ const cb=lbl.querySelector('input[type=checkbox]'); const isRunning=lbl.querySelector('.label-success'); cb.checked=!isRunning; }); } async function startInstall(apkId){ const checked=document.querySelectorAll('#install-dev-list input[type=checkbox]:checked'); const serials=Array.from(checked).map(c=>c.value); if(!serials.length){ showToast('请至少选择一台设备','error');return; } const r=await apiPost('/api/apks/install',{apk_id:apkId,serials:serials}); if(!r||!r.ok){showToast(r?r.error:'请求失败','error');return;} showToast(r.msg||'安装已启动','success'); showInstallProgress(); } function showInstallProgress(){ document.getElementById('modal-title').textContent='安装进度'; document.getElementById('modal-body').innerHTML= '

加载中...

'; document.getElementById('modal-footer').innerHTML= ''; pollInstallStatus(); if(_installTimer)clearInterval(_installTimer); _installTimer=setInterval(pollInstallStatus,3000); } async function pollInstallStatus(){ const r=await apiGet('/api/apks/install/status'); if(!r||!r.ok)return; const st=r.status; if(!st){ document.getElementById('install-progress').innerHTML='

无安装任务

'; if(_installTimer){clearInterval(_installTimer);_installTimer=null;} return; } const SS={pending:{t:'等待',c:'default'},installing:{t:'安装中',c:'warning'},success:{t:'成功',c:'success'},failed:{t:'失败',c:'danger'},skipped:{t:'跳过',c:'default'}}; const items=st.items||{}; const itemRows=Object.entries(items).map(([serial,info])=>{ const s=SS[info.status]||{t:info.status,c:'default'}; return ''+esc(info.name||serial)+''+ ''+esc(serial)+''+ ''+s.t+''+ ''+esc(info.msg||'')+''; }).join(''); const pct=st.total>0?Math.round((st.success+st.failed+st.skipped)*100/st.total):0; const summary= '
'+ '
'+ '应用:'+esc(st.apk_name)+''+ '成功 '+st.success+''+ '失败 '+st.failed+''+ '跳过 '+st.skipped+''+ '安装中 '+st.installing+''+ '等待 '+st.pending+''+ '
'+ '
'+ '
'; document.getElementById('install-progress').innerHTML= summary+''+itemRows+'
设备Serial状态详情
'; if(st.finished){ if(_installTimer){clearInterval(_installTimer);_installTimer=null;} document.getElementById('install-close-btn').style.display=''; showToast('安装完成:成功'+st.success+' 失败'+st.failed+' 跳过'+st.skipped,'success'); } } function closeInstallModal(){ if(_installTimer){clearInterval(_installTimer);_installTimer=null;} closeModal(); }