feat: quick execute panel in scripts.html (no script save required)
Add collapsible '直接执行命令' card at top of scripts page: - Textarea for ad-hoc command (no script_id, not saved to library) - Multi-select server list (loaded on first expand, same batch size) - Options: timeout, DB credential (* substitution), long_task nohup - Ctrl+Enter shortcut to submit - Uses same batch tracking + status panel + polling as saved scripts - Clear button to reset command - Batch hint shows server count and batch size - 全选 button for quick server selection Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+163
-2
@@ -2,11 +2,80 @@
|
||||
<body class="bg-slate-950 text-slate-100 min-h-screen flex" x-data="{sidebarOpen:true}">
|
||||
<aside x-show="sidebarOpen" x-transition class="w-64 bg-slate-900 border-r border-slate-800 flex flex-col shrink-0" data-sidebar></aside>
|
||||
<div class="flex-1 flex flex-col min-w-0"><header class="bg-slate-900 border-b border-slate-800 px-6 py-3"><div class="flex items-center justify-between"><div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">脚本库</h1></div><div class="flex items-center gap-2"><a href="/app/script-executions.html" class="px-3 py-1.5 bg-slate-800 hover:bg-slate-700 text-slate-300 text-sm rounded-lg">执行历史</a><button onclick="showNewScript()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg">+ 新建脚本</button></div></div></header>
|
||||
<main class="flex-1 overflow-y-auto p-6">
|
||||
<div id="execStatusPanel" class="hidden mb-6 bg-slate-900 rounded-xl border border-slate-800 p-4">
|
||||
<main class="flex-1 overflow-y-auto p-6 space-y-4">
|
||||
|
||||
<!-- ── 直接执行(无需保存到脚本库)── -->
|
||||
<div class="bg-slate-900 rounded-xl border border-slate-800 overflow-hidden">
|
||||
<button onclick="toggleQuickExec()" id="quickExecToggleBtn"
|
||||
class="w-full flex items-center justify-between px-5 py-3 text-left hover:bg-slate-800/50 transition group">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-brand-light font-mono text-sm">⚡</span>
|
||||
<span class="text-white font-medium text-sm">直接执行命令</span>
|
||||
<span class="text-slate-500 text-xs">(临时命令,不保存到脚本库)</span>
|
||||
</div>
|
||||
<svg id="quickExecChevron" class="w-4 h-4 text-slate-500 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="quickExecPanel" class="hidden border-t border-slate-800 p-5 space-y-4">
|
||||
<!-- Command input -->
|
||||
<div>
|
||||
<label class="block text-slate-400 text-xs mb-1.5">命令内容 <span class="text-slate-600">(支持多行 Shell 脚本)</span></label>
|
||||
<textarea id="quickCmd" rows="4" placeholder="echo 'hello world' df -h uptime"
|
||||
class="w-full px-4 py-3 bg-slate-950 border border-slate-700 rounded-xl text-green-400 text-sm font-mono
|
||||
focus:outline-none focus:ring-2 focus:ring-brand resize-y placeholder-slate-700"
|
||||
onkeydown="if(event.ctrlKey&&event.key==='Enter'){event.preventDefault();doQuickExec()}"></textarea>
|
||||
<p class="text-slate-600 text-xs mt-1">Ctrl + Enter 快捷执行</p>
|
||||
</div>
|
||||
<!-- Servers + options row -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1.5">
|
||||
<label class="text-slate-400 text-xs">目标服务器</label>
|
||||
<button onclick="selectAllQuickServers()" class="text-brand-light text-xs hover:underline">全选</button>
|
||||
</div>
|
||||
<select id="quickServers" multiple class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm h-28"></select>
|
||||
<p id="quickBatchHint" class="text-slate-600 text-xs mt-1"></p>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label class="block text-slate-400 text-xs mb-1">超时 (秒)</label>
|
||||
<input id="quickTimeout" type="number" value="60" min="10" max="600"
|
||||
class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-slate-400 text-xs mb-1">DB 凭据(可选)</label>
|
||||
<select id="quickCredential" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm">
|
||||
<option value="">不使用</option>
|
||||
</select>
|
||||
</div>
|
||||
<label class="flex items-center gap-2 cursor-pointer select-none">
|
||||
<input type="checkbox" id="quickLongTask" class="rounded border-slate-600 bg-slate-700 text-brand focus:ring-brand">
|
||||
<span class="text-xs text-slate-300">长任务(后台 nohup)</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Execute button -->
|
||||
<div class="flex items-center gap-3">
|
||||
<button onclick="doQuickExec()" id="quickExecBtn"
|
||||
class="px-5 py-2.5 bg-brand hover:bg-brand-dark text-white text-sm font-medium rounded-lg transition disabled:opacity-50">
|
||||
⚡ 执行
|
||||
</button>
|
||||
<button onclick="document.getElementById('quickCmd').value=''" class="px-3 py-2.5 bg-slate-800 hover:bg-slate-700 text-slate-400 text-xs rounded-lg transition">
|
||||
清空
|
||||
</button>
|
||||
<span class="text-slate-600 text-xs">命令不会被保存;执行结果在下方「批量执行状态」查看</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── 执行状态 ── -->
|
||||
<div id="execStatusPanel" class="hidden bg-slate-900 rounded-xl border border-slate-800 p-4">
|
||||
<h2 class="text-white font-semibold text-sm mb-3">批量执行状态</h2>
|
||||
<div id="execStatusList" class="space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- ── 脚本库 ── -->
|
||||
<div id="scriptList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
|
||||
|
||||
<!-- Add/Edit Script Form -->
|
||||
@@ -54,6 +123,98 @@
|
||||
let _trackedExecutions=[];
|
||||
let _execPollTimer=null;
|
||||
|
||||
// ── Quick Execute ──
|
||||
let _quickServersLoaded = false;
|
||||
|
||||
function toggleQuickExec(){
|
||||
const panel=document.getElementById('quickExecPanel');
|
||||
const chevron=document.getElementById('quickExecChevron');
|
||||
const open=panel.classList.toggle('hidden');
|
||||
chevron.style.transform=open?'':'rotate(180deg)';
|
||||
if(!open&&!_quickServersLoaded) loadQuickServers();
|
||||
}
|
||||
|
||||
async function loadQuickServers(){
|
||||
_quickServersLoaded=true;
|
||||
try{
|
||||
const [srvR,credR]=await Promise.all([apiFetch(API+'/servers/?per_page=200'),apiFetch(API+'/scripts/credentials')]);
|
||||
if(!srvR)return;
|
||||
const data=await srvR.json();
|
||||
const servers=data.items||data;
|
||||
document.getElementById('quickServers').innerHTML=servers.map(s=>`<option value="${s.id}">${esc(s.name)} (${esc(s.domain)})</option>`).join('');
|
||||
const hint=`已加载 ${servers.length} 台服务器 · 每批 ${_execBatchSize} 台并发`;
|
||||
document.getElementById('quickBatchHint').textContent=hint;
|
||||
if(credR&&credR.ok){
|
||||
const creds=await credR.json();
|
||||
const sel=document.getElementById('quickCredential');
|
||||
(Array.isArray(creds)?creds:[]).forEach(c=>{const o=document.createElement('option');o.value=c.id;o.textContent=`${esc(c.name)} (${esc(c.host)})`;sel.appendChild(o)});
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function selectAllQuickServers(){
|
||||
Array.from(document.getElementById('quickServers').options).forEach(o=>o.selected=true);
|
||||
}
|
||||
|
||||
async function doQuickExec(){
|
||||
const cmd=document.getElementById('quickCmd').value.trim();
|
||||
if(!cmd){toast('warning','请输入命令');return}
|
||||
const opts=document.getElementById('quickServers').selectedOptions;
|
||||
const ids=Array.from(opts).map(o=>parseInt(o.value));
|
||||
if(!ids.length){toast('warning','请选择服务器');return}
|
||||
const timeout=parseInt(document.getElementById('quickTimeout').value)||60;
|
||||
const longTask=document.getElementById('quickLongTask').checked;
|
||||
const credEl=document.getElementById('quickCredential');
|
||||
const credentialId=credEl&&credEl.value?parseInt(credEl.value,10):null;
|
||||
|
||||
const btn=document.getElementById('quickExecBtn');
|
||||
btn.disabled=true;btn.textContent='提交中...';
|
||||
|
||||
// Reuse the same batch tracking / status panel logic
|
||||
const batches=[];
|
||||
for(let i=0;i<ids.length;i+=_execBatchSize)batches.push(ids.slice(i,i+_execBatchSize));
|
||||
document.getElementById('execStatusPanel').classList.remove('hidden');
|
||||
const jobId='quick-'+Date.now();
|
||||
const cmdPreview=cmd.length>40?cmd.substring(0,40)+'...':cmd;
|
||||
const merged={};
|
||||
const job={
|
||||
id:jobId,
|
||||
scriptName:`[临时] ${cmdPreview}`,
|
||||
longTask,totalServers:ids.length,batchTotal:batches.length,batchCur:0,
|
||||
executionIds:[],submitting:true,data:{status:'running',results:'{}'},
|
||||
};
|
||||
_trackedExecutions.unshift(job);
|
||||
if(_trackedExecutions.length>20)_trackedExecutions.pop();
|
||||
renderExecStatus();
|
||||
|
||||
let failBatches=0;
|
||||
const batchStatuses=[];
|
||||
for(let b=0;b<batches.length;b++){
|
||||
const batchIds=batches[b];
|
||||
job.batchCur=b+1;job.submitting=true;job.data.status='running';
|
||||
renderExecStatus();
|
||||
const body={command:cmd,server_ids:batchIds,timeout,long_task:longTask};
|
||||
if(credentialId)body.credential_id=credentialId;
|
||||
const r=await apiFetch(API+'/scripts/exec',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});
|
||||
job.submitting=false;
|
||||
if(!r||!r.ok){failBatches++;batchStatuses.push('failed');toast('error',`批次 ${b+1} 失败`);continue}
|
||||
const d=await r.json();
|
||||
job.executionIds.push(d.id);
|
||||
batchStatuses.push(d.status||'running');
|
||||
mergeExecResults(merged,d.results);
|
||||
job.data.results=JSON.stringify(merged);
|
||||
job.data.status=aggregateExecStatus(batchStatuses);
|
||||
renderExecStatus();
|
||||
}
|
||||
job.data.status=aggregateExecStatus(batchStatuses)||'completed';
|
||||
renderExecStatus();
|
||||
btn.disabled=false;btn.textContent='⚡ 执行';
|
||||
if(failBatches===batches.length){toast('error','所有批次失败')}
|
||||
else{toast('success',`执行完成: ${ids.length} 台服务器`)}
|
||||
// Start polling if any long tasks
|
||||
if(longTask&&!_execPollTimer)startExecPoll();
|
||||
}
|
||||
|
||||
const EXEC_STATUS_LABEL={running:'执行中',completed:'执行完',failed:'执行完',partial:'执行完'};
|
||||
const EXEC_STATUS_CLASS={
|
||||
running:'bg-amber-500/20 text-amber-300 border-amber-500/40',
|
||||
|
||||
Reference in New Issue
Block a user