'use strict';
const $=id=>document.querySelector(id);
const html=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const pathParts=location.pathname.split('/').filter(Boolean);
const siteId=pathParts[1],initialPage=pathParts[2];
let csrf='',site=null,pages=[],schema=null,reusables=[],pageId=initialPage,blocks=[],kind='standard',layoutMode='append',selected=-1,dirty=false,timer=null;
const api=async(p,opt={})=>{if(!['GET','HEAD','OPTIONS'].includes((opt.method||'GET').toUpperCase()))opt.headers={...(opt.headers||{}),'X-SiteForge-CSRF':csrf};let r=await fetch(p,opt);if(r.status===401){location.assign('/login');throw Error('会话已过期')}let t=r.headers.get('content-type')||'';let body=t.includes('json')?await r.json():await r.text();if(!r.ok)throw Error(body.detail||body);return body};
const req=(method,data)=>({method,headers:{'Content-Type':'application/json'},body:JSON.stringify(data)});
const toast=(s,err=false)=>{const t=$('#toast');t.textContent=s;t.style.background=err?'#a23c44':'#1c3555';t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3400)};
const activePage=()=>pages.find(e=>e.id===pageId);
const Studio={
async init(){try{csrf=(await api('/api/auth/me')).csrf;site=await api(`/api/sites/${siteId}`);pages=await api(`/api/sites/${siteId}/entries`);schema=await api('/api/editor/blocks');$('#siteName').textContent=site.name;const select=$('#pageSelect');select.innerHTML=pages.map(e=>`${html(e.title)} (${html(e.route)}) `).join('');for(let [k,v] of Object.entries(schema.page_templates))$('#pageTemplate').innerHTML+=`${html(v.name)} `;Studio.renderPalette();await Studio.loadReusables();await Studio.choosePage(pageId||pages[0]?.id)}catch(e){toast('初始化失败:'+e.message,true);console.error(e)}},
async choosePage(id){if(dirty&&!confirm('当前页面尚未保存,确定切换?')){$('#pageSelect').value=pageId;return}pageId=id;$('#pageSelect').value=id;const data=await api(`/api/sites/${siteId}/pages/${id}/blueprint`);blocks=data.blocks.map(b=>structuredClone(b));kind=data.page_template;layoutMode=data.layout_mode||'append';$('#layoutMode').value=layoutMode;selected=blocks.length?0:-1;$('#pageTemplate').value=kind;$('#pageName').textContent=data.entry.title;$('#routeDisplay').textContent=data.entry.route;dirty=false;Studio.draw();Studio.refreshPreview()},
async loadReusables(){try{reusables=await api(`/api/sites/${siteId}/components`);$('#reusablePalette').innerHTML=reusables.map(x=>`✦ ${html(x.name)} v${x.version} `).join('')||'暂无保存的组件 '}catch(e){console.warn(e)}},
async saveReusable(){const block=blocks[selected];if(!block)return toast('请先选择一个区块',true);const name=prompt('为此组件命名',block.title||schema.blocks[block.type]?.name);if(!name?.trim())return;try{await api(`/api/sites/${siteId}/components`,req('POST',{name,block}));await Studio.loadReusables();toast('组件已保存到本站组件库')}catch(e){toast(e.message,true)}},
insertReusable(id){const saved=reusables.find(x=>x.id===id);if(!saved)return;const copy=structuredClone(saved.block);const refreshIds=item=>{item.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);if(item.type==='layout')(item.columns||[]).forEach(c=>(c.blocks||[]).forEach(refreshIds))};refreshIds(copy);blocks.push(copy);selected=blocks.length-1;Studio.modified();Studio.draw();},
renderPalette(){let p=$('#palette');p.innerHTML=Object.entries(schema.blocks).map(([key,b])=>`${html(b.icon)} ${html(b.name)} `).join('')},
draw(){let list=$('#blockList');$('#blockCount').textContent=blocks.length+' 区块';list.innerHTML=blocks.map((b,i)=>`
⠿
${html(schema.blocks[b.type]?.icon||'▣')}
${html(schema.blocks[b.type]?.name||b.type)} ${html(b.title||b.subtitle||b.type)}
⧉ ×
`).join('')||'当前没有区块,点击下方按钮添加。
';Studio.inspector()},
select(i){selected=i;Studio.draw()},
add(type){let data=structuredClone(schema.blocks[type].defaults);data.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);data.type=type;data.visible=true;blocks.push(data);selected=blocks.length-1;Studio.modified();Studio.draw()},
duplicate(i){let b=structuredClone(blocks[i]);b.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);blocks.splice(i+1,0,b);selected=i+1;Studio.modified();Studio.draw()},
remove(i){if(!confirm('确定删除这个区块?'))return;blocks.splice(i,1);selected=Math.min(i,blocks.length-1);Studio.modified();Studio.draw()},
drag(e,i){e.dataTransfer.setData('text/plain',String(i));e.dataTransfer.effectAllowed='move';e.currentTarget.classList.add('dragging')},
drop(e,i){e.preventDefault();let src=Number(e.dataTransfer.getData('text/plain'));if(!Number.isInteger(src)||src<0||src>=blocks.length||src===i)return;let [b]=blocks.splice(src,1);blocks.splice(i,0,b);selected=i;Studio.modified();Studio.draw()},
field(key,value){const b=blocks[selected];if(!b)return;b[key]=value;Studio.modified();Studio.draw()},
fieldQuiet(key,value){const b=blocks[selected];if(!b)return;b[key]=value;Studio.modified();const row=$('#blockList').children[selected];if(row)row.querySelector('small').textContent=b.title||b.subtitle||b.type},
jsonField(key,value){try{let data=JSON.parse(value);if(!Array.isArray(data))throw Error('必须为数组');Studio.fieldQuiet(key,data);$('#jsonError').textContent='✓ JSON 格式有效';$('#jsonError').style.color='#238460'}catch(e){$('#jsonError').textContent='JSON 格式有误:'+e.message;$('#jsonError').style.color='#bf5a58'}},
styleField(key,value){const b=blocks[selected];b.style={...(b.style||{}),[key]:value};Studio.modified()},
inspector(){let target=$('#inspector');let b=blocks[selected];$('#blockTypeLabel').textContent=b?schema.blocks[b.type].name:'页面';if(!b){target.innerHTML='请从左侧选择区块并编辑属性。
';return}const spec=schema.blocks[b.type];let out=`
${html(spec.name)} 显示 `;
for(const field of spec.fields){let value=b[field]??'',label=({title:'标题',subtitle:'副标题',description:'简介',image:'图片地址',body:'正文 HTML',button:'按钮文字',href:'跳转路径',source:'绑定内容类型',limit:'显示条数',columns:'每行列数',items:'重复条目',images:'画廊图片',image_side:'图片方向',align:'标题对齐',height:'间隔高度'})[field]||field;
out+=`${html(label)} `;
if(field==='items'||field==='images')out+=`请输入 JSON 数组,可编辑多个项目 `;
else if(field==='source')out+=`${['product','post','page','vehicle','brand','case','service','other','all'].map(t=>`${({product:'产品',post:'文章',page:'普通页面',vehicle:'车型',brand:'品牌',case:'案例',service:'服务',other:'其他',all:'全部类型'})[t]} `).join('')} 网站已有内容将自动绑定并生成卡片。 `;
else if(field==='image_side'||field==='align')out+=`${(field==='align'?['left','center']:['left','right']).map(v=>`${v==='left'?'左侧':v==='right'?'右侧':'居中'} `).join('')} `;
else if(field==='body'||field==='description')out+=``;
else if(['limit','columns','height'].includes(field))out+=` `;
else out+=` `;
if(['title','subtitle','description','image','body'].includes(field))out+='可输入 {{site.name}}、{{page.title}} 等变量。 ';
out+='
';
}
if(b.type==='layout'){
out+=`列内组件 · 无代码布局
一行最多四列,拖动整个容器可改变行顺序。列在手机上自动变为单列。
`;
b.columns.forEach((column,ci)=>{
out+=`第${ci+1}列 宽度 删除列
`;
column.blocks.forEach((child,j)=>{out+=`
${html(schema.blocks[child.type]?.name||child.type)} × `;
for(const key of (schema.blocks[child.type]?.fields||[]).filter(x=>['title','subtitle','description','image','body','href','button'].includes(x))){out+=`${html(key)} `}
if(b.columns.length>1){out+=`移动到其他列 ${b.columns.map((c,ki)=>ki===ci?'':`第${ki+1}列 `).join('')} `}
out+='
';
});
out+=`
${['richtext','image_text','collection','cta','features'].map(t=>`+${html(schema.blocks[t].name)} `).join('')}
`;
});
out+='+ 添加列 ';
}
out+=`区块设计(按设备覆盖)
背景颜色
上下间距(px)
平板上下间距(px)
手机上下间距(px)
手机上隐藏区块
上传区块图片
`;
out+='复制区块 删除区块
';
target.innerHTML=out;
},
modified(){dirty=true;$('#saveStatus').textContent='未保存更改';$('#saveStatus').classList.add('dirty');clearTimeout(timer);timer=setTimeout(()=>Studio.refreshPreview(),500)},
async refreshPreview(){if(!pageId)return;try{let frame=$('#sitePreview');let doc=await api(`/api/sites/${siteId}/pages/${pageId}/editor-preview`,req('POST',{blocks,page_template:kind,layout_mode:layoutMode}));frame.srcdoc=doc}catch(e){toast('预览失败:'+e.message,true)}},
device(type){$('#deviceFrame').className='device-frame '+(type==='desktop'?'':type);for(let name of ['Desktop','Tablet','Phone'])$('#device'+name).classList.toggle('on',name.toLowerCase()===type)},
changeTemplateKind(t){kind=t;Studio.modified()},
setLayoutMode(v){layoutMode=v;Studio.modified()},
async resetPreset(){if(!confirm('使用预设区块将覆盖当前页面布局,是否继续?'))return;try{let result=await api(`/api/sites/${siteId}/pages/${pageId}/blueprint/preset`,req('POST',{page_template:kind}));blocks=result.blocks;selected=0;dirty=false;Studio.draw();Studio.refreshPreview();toast('已加载页面预设并保存')}catch(e){toast(e.message,true)}},
async save(){try{const result=await api(`/api/sites/${siteId}/pages/${pageId}/blueprint`,req('PUT',{blocks,page_template:kind,layout_mode:layoutMode}));blocks=result.blocks;dirty=false;$('#saveStatus').textContent='已保存';$('#saveStatus').classList.remove('dirty');toast('页面蓝图已保存,重新构建后会生成正式静态页面')}catch(e){toast('保存失败:'+e.message,true)}},
addColumn(){const b=blocks[selected];if(!b||b.type!=='layout'||b.columns.length>=4)return;b.columns.push({width:1,blocks:[]});Studio.modified();Studio.inspector()},
removeColumn(i){const b=blocks[selected];if(!b||b.type!=='layout'||b.columns.length<=1)return;if(b.columns[i].blocks.length&&!confirm('这一列含有组件,确认删除?'))return;b.columns.splice(i,1);Studio.modified();Studio.inspector()},
setColumnWidth(i,value){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].width=Math.max(.5,Math.min(4,Number(value)||1));Studio.modified()},
addChild(i,type){const b=blocks[selected];if(!b||b.type!=='layout')return;const item=structuredClone(schema.blocks[type].defaults);item.type=type;item.id=crypto.randomUUID().replace(/-/g,'').slice(0,10);item.visible=true;b.columns[i].blocks.push(item);Studio.modified();Studio.inspector()},
removeChild(i,j){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].blocks.splice(j,1);Studio.modified();Studio.inspector()},
childField(i,j,key,value){const b=blocks[selected];if(!b||b.type!=='layout')return;b.columns[i].blocks[j][key]=value;Studio.modified()},
moveChild(ci,ri,target){const b=blocks[selected];if(!b||b.type!=='layout'||target<0||target>=b.columns.length)return;let [component]=b.columns[ci].blocks.splice(ri,1);b.columns[target].blocks.push(component);Studio.modified();Studio.inspector()},
async uploadImage(file){if(!file)return;try{const form=new FormData();form.append('file',file);let r=await api(`/api/sites/${siteId}/media`,{method:'POST',body:form});Studio.field('image',r.url);toast('图片已上传,保存页面后参与构建')}catch(e){toast(e.message,true)}},
async buildSite(){if(dirty){await Studio.save();if(dirty)return}try{toast('开始构建网站……');let result=await api(`/api/sites/${siteId}/builds`,req('POST',{mirror_images:false}));toast('构建完成:'+result.pages+' 个页面');window.open(`/preview/${siteId}/${result.version}/`,'_blank','noopener')}catch(e){toast('构建失败:'+e.message,true)}},
};
window.Studio=Studio;
window.addEventListener('DOMContentLoaded',()=>Studio.init());
window.addEventListener('beforeunload',e=>{if(dirty){e.preventDefault();e.returnValue='';}});
window.addEventListener('message',event=>{
const frame=document.querySelector('#sitePreview');
if(!frame||event.source!==frame.contentWindow)return;
const data=event.data||{};
if(data.source!=='siteforge-inline-editor')return;
const findChild=rows=>{for(const row of rows){if(row.id===data.blockId)return {block:row,parent:row};if(row.type==='layout'){for(const col of row.columns||[]){const child=findChild(col.blocks||[]);if(child)return {block:child.block,parent:row};}}}return null};
const found=findChild(blocks),b=found?.block;
if(!b||!['title','description'].includes(data.field)||typeof data.value!=='string')return;
b[data.field]=data.value.slice(0,500);
selected=blocks.indexOf(found.parent);Studio.modified();Studio.draw();
toast('已修改文字,点击保存页面写入数据库');
});