Files
auto_control/static/admin/base.js
T
butubb bf5071b14d Revert "feat: 设备端应用商店(平台侧)"
按 git 红线撤回:该功能未经 dev 验收就被合并进 main(是我提交时没切分支、
又把自己的自测当成了用户验收)—— main 必须保持"已验收可部署"的状态。

功能本身没问题,代码仍在 **dev**(dad1af8)与 feature 分支上,等设备端 Agent
写出来、端到端验收通过后,再从 dev 合并回 main。

main 内容已回到 66632bb(git diff 66632bb HEAD 为空)。
2026-09-13 14:44:00 +08:00

182 lines
8.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 通用基础:esc/CSRF/权限/API/Toast、Tab 切换与页内子分栏、模态框、行内按钮操作
// ================== 通用工具 ==================
const ACTION_LABELS={like:'点赞',comment:'评论',follow:'关注',share:'分享'};
const STATUS_MAP={idle:{t:'未启动',c:'default'},connecting:{t:'连接中',c:'warning'},running:{t:'运行中',c:'success'},done:{t:'完成',c:'primary'},error:{t:'异常',c:'danger'},released:{t:'已释放',c:'default'},failed:{t:'失败',c:'danger'}};
const TARGET_MAP={all:'全部空闲',group:'按分组',serial:'指定设备'};
// 设备显示文案:**名称是设备身份**,地址只是当前连接位置。
// 有名称 → 「名称 · serial」;没名称(历史数据)→ 只显示 serial
function devText(name, serial){
const n=String(name||'').trim();
return n ? (n + ' · ' + serial) : String(serial||'');
}
const APP_NAMES={'com.ss.android.ugc.aweme':'抖音','com.ss.android.ugc.aweme.lite':'抖音极速版','com.smile.gifmaker':'快手','com.kuaishou.nebula':'快手极速版','com.tencent.mm':'微信','com.tencent.mobileqq':'QQ','com.tencent.qqlive':'腾讯视频','com.eg.android.AlipayGphone':'支付宝','com.taobao.taobao':'淘宝','com.tmall.wireless':'天猫','com.jingdong.app.mall':'京东','com.xunmeng.pinduoduo':'拼多多','com.ss.android.article.news':'今日头条','com.zhihu.android':'知乎','com.miui.home':'桌面','com.android.launcher':'桌面','com.android.settings':'设置','com.android.systemui':'系统','com.github.uiautomator':'U2','com.github.uiautomator.test':'U2测试'};
function esc(s){return String(s||'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));}
// ===== CSRF =====
let _csrfToken='';
// 页面加载后调用:获取 CSRF token,非 GET 请求需携带
async function initCsrf(){
try{
const r=await fetch('/api/csrf');
const d=await r.json();
_csrfToken=d.token||'';
}catch(e){}
}
function _csrfHeaders(){
return _csrfToken?{'X-CSRF-Token':_csrfToken}:{};
}
// ===== 当前用户与权限 =====
// _me 来自 /api/me:{username, is_admin, perms:[tasks/devices/apks/logs]}
// 管理员 perms 恒为全部。无权限的功能入口(tab/按钮)用 data-perm 标记或 _can() 判断隐藏。
let _me={is_admin:false,perms:[],username:''};
async function loadMe(){
try{
const r=await apiGet('/api/me');
if(r&&r.ok)_me=r.user||{is_admin:false,perms:[],username:''};
const un=document.getElementById('me-username');
if(un)un.textContent=_me.username||'';
// 隐藏无权限的 tab 和按钮(data-perm="admin" 表示仅管理员)
document.querySelectorAll('[data-perm]').forEach(function(el){
const p=el.dataset.perm;
const ok=(p==='admin')?_me.is_admin:(_me.perms||[]).includes(p);
if(!ok)el.style.display='none';
});
}catch(e){}
}
function _can(perm){
return _me.is_admin||(_me.perms||[]).includes(perm);
}
const PERM_LABELS={tasks:'任务管理',devices:'设备控制',apks:'应用管理',logs:'日志查看'};
async function apiGet(url){
const r=await fetch(url);
if(r.status===401){window.location='/login';return null;}
return r.json();
}
async function apiPost(url,body){
const r=await fetch(url,{method:'POST',headers:{'Content-Type':'application/json',..._csrfHeaders()},body:body?JSON.stringify(body):'{}'});
if(r.status===401){window.location='/login';return null;}
return r.json();
}
async function apiPut(url,body){
const r=await fetch(url,{method:'PUT',headers:{'Content-Type':'application/json',..._csrfHeaders()},body:body?JSON.stringify(body):'{}'});
if(r.status===401){window.location='/login';return null;}
return r.json();
}
async function apiDelete(url){
const r=await fetch(url,{method:'DELETE',headers:_csrfHeaders()});
if(r.status===401){window.location='/login';return null;}
return r.json();
}
function showToast(msg,type){
const t=document.getElementById('toast');
t.className='toast show toast-'+(type||'success');
t.textContent=msg;
setTimeout(()=>t.className='toast',3000);
}
function handleResult(r,callback){
if(!r)return;
if(r.ok){showToast(r.msg||'操作成功','success');if(callback)callback();}
else{showToast(r.error||'操作失败','error');}
}
// ================== Tab 切换 ==================
let _activeTab='monitor';
let _monitorTimer=null;
let _logTimer=null;
function showTab(name){
_activeTab=name;
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
document.querySelectorAll('.navbar .tab').forEach(t=>t.classList.remove('active'));
const panel=document.getElementById('tab-'+name);
if(panel)panel.classList.add('active');
const btn=document.querySelector('.navbar .tab[data-tab="'+name+'"]');
if(btn)btn.classList.add('active');
// 停止所有定时器
if(_monitorTimer){clearInterval(_monitorTimer);_monitorTimer=null;}
if(_logTimer){clearInterval(_logTimer);_logTimer=null;}
if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;}
// 按需加载数据
if(name==='monitor'){loadMonitor();_monitorTimer=setInterval(loadMonitor,5000);}
if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();}
if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();loadAdbDevices();loadTailscaleDevices();loadApks();}
if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);}
if(name==='agent' && typeof initAgentChat==='function') initAgentChat();
if(name==='users')loadUsers();
if(name==='system'){showSubTab('system',_activeSubs.system||'backup');}
}
// ================== 页内子分栏(任务/工具 通用) ==================
// 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位
let _activeSubs = {tasks: 'plan', tools: 'clipboard', system: 'backup'};
let _discoveryTimer = null; // 设备自动发现 10s 轮询(仅 devpool 子分栏激活时)
function showSubTab(tabId, name){
_activeSubs[tabId] = name;
const tab = document.getElementById('tab-'+tabId);
tab.querySelectorAll('.sub-tab').forEach(b=>b.classList.toggle('active', b.dataset.sub===name));
tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name));
if(name==='groups' && typeof loadGroups==='function') loadGroups();
if(name==='devpool' && typeof loadDevPool==='function'){
loadDevPool();
if(typeof loadDiscovery==='function'){
loadDiscovery();
if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;}
_discoveryTimer=setInterval(loadDiscovery,10000); // 10s 轮询:扫完自动出新结果
}
} else if(_discoveryTimer){clearInterval(_discoveryTimer);_discoveryTimer=null;}
}
// ================== 模态框 ==================
function showModal(){
document.getElementById('modal-overlay').classList.add('show');
}
function closeModal(){
// 先关闭弹窗(最重要的一步,确保一定执行)
const ov=document.getElementById('modal-overlay');
if(ov)ov.classList.remove('show');
// 再做清理工作,任何异常都不影响弹窗关闭
try{
stopShotTimer();
_shotSerial='';
const box=document.getElementById('modal-box');
if(box){box.classList.remove('shot-modal');box.classList.remove('wide-modal');}
}catch(e){console.warn('closeModal cleanup error:',e);}
}
// 测试此步骤弹窗关闭(editor.js 的测试按钮用;曾随 STF 清理误删)
function closeTestModal(){
const ov=document.getElementById('test-overlay');
if(ov)ov.classList.remove('show');
}
// 关闭元素抓取弹窗(不影响任务编辑窗口)
function closeElPicker(){
var ov=document.getElementById('el-picker-overlay');
if(ov)ov.classList.remove('show');
// 清理状态
try{ _stepEditor._elData=null; _stepEditor._elSerial=''; }catch(e){}
}
// ================== 行内按钮操作(停止/执行/启停等,自动 toast + 刷新)==================
async function apiAction(url,body){
const r=await apiPost(url,body);
if(!r)return;
if(r.ok){showToast(r.msg||'操作成功','success');if(_activeTab==='monitor')loadMonitor();if(_activeTab==='tasks')loadTasks();}
else{showToast(r.error||'操作失败','error');}
}
async function scanForeground(){
const r=await apiPost('/api/scan_foreground');
if(!r)return;
if(r.ok){showToast('扫描已启动,请等待几秒...','success');loadMonitor();}
else{showToast(r.error||'扫描失败','error');}
}