8cea7dee20
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests) - Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR imports keep post/product types instead of collapsing to page - Docs: trade-pro theme port spec (R3) and implementation plan Co-Authored-By: Claude Code <noreply@anthropic.com>
119 lines
26 KiB
JavaScript
119 lines
26 KiB
JavaScript
'use strict';
|
||
/* SiteForge simplified workspace: native APIs, no parallel business backend. */
|
||
const $=(s,root=document)=>root.querySelector(s);
|
||
const esc=x=>String(x??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
||
const themeName={automotive:'汽车与出行',industrial:'工业与制造',corporate:'企业品牌',minimal:'极简展示'};
|
||
const typeName={page:'页面',product:'产品',vehicle:'车型',post:'新闻',brand:'品牌',case:'案例',service:'服务'};
|
||
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 state={sites:[],site:null,entries:[],builds:[],view:'home',csrf:'',search:'',typ:'',loading:false,job:null};
|
||
const headings={home:'工作台',sites:'我的网站',overview:'网站概览',content:'内容与页面',design:'外观与设计',import:'导入与迁移',build:'构建与交付',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)}
|
||
const res=await fetch(url,{method,headers,body,credentials:'same-origin'});
|
||
if(res.status===401){location.href='/login';throw Error('登录已过期,请重新登录')}
|
||
let value;try{value=await res.json()}catch{throw Error('服务器响应异常(HTTP '+res.status+')')}
|
||
if(!res.ok)throw Error(typeof value.detail==='string'?value.detail:JSON.stringify(value.detail||value));return value;
|
||
}
|
||
function toast(msg,bad=false){const el=$('#toast');el.textContent=msg;el.classList.toggle('bad',bad);el.classList.add('show');clearTimeout(el.timer);el.timer=setTimeout(()=>el.classList.remove('show'),4000)}
|
||
function notifyError(error){console.error(error);toast(error?.message||String(error),true)}
|
||
function htmlHeading(title,desc,actions=''){return `<div class="page-heading"><div><p class="overline">SITEFORGE / ${esc(headings[state.view]||'WORKSPACE')}</p><h1>${esc(title)}</h1><p>${esc(desc)}</p></div><div class="inline-actions">${actions}</div></div>`}
|
||
function actionButton(text,action=''){return `<button type="button" class="btn btn-primary" data-action="${action}">${esc(text)}</button>`}
|
||
function stat(title,num,icon){return `<div class="stat"><span class="glyph">${icon}</span><small>${title}</small><strong class="value">${esc(num)}</strong></div>`}
|
||
function emptyState(title,description,button=''){return `<div class="empty"><div class="empty-symbol">◇</div><h3>${esc(title)}</h3><p>${esc(description)}</p>${button}</div>`}
|
||
function nav(button){return `data-action="navigate" data-view="${button}"`}
|
||
function siteCard(s){return `<button class="site-card" data-action="select-site" data-id="${esc(s.id)}"><span class="site-icon ${esc(s.theme)}">${esc((s.name||'S').slice(0,1))}</span><span><h3>${esc(s.name)}</h3><p>${esc(s.domain||'尚未设置正式域名')}</p><span class="site-badges"><span>${esc(themeName[s.theme]||'企业网站')}</span><span>·</span><span>${Number(s.entries_count)||0} 条内容</span><span>·</span><span>${Number(s.build_count)||0} 次构建</span></span></span><span class="arrow">›</span></button>`}
|
||
function overviewShortcut(view,icon,title,desc){return `<button class="quick-card" ${nav(view)}><span class="quick-ico">${icon}</span><span><h3>${title}</h3><p>${desc}</p><span class="quick-more">前往操作 →</span></span></button>`}
|
||
function home(){const sites=state.sites;const total=sites.reduce((n,s)=>n+(+s.entries_count||0),0);const builds=sites.reduce((n,s)=>n+(+s.build_count||0),0);
|
||
return htmlHeading('欢迎回来','管理和制作企业网站,从这里开始。',actionButton('+ 创建新网站','new-site'))+
|
||
`<div class="stats">${stat('网站项目',sites.length,'▤')}${stat('内容总数',total,'▧')}${stat('构建版本',builds,'↗')}${stat('可用主题',4,'◈')}</div>`+
|
||
`<div class="section-heading"><div><h2>您想先做什么?</h2><p>按使用目的进入对应页面,不需要了解技术术语。</p></div></div><div class="quick-grid">`+
|
||
overviewShortcut('sites','+','创建或管理网站','从零搭建网站,或者接着制作已有网站。')+
|
||
overviewShortcut('import','⇩','迁移旧站 / 导入数据','导入 WordPress 或 Excel 产品资料。')+
|
||
overviewShortcut('design','◈','更换网站设计','选择行业主题,编辑网页与全站外观。')+
|
||
`</div><div class="section-heading"><div><h2>最近的网站</h2><p>点击网站继续制作。</p></div><button class="btn-link" ${nav('sites')}>查看全部网站 →</button></div>`+
|
||
(sites.length?`<div class="site-list">${sites.slice(0,4).map(siteCard).join('')}</div>`:emptyState('还没有网站项目','创建一个网站,选择主题后即可开始添加页面、产品和新闻。',actionButton('+ 创建第一个网站','new-site')));
|
||
}
|
||
function sites(){return htmlHeading('我的网站','统一管理所有网站项目,每个网站的数据、主题和构建记录彼此独立。',actionButton('+ 创建网站','new-site'))+
|
||
(state.sites.length?`<div class="site-list">${state.sites.map(siteCard).join('')}</div>`:emptyState('暂时没有网站','选择行业主题,开始制作您的第一个网站。',actionButton('创建网站','new-site')));
|
||
}
|
||
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=count>1;const hasDomain=!!s.domain;const steps=[
|
||
{title:'建立网站',desc:'选择行业主题并创建项目',done:true,view:'settings'},
|
||
{title:'准备内容',desc:'添加页面、产品或导入旧数据',done:hasContent,view:'content'},
|
||
{title:'完善设计',desc:'选用主题,调整图文和全站风格',done:false,view:'design'},
|
||
{title:'构建并预览',desc:'生成静态网站,检查效果与链接',done:!!build,view:'build'}];
|
||
const next=steps.find(x=>!x.done)||steps[3];
|
||
return htmlHeading(s.name,'这是您的网站工作台。按照下面的顺序操作,完成后就能预览网站。',actionButton('↗ 构建网站','build-now'))+
|
||
`<div class="task-note"><div><strong>下一步建议:${next.title}</strong><p>${next.desc}。${!hasDomain?'网站域名也可以稍后再设置。':''}</p></div><button class="btn btn-primary" ${nav(next.view)}>现在去做 →</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?'✓':i+1}</span><h3>${step.title}</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 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>`+
|
||
(arr.length?`<div class="table-wrap"><table class="data-table"><thead><tr><th>名称</th><th>内容类型</th><th>页面地址</th><th>状态</th><th>操作</th></tr></thead><tbody>${arr.slice(0,200).map(e=>`<tr><td><strong>${esc(e.title)}</strong></td><td>${esc(typeName[e.type]||e.type)}</td><td class="muted">${esc(e.route)}</td><td><span class="pill ${e.status==='draft'?'warn':'success'}">${e.status==='draft'?'草稿':'已保存'}</span></td><td class="action-cell"><button class="btn btn-tiny" data-action="edit-entry" data-id="${esc(e.id)}">修改内容</button> <a class="btn btn-tiny" href="/editor/${encodeURIComponent(state.site.id)}/${encodeURIComponent(e.id)}">设计页面 ↗</a></td></tr>`).join('')}</tbody></table></div>`:emptyState('没有找到内容','可以清除筛选条件,或者添加新内容。',actionButton('+ 添加内容','new-entry')))+
|
||
`${arr.length>200?'<p class="muted">当前显示前 200 条,请使用搜索缩小范围。</p>':''}</div>`;
|
||
}
|
||
function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`<div class="theme-card"><div class="theme-preview ${id}">${name}</div><div class="theme-desc"><h3>${name} ${s.theme===id?'<span class="pill success">当前使用</span>':''}</h3><p>${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象'})[id]}</p><button class="btn btn-tiny ${s.theme===id?'btn-muted':'btn-primary'}" ${s.theme===id?'disabled':''} data-action="set-theme" data-theme="${id}">${s.theme===id?'正在使用':'使用此主题'}</button></div></div>`).join('');
|
||
return htmlHeading('外观与设计','先选一套网站风格,再打开可视化编辑器修改图文、导航和手机端布局。')+
|
||
`<div class="section-heading"><div><h2>第一步 · 选择行业主题</h2><p>更换主题不会删除您已经录入的业务内容和 URL。</p></div><a href="/theme-gallery" class="btn btn-tiny">查看完整主题预览 ↗</a></div><div class="choice-cards">${cards}</div>`+
|
||
`<div class="section-heading"><div><h2>第二步 · 设计网页</h2><p>挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。</p></div></div><div class="panel"><div class="panel-head"><div><h2>选择要设计的页面</h2><p>修改先保存为草稿,正式发布设计后才会参与下一次静态构建。</p></div></div><div class="grid-2 grid-equal">${state.entries.filter(e=>e.type==='page').slice(0,8).map(e=>`<a class="site-card" href="/editor/${encodeURIComponent(s.id)}/${encodeURIComponent(e.id)}"><span class="site-icon">▤</span><span><h3>${esc(e.title)}</h3><p>${esc(e.route)}</p></span><span class="arrow">›</span></a>`).join('')||'<div class="muted">尚无页面,请先添加内容。</div>'}</div></div><div class="help-hint" style="margin-top:19px"><strong>提示</strong>:网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。</div>`;
|
||
}
|
||
function imports(){if(!currentSite())return noSite();return htmlHeading('导入与迁移','根据您已有的资料选择操作方式:Excel 产品、WordPress 网站,或者外部 API / 数据库。')+
|
||
`<div class="quick-grid">${overviewShortcut('import','▤','导入 Excel / CSV','批量增加产品、车型和新闻数据。')}<a class="quick-card" href="/production-v8"><span class="quick-ico">⇄</span><span><h3>迁移 WordPress 旧站</h3><p>迁移会话、文章采集、图片与 SEO 检查。</p><span class="quick-more">打开迁移工作台 ↗</span></span></a><a class="quick-card" href="/production-v8"><span class="quick-ico">⌘</span><span><h3>API / XML / MySQL</h3><p>连接外部业务系统,进行字段映射和分批导入。</p><span class="quick-more">进入高级导入 ↗</span></span></a></div>`+
|
||
`<div class="section-heading"><div><h2>快速导入文件</h2><p>适合简单的 Excel、CSV、JSON、WordPress XML 文件。</p></div></div><div class="panel"><h2>上传数据文件</h2><p class="panel-desc">已有网站会保留;重复页面路径默认跳过。复杂迁移请使用上方的专业迁移工作台先预检。</p><form id="importForm"><div class="import-box"><label for="importFile"><strong>选择要导入的数据文件</strong></label><input type="file" id="importFile" name="file" accept=".csv,.xlsx,.xls,.json,.xml" required><p class="muted">支持 CSV、Excel、JSON、WordPress XML;单文件最多 15 MB。</p></div><div class="form-actions"><button class="btn btn-primary" type="submit">确认导入数据</button><a href="/production-v8" class="btn">需要预演与批次回滚?</a></div></form><div id="importSummary" class="message" style="margin-top:15px" hidden></div></div>`+
|
||
`<div class="help-hint" style="margin-top:17px"><strong>迁移旧网站请注意:</strong>如果需要保留旧 URL、原有 SEO、复杂插件字段和全部图片,请优先使用 WordPress 迁移工作台,而不是仅上传一个内容文件。</div>`;
|
||
}
|
||
function builds(){if(!currentSite())return noSite();const latest=latestDone();return htmlHeading('构建与交付','将当前网站数据生成为独立的 HTML 文件。先预览和检查,再决定是否发布。',actionButton('↗ 开始构建','build-now'))+
|
||
`<div class="task-note"><div><strong>${latest?'上次构建已完成,可以继续预览':'您还没有完成网站构建'}</strong><p>内容或设计有更新时,需要重新构建才能生成新版本。</p></div><a class="btn" href="/operations">构建任务与发布审批 ↗</a></div>`+
|
||
`<div class="panel"><div class="panel-head"><div><h2>构建版本</h2><p>下载的 ZIP 是独立静态网站,可交给运维部署。</p></div></div><div id="jobNotice" class="message" ${state.job?'':'hidden'}>${esc(state.job||'')}</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"><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></div></div>`).join(''):emptyState('尚无构建版本','网站内容和设计准备好后,点击“开始构建”生成独立网站文件。',actionButton('开始构建','build-now')))+`<div id="reportContent"></div></div>`;
|
||
}
|
||
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>`;
|
||
}
|
||
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,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 selectSite(id,go='overview'){if(!id){state.site=null;state.entries=[];state.builds=[];state.view='sites';$('#sitePicker').value='';rerender();return}try{const [site,entries,builds]=await Promise.all([api(`/api/sites/${encodeURIComponent(id)}`),api(`/api/sites/${encodeURIComponent(id)}/entries`),api(`/api/sites/${encodeURIComponent(id)}/builds`)]);state.site=site;state.entries=entries;state.builds=builds;state.view=go;state.search='';state.typ='';$('#sitePicker').value=id;rerender();window.scrollTo({top:0,behavior:'instant'})}catch(e){notifyError(e)}}
|
||
function navigate(to){if(!state.site&&!['home','sites'].includes(to)){state.view='sites';rerender();toast('请先选择网站项目');return}state.view=to;rerender();closeMobile();window.scrollTo({top:0,behavior:'instant'})}
|
||
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)}}
|
||
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)}}
|
||
async function refreshSiteKeepView(){if(!state.site)return;const [site,entries,builds]=await Promise.all([api('/api/sites/'+state.site.id),api('/api/sites/'+state.site.id+'/entries'),api('/api/sites/'+state.site.id+'/builds')]);state.site=site;state.entries=entries;state.builds=builds}
|
||
async function buildNow(){if(!state.site)return toast('请先选择网站',true);const id=state.site.id;try{navigate('build');state.job='正在提交网站构建任务…';rerender();const job=await api(`/api/sites/${id}/builds`,'POST',{mirror_images:false},{'Idempotency-Key':crypto.randomUUID()});state.job=`网站已进入构建队列(任务 ${job.id}),关闭页面也可以继续运行。`;rerender();for(let i=0;i<120;i++){await new Promise(resolve=>setTimeout(resolve,1600));if(state.site?.id!==id||state.view!=='build')break;const status=await api(`/api/enterprise/sites/${id}/jobs/${job.id}`);if(['done','failed','cancelled'].includes(status.state)){state.job=status.state==='done'?'网站构建已完成,可以预览和下载。':`构建${status.state==='cancelled'?'已取消':'失败'}:${status.error||'请在任务中心查看详情'}`;await refreshSiteKeepView();rerender();toast(state.job,status.state!=='done');break}if(i%5===0){state.job=`Worker 正在构建(${status.state||'等待中'})…`;$('#jobNotice')?.replaceChildren(document.createTextNode(state.job))}}}catch(e){notifyError(e)}}
|
||
async function showReport(buildId){try{const r=await api(`/api/sites/${state.site.id}/builds/${buildId}/report`);const target=$('#reportContent');if(!target)return;target.innerHTML=`<div class="panel" style="margin-top:16px"><h3>质量检查结果</h3><p class="muted">本报告检查静态内容与资源,不保证搜索引擎排名。</p><div class="stats">${stat('HTML 页面',r.pages||0,'▤')}${stat('重定向',r.redirects||0,'⇄')}${stat('已本地化图片',r.mirrored_images||0,'▧')}${stat('提示数量',(r.warnings||[]).length,'!')}</div>${r.warnings?.length?`<div class="message">${r.warnings.slice(0,20).map(x=>esc(x)).join('\n')}</div>`:'<span class="pill success">当前检查没有警告</span>'}</div>`;target.scrollIntoView({behavior:'smooth',block:'nearest'})}catch(e){notifyError(e)}}
|
||
function closeMobile(){$('#sideMenu').classList.remove('open');$('#mobileBackdrop').classList.remove('open');$('#menuToggle').setAttribute('aria-expanded','false')}
|
||
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;
|
||
// Opening the home screen intentionally does not auto-select a site.
|
||
navigate('home')}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==='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==='report')await showReport(control.dataset.id)}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)});
|
||
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()}});
|
||
$('#sitePicker').addEventListener('change',ev=>selectSite(ev.target.value));
|
||
$('#newSiteTop').addEventListener('click',()=>openModal('siteModal'));
|
||
$('#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);
|
||
document.addEventListener('keydown',ev=>{if(ev.key==='Escape')closeMobile()});
|
||
document.addEventListener('DOMContentLoaded',init);
|