'use strict';
const $=(q,el=document)=>el.querySelector(q);
const E=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const ATTR=s=>E(s);
const cfg={automotive:{label:'汽车与出行',desc:'高级质感 · 深色车型展厅',prev:'auto',title:'Move beyond.'},industrial:{label:'工业制造',desc:'专业理性 · 工业产品展示',prev:'ind',title:'Built to last.'},corporate:{label:'企业品牌',desc:'现代明亮 · 集团企业官网',prev:'corp',title:'Ideas become impact.'},minimal:{label:'极简设计',desc:'编辑式排版 · 极简作品展示',prev:'min',title:'Less, but better.'}};
let csrfToken='';
const state={sites:[],siteId:null,site:null,entries:[],builds:[],tab:'overview',presets:[],query:'',typ:''};
const api=async(path,opt={})=>{
if(!['GET','HEAD','OPTIONS'].includes((opt.method||'GET').toUpperCase())) {opt.headers={...(opt.headers||{}),'X-SiteForge-CSRF':csrfToken}}
const res=await fetch(path,opt);if(res.status===401){location.assign('/login');throw new Error('登录已过期')}let data;try{data=await res.json()}catch{data={detail:'请求失败,响应不是 JSON'}}
if(!res.ok)throw new Error(typeof data.detail==='string'?data.detail:JSON.stringify(data.detail||data));return data;
};
const json=(method,data)=>({method,headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
const notice=(s,bad=false)=>{const t=$('#toast');t.innerText=s;t.style.background=bad?'#a03e45':'#243952';t.classList.add('show');clearTimeout(t._timer);t._timer=setTimeout(()=>t.classList.remove('show'),4300)};
const onError=e=>{console.error(e);notice(e.message||String(e),true)};
const simpleDate=s=>s?new Date(s).toLocaleString('zh-CN',{year:'numeric',month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}):'—';
const b=(text,fn,extra='')=>`${text} `;
const tabs=[['overview','工作概览'],['entries','内容管理'],['templates','模板与设计'],['design','可视化页面设计'],['models','内容字段模型'],['import','旧站 / 数据导入'],['seo','SEO / URL'],['builds','构建与导出']];
function topTitle(name){$('#topTitle').textContent=name}
function hero(title,desc,buttons='',eyebrow='SITEFORGE / WEBSITE FACTORY'){return `
${E(eyebrow)} ${E(title)} ${E(desc)}
${buttons}
`}
function themeCard(key,action){let t=cfg[key];return `THEME / ${E(key.toUpperCase())} ${E(t.title)}
${E(t.label)} ${E(t.desc)}
${action}
`}
const App={
async init(){try{const me=await api('/api/auth/me');csrfToken=me.csrf;state.sites=await api('/api/sites');state.presets=await api('/api/presets');App.renderSiteNav();App.openDashboard()}catch(e){onError(e)}},
async logout(){try{await api('/api/auth/logout',{method:'POST'});location.assign('/login')}catch(e){onError(e)}},
async reload(){state.sites=await api('/api/sites');App.renderSiteNav();if(state.siteId){state.site=await api('/api/sites/'+state.siteId);state.entries=await api('/api/sites/'+state.siteId+'/entries');state.builds=await api('/api/sites/'+state.siteId+'/builds')}},
renderSiteNav(){$('#siteNav').innerHTML=state.sites.map(s=>`${E((s.name||'S')[0])} ${E(s.name)} `).join('')||'暂无网站项目
'},
openDashboard(){state.siteId=null;topTitle('网站总览');App.renderSiteNav();document.querySelectorAll('.side-link').forEach(x=>x.classList.remove('selected'));$('#nav-dashboard').classList.add('selected');
let n=state.sites.length, counts=state.sites.reduce((x,s)=>x+s.entries_count,0),builds=state.sites.reduce((x,s)=>x+s.build_count,0);
$('#view').innerHTML=hero('网站生产工作台','从零创建、迁移旧站、导入内容、更换模板,统一生产独立静态网站。',`+ 创建网站 `,'WELCOME TO YOUR FACTORY')+
`网站总数 ${n} 独立站点项目
内容条目 ${counts} 页面、产品、文章等
构建版本 ${builds} 可预览与下载
可用主题 4 支持每站独立风格
`+
`我的网站 选择任意站点,继续编辑、导入数据或构建版本
`+
state.sites.map(s=>`
${E(cfg[s.theme]?.label||'企业站')} ${E(s.name)}
${E(s.name)} ${E(s.domain||'待绑定域名')} · ${E(s.tagline||'尚未填写站点简介')}
${s.entries_count} 条内容 ${s.build_count} 次构建 →
`).join('')+`
+
创建新网站 旧站迁移 / 新建站点 / 导入内容
`+
(!state.sites.length?`第一次体验 SiteForge? 生成演示网站,并立即构建一个可独立下载的纯静态网站。
快速创建演示网站 → `:'');
},
async openSite(id,tab='overview'){try{state.siteId=id;state.tab=tab;await App.reload();topTitle(state.site.name);App.renderSiteNav();App.renderSite()}catch(e){onError(e)}},
renderSite(){const s=state.site;let head=hero(s.name,s.domain||'独立网站 · 尚未设置域名',`+ 添加内容 ↗ 构建网站 `,'WEBSITE PROJECT / '+s.theme.toUpperCase());
let navigation=`${tabs.map(([id,name])=>`${name} `).join('')}
`;
$('#view').innerHTML=head+`页面 / 内容 ${state.entries.length} 统一内容数据中心
主题 ${E(cfg[s.theme]?.label)} 可随时切换
构建次数 ${state.builds.length} 独立构建版本
SEO 模式 ${s.mode==='migration'?'原址保留':'灵活新建'} 路由独立管理
`+navigation+`
`;App.renderTab();
},
changeTab(tab){state.tab=tab;App.renderSite()},
renderTab(){let html='';switch(state.tab){case'overview':html=App.overview();break;case'entries':html=App.entriesView();break;case'templates':html=App.templateView();break;case'design':html=App.designView();break;case'models':html=App.modelsView();break;case'import':html=App.importView();break;case'seo':html=App.seoView();break;case'builds':html=App.buildView();break;}$('#tab-content').innerHTML=html;if(state.tab==='import')App.loadMedia();if(state.tab==='seo')App.loadRedirects();if(state.tab==='models')App.loadModels()},
overview(){let s=state.site, latest=state.builds[0];return `网站生产流程 每次修改内容、主题或域名后都可以重新构建
STEP 01 创建网站 选择主题,完成独立站点初始化
STEP 02 添加或导入内容 手动录入、旧站采集、文件批量导入
STEP 03 主题与 SEO 自定义视觉样式、路由及页面元信息
STEP 04 构建并导出 生成静态 HTML 文件,预览并下载 ZIP
${latest?`
静态构建完成
${latest.total_pages} 个 HTML 页面 ${simpleDate(latest.created_at)}
`:`
⌁
还没有构建版本 保存内容并点击「构建网站」,即可生成首个网站。
开始第一次构建 `}
`},
settingsForm(){let s=state.site;return ``},
async saveSettings(ev){ev.preventDefault();try{let data=Object.fromEntries(new FormData(ev.target));state.site=await api('/api/sites/'+state.siteId,json('PATCH',data));await App.reload();notice('已保存站点设置');App.renderSite()}catch(e){onError(e)}},
entriesView(){let arr=state.entries.filter(e=>(!state.typ||e.type===state.typ)&&(!state.query||e.title.toLowerCase().includes(state.query.toLowerCase())||e.route.toLowerCase().includes(state.query.toLowerCase())));return `内容库 页面 / 产品 / 文章 / 自定义内容,支持独立 URL 和 SEO
+ 新建内容 全部类型 ${['page','product','post','service','case','vehicle','brand','other'].map(t=>`${{page:'页面',product:'产品',post:'文章',service:'服务',case:'案例',vehicle:'车型',brand:'品牌',other:'自定义'}[t]} `).join('')}${arr.length} 条结果
标题 类型 路由 状态 更新时间 操作 ${arr.map(e=>`${E(e.title)} ${E(e.summary||'')}
${E(e.type)} ${E(e.route)} ${e.status==='published'?'已发布':'草稿'} ${simpleDate(e.updated_at)} `).join('')}
${arr.length?'':'
'}
`},
filterContent(){const active=document.activeElement?.id;const caret=active==='searchEntry'?$('#searchEntry').selectionStart:null;state.query=$('#searchEntry')?.value||'';state.typ=$('#typeFilter')?.value||'';$('#tab-content').innerHTML=App.entriesView();if(active==='searchEntry'){const input=$('#searchEntry');input.focus();if(caret!==null)input.setSelectionRange(caret,caret)}},
async updateNavigation(ev){ev.preventDefault();try{const raw=$('#navRoutes').value.split('\n').map(x=>x.trim()).filter(Boolean);const entries=raw.map(line=>{const sep=line.indexOf('|');if(sep<0)throw new Error('每行必须为:导航文字 | /路径/');return {title:line.slice(0,sep).trim(),route:line.slice(sep+1).trim()}});let path='/api/sites/'+state.siteId+'/site-design';let current=await api(path);current.config.nav=entries;await api(path,json('PUT',{config:current.config,expected_revision:current.revision}));notice('导航已保存为全站草稿:'+entries.length+'项,请在设计器中发布全站设计')}catch(e){onError(e)}},
async scaffold(){if(!confirm('创建关于我们、联系我们和服务页面?已有同路径页面不会覆盖。'))return;try{let r=await api('/api/sites/'+state.siteId+'/scaffold',{method:'POST'});await App.reload();App.renderSite();notice('创建 '+r.created.length+' 个页面,跳过 '+r.skipped.length+' 个')}catch(e){onError(e)}},
modelsView(){return `内容字段模型 自定义产品、车型、品牌、文章等数据字段。字段保存在每个站点自己的配置内,不影响其他站点。
内容类型 ${['product','vehicle','post','brand','case','service','other'].map(x=>`${x} `).join('')} + 添加字段
`},
async loadModels(){try{state.models=await api('/api/sites/'+state.siteId+'/schemas');App.renderModelFields()}catch(e){onError(e)}},
renderModelFields(){let el=$('#modelFields');if(!el||!state.models)return;let kind=$('#modelKind').value;let defs=state.models[kind]||[];el.innerHTML=`字段键名 显示名称 字段类型 ${defs.map(d=>App.modelRow(d)).join('')}
`},
modelRow(d={}){return `${['text','textarea','number','date','url','boolean'].map(t=>`${t} `).join('')} 删除 `},
addModelField(){if(!$('#fieldRows'))return;$('#fieldRows').insertAdjacentHTML('beforeend',App.modelRow())},
async saveModelFields(){try{const fields=[...document.querySelectorAll('.field-row')].map(tr=>({name:tr.querySelector('.field-key').value.trim(),label:tr.querySelector('.field-label').value.trim(),type:tr.querySelector('.field-type').value}));let kind=$('#modelKind').value;let result=await api('/api/sites/'+state.siteId+'/schemas/'+kind,json('PUT',fields));state.models[kind]=result.fields;notice('已保存 '+kind+' 字段模型')}catch(e){onError(e)}},
designView(){const s=state.site;return `Visual Studio · 页面可视化工作台 拖动区块顺序、修改属性、自动绑定内容,并在同一个界面预览 PC 和移动端。
${state.entries.filter(e=>e.type==='page'||e.type==='product'||e.type==='post').slice(0,36).map(e=>`
`).join('')}
页面编辑后点击构建,系统生成对应的独立静态页面,不会丢失原有 URL 和 SEO。 一键补齐企业常用页面
顶部导航配置 每行:标题 | /原始路径/,不填写则按网站已有栏目自动生成导航。
${E((state.site.settings.nav||[]).map(n=>n.title+' | '+n.route).join('\n'))} `},
async applyTemplates(mode){if(!confirm(mode==='overwrite'?'将用当前主题的默认布局覆盖所有页面区块,内容和SEO不变,确定继续?':'将给尚未设置布局的页面自动套用当前主题,确定继续?'))return;try{let r=await api('/api/sites/'+state.siteId+'/apply-theme',json('POST',{mode}));await App.reload();notice('已装配 '+r.updated+' 个页面,跳过 '+r.skipped+' 个');App.renderSite()}catch(e){onError(e)}},
templateView(){let s=state.site;return `主题样式库 切换后内容、原有 URL 和 SEO 数据不变,重新构建立即产生全新视觉版本
${Object.keys(cfg).map(t=>themeCard(t,`${s.theme===t?'✓ 正在使用':'使用主题'} `)).join('')}
模板工厂 保存当前主题与首页区块配置,以后创建其他网站时可以直接套用。
保存为新模板 克隆整个站点
一键把已有内容装入新主题(保留已设计的页面) 全部应用当前主题预设布局
当前版本支持页面区块拖拽排序、PC / 手机预览、动态内容绑定。完全自由定位的像素级编辑仍不支持。
`},
importView(){return `上传文件 URL 将返回类似 media/abc123.jpg,静态站构建时自动携带。
批量导入 WordPress 媒体 ZIP
`},
seoView(){return `↗
页面 URL Registry 所有现有 URL 由内容库的 route 精确定义;冲突直接禁止保存。
◇
SEO Metadata 编辑内容可以设置 Title、Description、Canonical 和 Robots。
▤
Sitemap + 服务器规则 构建时生成 sitemap.xml、robots.txt、Nginx snippet 和 .htaccess。
`},
buildView(){return `静态构建中心 将当前内容、模板和 SEO 路由组合成完全独立的 HTML 文件。
▦
静态 HTML / CSS / JS 无需 PHP、WordPress 或中心 CMS,就能独立部署访问。
▰
可回溯构建版本 每次构建产生独立 ZIP 和版本报告,不会覆盖旧版本。
打包外部图片(较慢,外部资源可能下载失败)
历史构建 选择版本查看预览、下载 ZIP、检查 SEO/媒体报告
版本 ID 构建时间 页面数量 报告 操作 ${state.builds.map(x=>{let r=JSON.parse(x.report_json||'{}');return `${x.id} ${simpleDate(x.created_at)} ${x.total_pages} ${r.quality_status==='warning'?'有告警':'已通过基础检查'} `}).join('')}
${state.builds.length?'':'
⌘
尚无构建记录 完成内容编辑后,点击上方按钮生成版本。
'}
`},
async openTemplates(){state.siteId=null;topTitle('模板资源库');App.renderSiteNav();document.querySelectorAll('.side-link').forEach(x=>x.classList.remove('selected'));$('#nav-template').classList.add('selected');$('#view').innerHTML=hero('模板与主题资源库','从四套行业视觉风格开始,逐步沉淀企业自己的模板库。',`使用模板建站 → `)+`${Object.keys(cfg).map(t=>themeCard(t,`基于模板创建 `)).join('')}
已保存的模板预设 由已有网站保存,复用主题及首页区块结构。
${state.presets.length?state.presets.map(p=>`
${E(p.name)} ${E(cfg[p.theme]?.label||p.theme)} · ${simpleDate(p.created_at)}
使用此模板建站 `).join(''):'
暂未保存企业自定义模板 从任意网站的「模板与设计」中保存为模板。
'}
`},
openNewSite(theme='',preset=''){const f=$('#siteForm');f.reset();if(theme)f.elements.theme.value=theme;$('#presetSelector').innerHTML='不使用自定义模板 '+state.presets.map(p=>`${E(p.name)} · ${E(p.theme)} `).join('');if(preset)f.elements.preset_id.value=preset;$('#siteDialog').showModal()},
closeDialog(id){$('#'+id).close()},
async createSite(ev){ev.preventDefault();try{let values=Object.fromEntries(new FormData(ev.target));let site=await api('/api/sites',json('POST',values));App.closeDialog('siteDialog');notice('网站已创建,可以开始添加内容');await App.openSite(site.id)}catch(e){onError(e)}},
openEntry(id=''){let f=$('#entryForm');f.reset();let e=state.entries.find(x=>x.id===id);f.elements.entry_id.value=id;$('#entryHeading').textContent=id?'编辑内容':'新建内容';if(e){for(let key of ['title','route','summary','body','cover','seo_title','seo_description','canonical','type','status','robots'])if(f.elements[key])f.elements[key].value=e[key]??'';f.elements.meta.value=JSON.stringify(e.meta||{},null,2)}else{f.elements.meta.value='{}';f.elements.type.value='page';f.elements.status.value='published'}$('#entryDialog').showModal();App.loadEntryFields()},
async loadEntryFields(){try{state.models=await api('/api/sites/'+state.siteId+'/schemas');App.renderEntryFields()}catch(e){onError(e)}},
renderEntryFields(){let el=$('#dynamicContentFields');if(!el)return;let kind=$('#entryType').value;let list=(state.models||{})[kind]||[];let parsed={};try{parsed=JSON.parse($('#entryMeta').value||'{}')}catch{};const values=parsed.fields||parsed;el.innerHTML=list.length?`业务字段 · ${E(kind)} 来自当前网站的字段模型,保存时同步写入内容数据。
${list.map(f=>`${E(f.label||f.name)} `).join('')}
`:''},
addBlock(type){const el=$('#entryMeta');let parsed={};try{parsed=JSON.parse(el.value||'{}')}catch{return notice('当前 JSON 格式不合法,先修复后再添加区块',true)}if(!Array.isArray(parsed.blocks))parsed.blocks=[];let obj=({hero:{type:'hero',title:'核心主标题',subtitle:'OUR VISION',description:'可编辑的首页主视觉描述'},features:{type:'features',title:'核心优势',items:[{title:'专业实力',text:'填写优势简介'},{title:'可靠交付',text:'填写优势简介'},{title:'服务网络',text:'填写优势简介'}]},stats:{type:'stats',items:[{value:'30+',label:'合作品牌'},{value:'70+',label:'服务市场'}]},cta:{type:'cta',title:'期待与您合作',subtitle:'联系我们,了解更多服务',button:'联系我们',href:'/contact/'}})[type];parsed.blocks.push(obj);el.value=JSON.stringify(parsed,null,2);notice('已添加区块,请编辑 JSON 中的标题与内容')},
async saveEntry(ev){ev.preventDefault();try{let v=Object.fromEntries(new FormData(ev.target)),id=v.entry_id;delete v.entry_id;v.meta=JSON.parse(v.meta||'{}');const fieldInputs=[...document.querySelectorAll('#dynamicContentFields .sf-data-field')];if(fieldInputs.length){let fields={...(v.meta.fields||{})};for(const input of fieldInputs)fields[input.dataset.key]=input.value;v.meta.fields=fields}if(!id&&!v.route)delete v.route;let endpoint='/api/sites/'+state.siteId+'/entries'+(id?'/'+id:'');await api(endpoint,json(id?'PATCH':'POST',v));App.closeDialog('entryDialog');await App.openSite(state.siteId,'entries');notice(id?'已更新内容':'已创建内容')}catch(e){onError(e)}},
async deleteEntry(id){if(!confirm('确定删除此内容吗?删除之后重新构建才会影响线上静态网站。'))return;try{await api('/api/sites/'+state.siteId+'/entries/'+id,{method:'DELETE'});await App.openSite(state.siteId,'entries');notice('内容已删除')}catch(e){onError(e)}},
async setTheme(theme){if(!confirm('切换到「'+cfg[theme].label+'」?内容和 URL 保持不变,需重新构建才能更新站点。'))return;try{await api('/api/sites/'+state.siteId,json('PATCH',{theme}));await App.openSite(state.siteId,'templates');notice('主题已更换,请重新构建预览')}catch(e){onError(e)}},
async savePreset(){let name=$('#presetName')?.value?.trim();if(!name)return notice('请输入新模板名称',true);try{let result=await api('/api/sites/'+state.siteId+'/save-preset',json('POST',{name}));state.presets=await api('/api/presets');notice('已保存模板:'+result.name)}catch(e){onError(e)}},
async cloneSite(){let name=prompt('新站名称:',state.site.name+' 副本');if(!name)return;try{let newsite=await api('/api/sites/'+state.siteId+'/clone',json('POST',{name}));notice('已克隆全部内容到新网站');await App.openSite(newsite.id)}catch(e){onError(e)}},
async importFile(ev){ev.preventDefault();let btn=$('button[type=submit]',ev.target);btn.disabled=true;btn.innerText='导入中...';try{let form=new FormData(ev.target);let result=await api('/api/sites/'+state.siteId+'/import',{method:'POST',body:form});await App.openSite(state.siteId,'import');notice(`导入完成:新增 ${result.imported},更新 ${result.updated},跳过 ${result.skipped},错误 ${result.errors?.length||0}`);if(result.errors?.length)alert(JSON.stringify(result.errors.slice(0,15),null,2))}catch(e){onError(e)}finally{btn.disabled=false;btn.innerText='开始批量导入'}},
async crawl(ev){ev.preventDefault();if(!confirm('确认对该公开网站进行限定范围采集?请仅采集你拥有或获授权迁移的站点。'))return;let btn=$('button[type=submit]',ev.target);btn.disabled=true;btn.innerText='正在采集...';try{const input=Object.fromEntries(new FormData(ev.target));input.max_pages=+input.max_pages;const r=await api('/api/sites/'+state.siteId+'/crawl',json('POST',input));await App.openSite(state.siteId,'import');notice(`采集完成:发现 ${r.discovered} 页,新增 ${r.imported} 页,页面获取失败 ${r.fetch_errors?.length||0}`)}catch(e){onError(e)}finally{btn.disabled=false;btn.innerText='扫描并导入公开页面'}},
async uploadMediaArchive(file){if(!file)return;try{let form=new FormData();form.append('file',file);notice('正在解压媒体文件...');let r=await api('/api/sites/'+state.siteId+'/media-archive',{method:'POST',body:form});notice(`媒体导入:${r.imported} 文件,跳过 ${r.skipped} 文件`)}catch(e){onError(e)}},
async uploadMedia(file){if(!file)return;try{let fd=new FormData();fd.append('file',file);let r=await api('/api/sites/'+state.siteId+'/media',{method:'POST',body:fd});notice('图片已上传:'+r.url);App.loadMedia()}catch(e){onError(e)}},
async uploadCover(file){if(!file)return;try{let fd=new FormData();fd.append('file',file);let r=await api('/api/sites/'+state.siteId+'/media',{method:'POST',body:fd});$('#coverInput').value=r.url;notice('封面上传成功')}catch(e){onError(e)}},
async loadMedia(){if(state.tab!=='import')return;try{let data=await api('/api/sites/'+state.siteId+'/media');$('#mediaGrid').innerHTML=data.map(x=>`复制路径 `).join('')||'暂无本地素材
'}catch(e){onError(e)}},
async createRedirect(ev){ev.preventDefault();let value=Object.fromEntries(new FormData(ev.target));value.status_code=+value.status_code;try{await api('/api/sites/'+state.siteId+'/redirects',json('POST',value));ev.target.reset();notice('已创建重定向规则');App.loadRedirects()}catch(e){onError(e)}},
async loadRedirects(){if(state.tab!=='seo')return;try{let data=await api('/api/sites/'+state.siteId+'/redirects');$('#redirectList').innerHTML=data.length?'已有重定向 '+data.map(x=>`${E(x.old_path)} ${x.status_code} → ${E(x.new_path)} 删除
`).join(''):'当前没有重定向规则。
'}catch(e){onError(e)}},
async deleteRedirect(id){if(!confirm('确认删除这条规则?'))return;try{await api('/api/sites/'+state.siteId+'/redirects/'+id,{method:'DELETE'});App.loadRedirects()}catch(e){onError(e)}},
async publishLocal(id){if(!confirm('将此版本发布到 Docker 持久化目录?请确认配置好独立站点服务器。'))return;try{const x=await api('/api/sites/'+state.siteId+'/builds/'+id+'/publish-local',{method:'POST'});notice('已发布:'+x.release+'(发布文件位于 Docker 数据卷)')}catch(e){onError(e)}},
async build(){let s=state.siteId;if(!s)return;let mirror=$('#mirrorChoice')?.checked||false;
try{let job=await api('/api/sites/'+s+'/builds',{...json('POST',{mirror_images:mirror}),headers:{'Content-Type':'application/json','Idempotency-Key':crypto.randomUUID()}});
notice('已进入后台构建队列:'+job.id+',关闭浏览器也可继续执行');
for(let i=0;i<240;i++){await new Promise(resolve=>setTimeout(resolve,1500));
let status=await api('/api/enterprise/sites/'+s+'/jobs/'+job.id);
if(status.state==='done'){let result=JSON.parse(status.result_json||'{}');await App.openSite(s,'builds');notice('构建完成:'+(result.pages||0)+' 个页面');if(result.version)App.showReport(result.version);return}
if(status.state==='failed'||status.state==='cancelled'){notice('任务'+status.state+': '+(status.error||''),true);return}
}notice('构建仍在后台运行,请在任务中心查看状态');
}catch(e){onError(e)}},
async showReport(id){try{const r=await api('/api/sites/'+state.siteId+'/builds/'+id+'/report');let box=$('#reportBox');if(!box){alert(JSON.stringify(r,null,2));return}box.innerHTML=`质量报告:${E(id)} 本报告只覆盖当前实现的基础结构与资源检查,不代表 SEO 排名保证。
静态 HTML 数 ${r.pages}
重定向数 ${r.redirects}
已镜像图片 ${r.mirrored_images}
质量告警 ${r.warnings.length}
${r.warnings.length?`
${r.warnings.map(x=>`• ${E(x)}`).join(' ')}
`:'
✓ 当前规则下未发现告警
'}
详细报告 ${E(JSON.stringify(r,null,2))} `;box.scrollIntoView({behavior:'smooth',block:'nearest'})}catch(e){onError(e)}},
async seedDemo(){try{const s=await api('/api/sites',json('POST',{name:'HESU Global Motors · 演示站',tagline:'全球汽车采购、出口与交付服务',theme:'automotive',domain:'https://demo.example.com',mode:'new'}));let base='/api/sites/'+s.id+'/entries';for(let v of [
{type:'page',title:'关于我们',route:'/about/',summary:'专注全球汽车出口与本地交付',body:'这里是示例公司介绍。请在内容管理中替换为你的真实资料。
',seo_title:'About us'},
{type:'product',title:'ROX 01',route:'/product/rox-01/',summary:'豪华全地形 SUV · 示例数据,可在后台编辑',body:'产品概览 此条目为演示内容,不代表实际库存或车型参数。
'},
{type:'product',title:'BYD ATTO 3',route:'/product/byd-atto-3/',summary:'纯电 SUV · 示例车型信息',body:'可以从 WordPress、Excel 或 JSON 重新导入真正的车型数据。
'},
{type:'post',title:'网站动态示例',route:'/news/first-news/',summary:'一个独立的新网站如何快速发布',body:'编辑内容、切换主题、点击构建,即可导出静态网站。
'}
])await api(base,json('POST',v));await App.openSite(s.id);await App.build();notice('演示网站已创建并成功构建,可直接预览或下载 ZIP')}catch(e){onError(e)}},
};
window.App=App;
$('#siteForm').addEventListener('submit',App.createSite);
$('#entryForm').addEventListener('submit',App.saveEntry);
window.addEventListener('DOMContentLoaded',()=>App.init());