'use strict'; const $=s=>document.querySelector(s), H=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const state={sites:[],themes:[],site:null,tab:'overview',csrf:'',importRows:[],fields:[],mapping:{},kind:'product'}; const toast=(s,error=false)=>{let el=$('#alert');el.textContent=s;el.className='on';el.style.background=error?'#ffe6e6':'#e3f7ed';el.style.color=error?'#b13445':'#176e50';setTimeout(()=>el.className='',5500)}; async function api(url,method='GET',data=null){const opt={method,credentials:'same-origin',headers:{}};if(!['GET','HEAD'].includes(method))opt.headers['X-SiteForge-CSRF']=state.csrf; if(data instanceof FormData)opt.body=data;else if(data!==null){opt.headers['Content-Type']='application/json';opt.body=JSON.stringify(data)} const r=await fetch(url,opt);if(r.status===401){location.href='/login';throw Error('请登录')};let body=(r.headers.get('content-type')||'').includes('json')?await r.json():await r.text();if(!r.ok)throw Error(body.detail||body);return body;} const siteUrl=path=>`/api/sites/${state.site.id}${path}`; const factoryUrl=path=>`/api/factory/sites/${state.site.id}${path}`; function selected(){if(!state.site){$('#panel').innerHTML='

创建第一个网站

选择主题后即可开始添加新内容,或导入旧网站的数据。

';return false}return true;} function setTab(tab){state.tab=tab;$$('#navtabs button').forEach(b=>b.classList.toggle('active',b.dataset.tab===tab));render().catch(e=>toast(e.message,true))} function $$(s){return [...document.querySelectorAll(s)]} function setHeading(h,desc){$('#heading').textContent=h;$('#description').textContent=desc;$('#currentTab').textContent=h;} async function init(){try{state.csrf=(await api('/api/auth/me')).csrf;await refresh();document.querySelectorAll('#navtabs button').forEach(btn=>btn.addEventListener('click',()=>setTab(btn.dataset.tab))); $('#sitePicker').onchange=()=>{state.site=state.sites.find(s=>s.id===$('#sitePicker').value);render().catch(e=>toast(e.message,true))};$('#reload').onclick=refresh;$('#newSite').onclick=openCreate;$('#closeCreate').onclick=()=>$('#createDialog').close();$('#createForm').onsubmit=create; $('#buildQuick').onclick=()=>buildNow();await render()}catch(e){toast('初始化失败:'+e.message,true)}} async function refresh(){state.sites=await api('/api/sites');state.themes=await api('/api/factory/themes');let old=state.site?.id;state.site=state.sites.find(x=>x.id===old)||state.sites[0]||null; $('#sitePicker').innerHTML=state.sites.map(s=>``).join('');if(state.site)$('#sitePicker').value=state.site.id; $('#newTheme').innerHTML=state.themes.map(t=>``).join('');} function openCreate(){$('#createDialog').showModal()} async function create(e){e.preventDefault();try{let d=Object.fromEntries(new FormData(e.target));let result=await api('/api/sites','POST',d);state.site=result;await api(`/api/factory/sites/${result.id}/bootstrap`,'POST',{});$('#createDialog').close();toast('新网站已创建并生成基础页面结构');await refresh();await render()}catch(err){toast(err.message,true)}} async function render(){$('#openEditor').href=state.site?`/editor/${state.site.id}`:'#';if(!selected())return; const maps={overview:['网站总览','查看生产进度、页面数量和建站方式'],themes:['主题与版本','安装主题、切换主题、固定版本以及批量套版'],content:['内容与自动装配','导入 CSV/JSON、自动识别字段、预览映射并建立新内容'],migration:['迁移与 SEO 验收','核对旧站 URL、媒体资源和 SEO 差异'],build:['构建与发布','生成独立 HTML 站点、检测问题、下载与版本管理']};setHeading(...maps[state.tab]); const fn={overview:overview,themes:themes,content:content,migration:migration,build:buildPanel}[state.tab];$('#panel').innerHTML='
加载中……
';await fn();} async function overview(){let [entries,builds,source]=await Promise.all([api(siteUrl('/entries')),api(siteUrl('/builds')),api(factoryUrl('/theme'))]);let s=state.site; $('#panel').innerHTML=`
当前站点${H(s.name)}${H(s.mode==='migration'?'旧站迁移':'新站搭建')} · ${H(s.theme)}
内容总数${entries.length}页面/产品/品牌/文章
构建版本${builds.length}静态网站历史记录
主题版本${H(source.version)}已固定版本可追踪

网站工厂生产流程

01网站结构首页、栏目页和导航
02内容自动装配导入新数据或旧内容
03设计编辑切换主题、可视化编辑
04静态站交付构建、验收、发布

网站页面

${entries.slice(0,25).map(x=>``).join('')}
页面/内容类型URL操作
${H(x.title)}${H(x.type)}${H(x.route)}设计页面 ↗

联系信息与表单服务

邮箱会出现在主题的联系页与页脚。外部表单地址为空时,不显示假提交按钮。

常用功能

当没有真实产品时,主题不会虚构业务数据;可先完善基础页面,再逐步导入。

`;} async function bootstrap(){try{let r=await api(factoryUrl('/bootstrap'),'POST',{});toast(`新增 ${r.created.length} 个基础页面;不会覆盖原 URL`);await render()}catch(e){toast(e.message,true)}} async function themes(){let cur=await api(factoryUrl('/theme'));const schemes={automotive:['#172840','#cfab79','MOVE BEYOND'],industrial:['#dcebf4','#14517d','BUILD FURTHER'],corporate:['#e7e9fb','#535bb4','CREATE IMPACT'],minimal:['#e9e5dc','#3d3b35','LESS IS MORE']}; $('#panel').innerHTML=`

可安装、可版本化的主题包

当前主题:${H(cur.theme)} · v${H(cur.version)}
${state.themes.map(t=>{let id=t.id||t.theme_id,p=schemes[id]||['#e9eff8','#246c9e','YOUR BRAND'];return `
${H(p[2])}

${H(t.name)} v${H(t.version)}

${H(t.description)}

全部页面预览首页预览
`}).join('')}

安装新主题 ZIP

主题包由 manifest.json、theme.css、page-presets.json 组成。只能安装受控的页面组件和 CSS,不执行上传的 JavaScript 或模板逻辑。

批量重装配现有内容

会按页面类型给已有内容应用当前主题;保留现有 URL、SEO 和正文。选择「只补缺失」可避免覆盖已手工设计的区块。

`;} async function chooseTheme(id,version){if(!confirm(`为当前网站选择主题 ${id} v${version}?将保留网站内容。`))return;try{await api(factoryUrl('/theme'),'PUT',{theme:id,version});await refresh();toast('主题切换完成。需要重新构建才能更新正式网站');await render()}catch(e){toast(e.message,true)}} async function showVersions(id){let list=await api(`/api/factory/themes/${id}/versions`);$('#themeVersions').textContent='已安装版本:'+list.map(x=>x.version).join(' / ')} async function checkTheme(id,version){try{const r=await api(`/api/factory/themes/${id}/compatibility?version=${encodeURIComponent(version)}`);$('#themeVersions').textContent=`${r.valid?'✓ 主题结构可用':'✗ 存在阻断'} · ${r.checked_page_types||0} 类页面 · ${[...(r.errors||[]),...(r.warnings||[])].join(';')||'无结构警告'}。正式交付前仍需实际浏览器验收。`;}catch(e){toast(e.message,true)}} async function uploadTheme(){let f=$('#themePackage').files?.[0];if(!f)return toast('请选择主题 ZIP',true);let data=new FormData();data.append('file',f);try{let r=await api('/api/factory/themes/install','POST',data);toast(`已安装:${r.name} v${r.version}`);await refresh();await render()}catch(e){toast(e.message,true)}} async function applyTheme(mode){if(mode==='overwrite'&&!confirm('覆盖已有页面设计(内容和 SEO 不变),继续吗?'))return;try{let r=await api(siteUrl('/apply-theme'),'POST',{mode});toast(`套版完成:更新 ${r.updated} 个页面`)}catch(e){toast(e.message,true)}} async function content(){let entries=await api(siteUrl('/entries'));$('#panel').innerHTML=`

新内容 / 旧内容导入装配

上传 Excel、JSON 数组或 CSV;系统自动建议字段对应关系,例如「车型名称 → 标题」「品牌 → 产品品牌」。映射会经过预览与冲突检查。

本站内容统计

${['page','product','vehicle','brand','post','case','service'].map(t=>`
${H(({page:'页面',product:'产品',vehicle:'车型',brand:'品牌',post:'新闻',case:'案例',service:'服务'})[t])}${entries.filter(e=>e.type===t).length}
`).join('')}

其他导入方式(WordPress XML/REST、Excel、媒体 ZIP)可以在原版站点管理工作台使用,使用相同的数据存储。

打开完整内容管理 →
`;} function parseCSV(text){let rows=[],row=[],cell='',quoted=false;for(let i=0;ix.trim()))rows.push(row);row=[];cell=''}else cell+=ch;}row.push(cell);if(row.some(x=>x.trim()))rows.push(row);let cols=rows.shift()?.map(s=>s.trim())||[];return rows.map(r=>Object.fromEntries(cols.map((c,i)=>[c,r[i]||''])))} async function readData(){let f=$('#dataFile').files?.[0];if(!f)return toast('请选择 Excel、CSV 或 JSON 文件',true); try{let data=new FormData();data.append('file',f);const parsed=await api(factoryUrl('/mapping/parse-file'),'POST',data);state.importRows=parsed.rows;state.fields=parsed.columns;state.mapping=parsed.suggested;drawMapping();} catch(e){toast(e.message,true)}} async function previewRelations(){try{const r=await api(factoryUrl('/relationships'));$('#relationReport').innerHTML=`
产品-品牌关联检查

已有品牌 ${r.brands.length} 个、分类 ${r.categories.length} 个;缺少独立品牌页 ${r.brand_pages_missing.length} 个

${H(r.brand_pages_missing.map(x=>x.brand).join(' / ')||'无')}

${r.issues.slice(0,10).map(x=>`

${H(x.route)} · ${H(x.message)}

`).join('')}
`;}catch(e){toast(e.message,true)}} async function assembleRelations(){if(!confirm('依据真实产品品牌字段创建缺少的品牌页面?不会自动生成虚构文案。'))return;try{const r=await api(factoryUrl('/relationships/assemble'),'POST',{});toast(`已创建 ${r.count} 个品牌页面`);await previewRelations()}catch(e){toast(e.message,true)}} function drawMapping(){let fields=['title','route','summary','body','cover','brand','category','model','energy','range','gallery','specs','sku','price','seo_title','seo_description','canonical'];let labels={title:'标题(必填)',route:'旧站 URL',summary:'摘要',body:'详情正文',cover:'封面',brand:'品牌',category:'分类',seo_title:'SEO标题',seo_description:'SEO描述',canonical:'Canonical',model:'型号',energy:'动力形式',range:'续航',gallery:'图库(JSON数组或分号分隔)',specs:'技术参数',sku:'SKU',price:'价格'}; $('#mappingUI').innerHTML=`

字段映射(已读取 ${state.importRows.length} 行)

${fields.map(t=>`
${H(labels[t])}
`).join('')}
`;} function gather(){let rules={};$$('.mapfield').forEach(x=>{if(x.value)rules[x.dataset.target]=x.value});return {rows:state.importRows,rules,content_type:$('#mapKind').value}} async function mappingPreview(){try{let r=await api(factoryUrl('/mapping/run'),'POST',{...gather(),dry_run:true});$('#mappingReport').innerHTML=`
预览:${r.mapped} 条可映射

错误 ${r.errors.length} 条,URL 冲突 ${r.conflicts.length} 条

${r.sample.map(x=>``).join('')}
标题新 URL内容类型
${H(x.title)}${H(x.route)}${H(x.type)}
${r.errors.length?'

'+H(JSON.stringify(r.errors.slice(0,6)))+'

':''}
`}catch(e){toast(e.message,true)}} async function mappingCommit(){if(!confirm('确认将已映射的数据写入当前网站?'))return;try{let r=await api(factoryUrl('/mapping/run'),'POST',{...gather(),dry_run:false,on_conflict:'skip'});toast(`新增 ${r.created} 条,跳过 ${r.skipped} 条,错误 ${r.errors.length} 条`);await render()}catch(e){toast(e.message,true)}} async function migration(){let [data,assets,builds]=await Promise.all([api(factoryUrl('/migration/readiness')),api(factoryUrl('/assets/inventory')),api(siteUrl('/builds'))]); let audit=builds.length?await api(`/api/audit/sites/${state.site.id}/builds/${builds[0].id}/report`).catch(()=>null):null;const gate=data.gates||[];$('#panel').innerHTML=`
旧站 URL 基线${data.baseline.baseline_count}应保持有效的历史路径
缺失 URL${data.baseline.errors}发布阻断项
SEO 差异${data.baseline.warnings}需要人工核查
资源引用${assets.references}实际已识别的内容图片

迁移验收门禁

${gate.length?gate.map(g=>`
${g.level==='block'?'阻止发布':'需人工复核'}

${H(g.message)}

`).join(''):'

当前已检测范围内未发现阻断项。

'}
资源清单 JSON

提示:完整旧站 URL 基线应来自迁移前的原网站清单,不要用迁移后不完整的网站替代。

实际生成 HTML 与旧站对比

${audit?`

旧站 ${audit.baseline_count} 条 / 已匹配 ${audit.matched_pages||0} 条;阻断 ${audit.errors} 项;待复核 ${audit.unreviewed_warnings||0} 项

下载差异 CSV
${audit.issues.slice(0,40).map(i=>``).join('')}
问题路径操作
${H(i.field)} · ${H(i.message)}${H(i.route)}${i.severity==='warning'&&!i.reviewed?``:i.reviewed?'已复核':'必须修复'}
`:'

请先构建新站,再与旧站基线对比。

'}

前 12 项差异

${data.baseline.issues.slice(0,12).map(x=>`
${H(x.route)}${H(x.field)} · ${H(x.severity)}
`).join('')||'

没有基线差异。

'}
`;} async function saveContactSettings(){try{await api(siteUrl('/contact-settings'),'PUT',{email:$('#configEmail').value,form_endpoint:$('#configForm').value,footer_text:$('#configFooter').value});toast('网站联系配置已保存');await refresh()}catch(e){toast(e.message,true)}} async function approveAuditWarning(bid,issue){const reason=prompt('填写核查原因及处理依据(至少10字,审核后对本构建生效):','');if(!reason)return;try{await api(`/api/audit/sites/${state.site.id}/builds/${bid}/review-warning`,'POST',{issue_id:issue,reason});toast('已记录审查决定');await render()}catch(e){toast(e.message,true)}} async function uploadBaseline(){const file=$('#baselineFile')?.files?.[0];if(!file)return toast('请选择原站清单文件',true);const data=new FormData();data.append('file',file); try{let r=await api(`/api/audit/sites/${state.site.id}/import-baseline`,'POST',data);if(r.errors?.length){$('#baselineImportResult').textContent=JSON.stringify(r.errors.slice(0,15));toast('源站清单存在错误,未写入基线',true);return}toast(`已导入 ${r.imported} 个旧站 URL`);await render()}catch(e){toast(e.message,true)}} async function openLatestAudit(){try{let builds=await api(siteUrl('/builds'));if(!builds.length)return toast('请先构建网站',true);window.open(`/api/audit/sites/${state.site.id}/builds/${builds[0].id}/report`,'_blank','noopener')}catch(e){toast(e.message,true)}} async function snapshot(){if(!confirm('仅适用于尚未迁移的测试网站。真实迁移应导入旧站原始 URL 清单!继续吗?'))return;try{let r=await api(`/api/enterprise/sites/${state.site.id}/baseline/snapshot`,'POST',{});toast('已建立 '+r.captured+' 条当前内容基线');await render()}catch(e){toast(e.message,true)}} async function buildPanel(){let [builds,ready,releaseRequests]=await Promise.all([api(siteUrl('/builds')),api(factoryUrl('/migration/readiness')),api(factoryUrl('/release-requests'))]);$('#panel').innerHTML=`

静态构建版本

${ready.can_publish?'可进入发布审查':'发布前有阻断项'}
${builds.slice(0,15).map(b=>``).join('')}
版本页面数生成时间交付
${H(b.id)}${b.total_pages}${H(b.created_at)}预览 · 下载 · ·

交付说明

静态网站包含 HTML、CSS、JavaScript、独立搜索索引、站点地图和 Nginx 重定向示例配置。

通过实际发布前,需要校验 SEO 基线、静态资源和功能;当前本地发布仅适用于事先配置的发布目录,公网 DNS/CDN 连接器仍需另行开发。

${ready.gates.map(g=>`
${H(g.level)}

${H(g.message)}

`).join('')}

发布审批记录

${releaseRequests.slice(0,8).map(r=>`
${H(r.build_id)}${H(r.requested_by)} · ${H(r.state)}
${r.state==='pending'?` `:''}
`).join('')||'

暂无发布审批记录。

'}
`;} async function requestRelease(build){try{let r=await api(factoryUrl('/release-requests'),'POST',{build_id:build});toast('审批申请已提交:'+r.id);await render()}catch(e){toast(e.message,true)}} async function reviewRelease(id,status){try{let note=prompt('请输入审批说明(可留空)','')||'';await api(factoryUrl('/release-requests/'+id+'/review'),'POST',{state:status,note});toast('审批状态已更新');await render()}catch(e){toast(e.message,true)}} async function publishRelease(build){if(!confirm('确认将此版本发布到预先配置的本地持久化站点目录?'))return;try{let r=await api(siteUrl('/builds/'+build+'/publish-local'),'POST',{});toast('已发布:'+r.release);await render()}catch(e){toast(e.message,true)}} async function buildNow(){if(!state.site)return;try{ toast('构建已提交后台队列……');const sid=state.site.id; const job=await api(siteUrl('/builds'),'POST',{mirror_images:false}); toast('任务 '+job.id+' 排队中;关闭窗口不会中断'); for(let i=0;i<240;i++){ await new Promise(resolve=>setTimeout(resolve,1500)); const status=await api(`/api/enterprise/sites/${sid}/jobs/${job.id}`); if(status.state==='done'){const r=JSON.parse(status.result_json||'{}');toast(`构建成功,${r.pages} 个页面`);await render();if(r.version)window.open(`/preview/${sid}/${r.version}/`,'_blank','noopener');return} if(['failed','cancelled'].includes(status.state)){toast('构建'+status.state+':'+(status.error||''),true);return} }toast('任务仍在后台运行,请到任务中心查看'); }catch(e){toast(e.message,true)}} window.addEventListener('DOMContentLoaded',init); window.requestRelease=requestRelease;window.reviewRelease=reviewRelease;window.publishRelease=publishRelease;window.bootstrap=bootstrap;window.chooseTheme=chooseTheme;window.checkTheme=checkTheme;window.showVersions=showVersions;window.uploadTheme=uploadTheme;window.applyTheme=applyTheme;window.readData=readData;window.uploadBaseline=uploadBaseline;window.approveAuditWarning=approveAuditWarning;window.saveContactSettings=saveContactSettings;window.openLatestAudit=openLatestAudit;window.previewRelations=previewRelations;window.assembleRelations=assembleRelations;window.mappingPreview=mappingPreview;window.mappingCommit=mappingCommit;window.snapshot=snapshot;window.buildNow=buildNow;window.setTab=setTab;window.openCreate=openCreate;