Files
Nexus/web/app/schedules.html
T
Your Name e9feaa6cdc 前后端一致性审计 + 批量操作 + 调度同步模式
- 服务器列表: 添加批量选择(checkbox)+批量推送/删除操作栏
- 推送页面: 支持从服务器列表页预选服务器跳转(sessionStorage)
- 调度页面: 添加同步模式选择(增量/全量/校验和)
- 后端: PushSchedule模型+Schema+API+ScheduleRunner添加sync_mode字段
- 仪表盘: 移除重复的函数定义和初始化调用
- 数据库迁移: 启动时自动执行schema migration(添加sync_mode列)
- 安装向导: 添加schema migration步骤

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 11:01:17 +08:00

66 lines
8.5 KiB
HTML

<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>Nexus — 定时调度</title><script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script><script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.8/dist/cdn.min.js"></script><style type="text/tailwindcss">@theme{--color-brand:oklch(55% 0.2 250);--color-brand-light:oklch(75% 0.15 250);--color-brand-dark:oklch(35% 0.18 250)}</style></head>
<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 flex items-center justify-between">
<div class="flex items-center gap-4"><button @click="sidebarOpen=!sidebarOpen" class="text-slate-400 hover:text-white transition"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></button><h1 class="text-white font-semibold">定时调度</h1></div>
<button onclick="showAddSched()" class="px-3 py-1.5 bg-brand hover:bg-brand-dark text-white text-sm rounded-lg transition">+ 新建</button>
</header>
<main class="flex-1 overflow-y-auto p-6">
<div id="schedList" class="space-y-3"><div class="text-slate-500 text-center py-8">加载中...</div></div>
<div id="schedModal" 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-lg space-y-3">
<h2 id="schedModalTitle" class="text-white font-semibold text-lg">新建调度</h2>
<input type="hidden" id="editSchedId">
<div><label class="block text-slate-400 text-xs mb-1">名称</label><input id="sName" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="每日推送"></div>
<div><label class="block text-slate-400 text-xs mb-1">源路径</label><input id="sPath" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm" placeholder="/home/deploy/project/"></div>
<div><label class="block text-slate-400 text-xs mb-1">Cron 表达式</label><input id="sCron" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm font-mono" placeholder="0 2 * * *"><div class="text-slate-600 text-xs mt-1">分 时 日 月 周 · 例: 0 2 * * * = 每天凌晨2点</div></div>
<div><label class="block text-slate-400 text-xs mb-1">目标服务器</label><select id="sServers" 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><select id="sSyncMode" class="w-full px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"><option value="incremental">增量同步</option><option value="full">全量同步</option><option value="checksum">校验和</option></select></div>
<div class="flex gap-2 pt-2"><button onclick="saveSched()" class="px-4 py-2 bg-brand hover:bg-brand-dark text-white rounded-lg text-sm">保存</button><button onclick="hideSchedModal()" class="px-4 py-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-lg text-sm">取消</button></div>
</div>
</div>
</main>
</div>
<script src="/app/api.js"></script>
<script src="/app/layout.js"></script>
<script>
initLayout('schedules');
let _schedsCache={};
async function loadScheds(){
try{
const r=await apiFetch(API+'/schedules/');if(!r)return;const scheds=await r.json();
_schedsCache={};scheds.forEach(s=>_schedsCache[s.id]=s);
document.getElementById('schedList').innerHTML=scheds.length?scheds.map(s=>`<div class="bg-slate-900 rounded-xl border border-slate-800 p-4">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<button onclick="toggleSched(${s.id},${!s.enabled})" class="w-10 h-6 rounded-full transition-colors ${s.enabled?'bg-brand':'bg-slate-700'} relative shrink-0" title="${s.enabled?'点击禁用':'点击启用'}"><span class="absolute top-0.5 ${s.enabled?'right-0.5':'left-0.5'} w-5 h-5 bg-white rounded-full shadow transition-all"></span></button>
<div><span class="text-white font-medium">${esc(s.name)}</span><div class="text-slate-500 text-xs mt-0.5"><span class="font-mono">${esc(s.cron_expr)}</span> · ${esc(s.sync_mode||'incremental')}${s.last_run_at?' · 上次: '+fmtTime(s.last_run_at):''}</div></div>
</div>
<div class="flex items-center gap-2"><button onclick="showEditSched(${s.id})" class="text-slate-400 hover:text-white text-xs transition">编辑</button><button onclick="deleteSched(${s.id})" class="text-red-400 text-xs hover:underline">删除</button></div>
</div>
</div>`).join(''):'<div class="text-slate-500 text-center py-8">暂无调度</div>';
}catch(e){document.getElementById('schedList').innerHTML='<div class="text-red-400 text-center py-8">加载失败</div>'}
}
async function loadServersIntoSelect(selectedIds){
const r=await apiFetch(API+'/servers/');if(!r)return;const data=await r.json();const servers=data.items||data;
document.getElementById('sServers').innerHTML=servers.map(s=>`<option value="${s.id}" ${selectedIds&&selectedIds.includes(s.id)?'selected':''}>${esc(s.name)} (${esc(s.domain)})</option>`).join('');
}
function showAddSched(){document.getElementById('editSchedId').value='';document.getElementById('schedModalTitle').textContent='新建调度';document.getElementById('sName').value='';document.getElementById('sPath').value='';document.getElementById('sCron').value='';document.getElementById('sSyncMode').value='incremental';loadServersIntoSelect([]);document.getElementById('schedModal').classList.remove('hidden')}
async function showEditSched(id){const s=_schedsCache[id];if(!s)return;document.getElementById('editSchedId').value=s.id;document.getElementById('schedModalTitle').textContent='编辑调度';document.getElementById('sName').value=s.name||'';document.getElementById('sPath').value=s.source_path||'';document.getElementById('sCron').value=s.cron_expr||'';document.getElementById('sSyncMode').value=s.sync_mode||'incremental';let serverIds=[];try{serverIds=JSON.parse(s.server_ids||'[]')}catch(e){}await loadServersIntoSelect(serverIds);document.getElementById('schedModal').classList.remove('hidden')}
function hideSchedModal(){document.getElementById('schedModal').classList.add('hidden')}
async function saveSched(){
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),sync_mode:document.getElementById('sSyncMode').value||'incremental',enabled:true};
if(!body.name||!body.cron_expr){alert('名称和Cron必填');return}
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){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();
</script>
</body></html>