feat: 新增工具Tab - 剪贴板注入(多选本地adb/STF设备批量注入文字,u2 setClipboard 实现,实测支持中文/引号/换行)
This commit is contained in:
+58
-3
@@ -281,7 +281,7 @@ function showTab(name){
|
||||
document.querySelectorAll('.tab-panel').forEach(p=>p.classList.remove('active'));
|
||||
document.querySelectorAll('.navbar .tab').forEach(t=>t.classList.remove('active'));
|
||||
document.getElementById('tab-'+name).classList.add('active');
|
||||
document.querySelectorAll('.navbar .tab')[['monitor','tasks','apps','groups','logs','users','maintenance'].indexOf(name)].classList.add('active');
|
||||
document.querySelectorAll('.navbar .tab')[['monitor','tasks','apps','groups','logs','users','maintenance','tools'].indexOf(name)].classList.add('active');
|
||||
// 停止所有定时器
|
||||
if(_monitorTimer){clearInterval(_monitorTimer);_monitorTimer=null;}
|
||||
if(_logTimer){clearInterval(_logTimer);_logTimer=null;}
|
||||
@@ -290,14 +290,15 @@ function showTab(name){
|
||||
if(name==='tasks'){showSubTab('tasks',_activeSubs.tasks);loadTasks();loadCustomActions();}
|
||||
if(name==='apps'){showSubTab('apps',_activeSubs.apps);loadApks();}
|
||||
if(name==='maintenance'){showSubTab('maintenance',_activeSubs.maintenance);loadAdbDevices();loadTailscaleDevices();}
|
||||
if(name==='tools'){showSubTab('tools',_activeSubs.tools);loadToolsDevices();}
|
||||
if(name==='groups')loadGroups();
|
||||
if(name==='logs'){loadLogs();if(document.getElementById('log-auto').checked)_logTimer=setInterval(loadLogs,3000);}
|
||||
if(name==='users')loadUsers();
|
||||
}
|
||||
|
||||
// ================== 页内子分栏(任务/应用/维护 通用) ==================
|
||||
// ================== 页内子分栏(任务/应用/维护/工具 通用) ==================
|
||||
// 每个带子分栏的 Tab 记住上次选中的子分栏,切走再切回来保持原位
|
||||
let _activeSubs = {tasks: 'plan', apps: 'apks', maintenance: 'stf'};
|
||||
let _activeSubs = {tasks: 'plan', apps: 'apks', maintenance: 'stf', tools: 'clipboard'};
|
||||
|
||||
function showSubTab(tabId, name){
|
||||
_activeSubs[tabId] = name;
|
||||
@@ -306,6 +307,60 @@ function showSubTab(tabId, name){
|
||||
tab.querySelectorAll('.sub-panel').forEach(p=>p.classList.toggle('active', p.id===tabId+'-sub-'+name));
|
||||
}
|
||||
|
||||
// ================== 工具:剪贴板注入 ==================
|
||||
let _clipDevices = []; // [{serial, state}],来源 /api/adb/devices(本地 adb + STF 池)
|
||||
let _clipSelected = new Set();
|
||||
|
||||
function _escHtml(s){
|
||||
return String(s==null?'':s).replace(/[&<>"']/g, c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
}
|
||||
|
||||
async function loadToolsDevices(){
|
||||
const r = await apiGet('/api/adb/devices');
|
||||
const status = document.getElementById('clip-status');
|
||||
if(!r||!r.ok){status.textContent='加载失败';showToast('加载设备失败: '+(r&&r.error||''),'error');return;}
|
||||
_clipDevices = r.devices||[];
|
||||
const wrap = document.getElementById('clip-devices');
|
||||
if(!_clipDevices.length){
|
||||
wrap.innerHTML='<div class="empty" style="padding:20px">无可用设备(本地 adb 或 STF 池均无)</div>';
|
||||
status.textContent='';
|
||||
return;
|
||||
}
|
||||
wrap.innerHTML = _clipDevices.map(d=>{
|
||||
const st = d.state==='stf' ? 'STF池' : (d.state==='device' ? '已连接' : d.state);
|
||||
const stCls = (d.state==='device'||d.state==='stf') ? 'label-success' : 'label-warning';
|
||||
return '<label data-serial="'+_escHtml(d.serial)+'">'
|
||||
+'<input type="checkbox" value="'+_escHtml(d.serial)+'"'+( _clipSelected.has(d.serial)?' checked':'')+' onchange="toggleClipDevice(this)">'
|
||||
+'<span style="font-family:monospace;flex:1;overflow:hidden;text-overflow:ellipsis">'+_escHtml(d.serial)+'</span>'
|
||||
+'<span class="label '+stCls+'">'+st+'</span></label>';
|
||||
}).join('');
|
||||
status.textContent = '共 '+_clipDevices.length+' 台设备';
|
||||
}
|
||||
|
||||
function toggleClipDevice(cb){
|
||||
if(cb.checked)_clipSelected.add(cb.value);
|
||||
else _clipSelected.delete(cb.value);
|
||||
}
|
||||
|
||||
async function injectClipboard(){
|
||||
const text = document.getElementById('clip-text').value.trim();
|
||||
if(!text){showToast('请输入要注入的文字','error');return;}
|
||||
if(!_clipSelected.size){showToast('请先勾选目标设备','error');return;}
|
||||
const btn = document.querySelector('#tools-sub-clipboard .btn-primary');
|
||||
btn.disabled = true; const old = btn.textContent; btn.textContent = '注入中...';
|
||||
try{
|
||||
const r = await apiPost('/api/tools/clipboard/set', {serials:[..._clipSelected], text});
|
||||
if(!r||!r.ok){showToast('注入失败: '+(r&&r.error||''),'error');return;}
|
||||
const rows = Object.entries(r.results).map(([s,v])=>
|
||||
'<tr><td style="font-family:monospace">'+_escHtml(s)+'</td><td>'+(v.ok?'<span class="label label-success">成功</span>':'<span class="label label-danger">失败</span> '+_escHtml(v.msg))+'</td></tr>').join('');
|
||||
document.getElementById('clip-result').innerHTML = rows;
|
||||
document.getElementById('clip-result-wrap').style.display = 'block';
|
||||
showToast('注入完成:成功 '+r.ok_count+' 台,失败 '+r.fail_count+' 台', r.fail_count?'error':'success');
|
||||
} finally {
|
||||
btn.disabled = false; btn.textContent = old;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 通用列表:搜索 + 分页 ==================
|
||||
// 用法:
|
||||
// setListPager(key,{data:()=>array, filter:(item,kw)=>bool, render:(item)=>html,
|
||||
|
||||
Reference in New Issue
Block a user