'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=>`${H(s.name)} `).join('');if(state.site)$('#sitePicker').value=state.site.id;
$('#newTheme').innerHTML=state.themes.map(t=>`${H(t.name)} `).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静态站交付 构建、验收、发布
常用功能 切换完整主题 → 导入内容 / 字段映射 → 检查 SEO 和旧站路径 → 生成网站 HTML →
当没有真实产品时,主题不会虚构业务数据;可先完善基础页面,再逐步导入。
`;}
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(t.name)} v${H(t.version)} ${H(t.description)}
`}).join('')}
批量重装配现有内容 会按页面类型给已有内容应用当前主题;保留现有 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])} 不映射 ${state.fields.map(x=>`${H(x)} `).join('')}
`).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.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(''):'
当前已检测范围内未发现阻断项。
'}
上传原站清单(WXR / Sitemap XML / CSV / JSON) 导入原站 URL 基线
提示:完整旧站 URL 基线应来自迁移前的原网站清单,不要用迁移后不完整的网站替代。
实际生成 HTML 与旧站对比 ${audit?`
旧站 ${audit.baseline_count} 条 / 已匹配 ${audit.matched_pages||0} 条;阻断 ${audit.errors} 项;待复核 ${audit.unreviewed_warnings||0} 项
下载差异 CSV `:'
请先构建新站,再与旧站基线对比。
'}
前 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?'可进入发布审查':'发布前有阻断项'}
交付说明 静态网站包含 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;