upgrade: adopt V8.31.1 (theme 1.3/1.4 redesigns, editor deep-canvas, guided launch, component library)
Overlay 190 upstream items (V8.22→8.31) preserving trade-pro theme, registry tests, and design docs. Re-applied full patch set on identical anchors (discovery/env_for/blocks.css/type-mapping/locale-override/ theme_color/frontend entries/test_theme_pro_v3 mods) plus compose pins. Verified: theme suites 35/35; dual-baseline full run 415 passed vs pristine 386+29 ours, FAILED-set diff EMPTY (same 10 Windows env fails). Live: container 8.31.1-dev.1 healthy, hesupipe 192 entries intact, trade-pro build 203 pages 0 errors. Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+150
-30
@@ -7,8 +7,8 @@ const typeName={page:'页面',product:'产品',vehicle:'车型',post:'新闻',br
|
||||
const fmtDate=t=>{if(!t)return '—';const d=new Date(t);return Number.isNaN(d.getTime())?String(t).slice(0,16):d.toLocaleDateString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit'})};
|
||||
const sfSiteKey='siteforge:selected-site';
|
||||
const sfRemember=id=>{try{if(id)localStorage.setItem(sfSiteKey,id)}catch{}};
|
||||
const state={sites:[],site:null,entries:[],builds:[],jobs:[],view:'home',csrf:'',search:'',typ:'',loading:false,job:null,selectionToken:0,building:false,delivery:{jobs:[],history:[],configured:{}},language:{group:null,sites:[],units:[]},languageGroups:[],languageEntries:[]};
|
||||
const headings={home:'工作台',sites:'我的网站',overview:'网站概览',content:'内容与页面',design:'外观与设计',import:'导入与迁移',build:'构建与交付',languages:'多语言管理',settings:'网站设置'};
|
||||
const state={launchDeploying:false,releaseRequestSeq:0,releaseRequests:[],releaseRequestsKnown:false,launchBuildId:'',launchTarget:'staging',launchReview:false,launchPreflight:null,sites:[],site:null,entries:[],builds:[],jobs:[],view:'home',csrf:'',search:'',typ:'',loading:false,job:null,selectionToken:0,building:false,delivery:{jobs:[],history:[],configured:{}},language:{group:null,sites:[],units:[]},languageGroups:[],languageEntries:[]};
|
||||
const headings={home:'工作台',sites:'我的网站',guide:'建站操作指引',launch:'上线操作中心',overview:'网站概览',content:'内容与页面',design:'外观与设计',import:'导入与迁移',build:'生成与预览',languages:'多语言管理',settings:'网站设置'};
|
||||
async function api(url,method='GET',data=null,extraHeaders={}){
|
||||
const headers={...extraHeaders};if(method!=='GET'&&method!=='HEAD')headers['X-SiteForge-CSRF']=state.csrf;
|
||||
let body=data;if(data!=null && !(data instanceof FormData)){headers['Content-Type']='application/json';body=JSON.stringify(data)}
|
||||
@@ -49,20 +49,74 @@ function sites(){return htmlHeading('我的网站','统一管理所有网站项
|
||||
function currentSite(){return !!state.site}
|
||||
function noSite(){return htmlHeading('请先选择一个网站','选择现有网站或创建新网站,才能继续进行内容编辑、迁移和构建。')+emptyState('尚未选择网站','点击右上角选择网站,或直接创建一个新的企业网站。',actionButton('+ 创建网站','new-site'))}
|
||||
function latestDone(){return state.builds.find(b=>['done','complete','success','completed'].includes(b.status))||null}
|
||||
function overview(){const s=state.site;if(!s)return noSite();const count=state.entries.length;const build=latestDone();const hasContent=buildReadyStep();const hasDomain=!!s.domain;const steps=[
|
||||
{title:'建立网站',desc:'选择行业主题并创建项目',done:true,view:'settings'},
|
||||
{title:'准备内容',desc:'添加页面、产品或导入旧数据',done:hasContent,view:'content'},
|
||||
{title:'调整设计',desc:'可反复完善主题、导航和移动端样式',optional:true,view:'design'},
|
||||
{title:'构建并检查',desc:'生成静态网站,查看质量报告',done:!!build,view:'build'}];
|
||||
const next=!hasContent?steps[1]:!build?steps[3]:null;
|
||||
return htmlHeading(s.name,'这里显示真实的内容和构建状态。设计可随时调整,正式上线需要单独审核。',actionButton('↗ 构建网站','build-now'))+
|
||||
`<div class="task-note"><div><strong>${next?'建议下一步:'+next.title:'网站已经生成过构建版本'}</strong><p>${next?next.desc:'内容或设计有修改时,重新构建并检查新版即可。'}。${!hasDomain?'正式域名还未填写;测试预览不受影响。':''}</p></div><button class="btn btn-primary" ${nav(next?.view||'build')}>${next?'现在去做':'查看构建版本'} →</button></div>`+
|
||||
`<div class="stats">${stat('网站内容',count,'▧')}${stat('已构建版本',state.builds.length,'↗')}${stat('当前主题',themeName[s.theme]||'企业品牌','◈')}${stat('域名状态',hasDomain?'已填写':'待设置','◎')}</div>`+
|
||||
`<div class="section-heading"><div><h2>四步完成网站制作</h2><p>步骤反映已有数据,设计可以随时修改,不会虚报“设计完成”。</p></div></div>`+
|
||||
`<div class="wizard-grid">${steps.map((step,i)=>`<div class="wizard-step ${step.done?'done':next===step?'next':''}"><span class="wizard-index">${step.done?'✓':step.optional?'◈':i+1}</span><h3>${step.title}${step.optional?'(持续优化)':''}</h3><p>${step.desc}</p><button ${nav(step.view)}>打开 →</button></div>`).join('')}</div>`+
|
||||
`<div class="section-heading"><div><h2>常用操作</h2><p>通常只需要下面三个入口。</p></div></div>`+
|
||||
`<div class="quick-grid">${overviewShortcut('content','▧','添加和编辑内容','管理页面、产品、新闻和案例。')}${overviewShortcut('design','◈','修改网站样式','更换主题、编辑页面和移动端样式。')}${overviewShortcut('build','↗','查看和下载网站','查看任务状态、预览及下载静态网站。')}</div>`;
|
||||
function overview(){
|
||||
const s=state.site;if(!s)return noSite();
|
||||
const journey=window.SiteForgeJourney?.plan({site:s,entries:state.entries,builds:state.builds,jobs:state.jobs,delivery:state.delivery});
|
||||
const next=journey?.next||{label:'查看操作指引',hint:'检查各步骤是否已经完成。',view:'guide'};
|
||||
const contentCount=state.entries.filter(e=>e.source!=='starter').length;
|
||||
return htmlHeading(s.name,'所有网站操作都从这里开始;保存内容、生成网站和正式上线是不同的步骤。',`<button type="button" class="btn btn-primary" data-action="navigate" data-view="guide">查看完整操作指引 →</button>`)+
|
||||
`<section class="sf-journey-intro"><div><span class="sf-journey-eyebrow">建议下一步</span><h2>${esc(next.label)}</h2><p>${esc(next.hint)}</p></div><button type="button" class="btn btn-primary" data-action="navigate" data-view="${esc(next.view)}">现在去做 →</button></section>`+
|
||||
`<div class="stats">${stat('人工添加的内容',contentCount,'▧')}${stat('静态构建版本',state.builds.length,'↗')}${stat('当前主题',themeName[s.theme]||'企业品牌','◈')}${stat('正式域名',s.domain?'已填写':'未设置','◎')}</div>`+
|
||||
`<div class="section-heading"><div><h2>最常用的三项操作</h2><p>不需要了解技术细节,选择你想完成的事情即可。</p></div></div>`+
|
||||
`<div class="quick-grid">${overviewShortcut('content','▧','修改文字和产品','填写网站内容,修改已经创建的页面。')}${overviewShortcut('design','◈','调整网页外观','编辑图片、版式、导航和手机端。')}${overviewShortcut('launch','↗','检查并上线网站','一步一步完成预览、审批和正式部署。')}</div>`+
|
||||
`<div class="sf-journey-notice" role="note"><strong>关于上线</strong>:页面编辑和保存不会自动修改线上网站。上线仍需生成静态版本、检查、独立审批和部署。</div>`;
|
||||
}
|
||||
function guide(){
|
||||
if(!currentSite())return noSite();
|
||||
const journey=window.SiteForgeJourney?.plan({site:state.site,entries:state.entries,builds:state.builds,jobs:state.jobs,delivery:state.delivery});
|
||||
if(!journey)return htmlHeading('建站操作指引','操作引导暂时无法加载,请刷新网页。');
|
||||
const checked=journey.completed;
|
||||
const rows=journey.steps.map((step,i)=>{
|
||||
const label=step.optional?'可选:随时完善':step.done?'已记录完成':step.id===journey.next.id?'建议现在进行':'等待前面的步骤';
|
||||
return `<li class="sf-journey-step ${step.done?'is-done':''} ${step.id===journey.next.id?'is-next':''}">
|
||||
<span class="sf-journey-number" aria-hidden="true">${step.done?'✓':i+1}</span>
|
||||
<div><div class="sf-journey-title"><strong>${esc(step.label)}</strong><span>${esc(label)}</span></div>
|
||||
<p>${esc(step.hint)}</p></div>
|
||||
<button type="button" class="btn ${step.id===journey.next.id?'btn-primary':'btn-muted'}" data-action="navigate" data-view="${esc(step.view)}">${step.id===journey.next.id?'开始操作':'查看 / 修改'} →</button>
|
||||
</li>`;
|
||||
}).join('');
|
||||
return htmlHeading('建站操作指引','按照实际操作顺序完成网站制作,系统只根据已保存的数据与任务记录显示进度。')+
|
||||
`<section class="sf-journey-intro"><div><span class="sf-journey-eyebrow">当前项目</span><h2>${esc(state.site.name)}</h2><p>${esc(journey.next.hint)}</p></div>
|
||||
<div class="sf-journey-progress"><strong>${checked}/4</strong><span>已记录的必要步骤</span></div></section>
|
||||
<ol class="sf-journey-list">${rows}</ol>
|
||||
<section class="sf-journey-after"><h2>保存、构建和上线有什么不同?</h2><div class="sf-glossary-grid">
|
||||
<p><strong>保存内容</strong><span>把修改写入管理系统,外部访客还看不到新版。</span></p>
|
||||
<p><strong>生成网站</strong><span>制作一份可预览、可下载的静态网站版本。</span></p>
|
||||
<p><strong>检查与审批</strong><span>核对页面、图片、网址和 SEO,并取得相应审批。</span></p>
|
||||
<p><strong>正式上线</strong><span>把批准的版本部署到正式服务器,再核对公网访问。</span></p></div></section>
|
||||
<section class="sf-journey-notice" role="note"><strong>重要说明</strong>:${journey.live?'系统发现正式发布记录,但无法仅凭该记录确认当前公网网站可访问,请人工检查实际域名。':'当前没有可确认的正式发布记录;生成 ZIP 或点击“发布设计”都不代表网站已上线。'}${journey.changedAfterBuild?' 网站内容在最近一次构建后可能有变化,建议重新构建并预览。':''}${!journey.domain?' 正式域名尚未填写,不影响预览。':''}</section>`;
|
||||
}
|
||||
// V8.28 guided delivery: one deliberate action per stage, existing secure APIs only.
|
||||
function launch(){
|
||||
if(!currentSite())return noSite();
|
||||
const buildList=state.builds.filter(b=>b.status==='complete');
|
||||
const selection=buildList.find(b=>b.id===state.launchBuildId)||buildList[0]||null;
|
||||
const selected=selection?.id||'';
|
||||
if(selected&&!state.launchBuildId){state.launchBuildId=selected;state.launchReview=false;state.launchPreflight=null;}
|
||||
const requests=state.releaseRequests.filter(r=>r.build_id===selected);
|
||||
const result=window.SiteForgeLaunch?.plan({site:state.site,build:selection,requests,requestsKnown:state.releaseRequestsKnown,
|
||||
delivery:state.delivery,target:state.launchTarget,previewChecked:state.launchReview,preflight:state.launchPreflight});
|
||||
const row=(index,name,body,controls,kind='')=>`<section class="sf-launch-step ${kind}"><div class="sf-launch-index">${index}</div><div class="sf-launch-detail"><h2>${esc(name)}</h2><p>${esc(body)}</p><div class="sf-launch-controls">${controls}</div></div></section>`;
|
||||
const buildOptions=buildList.map(b=>`<option value="${esc(b.id)}" ${selected===b.id?'selected':''}>${esc(fmtDate(b.created_at))} · ${esc(b.id.slice(0,12))} · ${Number(b.total_pages)||0} 页</option>`).join('');
|
||||
const select=`<label class="sf-launch-field">静态构建版本<select id="launchBuildSelect" ${buildList.length?'':'disabled'}><option value="">${buildList.length?'请选择版本':'暂无成功构建'}</option>${buildOptions}</select></label>`;
|
||||
const checked=Boolean(state.launchReview&&selected);
|
||||
const canRequestApproval=Boolean(selected&&checked&&state.releaseRequestsKnown&&['none','rejected'].includes(result?.approval));
|
||||
const preview=selected?`<a class="btn" target="_blank" rel="noopener" href="/preview/${encodeURIComponent(state.site.id)}/${encodeURIComponent(selected)}/">打开网站预览 ↗</a><button type="button" class="btn" data-action="launch-report" data-id="${esc(selected)}">检查质量报告</button>`:`<button class="btn btn-primary" data-action="launch-build">去生成网站</button>`;
|
||||
const approvalText=result?.approval==='approved'?'已由其他审核人批准。':result?.approval==='pending'?'已提交审批,请由其他有审核权限的人员处理。':result?.approval==='rejected'?'最近申请被驳回,请解决问题后重新申请。':!state.releaseRequestsKnown?'审批记录暂时无法确认,请刷新或到审批中心查询。':'尚无已批准的申请。';
|
||||
const config=Boolean(state.delivery.configured?.[state.launchTarget]);
|
||||
const domainUrl=/^https:\/\/[^\s<>]+$/i.test(state.site.domain||'')?state.site.domain:'';
|
||||
const deploymentText=state.launchTarget==='production'?'正式环境':'测试环境';
|
||||
return htmlHeading('上线操作中心','按顺序完成检查和审核,每一步都解释操作用途。网站不会因为保存、预览或提交审批而自动上线。',`<button type="button" class="btn" data-action="launch-refresh">刷新状态</button>`)+
|
||||
`<div class="sf-launch-hero"><div><span class="sf-journey-eyebrow">当前网站</span><h2>${esc(state.site.name)}</h2><p>上线属于独立审批和服务器部署流程;如有不确定之处,可以随时离开,已经保存的记录不会消失。</p></div><strong>${result?.approval==='approved'?'已获审批':result?.approval==='pending'?'等待审批':selected?'准备上线':'请先构建'}</strong></div>`+
|
||||
row('1','选择网站版本','选择已生成成功、准备交付的静态版本。内容有变更时需要重新构建。',select+`<button class="btn" data-action="launch-build">${selected?'重新构建版本':'开始构建网站'}</button>`,selected?'is-ready':'')+
|
||||
row('2','预览网站并检查质量','在新窗口检查页面、图片、移动端、链接和 SEO。质量报告是自动检查,仍需要人工确认。',preview+`<label class="sf-launch-check"><input id="launchReviewCheck" type="checkbox" ${checked?'checked':''} ${selected?'':'disabled'}> 我已经检查此版本的预览与质量报告</label>`,checked?'is-ready':'')+
|
||||
row('3','提交给其他人审批',approvalText,`<button class="btn" data-action="launch-request-approval" ${canRequestApproval?'':'disabled'}>${result?.approval==='pending'?'已提交,等待审核':result?.approval==='approved'?'此版本已批准':'提交审批申请'}</button><a class="btn" href="/operations">查看审批中心 ↗</a>`,result?.approval==='approved'?'is-ready':'')+
|
||||
row('4','检查正式发布条件','审批通过后,再检查构建质量、旧站迁移复核和发布归档是否满足条件。',`<button class="btn" data-action="launch-preflight" ${result?.approval==='approved'&&checked?'':'disabled'}>执行发布前检查</button><span class="sf-launch-status">${esc(result?.preflightMessage||'尚未完成此版本的发布前检查')}</span>`,result?.preflightReady?'is-ready':'')+
|
||||
row('5','提交服务器部署','只有目标环境已配置、审批通过且发布前检查成功,才能从这里提交部署任务。',`<label class="sf-launch-field">选择发布环境<select id="launchTarget"><option value="staging" ${state.launchTarget==='staging'?'selected':''}>测试环境</option><option value="production" ${state.launchTarget==='production'?'selected':''}>正式环境</option></select></label><button class="btn btn-primary" data-action="launch-deploy" ${result?.canDeploy&&!state.launchDeploying?'':'disabled'}>${state.launchDeploying?'正在提交…':'提交部署到'}${deploymentText}</button><span class="sf-launch-status">${state.delivery.unavailable?'无法读取发布目标状态,请刷新或联系管理员。':config?'目标服务器已配置':'当前环境没有配置可用的发布目标,请联系管理员。'}</span>`,result?.hasRemoteRecord?'is-ready':'')+
|
||||
row('6','核对网站是否真正上线','提交部署只是创建任务。请确认任务成功,再在真实域名检查页面和联系表单。',`<button class="btn" data-action="launch-refresh">刷新任务</button>${domainUrl?`<a class="btn" href="${esc(domainUrl)}" target="_blank" rel="noopener noreferrer">打开正式域名 ↗</a>`:'<button class="btn" data-action="navigate" data-view="settings">设置正式域名</button>'}<p class="sf-launch-status">${esc(result?.remoteDescription||'未确认有当前环境的部署记录。')} 系统不会仅凭发布记录声称公网验收通过。</p>`)+
|
||||
`<div class="sf-journey-notice" role="note"><strong>注意</strong>:这里的确认只影响当前操作界面,正式发布仍由服务器重新验证权限、审批、质量、文件和目标配置。页面预览正常也不代表公网已上线。</div><div id="launchNotice" class="sf-launch-notice" role="status" aria-live="polite" ${state.launchMessage?'':'hidden'}>${esc(state.launchMessage||'')}</div><div id="launchReport"></div>`;
|
||||
}
|
||||
|
||||
function content(){if(!currentSite())return noSite();let arr=state.entries.filter(e=>(!state.typ||e.type===state.typ)&&(!state.search||`${e.title} ${e.route}`.toLowerCase().includes(state.search.toLowerCase())));
|
||||
return htmlHeading('内容与页面','所有网页、产品、车型和新闻都在这里维护,保存后重新构建即可更新网站。',actionButton('+ 添加内容','new-entry'))+
|
||||
`<div class="panel"><div class="panel-head"><div><h2>内容列表</h2><p>共 ${state.entries.length} 条内容。点击“设计页面”可进行可视化排版。</p></div></div><div class="toolbar"><input id="contentSearch" aria-label="搜索内容" placeholder="搜索标题或页面路径…" value="${esc(state.search)}"><select id="contentType" aria-label="按类型筛选"><option value="">全部类型</option>${Object.entries(typeName).map(([key,value])=>`<option value="${key}" ${state.typ===key?'selected':''}>${value}</option>`).join('')}</select></div>`+
|
||||
@@ -80,20 +134,20 @@ function imports(){if(!currentSite())return noSite();return htmlHeading('导入
|
||||
`<div class="help-hint" style="margin-top:17px"><strong>迁移旧网站请注意:</strong>如果需要保留旧 URL、原有 SEO、复杂插件字段和全部图片,请优先使用 WordPress 迁移工作台,而不是仅上传一个内容文件。</div>`;
|
||||
}
|
||||
function builds(){if(!currentSite())return noSite();const latest=latestDone();const pending=activeJobs();const first=pending[0];
|
||||
return htmlHeading('构建与交付','网站构建由独立 Worker 执行。离开页面或刷新后仍可查看任务状态。',`<button type="button" class="btn btn-primary" data-action="build-now" ${pending.length||state.building?'disabled':''}>${pending.length?'已有构建在运行':'↗ 开始构建'}</button>`)+
|
||||
return htmlHeading('生成与预览','先生成网站版本,确认效果后再使用上线操作中心提交审批。任务会在后台执行。',`<button type="button" class="btn btn-primary" data-action="build-now" ${pending.length||state.building?'disabled':''}>${pending.length?'已有构建在运行':'↗ 开始构建'}</button>`)+
|
||||
`<div class="task-note"><div><strong>${first?'当前:'+buildStatusLabel(first.state):latest?'已有构建版本,可以预览':'还没有已完成的网站构建'}</strong><p>${first?'当前任务 '+esc(first.id.slice(0,12))+';Worker 在后台执行,您可以关闭此页面。':'内容或设计有更新时,重新生成网站版本。'}</p></div><div class="inline-actions"><button class="btn" data-action="check-readiness">交付前检查</button><a class="btn" href="/operations">任务与发布审批 ↗</a></div></div>`+
|
||||
`<div class="panel"><div class="panel-head"><div><h2>后台任务</h2><p>可以查看任务状态。仅取消当前仍在队列或运行中的构建。</p></div><button class="btn btn-tiny" data-action="refresh-builds">刷新状态</button></div><div id="jobNotice" class="message" ${state.job?'':'hidden'}>${esc(state.job||'')}</div>`+
|
||||
(state.jobs.length?state.jobs.slice(0,8).map(j=>`<div class="build-row"><span class="quick-ico">${j.state==='done'?'✓':j.state==='failed'?'!':'◷'}</span><div class="build-meta"><h3>${esc(buildStatusLabel(j.state))}</h3><p>${esc(fmtDate(j.created_at))} · 任务 ${esc(j.id.slice(0,12))}${j.state==='failed'&&j.error?' · '+esc(j.error):''}</p></div>${['queued','running','retrying'].includes(j.state)?`<button class="btn btn-tiny btn-danger" data-action="cancel-job" data-id="${esc(j.id)}">取消任务</button>`:''}</div>`).join(''):'<p class="muted">当前网站还没有后台构建任务。</p>')+`</div>`+
|
||||
`<div class="panel" style="margin-top:18px"><div class="panel-head"><div><h2>构建版本</h2><p>建议先预览和核对质量报告,再下载 ZIP。预览不代表正式发布。</p></div></div>`+
|
||||
(state.builds.length?state.builds.slice(0,20).map(b=>`<div class="build-row"><span class="quick-ico">${['done','completed','success','complete'].includes(b.status)?'✓':'◷'}</span><div class="build-meta"><h3>${esc(fmtDate(b.created_at))} · ${esc(b.total_pages||'—')} 个页面</h3><p>版本 ${esc(b.id.slice(0,12))} · ${esc(b.status||'未知')}</p></div><div class="inline-actions">${['done','completed','success','complete'].includes(b.status)?`<a class="btn btn-tiny" href="/preview/${encodeURIComponent(state.site.id)}/${encodeURIComponent(b.id)}/" target="_blank" rel="noopener">预览 ↗</a><button class="btn btn-tiny" data-action="report" data-id="${esc(b.id)}">质量报告</button><a class="btn btn-tiny btn-primary" href="/api/sites/${encodeURIComponent(state.site.id)}/builds/${encodeURIComponent(b.id)}/download">下载 ZIP</a>`:'<span class="muted">尚未完成,暂不能下载</span>'}</div></div>`).join(''):emptyState('尚无完成版本','准备好网站数据后,在上方提交构建任务。'))+
|
||||
`<div id="reportContent"></div></div>`+
|
||||
`<div class="panel" style="margin-top:18px"><div class="panel-head"><div><h2>正式发布与回滚</h2><p>先完成质量检查与独立审批,Worker 负责上传、校验和切换版本。没有配置目标服务器时不能发布。</p></div><a href="/operations" class="btn btn-tiny">审批与复核工具 ↗</a></div>
|
||||
`<div class="sf-launch-followup"><strong>下一步:预览检查后提交审批</strong><p>完成构建后,请在上线操作中心按照步骤检查质量、提交审批并部署。</p><button class="btn btn-primary" data-action="navigate" data-view="launch">前往上线操作中心 →</button></div><details class="form-expand sf-advanced-publish"><summary>高级:直接进行发布和历史版本回滚</summary><div class="panel" style="margin-top:18px"><div class="panel-head"><div><h2>正式发布与回滚</h2><p>先完成质量检查与独立审批,Worker 负责上传、校验和切换版本。没有配置目标服务器时不能发布。</p></div><a href="/operations" class="btn btn-tiny">审批与复核工具 ↗</a></div>
|
||||
<div class="field-grid"><label>选择已完成构建<select id="deliveryBuild">${state.builds.filter(b=>b.status==='complete').map(b=>`<option value="${esc(b.id)}">${esc(b.id.slice(0,12))} · ${esc(fmtDate(b.created_at))}</option>`).join('')}</select></label>
|
||||
<label>目标环境<select id="deliveryTarget"><option value="staging">测试环境(Staging)</option><option value="production">正式环境(Production)</option></select></label></div>
|
||||
<div class="form-actions"><button class="btn" data-action="release-preflight">检查发布条件</button><button class="btn" data-action="release-approval">提交独立审批</button><button class="btn btn-primary" data-action="release-deploy">提交发布任务</button></div>
|
||||
<div id="deliveryNotice" class="message" aria-live="polite" hidden></div>
|
||||
<h3>最近发布任务</h3>${(state.delivery.jobs||[]).length?state.delivery.jobs.slice(0,8).map(j=>`<div class="build-row"><div class="build-meta"><strong>${j.kind==='remote_rollback'?'回滚':'部署'} · ${esc(buildStatusLabel(j.state))}</strong><p>${esc(j.id.slice(0,12))} · ${esc(j.created_at)}${j.error?' · '+esc(j.error):''}</p></div>${['queued','running','retrying'].includes(j.state)?`<button class="btn btn-tiny btn-danger" data-action="cancel-job" data-id="${esc(j.id)}">取消</button>`:''}</div>`).join(''):'<p class="muted">暂无发布任务。</p>'}
|
||||
<h3>已发布版本</h3>${(state.delivery.history||[]).length?state.delivery.history.slice(0,8).map(r=>`<div class="build-row"><div class="build-meta"><strong>${esc(r.target)} · ${esc(r.release_id)}</strong><p>构建 ${esc(r.build_id.slice(0,12))} · ${esc(r.deployed_at)}</p></div><button class="btn btn-tiny" data-action="release-rollback" data-target="${esc(r.target)}" data-id="${esc(r.release_id)}">回滚到此版本</button></div>`).join(''):'<p class="muted">当前没有远程部署记录。</p>'}</div>`;
|
||||
<h3>已发布版本</h3>${(state.delivery.history||[]).length?state.delivery.history.slice(0,8).map(r=>`<div class="build-row"><div class="build-meta"><strong>${esc(r.target)} · ${esc(r.release_id)}</strong><p>构建 ${esc(r.build_id.slice(0,12))} · ${esc(r.deployed_at)}</p></div><button class="btn btn-tiny" data-action="release-rollback" data-target="${esc(r.target)}" data-id="${esc(r.release_id)}">回滚到此版本</button></div>`).join(''):'<p class="muted">当前没有远程部署记录。</p>'}</div></details>`;
|
||||
}
|
||||
function settings(){if(!currentSite())return noSite();const s=state.site;return htmlHeading('网站设置','这里只管理公司资料、域名与站点属性。网页样式请到“外观与设计”修改。')+
|
||||
`<div class="panel"><h2>基本资料</h2><p class="panel-desc">没有域名也可以制作与预览网站,正式上线前再填写即可。</p><form id="settingsForm"><div class="field-grid"><label class="full">网站名称<input name="name" required value="${esc(s.name)}"></label><label class="full">正式域名<input name="domain" placeholder="https://www.example.com" value="${esc(s.domain||'')}"></label><label class="full">企业介绍 / 网站标语<textarea name="tagline" rows="3">${esc(s.tagline||'')}</textarea></label><label class="full">Logo 图片地址<input name="logo" value="${esc(s.logo||'')}"></label><label>网站类型<select name="mode"><option value="new" ${s.mode==='new'?'selected':''}>新建网站</option><option value="migration" ${s.mode==='migration'?'selected':''}>旧站迁移</option><option value="hybrid" ${s.mode==='hybrid'?'selected':''}>混合建站</option></select></label><label>主题色<input type="color" name="accent" value="${esc(s.accent||'#245ac6')}" style="padding:4px;height:43px"></label></div><div class="form-actions"><button class="btn btn-primary" type="submit">保存网站设置</button></div></form></div>`;
|
||||
@@ -125,29 +179,93 @@ async function submitLanguageUnit(ev){ev.preventDefault();const d=Object.fromEnt
|
||||
async function submitLanguageLink(ev){ev.preventDefault();const d=Object.fromEntries(new FormData(ev.target));try{await api('/api/i18n/groups/'+state.language.group.group_id+'/units/'+encodeURIComponent(d.unit_id)+'/members','POST',{site_id:d.site_id,entry_id:d.entry_id,state:d.state});toast('内容翻译关系已保存');await loadLanguage()}catch(e){notifyError(e)}}
|
||||
function rerender(){const area=$('#mainContent');const selected=state.view;$('#breadcrumbTitle').textContent=headings[selected]||'工作台';$('#sideSiteName').textContent=state.site?.name||'请先选择网站';
|
||||
document.querySelectorAll('[data-view].nav-item').forEach(el=>{const yes=el.dataset.view===selected;el.classList.toggle('active',yes);yes?el.setAttribute('aria-current','page'):el.removeAttribute('aria-current')});
|
||||
area.innerHTML=({home,sites,overview,content,design,import:imports,build:builds,languages,settings}[selected]||home)();
|
||||
area.innerHTML=({home,sites,overview,guide,launch,content,design,import:imports,build:builds,languages,settings}[selected]||home)();
|
||||
}
|
||||
async function loadSites(){state.sites=await api('/api/sites');const picker=$('#sitePicker'),prev=state.site?.id||'';picker.innerHTML='<option value="">选择网站…</option>'+state.sites.map(s=>`<option value="${esc(s.id)}">${esc(s.name)}</option>`).join('');picker.value=state.sites.some(s=>s.id===prev)?prev:''}
|
||||
async function readDelivery(siteId){try{return await api(`/api/production/sites/${encodeURIComponent(siteId)}/delivery-status`)}catch(e){return {jobs:[],history:[],configured:{}}}}
|
||||
async function readDelivery(siteId){try{return await api(`/api/production/sites/${encodeURIComponent(siteId)}/delivery-status`)}catch(e){return {jobs:[],history:[],configured:{},unavailable:true}}}
|
||||
async function readJobs(siteId){try{return await api(`/api/enterprise/sites/${encodeURIComponent(siteId)}/jobs`)}catch(e){if(![403,404].includes(Number(e.status)))console.warn('加载任务状态失败',e);return []}}
|
||||
async function selectSite(id,go='overview',historyMode='push'){
|
||||
const token=++state.selectionToken;
|
||||
if(!id){state.site=null;state.entries=[];state.builds=[];state.jobs=[];state.delivery={jobs:[],history:[],configured:{}};state.job=null;state.view=go==='home'?'home':'sites';$('#sitePicker').value='';rerender();if(historyMode!=='none')rememberView(historyMode==='replace');return}
|
||||
if(!id){state.site=null;resetLaunch();state.entries=[];state.builds=[];state.jobs=[];state.delivery={jobs:[],history:[],configured:{}};state.job=null;state.view=go==='home'?'home':'sites';$('#sitePicker').value='';rerender();if(historyMode!=='none')rememberView(historyMode==='replace');return}
|
||||
if(!state.sites.some(x=>x.id===id)){toast('该网站不在您的可见列表中',true);return}
|
||||
try{const [site,entries,builds,jobs,delivery]=await Promise.all([api(`/api/sites/${encodeURIComponent(id)}`),api(`/api/sites/${encodeURIComponent(id)}/entries`),api(`/api/sites/${encodeURIComponent(id)}/builds`),readJobs(id),readDelivery(id)]);
|
||||
if(token!==state.selectionToken)return;
|
||||
state.site=site;sfRemember(id);state.entries=entries;state.builds=builds;state.jobs=jobs;state.delivery=delivery;state.job=null;state.view=go;state.search='';state.typ='';$('#sitePicker').value=id;rerender();if(historyMode!=='none')rememberView(historyMode==='replace');if(go==='languages')loadLanguage();closeMobile();window.scrollTo({top:0,behavior:'instant'});
|
||||
state.site=site;resetLaunch();sfRemember(id);state.entries=entries;state.builds=builds;state.jobs=jobs;state.delivery=delivery;state.job=null;state.view=go;state.search='';state.typ='';$('#sitePicker').value=id;rerender();if(historyMode!=='none')rememberView(historyMode==='replace');if(go==='languages')loadLanguage();if(go==='launch')loadReleaseRequests();closeMobile();window.scrollTo({top:0,behavior:'instant'});
|
||||
}catch(e){if(token===state.selectionToken)notifyError(e)}
|
||||
}
|
||||
function navigate(to,historyMode='push'){if(!state.site&&!['home','sites'].includes(to)){state.view='sites';rerender();toast('请先选择网站项目');return}state.view=to;rerender();closeMobile();if(historyMode!=='none')rememberView(historyMode==='replace');if(to==='build')refreshBuilds();if(to==='languages')loadLanguage();window.scrollTo({top:0,behavior:'instant'})}
|
||||
async function refreshBuilds(){const id=state.site?.id;if(!id)return;try{const previouslyRunning=activeJobs().length>0;const [jobs,builds,delivery]=await Promise.all([readJobs(id),api(`/api/sites/${encodeURIComponent(id)}/builds`),readDelivery(id)]);if(state.site?.id!==id)return;state.jobs=jobs;state.builds=builds;state.delivery=delivery;if(previouslyRunning&&!activeJobs().length){const last=jobs[0];state.job=last?.state==='done'?'后台构建已完成,请预览并查看质量报告。':last?.state==='failed'?'本次构建失败,请在任务中心检查原因。':last?.state==='cancelled'?'本次构建已取消。':null}if(state.view==='build')rerender()}catch(e){notifyError(e)}}
|
||||
function navigate(to,historyMode='push'){if(!state.site&&!['home','sites'].includes(to)){state.view='sites';rerender();toast('请先选择网站项目');return}state.view=to;rerender();closeMobile();if(historyMode!=='none')rememberView(historyMode==='replace');if(to==='build'||to==='launch')refreshBuilds();if(to==='launch')loadReleaseRequests();if(to==='languages')loadLanguage();window.scrollTo({top:0,behavior:'instant'})}
|
||||
async function refreshBuilds(){const id=state.site?.id;if(!id)return;try{const previouslyRunning=activeJobs().length>0;const [jobs,builds,delivery]=await Promise.all([readJobs(id),api(`/api/sites/${encodeURIComponent(id)}/builds`),readDelivery(id)]);if(state.site?.id!==id)return;state.jobs=jobs;state.builds=builds;state.delivery=delivery;if(state.view==='launch'){const selected=state.launchBuildId;const stillValid=builds.some(b=>b.id===selected&&b.status==='complete');if(selected&&!stillValid){state.launchBuildId='';state.launchReview=false;state.launchPreflight=null;state.launchMessage='原先选择的构建版本已不可用,请重新选择并核对。'}}if(previouslyRunning&&!activeJobs().length){const last=jobs[0];state.job=last?.state==='done'?'后台构建已完成,请预览并查看质量报告。':last?.state==='failed'?'本次构建失败,请在任务中心检查原因。':last?.state==='cancelled'?'本次构建已取消。':null}if(['build','launch'].includes(state.view))rerender()}catch(e){notifyError(e)}}
|
||||
function resetLaunch(){state.releaseRequestSeq++;state.launchDeploying=false;state.releaseRequests=[];state.releaseRequestsKnown=false;state.launchBuildId='';state.launchTarget='staging';state.launchReview=false;state.launchPreflight=null;state.launchMessage=''}
|
||||
async function loadReleaseRequests(){const siteId=state.site?.id;if(!siteId)return;const seq=++state.releaseRequestSeq;try{
|
||||
const rows=await api(`/api/factory/sites/${encodeURIComponent(siteId)}/release-requests`);
|
||||
if(state.site?.id!==siteId||seq!==state.releaseRequestSeq)return;
|
||||
state.releaseRequests=Array.isArray(rows)?rows:[];state.releaseRequestsKnown=true;
|
||||
}catch(e){if(state.site?.id!==siteId||seq!==state.releaseRequestSeq)return;state.releaseRequestsKnown=false;state.releaseRequests=[];if(![403,404].includes(Number(e.status)))console.warn('读取审批记录失败',e)}
|
||||
if(state.view==='launch')rerender();
|
||||
}
|
||||
async function launchRefresh(){if(!state.site)return;const siteId=state.site.id;await Promise.all([refreshBuilds(),loadReleaseRequests()]);if(state.site?.id!==siteId)return;state.launchMessage='状态已刷新。审批记录、部署任务及公网验收是不同状态。';rerender()}
|
||||
function launchPickBuild(id){const known=state.builds.some(b=>b.id===id&&b.status==='complete');state.launchBuildId=known?id:'';state.launchReview=false;state.launchPreflight=null;state.launchMessage='切换构建版本后,请重新检查预览并获取审批。';rerender()}
|
||||
function launchPickTarget(target){if(!['staging','production'].includes(target))return;state.launchTarget=target;state.launchPreflight=null;state.launchMessage='已切换目标环境。发布前检查将重新确认。';rerender()}
|
||||
function launchChosenBuild(){const id=state.launchBuildId||state.builds.find(b=>b.status==='complete')?.id;if(!id)throw Error('请先生成一个完整的静态构建版本');return id}
|
||||
async function launchRequestApproval(){const sid=state.site?.id,bid=launchChosenBuild();if(!state.launchReview)throw Error('请先检查网站预览并勾选人工核对');if(!state.releaseRequestsKnown)throw Error('审批状态读取失败,请刷新后再操作');if(state.releaseRequests.some(r=>r.build_id===bid&&['pending','approved'].includes(r.state)))throw Error('当前版本已经存在审批申请,无需重复提交');if(!confirm('确定为此构建版本提交独立审批吗?提交后必须由另一位审核人员批准。'))return;
|
||||
const r=await api(`/api/factory/sites/${encodeURIComponent(sid)}/release-requests`,'POST',{build_id:bid});if(state.site?.id!==sid)return;
|
||||
state.launchMessage=`已创建审批申请 ${r.id},当前仍在等待他人审核。`;
|
||||
await loadReleaseRequests();rerender();
|
||||
}
|
||||
async function launchPreflight(){const sid=state.site?.id,bid=launchChosenBuild(),target=state.launchTarget;if(!state.launchReview)throw Error('请先完成人工预览检查');
|
||||
const r=await api(`/api/production/sites/${encodeURIComponent(sid)}/preflight/${encodeURIComponent(bid)}`);if(state.site?.id!==sid||state.launchTarget!==target||launchChosenBuild()!==bid)return;
|
||||
state.launchPreflight={siteId:sid,buildId:bid,ready:r.ready===true,message:r.message||'',target};
|
||||
state.launchMessage=r.ready?'发布前检查通过;提交任务时服务器仍会再次校验。':'尚不能发布:'+r.message;
|
||||
rerender();
|
||||
}
|
||||
async function launchDeploy(){const sid=state.site?.id,bid=launchChosenBuild(),target=state.launchTarget;const approvals=state.releaseRequests.filter(r=>r.build_id===bid);
|
||||
const model=window.SiteForgeLaunch?.plan({site:state.site,build:state.builds.find(b=>b.id===bid),requests:approvals,requestsKnown:state.releaseRequestsKnown,delivery:state.delivery,target,previewChecked:state.launchReview,preflight:state.launchPreflight});
|
||||
if(!model?.canDeploy)throw Error('请先完成预览检查、独立审批、发布前检查,并确认目标服务器已配置');
|
||||
if(state.launchDeploying)return;
|
||||
if(!confirm(`确定将版本 ${bid.slice(0,12)} 提交到${target==='production'?'正式':'测试'}环境吗?这会触发服务器部署。`))return;
|
||||
state.launchDeploying=true;rerender();
|
||||
try{const r=await api(`/api/production/sites/${encodeURIComponent(sid)}/deploy`,'POST',{build_id:bid,target},{'Idempotency-Key':crypto.randomUUID()});if(state.site?.id!==sid)return;
|
||||
await launchRefresh();state.launchMessage=`部署任务 ${r.id} 已进入后台队列。请刷新任务后,再核查真实网站。`;
|
||||
}finally{if(state.site?.id===sid){state.launchDeploying=false;rerender();}}
|
||||
}
|
||||
async function launchReport(id){const sid=state.site?.id;if(!sid||!state.builds.some(b=>b.id===id))return;const r=await api(`/api/sites/${encodeURIComponent(sid)}/builds/${encodeURIComponent(id)}/report`);if(state.site?.id!==sid||state.view!=='launch')return;
|
||||
const t=$('#launchReport');if(!t)return;t.innerHTML=`<section class="sf-journey-after"><h2>版本质量报告</h2><p>HTML 页面:${esc(r.pages||0)} · 警告 ${esc((r.warnings||[]).length)} 项</p>${r.warnings?.length?`<ul>${r.warnings.slice(0,20).map(w=>`<li>${esc(w)}</li>`).join('')}</ul>`:'<p>自动检查未返回警告,仍需核对业务内容和手机页面。</p>'}</section>`;
|
||||
}
|
||||
function openModal(name){const node=$('#'+name);node.showModal();node.querySelector('input:not([type=hidden]),select')?.focus()}
|
||||
function closeModal(name){$('#'+name)?.close()}
|
||||
function openEntry(id){const form=$('#entryForm');form.reset();form.elements.entry_id.value='';$('#entryModalTitle').textContent=id?'修改内容':'添加内容';if(id){const e=state.entries.find(x=>x.id===id);if(!e)return toast('这条内容不存在',true);for(const field of ['title','route','summary','body','seo_title','seo_description','type','status']){if(form.elements[field])form.elements[field].value=e[field]||''}form.elements.entry_id.value=id}openModal('entryModal')}
|
||||
async function saveEntry(ev){ev.preventDefault();if(!state.site)return;const el=ev.currentTarget;const data=Object.fromEntries(new FormData(el));const id=data.entry_id;delete data.entry_id;
|
||||
if(!data.route?.trim())delete data.route;
|
||||
try{await api(`/api/sites/${state.site.id}/entries${id?'/'+encodeURIComponent(id):''}`,id?'PATCH':'POST',data);closeModal('entryModal');toast(id?'内容已更新':'内容已添加');await selectSite(state.site.id,'content')}catch(e){notifyError(e)}}
|
||||
async function saveSite(ev){ev.preventDefault();try{const f=ev.currentTarget;const data=Object.fromEntries(new FormData(f));const s=await api('/api/sites','POST',data);closeModal('siteModal');toast('网站创建成功,现在可以添加内容');await loadSites();await selectSite(s.id,'overview')}catch(e){notifyError(e)}}
|
||||
let createStep=0;
|
||||
function createValidate(){
|
||||
const f=$('#siteForm'),name=f.elements.name.value.trim(),domain=f.elements.domain.value.trim();
|
||||
if(!name){return '请填写网站名称。'}
|
||||
if(name.length>150)return '网站名称过长,请缩短后再继续。';
|
||||
if(domain&&!/^https:\/\/[^\s/]+(?:[\/?#][^\s]*)?$/i.test(domain))return '正式域名请填写完整的 https:// 地址,或先留空。';
|
||||
return '';
|
||||
}
|
||||
function renderCreateStep(){
|
||||
const form=$('#siteForm');if(!form)return;
|
||||
form.querySelectorAll('[data-create-step]').forEach(el=>{el.hidden=Number(el.dataset.createStep)!==createStep});
|
||||
form.querySelectorAll('[data-create-indicator]').forEach(el=>{const current=Number(el.dataset.createIndicator)===createStep;if(current)el.setAttribute('aria-current','step');else el.removeAttribute('aria-current')});
|
||||
$('#createProgressText').textContent=`第 ${createStep+1} 步,共 3 步 · ${['填写信息','选择风格','确认创建'][createStep]}`;
|
||||
$('#createBack').hidden=createStep===0;
|
||||
$('#createNext').hidden=createStep===2;
|
||||
$('#createNext').textContent=createStep===0?'下一步:选择风格 →':'下一步:确认信息 →';
|
||||
$('#createSubmit').hidden=createStep!==2;
|
||||
$('#createError').hidden=true;
|
||||
if(createStep===2){const data=Object.fromEntries(new FormData(form));$('#createSummary').innerHTML=`<p><strong>网站名称:</strong>${esc(data.name)}</p><p><strong>网站类型:</strong>${esc(({new:'全新制作网站',migration:'迁移旧站',hybrid:'旧站内容重新设计'})[data.mode]||data.mode)}</p><p><strong>网站风格:</strong>${esc(themeName[data.theme]||'企业品牌')}</p>`}
|
||||
}
|
||||
function openCreateWizard(){const form=$('#siteForm');form.reset();createStep=0;renderCreateStep();openModal('siteModal')}
|
||||
async function saveSite(ev){ev.preventDefault();const error=createValidate();if(error){$('#createError').textContent=error;$('#createError').hidden=false;return}
|
||||
if(createStep!==2){createStep=2;renderCreateStep();return}
|
||||
const button=$('#createSubmit');if(button.disabled)return;button.disabled=true;button.textContent='正在创建…';
|
||||
try{const data=Object.fromEntries(new FormData(ev.currentTarget));const s=await api('/api/sites','POST',data);closeModal('siteModal');toast('网站已建立,下一步请完善真实内容');await loadSites();await selectSite(s.id,'guide')}
|
||||
catch(e){notifyError(e);$('#createError').textContent=e.message||'创建失败,请重试';$('#createError').hidden=false}
|
||||
finally{button.disabled=false;button.textContent='确认创建网站'}
|
||||
}
|
||||
|
||||
async function setTheme(theme){if(!state.site||theme===state.site.theme)return;if(!confirm(`切换到「${themeName[theme]}」?网站内容及已有 URL 将保持不变,外观会在下次构建后更新。`))return;try{await api(`/api/sites/${state.site.id}`,'PATCH',{theme});toast('主题已切换,重新构建后生效');await selectSite(state.site.id,'design')}catch(e){notifyError(e)}}
|
||||
async function saveSettings(ev){ev.preventDefault();try{await api(`/api/sites/${state.site.id}`,'PATCH',Object.fromEntries(new FormData(ev.currentTarget)));toast('网站设置已保存');await loadSites();await selectSite(state.site.id,'settings')}catch(e){notifyError(e)}}
|
||||
async function importFile(ev){ev.preventDefault();try{if(!state.site)return;const form=new FormData(ev.currentTarget);if(!form.get('file')?.name)throw Error('请选择文件');if(!confirm('将文件内容直接写入当前网站,重复路径默认跳过。需要预演和批次回滚请先使用高级导入。确认继续?'))return;const result=await api(`/api/sites/${state.site.id}/import`,'POST',form);const box=$('#importSummary');box.hidden=false;box.textContent=`导入完成:新增 ${result.imported||0} 条,更新 ${result.updated||0} 条,跳过 ${result.skipped||0} 条。错误 ${result.errors?.length||0} 条。`;toast('导入已完成');await refreshSiteKeepView()}catch(e){notifyError(e)}}
|
||||
@@ -178,23 +296,25 @@ function closeMobile(){$('#sideMenu').classList.remove('open');$('#mobileBackdro
|
||||
async function init(){try{const me=await api('/api/auth/me');state.csrf=me.csrf;await loadSites();const health=await api('/api/health');$('#appVersion').textContent='版本 '+health.version;
|
||||
// Restore URL first, then a previously selected site *only* after verifying /api/sites.
|
||||
const params=new URLSearchParams(location.search),requested=params.get('site');
|
||||
const target=params.get('view');const allowed=new Set(['home','sites','overview','content','design','import','build','languages','settings']);
|
||||
const target=params.get('view');const allowed=new Set(['home','sites','overview','guide','launch','content','design','import','build','languages','settings']);
|
||||
let stored='';try{stored=localStorage.getItem(sfSiteKey)||''}catch{}
|
||||
const chosen=[requested,stored].find(id=>id&&state.sites.some(x=>x.id===id));
|
||||
if(chosen){await selectSite(chosen,allowed.has(target)?target:'overview','replace')}
|
||||
else {state.view=target==='sites'?'sites':'home';rerender();rememberView(true)}
|
||||
}catch(e){notifyError(e);$('#mainContent').innerHTML=emptyState('后台加载失败',e.message,'<button class="btn btn-primary" onclick="location.reload()">重新加载</button>')}}
|
||||
// Delegated UI interactions keep navigation and form actions centrally auditable.
|
||||
document.addEventListener('click',async ev=>{const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='new-site')openModal('siteModal');if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id)}catch(e){notifyError(e)}});
|
||||
document.addEventListener('click',async ev=>{const sidebar=ev.target.closest('.nav-item[data-view]');if(sidebar&&!sidebar.hasAttribute('data-action')){navigate(sidebar.dataset.view);return;}const control=ev.target.closest('[data-action]');if(!control)return;const action=control.dataset.action;try{if(action==='navigate')navigate(control.dataset.view);if(action==='language-refresh')await loadLanguage();if(action==='new-site')openCreateWizard();if(action==='select-site')await selectSite(control.dataset.id);if(action==='new-entry')openEntry();if(action==='edit-entry')openEntry(control.dataset.id);if(action==='set-theme')await setTheme(control.dataset.theme);if(action==='build-now')await buildNow();if(action==='refresh-builds')await refreshBuilds();if(action==='check-readiness')await checkReadiness();if(action==='cancel-job')await cancelJob(control.dataset.id);if(action==='report')await showReport(control.dataset.id);if(action==='release-preflight')await releasePreflight();if(action==='release-approval')await releaseApproval();if(action==='release-deploy')await releaseDeploy();if(action==='release-rollback')await releaseRollback(control.dataset.target,control.dataset.id);if(action==='launch-refresh')await launchRefresh();if(action==='launch-build')await buildNow();if(action==='launch-report')await launchReport(control.dataset.id);if(action==='launch-request-approval')await launchRequestApproval();if(action==='launch-preflight')await launchPreflight();if(action==='launch-deploy')await launchDeploy()}catch(e){notifyError(e)}});
|
||||
document.addEventListener('click',ev=>{const btn=ev.target.closest('[data-close]');if(btn)closeModal(btn.dataset.close)});
|
||||
$('#siteForm').addEventListener('submit',saveSite);$('#entryForm').addEventListener('submit',saveEntry);
|
||||
document.addEventListener('submit',ev=>{if(ev.target.id==='settingsForm')saveSettings(ev);if(ev.target.id==='importForm')importFile(ev);if(ev.target.id==='langGroupForm')submitLanguageGroup(ev);if(ev.target.id==='langJoinForm')submitLanguageJoin(ev);if(ev.target.id==='langUnitForm')submitLanguageUnit(ev);if(ev.target.id==='langLinkForm')submitLanguageLink(ev)});
|
||||
document.addEventListener('input',ev=>{if(ev.target.id==='contentSearch'){state.search=ev.target.value;const cursor=ev.target.selectionStart;const type=state.typ;rerender();const el=$('#contentSearch');el.focus();el.setSelectionRange(cursor,cursor);state.typ=type}});
|
||||
document.addEventListener('change',ev=>{if(ev.target.id==='contentType'){state.typ=ev.target.value;rerender()}if(ev.target.id==='langMemberSite')loadLanguageEntries(ev.target.value)});
|
||||
document.addEventListener('change',ev=>{if(ev.target.id==='launchBuildSelect')launchPickBuild(ev.target.value);if(ev.target.id==='launchTarget')launchPickTarget(ev.target.value);if(ev.target.id==='launchReviewCheck'){state.launchReview=ev.target.checked;state.launchPreflight=null;rerender()}if(ev.target.id==='contentType'){state.typ=ev.target.value;rerender()}if(ev.target.id==='langMemberSite')loadLanguageEntries(ev.target.value)});
|
||||
$('#sitePicker').addEventListener('change',ev=>selectSite(ev.target.value));
|
||||
window.addEventListener('popstate',async()=>{const p=new URLSearchParams(location.search);const site=p.get('site'),view=p.get('view')||'home';const allowed=['home','sites','overview','content','design','import','build','languages','settings'];if(site&&state.sites.some(s=>s.id===site))await selectSite(site,allowed.includes(view)?view:'overview','none');else{state.selectionToken++;state.site=null;state.entries=[];state.jobs=[];state.builds=[];state.view=view==='sites'?'sites':'home';rerender()}});
|
||||
window.addEventListener('popstate',async()=>{const p=new URLSearchParams(location.search);const site=p.get('site'),view=p.get('view')||'home';const allowed=['home','sites','overview','guide','launch','content','design','import','build','languages','settings'];if(site&&state.sites.some(s=>s.id===site))await selectSite(site,allowed.includes(view)?view:'overview','none');else{state.selectionToken++;resetLaunch();state.site=null;state.entries=[];state.jobs=[];state.builds=[];state.view=view==='sites'?'sites':'home';rerender()}});
|
||||
setInterval(()=>{if(!document.hidden&&state.view==='build'&&activeJobs().length)refreshBuilds()},5000);
|
||||
$('#newSiteTop').addEventListener('click',()=>openModal('siteModal'));
|
||||
$('#newSiteTop').addEventListener('click',openCreateWizard);
|
||||
$('#createNext').addEventListener('click',()=>{const error=createValidate();if(error){$('#createError').textContent=error;$('#createError').hidden=false;return}createStep=Math.min(2,createStep+1);renderCreateStep()});
|
||||
$('#createBack').addEventListener('click',()=>{createStep=Math.max(0,createStep-1);renderCreateStep()});
|
||||
$('#logoutBtn').addEventListener('click',async()=>{try{await api('/api/auth/logout','POST');location.href='/login'}catch(e){notifyError(e)}});
|
||||
$('#menuToggle').addEventListener('click',()=>{const open=$('#sideMenu').classList.toggle('open');$('#mobileBackdrop').classList.toggle('open',open);$('#menuToggle').setAttribute('aria-expanded',String(open))});
|
||||
$('#mobileBackdrop').addEventListener('click',closeMobile);
|
||||
|
||||
Reference in New Issue
Block a user