// 管理 Tab:分组/日志/用户 // ================== Tab 3: 分组 ================== async function loadGroups(){ const r=await apiGet('/api/groups');if(!r)return; _groupsList=r.groups||[]; const dr=await apiGet('/api/devices'); // items 带名称(设备身份);老接口只有 serial 列表时兜底成对象 _devicesList=(dr&&dr.items)?dr.items :((dr&&dr.devices)||[]).map(s=>({serial:s,name:'',model:''})); renderGroupTable(); } function renderGroupTable(){ const groups=_groupsList||[]; 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:'暂无分组,点击"新建分组"创建' }); 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(', '):'-')+''+ ''+ (_can('tasks')?( ''+ '' ):'只读')+ ''+ ''; } function openGroupModal(name){ const group=name?(_groupsList||[]).find(g=>g.name===name):null; const isEdit=!!group; document.getElementById('modal-title').textContent=isEdit?'编辑分组':'新建分组'; const g=group||{name:'',description:'',serials:[]}; const current= new Set(g.serials||[]); const nameOf={}; (_devicesList||[]).forEach(x=>{ if(x&&x.serial) nameOf[x.serial]=x.name||''; }); const allSerials=[...new Set([...(_devicesList||[]).map(x=>x.serial||x), ...(g.serials||[])])].sort(); const deviceCheckboxes=allSerials.map(s=> '' ).join(''); document.getElementById('modal-body').innerHTML= '
'+ '
'+ '
'+ '
'+(deviceCheckboxes||'
暂无在线设备
')+'
'+ '
仅显示当前在线设备,已选设备即使离线也会保留
'+ '
'; document.getElementById('modal-footer').innerHTML= ''+ ''; showModal(); } async function saveGroup(name){ const newName=document.getElementById('g-name').value.trim(); if(!newName){showToast('分组名不能为空','error');return;} const desc=document.getElementById('g-description').value; const serials=Array.from(document.querySelectorAll('.device-list input:checked')).map(c=>c.value); const data={name:newName,description:desc,serials:serials}; if(name){ const r=await apiPut('/api/groups/'+encodeURIComponent(name),data); handleResult(r,()=>{closeModal();loadGroups();}); }else{ const r=await apiPost('/api/groups',data); handleResult(r,()=>{closeModal();loadGroups();}); } } async function deleteGroup(name){ if(!confirm('确认删除分组"'+name+'"吗?'))return; const r=await apiDelete('/api/groups/'+encodeURIComponent(name)); handleResult(r,()=>loadGroups()); } // ================== Tab 4: 日志 ================== async function loadLogs(){ const file=document.getElementById('log-file').value; const lines=document.getElementById('log-lines').value; const r=await apiGet('/api/logs?file='+encodeURIComponent(file)+'&lines='+lines); if(!r||!r.ok)return; const content=r.content||''; // 简单着色 const colored=esc(content) .replace(/\[ERROR\]/g,'[ERROR]') .replace(/\[WARNING\]/g,'[WARNING]') .replace(/\[INFO\]/g,'[INFO]'); document.getElementById('log-content').innerHTML=colored||'(空)'; document.getElementById('log-hint').textContent=file+' · '+content.split('\n').length+' 行'; // 自动滚动到底部 const el=document.getElementById('log-content'); el.scrollTop=el.scrollHeight; } function toggleLogAuto(){ if(_logTimer){clearInterval(_logTimer);_logTimer=null;} if(document.getElementById('log-auto').checked && _activeTab==='logs'){ _logTimer=setInterval(loadLogs,3000); } } // ================== Tab 5: 用户 ================== async function loadUsers(){ const r=await apiGet('/api/users');if(!r)return; const users=r.users||[]; 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:'暂无用户' }); applyListPager('users'); initListSearch('users','search-users'); } function renderUserRow(u){ const admin=u.is_admin?'管理员':'普通'; const perms=(u.perms||[]).map(p=>''+(PERM_LABELS[p]||p)+'').join(' ') ||'-'; return ''+ ''+u.id+''+ ''+esc(u.username)+''+ ''+admin+''+ ''+perms+''+ ''+ ''+ (u.username==='admin'?'':'')+ ''+ ''; } function togglePermBoxes(){ // 勾选"管理员权限"时:权限位全选并禁用(管理员拥有全部权限) const isAdmin=document.getElementById('u-is_admin').checked; document.querySelectorAll('.u-perm').forEach(cb=>{ cb.disabled=isAdmin; if(isAdmin)cb.checked=true; }); } function openUserModal(uid){ const isEdit=!!uid; document.getElementById('modal-title').textContent=isEdit?'编辑用户':'新建用户'; const permBoxes=Object.keys(PERM_LABELS).map(p=> '' ).join(''); document.getElementById('modal-body').innerHTML= '
'+ '
'+ '
'+ '
'+ '
'+permBoxes+'
'+ '
任务管理=任务/自定义动作/分组;设备控制=停止/释放/清异常/扫描/抓元素;应用管理=APK上传安装;日志查看=日志页
'+ '
'; document.getElementById('modal-footer').innerHTML= ''+ ''; if(isEdit){ // 预填充(从表格数据获取) apiGet('/api/users').then(r=>{ const u=(r&&r.users||[]).find(x=>x.id===uid); if(u){ document.getElementById('u-username').value=u.username; document.getElementById('u-is_admin').checked=u.is_admin; document.querySelectorAll('.u-perm').forEach(cb=>{ cb.checked=(u.perms||[]).includes(cb.value); }); togglePermBoxes(); } }); }else{ togglePermBoxes(); } showModal(); } async function saveUser(uid){ const username=document.getElementById('u-username').value.trim(); const password=document.getElementById('u-password').value; const is_admin=document.getElementById('u-is_admin').checked; const perms=[...document.querySelectorAll('.u-perm:checked')].map(cb=>cb.value); if(uid){ const data={is_admin:is_admin,perms:perms}; if(password)data.password=password; const r=await apiPut('/api/users/'+uid,data); handleResult(r,()=>{closeModal();loadUsers();}); }else{ if(!username||!password){showToast('用户名和密码不能为空','error');return;} const r=await apiPost('/api/users',{username:username,password:password,is_admin:is_admin,perms:perms}); handleResult(r,()=>{closeModal();loadUsers();}); } } async function deleteUser(uid){ if(!confirm('确认删除此用户吗?'))return; const r=await apiDelete('/api/users/'+uid); handleResult(r,()=>loadUsers()); } // ================== 初始化 ================== initCsrf(); // 获取 CSRF token(非 GET 请求需携带) loadMe(); // 获取当前用户权限,隐藏无权限的功能入口 showTab('monitor');