upgrade: adopt V8.34.1 (multi-image banner carousel + spec-table source modes)
Plain overlay (33 items, same-source lineage, zero patch replay). Theme suites 35/35; full run 453 passed + 10 known Windows env fails (suspect-module rerun confirms identical failure set). Live: container 8.34.1-dev.1; hesupipe home leftover industrial-preset blocks cleared via studio draft+publish flow — home now renders pure trade-pro template (sf-section/Chinese blocks = 0). Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
+22
-12
@@ -347,16 +347,16 @@ const Studio={
|
||||
fieldQuiet(key,value){const b=blocks[selected];if(!b)return;b[key]=value;Studio.modified(true);const row=$('#blockList').querySelector(`[data-index="${selected}"]`);if(row)row.querySelector('small').textContent=b.title||b.subtitle||b.type},
|
||||
arrayEditor(field,value,type){
|
||||
const rows=Array.isArray(value)?value:[];
|
||||
const labels={title:'标题',text:'文字介绍',value:'数字',label:'名称',question:'问题',answer:'回答',url:'图片地址',alt:'图片说明',image:'图片地址',caption:'图片说明',name:'名称',price:'价格',features:'方案包含的服务',href:'按钮跳转路径'};
|
||||
const spec=schema.blocks[type];const sample=(spec?.defaults?.[field]||[])[0]||{url:'',alt:''};
|
||||
const labels={title:'标题',text:'文字介绍',value:'数值',label:'参数名称',question:'问题',answer:'回答',url:'图片地址',alt:'图片说明',image:'图片地址',caption:'图片说明',name:'名称',price:'价格',features:'方案包含的服务',href:'按钮跳转路径',subtitle:'副标题',description:'说明文字',button:'按钮文字',image:'图片地址',value:'参数值'};
|
||||
const spec=schema.blocks[type];const sample=(spec?.defaults?.[field]||[])[0]||(field==='slides'?{title:'',subtitle:'',description:'',image:'',button:'',href:'/product/'}:type==='attributes'?{label:'',value:''}:{url:'',alt:''});
|
||||
const keys=Object.keys(sample);
|
||||
return `<div class="sf-array-editor"><p class="sf-array-hint">无需填写 JSON:按顺序编辑每一项,修改会自动显示在预览中。</p>`+
|
||||
rows.map((row,i)=>`<div class="sf-array-card"><div class="sf-array-card-head"><b>第 ${i+1} 项</b><div><button type="button" title="上移" ${i===0?'disabled':''} onclick="Studio.moveItem('${field}',${i},-1)">↑</button><button type="button" title="下移" ${i===rows.length-1?'disabled':''} onclick="Studio.moveItem('${field}',${i},1)">↓</button><button type="button" title="删除这一项" onclick="Studio.removeItem('${field}',${i})">删除</button></div></div>`+
|
||||
keys.map(key=>`<label class="sf-array-field">${html(labels[key]||key)}${key==='text'||key==='answer'||key==='features'?`<textarea oninput="Studio.itemField('${field}',${i},'${key}',this.value)">${html(Array.isArray(row?.[key])?(row[key]||[]).join('\n'):row?.[key]??'')}</textarea>`:`<input type="text" value="${html(row?.[key]??'')}" oninput="Studio.itemField('${field}',${i},'${key}',this.value)">`}</label>`).join('')+
|
||||
`</div>`).join('')+`<button type="button" class="sf-array-add" onclick="Studio.addItem('${field}')">+ 添加${field==='images'?'图片':'一项'}</button></div>`;
|
||||
keys.map(key=>`<label class="sf-array-field">${html(labels[key]||key)}${key==='text'||key==='answer'||key==='features'||key==='description'?`<textarea oninput="Studio.itemField('${field}',${i},'${key}',this.value)">${html(Array.isArray(row?.[key])?(row[key]||[]).join('\n'):row?.[key]??'')}</textarea>`:`<input type="text" value="${html(row?.[key]??'')}" oninput="Studio.itemField('${field}',${i},'${key}',this.value)">`}</label>`).join('')+
|
||||
`${field==='slides'?`<div class="sf-slide-media"><button type="button" onclick="Studio.chooseMedia('slides',${i})">从素材库换图</button><label>上传图片<input aria-label="上传第 ${i+1} 张横幅图片" type="file" accept="image/jpeg,image/png,image/webp,image/avif" onchange="Studio.uploadImage(this.files[0],'slides',${i});this.value=''"></label></div>`:''}</div>`).join('')+`<button type="button" class="sf-array-add" onclick="Studio.addItem('${field}')">+ 添加${field==='slides'?'一张横幅':field==='images'?'图片':'一项'}</button></div>`;
|
||||
},
|
||||
itemField(field,index,key,value){const block=blocks[selected],row=block?.[field]?.[index];if(!row)return;row[key]=key==='features'?String(value).split('\n').map(x=>x.trim()).filter(Boolean):String(value).slice(0,1500);Studio.modified(true)},
|
||||
addItem(field){const block=blocks[selected];if(!block)return;const rows=block[field];if(!Array.isArray(rows))return;const limit=field==='images'?24:16;if(rows.length>=limit)return toast(`最多添加 ${limit} 项`,true);const sample=schema.blocks[block.type]?.defaults?.[field]?.[0]||{url:'',alt:''};const next={};for(const [key,val] of Object.entries(sample))next[key]=Array.isArray(val)?[]:'';rows.push(next);Studio.modified();Studio.inspector()},
|
||||
addItem(field){const block=blocks[selected];if(!block)return;const rows=block[field];if(!Array.isArray(rows))return;const limit=field==='slides'?6:field==='images'?24:block.type==='attributes'?30:16;if(rows.length>=limit)return toast(`最多添加 ${limit} 项`,true);const sample=schema.blocks[block.type]?.defaults?.[field]?.[0]||(field==='slides'?{title:'',subtitle:'',description:'',image:'',button:'',href:'/product/'}:block.type==='attributes'?{label:'',value:''}:{url:'',alt:''});const next={};for(const [key,val] of Object.entries(sample))next[key]=Array.isArray(val)?[]:'';if(field==='slides'&&rows.length===0){for(const key of ['title','subtitle','description','image','button','href'])next[key]=String(block[key]||'')}rows.push(next);Studio.modified();Studio.inspector()},
|
||||
removeItem(field,index){const rows=blocks[selected]?.[field];if(!Array.isArray(rows)||!rows[index])return;rows.splice(index,1);Studio.modified();Studio.inspector()},
|
||||
moveItem(field,index,by){const rows=blocks[selected]?.[field],target=index+by;if(!Array.isArray(rows)||target<0||target>=rows.length)return;[rows[index],rows[target]]=[rows[target],rows[index]];Studio.modified();Studio.inspector()},
|
||||
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'}},
|
||||
@@ -370,10 +370,16 @@ const Studio={
|
||||
toast('已应用区块外观,保存草稿前可以撤销');
|
||||
},
|
||||
inspector(){let target=$('#inspector');let b=blocks[selected];$('#blockTypeLabel').textContent=b?schema.blocks[b.type].name:'页面';if(!b){target.innerHTML='<div class="no-selection"><b>还没有选中内容</b><p>先到左侧选择一个区块,或点击“添加内容区块”。</p><button type="button" class="ui-btn primary" onclick="Studio.toggleLibrary(true)">+ 添加内容</button></div>';return}const spec=schema.blocks[b.type];let out=`<div class="inspector-title"><h3>${html(spec.name)}</h3><label class="toggle"><input type="checkbox" ${b.visible!==false?'checked':''} onchange="Studio.field('visible',this.checked)">显示</label></div><div data-inspector-section="content">`;
|
||||
for(const field of spec.fields.filter(f=>b.type!=='layout'||!['columns','gap','tablet_columns','mobile_columns'].includes(f))){let value=b[field]??'',label=({title:'标题',subtitle:'副标题',description:'简介',image:'图片地址',body:'正文 HTML',button:'按钮文字',href:'跳转路径',source:'绑定内容类型',limit:'显示条数',columns:'每行列数',items:'重复条目',images:'画廊图片',image_side:'图片方向',align:'标题对齐',height:'间隔高度'})[field]||field;
|
||||
for(const field of spec.fields.filter(f=>(b.type!=='layout'||!['columns','gap','tablet_columns','mobile_columns'].includes(f))&&!(b.type==='hero'&&b.slides?.length&&['title','subtitle','description','image','button','href'].includes(f))&&!(b.type==='attributes'&&(b.source_mode||'page')==='page'&&f==='items'))){let value=b[field]??'',label=({title:'标题',subtitle:'副标题',description:'简介',image:'图片地址',body:'正文 HTML',button:'按钮文字',href:'跳转路径',source:'绑定内容类型',limit:'显示条数',columns:'每行列数',items:'重复条目',images:'画廊图片',image_side:'图片方向',align:'标题对齐',height:'间隔高度',layout_variant:'Banner 排版',card_style:'卡片展示方式',image_fit:'图片显示方式',slides:'多图 Banner(可选)',source_mode:'参数内容来源'})[field]||field;
|
||||
out+=`<div class="prop"><label>${html(label)}</label>`;
|
||||
if(field==='items'||field==='images')out+=Studio.arrayEditor(field,value,b.type);
|
||||
if(field==='items'||field==='images'||field==='slides')out+=(field==='slides'?'<p class="prop-tip">首次添加会复制当前 Banner;之后直接修改每张图的文字、按钮和图片。最多 6 张,访客手动切换。删除全部幻灯片后恢复原来的单图 Banner。</p>':field==='items'&&b.type==='attributes'?'<p class="prop-tip">可以手动添加参数,不需要填写 JSON;仅在选择“手动参数”或“数据 + 补充”时显示。</p>':'')+Studio.arrayEditor(field,value,b.type);
|
||||
else if(field==='source_mode')out+=`<select onchange="Studio.field('${field}',this.value);Studio.inspector()">${[['page','从产品数据自动读取'],['custom','只显示手动填写'],['merge','产品数据 + 手动补充']].map(([v,n])=>`<option value="${v}" ${value===v?'selected':''}>${n}</option>`).join('')}</select><small class="prop-tip">默认读取产品数据,不会更改原产品信息。</small>`;
|
||||
else if(field==='source')out+=`<select onchange="Studio.field('${field}',this.value)">${['product','post','page','vehicle','brand','case','service','other','all'].map(t=>`<option value="${t}" ${t===value?'selected':''}>${({product:'产品',post:'文章',page:'普通页面',vehicle:'车型',brand:'品牌',case:'案例',service:'服务',other:'其他',all:'全部类型'})[t]}</option>`).join('')}</select><span class="prop-tip">网站已有内容将自动绑定并生成卡片。</span>`;
|
||||
else if(['layout_variant','card_style','image_fit'].includes(field)){
|
||||
const choices={layout_variant:[['split','文字左侧 · 图片右侧'],['image-first','图片左侧 · 文字右侧'],['centered','标题居中 · 图片下方']],card_style:[['standard','标准卡片'],['compact','紧凑型 · 展示更多产品'],['showcase','图片优先 · 大图展示']],image_fit:[['cover','铺满画面(可能裁切)'],['contain','完整显示(不裁切)']]}[field];
|
||||
out+=`<div class="sf-visual-options" role="group" aria-label="${html(label)}">${choices.map(([v,name])=>`<button type="button" class="sf-visual-option ${value===v?'on':''}" aria-pressed="${String(value===v)}" onclick="Studio.field('${field}','${v}');Studio.inspector()"><span class="sf-option-mark" aria-hidden="true">${v==='split'?'▤':v==='image-first'?'▥':v==='centered'?'▦':v==='compact'?'▤':v==='showcase'?'▧':v==='contain'?'▣':'▦'}</span><span>${html(name)}</span></button>`).join('')}</div>`;
|
||||
if(field==='image_fit')out+='<small class="prop-tip">汽车与管材产品建议选择“完整显示”,避免产品轮廓被裁切。</small>';
|
||||
}
|
||||
else if(field==='image_side'||field==='align')out+=`<select onchange="Studio.field('${field}',this.value)">${(field==='align'?['left','center']:['left','right']).map(v=>`<option value="${v}" ${value===v?'selected':''}>${v==='left'?'左侧':v==='right'?'右侧':'居中'}</option>`).join('')}</select>`;
|
||||
else if(field==='body'||field==='description')out+=`<textarea onchange="Studio.fieldQuiet('${field}',this.value)">${html(value)}</textarea>`;
|
||||
else if(['limit','columns','height'].includes(field))out+=`<input type="number" min="1" max="120" value="${html(value)}" onchange="Studio.fieldQuiet('${field}',Number(this.value))">`;
|
||||
@@ -383,7 +389,7 @@ const Studio={
|
||||
}
|
||||
const mediaTarget=selectedChildId?editorTools.findChildInRoot(blocks,b.id,selectedChildId)?.child:b;
|
||||
const mediaFields=schema.blocks[mediaTarget?.type]?.fields||[];
|
||||
if(mediaTarget&&(['gallery','carousel'].includes(mediaTarget.type)||mediaFields.includes('image')))out+=`<div class="sf-photo-action"><strong>图片设置</strong><button class="ui-btn ghost" type="button" onclick="Studio.chooseMedia()">从图片素材库选择</button><label class="sf-direct-upload">或上传一张新图片<input aria-label="上传新图片" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" onchange="Studio.uploadImage(this.files[0]);this.value=''"></label><small>预览先显示在草稿里,点击保存后才会保留。</small></div>`;
|
||||
if(mediaTarget&&!(mediaTarget.type==='hero'&&mediaTarget.slides?.length)&&(['gallery','carousel'].includes(mediaTarget.type)||mediaFields.includes('image')))out+=`<div class="sf-photo-action"><strong>图片设置</strong><button class="ui-btn ghost" type="button" onclick="Studio.chooseMedia()">从图片素材库选择</button><label class="sf-direct-upload">或上传一张新图片<input aria-label="上传新图片" type="file" accept="image/png,image/jpeg,image/webp,image/gif,image/avif" onchange="Studio.uploadImage(this.files[0]);this.value=''"></label><small>预览先显示在草稿里,点击保存后才会保留。</small></div>`;
|
||||
if(b.type==='layout')out+=Studio.childQuickEditor(b);
|
||||
out+='</div><div data-inspector-section="responsive">';
|
||||
if(b.type==='layout'){
|
||||
@@ -619,20 +625,24 @@ const Studio={
|
||||
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 chooseMedia(){try{const data=await api(`/api/sites/${siteId}/media`);const grid=$('#siteMediaGrid');grid.replaceChildren();if(!data.length){grid.textContent='暂无上传图片。可先在当前组件中上传图片。'}for(const item of data){const button=document.createElement('button');button.type='button';button.style.cssText='border:1px solid #e2e8f0;border-radius:9px;background:white;padding:7px;cursor:pointer';const img=document.createElement('img');img.alt=item.name;img.loading='lazy';img.style.cssText='width:100%;height:105px;object-fit:contain';img.src=`/local-media/${encodeURIComponent(siteId)}/${encodeURIComponent(item.name)}`;const label=document.createElement('small');label.textContent=item.name;label.style.cssText='display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';button.append(img,label);button.addEventListener('click',()=>Studio.applyMedia(item.url));grid.append(button)}$('#siteMediaModal').showModal()}catch(e){toast(e.message,true)}},
|
||||
applyMedia(url){
|
||||
async chooseMedia(field=null,index=null){try{const data=await api(`/api/sites/${siteId}/media`);const grid=$('#siteMediaGrid');grid.replaceChildren();if(!data.length){grid.textContent='暂无上传图片。可先在当前组件中上传图片。'}for(const item of data){const button=document.createElement('button');button.type='button';button.style.cssText='border:1px solid #e2e8f0;border-radius:9px;background:white;padding:7px;cursor:pointer';const img=document.createElement('img');img.alt=item.name;img.loading='lazy';img.style.cssText='width:100%;height:105px;object-fit:contain';img.src=`/local-media/${encodeURIComponent(siteId)}/${encodeURIComponent(item.name)}`;const label=document.createElement('small');label.textContent=item.name;label.style.cssText='display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap';button.append(img,label);button.addEventListener('click',()=>Studio.applyMedia(item.url,field,index));grid.append(button)}$('#siteMediaModal').showModal()}catch(e){toast(e.message,true)}},
|
||||
applyMedia(url,field=null,index=null){
|
||||
const root=blocks[selected];if(!root)return;
|
||||
const found=selectedChildId?editorTools.findChildInRoot(blocks,root.id,selectedChildId):null;
|
||||
if(selectedChildId&&!found)return toast('当前子组件已失效,请重新选中后添加图片',true);
|
||||
const block=found?.child||root;
|
||||
const fields=schema?.blocks?.[block.type]?.fields||[];
|
||||
if(block.type==='gallery')block.images=[...(block.images||[]),{url,alt:''}];
|
||||
if(field==='slides'){
|
||||
if(block.type!=='hero'||!Number.isInteger(index)||index<0||index>=(block.slides||[]).length)return toast('所选横幅不存在,请重新选择',true);
|
||||
block.slides[index].image=url;
|
||||
}
|
||||
else if(block.type==='gallery')block.images=[...(block.images||[]),{url,alt:''}];
|
||||
else if(block.type==='carousel')block.items=[...(block.items||[]),{image:url,alt:'',caption:''}];
|
||||
else if(fields.includes('image'))block.image=url;
|
||||
else return toast('选中的组件不支持图片,请先选择一个图片组件',true);
|
||||
Studio.modified();Studio.draw();$('#siteMediaModal').close();toast('已应用素材到草稿');
|
||||
},
|
||||
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.applyMedia(r.url);toast('图片已上传,保存页面后参与构建')}catch(e){toast(e.message,true)}},
|
||||
async uploadImage(file,field=null,index=null){if(!file)return;try{const form=new FormData();form.append('file',file);let r=await api(`/api/sites/${siteId}/media`,{method:'POST',body:form});if(field==='slides')Studio.applyMedia(r.url,field,index);else Studio.applyMedia(r.url);toast('图片已上传,保存页面后参与构建')}catch(e){toast(e.message,true)}},
|
||||
async buildSite(){if(dirty||draftRevision||siteDesignRevision)return toast('有尚未发布的页面或全站草稿,请先发布,再构建网站',true);try{
|
||||
toast('已提交后台构建任务');const task=await api(`/api/sites/${siteId}/builds`,req('POST',{mirror_images:false}));
|
||||
for(let i=0;i<240;i++){
|
||||
|
||||
Reference in New Issue
Block a user