P1: Toast通知系统 + 脚本编辑/执行结果格式化
- api.js: 添加全局toast()通知函数,支持success/error/warning/info四种类型 - scripts.html: 完整重写 — 点击脚本名编辑、执行结果按服务器格式化显示(command/stdout/stderr/exit_code) - servers.html/credentials.html/schedules.html/settings.html: 所有操作添加toast反馈 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -102,3 +102,62 @@ function doLogout() {
|
||||
const token = localStorage.getItem('access_token') || '';
|
||||
if (!token) window.location.href = '/app/login.html';
|
||||
function ah() { return apiHeaders(); }
|
||||
|
||||
// ── Global Toast Notification System ──
|
||||
// Call toast('success', '操作成功') or toast('error', '操作失败') from any page.
|
||||
|
||||
let _toastContainer = null;
|
||||
let _toastId = 0;
|
||||
|
||||
function _ensureToastContainer() {
|
||||
if (_toastContainer) return;
|
||||
_toastContainer = document.createElement('div');
|
||||
_toastContainer.id = 'toastContainer';
|
||||
_toastContainer.className = 'fixed top-4 right-4 z-[9999] flex flex-col gap-2 pointer-events-none';
|
||||
_toastContainer.style.maxWidth = '380px';
|
||||
document.body.appendChild(_toastContainer);
|
||||
}
|
||||
|
||||
function toast(type, message, duration) {
|
||||
_ensureToastContainer();
|
||||
duration = duration || 3000;
|
||||
const id = ++_toastId;
|
||||
|
||||
const colors = {
|
||||
success: 'bg-green-900/90 border-green-700/50 text-green-200',
|
||||
error: 'bg-red-900/90 border-red-700/50 text-red-200',
|
||||
warning: 'bg-yellow-900/90 border-yellow-700/50 text-yellow-200',
|
||||
info: 'bg-blue-900/90 border-blue-700/50 text-blue-200',
|
||||
};
|
||||
const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' };
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.id = 'toast-' + id;
|
||||
el.className = `pointer-events-auto flex items-center gap-2 px-4 py-3 rounded-xl border shadow-lg text-sm backdrop-blur-sm transition-all duration-300 opacity-0 translate-x-4 ${colors[type] || colors.info}`;
|
||||
el.innerHTML = `<span class="text-base">${icons[type] || 'ℹ'}</span><span class="flex-1">${_toastEsc(message)}</span><button onclick="dismissToast(${id})" class="text-current opacity-50 hover:opacity-100 text-xs ml-2">✕</button>`;
|
||||
|
||||
_toastContainer.appendChild(el);
|
||||
|
||||
// Animate in
|
||||
requestAnimationFrame(() => {
|
||||
el.classList.remove('opacity-0', 'translate-x-4');
|
||||
el.classList.add('opacity-100', 'translate-x-0');
|
||||
});
|
||||
|
||||
// Auto-dismiss
|
||||
if (duration > 0) {
|
||||
setTimeout(() => dismissToast(id), duration);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
function dismissToast(id) {
|
||||
const el = document.getElementById('toast-' + id);
|
||||
if (!el) return;
|
||||
el.classList.add('opacity-0', 'translate-x-4');
|
||||
el.classList.remove('opacity-100', 'translate-x-0');
|
||||
setTimeout(() => el.remove(), 300);
|
||||
}
|
||||
|
||||
function _toastEsc(s) { if (!s) return ''; const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
|
||||
|
||||
@@ -124,14 +124,16 @@
|
||||
|
||||
async function createCred(){
|
||||
const body={name:document.getElementById('credName').value,db_type:document.getElementById('credType').value,host:document.getElementById('credHost').value,port:parseInt(document.getElementById('credPort').value)||3306,username:document.getElementById('credUser').value,password:document.getElementById('credPass').value,database:document.getElementById('credDb').value||null};
|
||||
if(!body.name||!body.host||!body.username||!body.password){alert('名称、主机、用户名、密码必填');return}
|
||||
await apiFetch(API+'/scripts/credentials',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});
|
||||
if(!body.name||!body.host||!body.username||!body.password){toast('warning','名称、主机、用户名、密码必填');return}
|
||||
const r=await apiFetch(API+'/scripts/credentials',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});
|
||||
if(r&&r.ok)toast('success','凭据已创建');else toast('error','创建失败');
|
||||
hideCredModal();loadCreds();
|
||||
}
|
||||
|
||||
async function deleteCred(id){
|
||||
if(!confirm('确定删除凭据?'))return;
|
||||
await apiFetch(API+'/scripts/credentials/'+id,{method:'DELETE'});
|
||||
const r=await apiFetch(API+'/scripts/credentials/'+id,{method:'DELETE'});
|
||||
if(r&&r.ok)toast('success','凭据已删除');else toast('error','删除失败');
|
||||
loadCreds();
|
||||
}
|
||||
|
||||
@@ -158,14 +160,16 @@
|
||||
async function createPreset(){
|
||||
const name=document.getElementById('presetName').value;
|
||||
const pw=document.getElementById('presetPass').value;
|
||||
if(!name||!pw){alert('名称和密码必填');return}
|
||||
await apiFetch(API+'/presets/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({name,encrypted_pw:pw})});
|
||||
if(!name||!pw){toast('warning','名称和密码必填');return}
|
||||
const r=await apiFetch(API+'/presets/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({name,encrypted_pw:pw})});
|
||||
if(r&&r.ok)toast('success','密码预设已创建');else toast('error','创建失败');
|
||||
hidePresetModal();loadPresets();
|
||||
}
|
||||
|
||||
async function deletePreset(id){
|
||||
if(!confirm('确定删除密码预设?'))return;
|
||||
await apiFetch(API+'/presets/'+id,{method:'DELETE'});
|
||||
const r=await apiFetch(API+'/presets/'+id,{method:'DELETE'});
|
||||
if(r&&r.ok)toast('success','预设已删除');else toast('error','删除失败');
|
||||
loadPresets();
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
const id=document.getElementById('editSchedId').value;const opts=document.getElementById('sServers').selectedOptions;const serverIds=Array.from(opts).map(o=>parseInt(o.value));
|
||||
const body={name:document.getElementById('sName').value,source_path:document.getElementById('sPath').value,cron_expr:document.getElementById('sCron').value,server_ids:JSON.stringify(serverIds),enabled:true};
|
||||
if(!body.name||!body.cron_expr){alert('名称和Cron必填');return}
|
||||
if(id){await apiFetch(API+'/schedules/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)})}else{await apiFetch(API+'/schedules/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)})}
|
||||
if(id){const r=await apiFetch(API+'/schedules/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)});if(r&&r.ok)toast('success','调度已更新');else toast('error','更新失败')}else{const r=await apiFetch(API+'/schedules/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});if(r&&r.ok)toast('success','调度已创建');else toast('error','创建失败')}
|
||||
hideSchedModal();loadScheds();
|
||||
}
|
||||
async function toggleSched(id,enabled){await apiFetch(API+'/schedules/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({enabled})});loadScheds()}
|
||||
async function deleteSched(id){if(!confirm('确定删除调度?'))return;await apiFetch(API+'/schedules/'+id,{method:'DELETE'});loadScheds()}
|
||||
async function toggleSched(id,enabled){const r=await apiFetch(API+'/schedules/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({enabled})});if(r&&r.ok)toast('info',enabled?'已启用':'已禁用');loadScheds()}
|
||||
async function deleteSched(id){if(!confirm('确定删除调度?'))return;const r=await apiFetch(API+'/schedules/'+id,{method:'DELETE'});if(r&&r.ok)toast('success','已删除');else toast('error','删除失败');loadScheds()}
|
||||
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
||||
function fmtTime(t){if(!t)return'';return new Date(t+'Z').toLocaleString('zh-CN',{month:'short',day:'numeric',hour:'2-digit',minute:'2-digit'})}
|
||||
loadScheds();
|
||||
|
||||
+75
-14
@@ -4,21 +4,26 @@
|
||||
<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><button onclick="showNewScript()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg">+ 新建脚本</button></div></header>
|
||||
<main class="flex-1 overflow-y-auto p-6">
|
||||
<div id="scriptList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
|
||||
<div id="newScriptForm" class="hidden mt-6 bg-slate-900 rounded-xl border border-slate-800 p-6 space-y-3">
|
||||
|
||||
<!-- Add/Edit Script Form -->
|
||||
<div id="scriptForm" class="hidden mt-6 bg-slate-900 rounded-xl border border-slate-800 p-6 space-y-3">
|
||||
<input type="hidden" id="editScriptId">
|
||||
<h2 id="scriptFormTitle" class="text-white font-semibold">新建脚本</h2>
|
||||
<input id="scriptName" placeholder="脚本名称" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm">
|
||||
<input id="scriptCategory" placeholder="分类 (ops/deploy/check)" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm">
|
||||
<textarea id="scriptContent" rows="8" placeholder="Shell 命令内容" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm font-mono"></textarea>
|
||||
<div class="flex gap-2"><button onclick="createScript()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideNewScript()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
|
||||
<textarea id="scriptContent" rows="10" placeholder="Shell 命令内容" class="w-full px-4 py-3 bg-slate-800 border border-slate-700 rounded-xl text-white text-sm font-mono"></textarea>
|
||||
<div class="flex gap-2"><button onclick="saveScript()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideScriptForm()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
|
||||
</div>
|
||||
|
||||
<!-- Execute Modal -->
|
||||
<div id="execModal" class="hidden fixed inset-0 bg-black/60 flex items-center justify-center z-50">
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-md space-y-3">
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-xl p-6 w-full max-w-lg space-y-3">
|
||||
<h2 class="text-white font-semibold text-lg">执行脚本</h2>
|
||||
<p id="execScriptName" class="text-slate-400 text-sm"></p>
|
||||
<div><label class="block text-slate-400 text-xs mb-1">选择服务器</label><select id="execServers" multiple class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm h-32"></select></div>
|
||||
<div><label class="block text-slate-400 text-xs mb-1">超时(秒)</label><input id="execTimeout" type="number" value="60" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"></div>
|
||||
<div id="execResult" class="hidden p-3 bg-slate-950 rounded-lg text-sm font-mono text-slate-300 max-h-48 overflow-y-auto"></div>
|
||||
<div class="flex gap-2"><button onclick="doExec()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">执行</button><button onclick="hideExecModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">关闭</button></div>
|
||||
<div id="execResult" class="hidden p-3 bg-slate-950 rounded-lg text-sm font-mono text-slate-300 max-h-64 overflow-y-auto whitespace-pre-wrap"></div>
|
||||
<div class="flex gap-2"><button onclick="doExec()" id="execBtn" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">执行</button><button onclick="hideExecModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">关闭</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -26,15 +31,71 @@
|
||||
<script src="/app/api.js"></script>
|
||||
<script>
|
||||
let _scriptsCache=[];
|
||||
async function loadScripts(){try{const r=await apiFetch(API+'/scripts/');if(!r)return;const scripts=await r.json();_scriptsCache=scripts;document.getElementById('scriptList').innerHTML=scripts.length?scripts.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4"><div class="flex items-center justify-between"><span class="text-white font-medium">${esc(s.name)}</span><div class="flex items-center gap-2"><span class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-400">${s.category||'ops'}</span><button onclick="showExec(${s.id})" class="text-brand-light text-xs hover:underline">执行</button><button onclick="deleteScript(${s.id})" class="text-red-400 text-xs hover:underline">删除</button></div></div><pre class="mt-2 text-sm text-slate-400 font-mono bg-slate-950 rounded-lg p-3 overflow-x-auto">${esc(s.content?.substring(0,500)||'')}</pre></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无脚本</div>'}catch(e){}}
|
||||
function showNewScript(){document.getElementById('newScriptForm').classList.remove('hidden')}
|
||||
function hideNewScript(){document.getElementById('newScriptForm').classList.add('hidden')}
|
||||
async function createScript(){const b={name:document.getElementById('scriptName').value,category:document.getElementById('scriptCategory').value||'ops',content:document.getElementById('scriptContent').value};await apiFetch(API+'/scripts/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(b)});hideNewScript();loadScripts()}
|
||||
async function deleteScript(id){if(!confirm('确定删除脚本?'))return;await apiFetch(API+'/scripts/'+id,{method:'DELETE'});loadScripts()}
|
||||
async function showExec(id){const s=_scriptsCache.find(x=>x.id===id);if(!s)return;document.getElementById('execScriptName').textContent=s.name;document.getElementById('execResult').classList.add('hidden');const r=await apiFetch(API+'/servers/');if(!r)return;const data=await r.json();const servers=data.items||data;document.getElementById('execServers').innerHTML=servers.map(s=>`<option value="${s.id}">${esc(s.name)} (${esc(s.domain)})</option>`).join('');document.getElementById('execModal').classList.remove('hidden');document.getElementById('execModal').dataset.scriptId=id;document.getElementById('execModal').dataset.cmd=s.content||''}
|
||||
async function loadScripts(){try{const r=await apiFetch(API+'/scripts/');if(!r)return;const scripts=await r.json();_scriptsCache=scripts;document.getElementById('scriptList').innerHTML=scripts.length?scripts.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4"><div class="flex items-center justify-between"><span class="text-white font-medium cursor-pointer hover:text-brand-light transition" onclick="showEditScript(${s.id})">${esc(s.name)}</span><div class="flex items-center gap-2"><span class="text-xs px-2 py-0.5 rounded bg-slate-800 text-slate-400">${s.category||'ops'}</span><button onclick="showExec(${s.id})" class="text-brand-light text-xs hover:underline">执行</button><button onclick="showEditScript(${s.id})" class="text-slate-400 text-xs hover:underline">编辑</button><button onclick="deleteScript(${s.id})" class="text-red-400 text-xs hover:underline">删除</button></div></div><pre class="mt-2 text-sm text-slate-400 font-mono bg-slate-950 rounded-lg p-3 overflow-x-auto max-h-32">${esc(s.content?.substring(0,300)||'')}</pre></div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无脚本</div>'}catch(e){}}
|
||||
|
||||
function showNewScript(){
|
||||
document.getElementById('editScriptId').value='';
|
||||
document.getElementById('scriptFormTitle').textContent='新建脚本';
|
||||
document.getElementById('scriptName').value='';
|
||||
document.getElementById('scriptCategory').value='';
|
||||
document.getElementById('scriptContent').value='';
|
||||
document.getElementById('scriptForm').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function showEditScript(id){
|
||||
const s=_scriptsCache.find(x=>x.id===id);if(!s)return;
|
||||
document.getElementById('editScriptId').value=s.id;
|
||||
document.getElementById('scriptFormTitle').textContent='编辑脚本';
|
||||
document.getElementById('scriptName').value=s.name||'';
|
||||
document.getElementById('scriptCategory').value=s.category||'';
|
||||
document.getElementById('scriptContent').value=s.content||'';
|
||||
document.getElementById('scriptForm').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function hideScriptForm(){document.getElementById('scriptForm').classList.add('hidden')}
|
||||
|
||||
async function saveScript(){
|
||||
const id=document.getElementById('editScriptId').value;
|
||||
const body={name:document.getElementById('scriptName').value,category:document.getElementById('scriptCategory').value||'ops',content:document.getElementById('scriptContent').value};
|
||||
if(!body.name||!body.content){toast('warning','名称和内容必填');return}
|
||||
let r;
|
||||
if(id){r=await apiFetch(API+'/scripts/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)})}
|
||||
else{r=await apiFetch(API+'/scripts/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)})}
|
||||
if(r&&r.ok)toast('success',id?'脚本已更新':'脚本已创建');else toast('error','保存失败');
|
||||
hideScriptForm();loadScripts();
|
||||
}
|
||||
|
||||
async function deleteScript(id){if(!confirm('确定删除脚本?'))return;const r=await apiFetch(API+'/scripts/'+id,{method:'DELETE'});if(r&&r.ok)toast('success','脚本已删除');else toast('error','删除失败');loadScripts()}
|
||||
|
||||
async function showExec(id){const s=_scriptsCache.find(x=>x.id===id);if(!s)return;document.getElementById('execScriptName').textContent=s.name;document.getElementById('execResult').classList.add('hidden');document.getElementById('execBtn').disabled=false;const r=await apiFetch(API+'/servers/');if(!r)return;const data=await r.json();const servers=data.items||data;document.getElementById('execServers').innerHTML=servers.map(s=>`<option value="${s.id}">${esc(s.name)} (${esc(s.domain)})</option>`).join('');document.getElementById('execModal').classList.remove('hidden');document.getElementById('execModal').dataset.scriptId=id;document.getElementById('execModal').dataset.cmd=s.content||''}
|
||||
function hideExecModal(){document.getElementById('execModal').classList.add('hidden')}
|
||||
async function doExec(){const opts=document.getElementById('execServers').selectedOptions;const ids=Array.from(opts).map(o=>parseInt(o.value));if(!ids.length){alert('请选择服务器');return}const timeout=parseInt(document.getElementById('execTimeout').value)||60;const el=document.getElementById('execResult');el.classList.remove('hidden');el.textContent='执行中...';const modal=document.getElementById('execModal');const scriptId=parseInt(modal.dataset.scriptId)||null;const cmd=modal.dataset.cmd||'';const r=await apiFetch(API+'/scripts/exec',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({script_id:scriptId,command:cmd,server_ids:ids,timeout:timeout})});if(!r){el.textContent='认证失败';return}const d=await r.json();el.textContent=JSON.stringify(d,null,2)}
|
||||
|
||||
async function doExec(){
|
||||
const opts=document.getElementById('execServers').selectedOptions;const ids=Array.from(opts).map(o=>parseInt(o.value));
|
||||
if(!ids.length){toast('warning','请选择服务器');return}
|
||||
const timeout=parseInt(document.getElementById('execTimeout').value)||60;
|
||||
const el=document.getElementById('execResult');el.classList.remove('hidden');el.textContent='执行中...';
|
||||
const btn=document.getElementById('execBtn');btn.disabled=true;btn.textContent='执行中...';
|
||||
const modal=document.getElementById('execModal');
|
||||
const scriptId=parseInt(modal.dataset.scriptId)||null;const cmd=modal.dataset.cmd||'';
|
||||
const r=await apiFetch(API+'/scripts/exec',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify({script_id:scriptId,command:cmd,server_ids:ids,timeout:timeout})});
|
||||
btn.disabled=false;btn.textContent='执行';
|
||||
if(!r){el.textContent='认证失败';toast('error','认证失败');return}
|
||||
const d=await r.json();
|
||||
// Format execution results
|
||||
let output='';
|
||||
if(d.results){
|
||||
for(const[sid,res]of Object.entries(d.results)){
|
||||
output+=`\n── 服务器 #${sid} ──\n`;
|
||||
if(typeof res==='object'&&res.results){res.results.forEach(r=>{output+=`$ ${r.command||''}\n${r.stdout||''}${r.stderr?'STDERR: '+r.stderr+'\n':''}退出码: ${r.exit_code}\n`})}
|
||||
else{output+=JSON.stringify(res,null,2)}
|
||||
}
|
||||
}else{output=JSON.stringify(d,null,2)}
|
||||
el.textContent=output||'无输出';
|
||||
toast(d.status==='success'?'success':'warning','脚本执行完成');
|
||||
}
|
||||
|
||||
function esc(s){const d=document.createElement('div');d.textContent=s||'';return d.innerHTML}
|
||||
loadScripts()
|
||||
</script>
|
||||
</body></html>
|
||||
</body></html>
|
||||
|
||||
@@ -217,7 +217,7 @@
|
||||
if(tab==='ssh')loadSSHSessions();
|
||||
}
|
||||
|
||||
async function deleteServer(id){if(!confirm('确定删除服务器 #'+id+'?'))return;await apiFetch(API+'/servers/'+id,{method:'DELETE'});if(selectedServerId===id)hideDetail();loadServers()}
|
||||
async function deleteServer(id){if(!confirm('确定删除服务器 #'+id+'?'))return;const r=await apiFetch(API+'/servers/'+id,{method:'DELETE'});if(r&&r.ok)toast('success','服务器已删除');else toast('error','删除失败');if(selectedServerId===id)hideDetail();loadServers()}
|
||||
function showAddServer(){document.getElementById('editServerId').value='';document.getElementById('modalTitle').textContent='添加服务器';['srvName','srvDomain','srvPassword','srvCategory','srvTarget','srvDesc'].forEach(id=>document.getElementById(id).value='');document.getElementById('srvPort').value='22';document.getElementById('srvUser').value='root';document.getElementById('srvAuth').value='key';document.getElementById('serverModal').classList.remove('hidden')}
|
||||
function showEditServer(id){const s=_serversCache[id];if(!s)return;document.getElementById('editServerId').value=s.id;document.getElementById('modalTitle').textContent='编辑服务器';document.getElementById('srvName').value=s.name||'';document.getElementById('srvDomain').value=s.domain||'';document.getElementById('srvPort').value=s.port||22;document.getElementById('srvUser').value=s.username||'root';document.getElementById('srvAuth').value=s.auth_method||'key';document.getElementById('srvPassword').value='';document.getElementById('srvCategory').value=s.category||'';document.getElementById('srvTarget').value=s.target_path||'';document.getElementById('srvDesc').value=s.description||'';document.getElementById('serverModal').classList.remove('hidden')}
|
||||
function hideServerModal(){document.getElementById('serverModal').classList.add('hidden')}
|
||||
@@ -227,7 +227,7 @@
|
||||
const body={name:document.getElementById('srvName').value,domain:document.getElementById('srvDomain').value,port:parseInt(document.getElementById('srvPort').value)||22,username:document.getElementById('srvUser').value||'root',auth_method:document.getElementById('srvAuth').value,category:document.getElementById('srvCategory').value||null,target_path:document.getElementById('srvTarget').value||null,description:document.getElementById('srvDesc').value||null};
|
||||
if(body.auth_method==='password'){body.password=document.getElementById('srvPassword').value}
|
||||
if(!body.name||!body.domain){alert('名称和地址必填');return}
|
||||
if(id){await apiFetch(API+'/servers/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)})}else{await apiFetch(API+'/servers/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)})}
|
||||
if(id){const r=await apiFetch(API+'/servers/'+id,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify(body)});if(r&&r.ok)toast('success','服务器已更新');else toast('error','更新失败')}else{const r=await apiFetch(API+'/servers/',{method:'POST',headers:apiHeadersJSON(),body:JSON.stringify(body)});if(r&&r.ok)toast('success','服务器已添加');else toast('error','添加失败')}
|
||||
hideServerModal();loadServers();
|
||||
if(selectedServerId&&id==selectedServerId)selectServer(parseInt(id));
|
||||
}
|
||||
|
||||
@@ -91,10 +91,8 @@
|
||||
|
||||
async function saveSetting(key){
|
||||
const v=document.getElementById('set_'+key).value;
|
||||
await apiFetch(API+'/settings/'+key,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({value:v})});
|
||||
const msg=document.getElementById('saveMsg');
|
||||
msg.classList.remove('hidden');
|
||||
setTimeout(()=>msg.classList.add('hidden'),2000);
|
||||
const r=await apiFetch(API+'/settings/'+key,{method:'PUT',headers:apiHeadersJSON(),body:JSON.stringify({value:v})});
|
||||
if(r&&r.ok)toast('success','设置已保存');else toast('error','保存失败');
|
||||
}
|
||||
|
||||
function esc(s){if(!s)return'';const d=document.createElement('div');d.textContent=s;return d.innerHTML}
|
||||
|
||||
Reference in New Issue
Block a user