Files
cms-SiteForge/app/static/operations.js
T

43 lines
7.1 KiB
JavaScript
Raw Normal View History

'use strict';
const $=s=>document.querySelector(s), esc=v=>String(v??'').replace(/[&<>"']/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));
let csrf='',sites=[],siteId='',jobs=[],builds=[],releases=[],lastPreview=null;
const notify=(message,error=false)=>{$('#notice').textContent=message;$('#notice').style.color=error?'#ac3d45':'#256448'};
async function request(url,{method='GET',body=null,upload=false}={}){
const headers={};if(method!=='GET')headers['X-SiteForge-CSRF']=csrf;
if(body&&!upload)headers['Content-Type']='application/json';
const res=await fetch(url,{method,credentials:'same-origin',headers,body:body?(upload?body:JSON.stringify(body)):undefined});
if(res.status===401){location.assign('/login');throw Error('登录已过期')}
const result=await res.json();if(!res.ok)throw Error(result.detail||'请求失败');return result;
}
const post=(url,body)=>request(url,{method:'POST',body});
function formatJobs(list){return list.length?list.map(j=>`<div class="row"><div><b>${esc(j.kind||'build')} <span class="flag">${esc(j.state)}</span></b><small>${esc(j.id)} · 尝试 ${esc(j.attempts||0)} 次</small></div>${['queued','running','retrying'].includes(j.state)?`<button class="secondary" onclick="cancelJob('${esc(j.id)}')">取消</button>`:''}</div>`).join(''):'当前没有任务';}
async function chooseSite(){siteId=$('#site').value;lastPreview=null;$('#commitImport').disabled=true;await reload()}
async function init(){try{csrf=(await request('/api/auth/me')).csrf;sites=await request('/api/sites');$('#site').innerHTML=sites.map(s=>`<option value="${esc(s.id)}">${esc(s.name)}</option>`).join('');siteId=sites[0]?.id||'';$('#site').onchange=chooseSite;$('#reload').onclick=reload;$('#previewImport').onclick=()=>importJSON(false);$('#commitImport').onclick=()=>importJSON(true);$('#build').onclick=buildSite;$('#deploy').onclick=deploySite;$('#refreshRelease').onclick=reload;await reload()}catch(e){notify(e.message,true)}}
async function reload(){if(!siteId)return;try{
const [list,jobsData,buildData,approvals,history,reviewData]=await Promise.all([
request(`/api/sites/${siteId}`),request(`/api/enterprise/sites/${siteId}/jobs`),request(`/api/sites/${siteId}/builds`),request(`/api/factory/sites/${siteId}/release-requests`),request(`/api/enterprise/sites/${siteId}/releases`),request(`/api/migrations/sites/${siteId}/reviews`)]);
jobs=jobsData;builds=buildData;releases=history;$('#statBuild').textContent=builds.length;$('#statJobs').textContent=jobs.filter(j=>['queued','running','retrying'].includes(j.state)).length;$('#statApprovals').textContent=approvals.filter(j=>j.state==='pending').length;$('#statMode').textContent=list.mode==='migration'?'迁移':'新建';$('#jobList').innerHTML=formatJobs(jobs);
$('#buildList').innerHTML=builds.length?builds.slice(0,14).map(b=>`<div class="row"><div><b>${esc(b.id)}</b><small>${esc(b.created_at)} · ${esc(b.total_pages)} 页</small></div><button class="secondary" onclick="requestApproval('${esc(b.id)}')">提交审批</button></div>`).join(''):'当前没有构建';
$('#releaseBuild').innerHTML=builds.map(b=>`<option value="${esc(b.id)}">${esc(b.id)} · ${esc(b.total_pages)} 页</option>`).join('');
$('#approvals').innerHTML=approvals.length?approvals.slice(0,12).map(a=>`<div class="row"><div><b>${esc(a.build_id)} <span class="flag ${a.state==='pending'?'warn':''}">${esc(a.state)}</span></b><small>由 ${esc(a.requested_by)} 提交</small></div>${a.state==='pending'?`<button class="secondary" onclick="approve('${esc(a.id)}')">批准</button>`:''}</div>`).join(''):'暂无审批记录';
$('#reviews').innerHTML=reviewData.items.length?reviewData.items.map(x=>`<div class="row"><div><b>${esc(x.platform)} · 第 ${esc(x.row_number)} 条</b><small>${esc(x.issue)}</small></div><button class="secondary" onclick="resolveReview('${esc(x.id)}')">已检查并处理</button></div>`).join(''):'暂无待复核事项';
$('#releases').innerHTML=history.length?history.slice(0,10).map(x=>`<div class="row"><div><b>${esc(x.build_id)} · ${esc(x.action)}</b><small>${esc(x.created_at)}</small></div></div>`).join(''):'暂无部署记录';
const entries=await request(`/api/sites/${siteId}/entries`);const entry=entries.find(p=>p.route==='/')||entries[0];$('#edit').href=entry?`/editor/${siteId}/${entry.id}`:'/factory-studio';
}catch(e){notify(e.message,true)}}
async function importJSON(commit){const file=$('#file').files?.[0];if(!file)return notify('请选择 JSON 文件',true);if(!siteId)return;const platform=$('#platform').value;
if(commit&&!lastPreview)return;const data=new FormData();data.append('platform',platform);data.append('file',file);if(commit)data.append('conflict','skip');
try{const result=await request(`/api/migrations/sites/${siteId}/${commit?'import':'preview'}`,{method:'POST',body:data,upload:true});if(commit){notify(`导入完成:${result.result.imported} 条,人工复核 ${result.migration_warnings.length} 项`);lastPreview=null;$('#commitImport').disabled=true;await reload()}else{lastPreview=result;$('#commitImport').disabled=result.errors.length>0;$('#report').innerHTML=`<b>输入 ${result.input} 条 · 可映射 ${result.mapped} 条 · 错误 ${result.errors.length} 条 · 复核项 ${result.warnings.length} 条</b><div>${result.errors.slice(0,12).map(x=>`<p>第${x.row}条:${esc(x.error)}</p>`).join('')}${result.warnings.slice(0,12).map(x=>`<p>待复核 第${x.row}条:${esc(x.issue)}</p>`).join('')}</div>`}}
catch(e){notify(e.message,true)}}
async function resolveReview(id){
const answer=prompt('请详细填写对照旧站完成检查/修复的说明(至少12个字符)。未经核查不要批准。');
if(answer===null)return;
try{await post(`/api/migrations/sites/${siteId}/reviews/${id}/resolve`,{resolution:answer});notify('复核记录已保存');await reload()}catch(e){notify(e.message,true)}
}
async function buildSite(){try{const task=await post(`/api/sites/${siteId}/builds`,{mirror_images:false});notify(`任务 ${task.id} 已进入 Worker 队列`);await reload()}catch(e){notify(e.message,true)}}
async function cancelJob(id){try{await post(`/api/enterprise/sites/${siteId}/jobs/${id}/cancel`,{});await reload()}catch(e){notify(e.message,true)}}
async function requestApproval(build){try{await post(`/api/factory/sites/${siteId}/release-requests`,{build_id:build});notify('已提交发布审批');await reload()}catch(e){notify(e.message,true)}}
async function approve(id){if(!confirm('审批人必须与申请人不同。确定审批通过吗?'))return;try{await post(`/api/factory/sites/${siteId}/release-requests/${id}/review`,{state:'approved'});notify('审批已完成');await reload()}catch(e){notify(e.message,true)}}
async function deploySite(){if(!confirm('请确保目标环境已配置 HTTPS 和发布 Agent。继续发布?'))return;try{const data=await post(`/api/production/sites/${siteId}/deploy`,{build_id:$('#releaseBuild').value,target:$('#target').value});notify(`已发布:${data.remote.release}`);await reload()}catch(e){notify(e.message,true)}}
window.resolveReview=resolveReview;window.cancelJob=cancelJob;window.requestApproval=requestApproval;window.approve=approve;
window.addEventListener('DOMContentLoaded',init);