// 工具 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+'';
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();
}