upgrade: adopt V8.32.0-dev.1 (theme preview iframe fix, editor simple mode)

Overlay 46 upstream items; full patch set re-applied (all patched files
reset by overlay regardless of upstream change status — lesson recorded).
Verified: theme suites 35/35; dual-baseline 418 passed vs pristine
389+29 ours, FAILED-set diff EMPTY (same 10 Windows env fails).
Live: container 8.32.0-dev.1 healthy, hesupipe 192 entries intact,
trade-pro build 203 pages 0 errors.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 11:18:45 +08:00
parent 8884e100f9
commit 95982fad23
25 changed files with 561 additions and 232 deletions
-1
View File
@@ -18,7 +18,6 @@ def discover_themes():
raise RuntimeError(f'themes directory missing: {themes_dir}')
found = []
for entry in sorted(themes_dir.iterdir()):
# Underscore prefix marks non-theme assets (e.g. _professional_components.html).
if not entry.is_dir() or entry.name.startswith('_'):
continue
try:
+7 -1
View File
@@ -133,7 +133,13 @@ async def require_login(request:Request, call_next):
data=_json.loads(raw)
response=JSONResponse([x for x in data if x['id'] in ids])
response.headers['X-Content-Type-Options']='nosniff'
response.headers['X-Frame-Options']='SAMEORIGIN' if path.startswith('/editor/') else 'DENY'
# Only read-only theme previews (and the existing editor) may be framed by this origin.
# All other application pages stay protected from clickjacking.
is_theme_preview=bool(__import__('re').fullmatch(r'/api/factory/themes/[A-Za-z0-9_-]+/preview',path))
response.headers['X-Frame-Options']='SAMEORIGIN' if (path.startswith('/editor/') or is_theme_preview) else 'DENY'
if is_theme_preview:
response.headers['Content-Security-Policy']="frame-ancestors 'self'"
response.headers['Cache-Control']='no-store'
response.headers['Referrer-Policy']='strict-origin-when-cross-origin'
response.headers['Cache-Control']='no-store' if path.startswith('/api/auth/') else response.headers.get('Cache-Control','no-cache')
return response
+15
View File
@@ -156,3 +156,18 @@ iframe{min-height:590px;height:calc(100vh - 224px)}
.studio-body .left-pane,.studio-body .right-pane{top:140px!important}
.studio-actions-menu{right:0;max-width:calc(100vw - 18px)}
}
/* V8.32: first-use focused studio without removing advanced capabilities. */
.sf-next-action{margin-left:auto;font-size:12px;color:#30516b;font-weight:650;background:#eef4f8;padding:6px 10px;border-radius:7px;white-space:normal}
.sf-inspector-mode{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:9px 14px 12px;border-bottom:1px solid #eef1f4;color:#66798a;font-size:11px}
.sf-inspector-mode button{background:#f5f8fb;border:1px solid #dce5ee;border-radius:7px;padding:8px 10px;color:#285276;font-weight:700;cursor:pointer;font-size:11px}
body:not(.sf-advanced-mode) #tabStyle,body:not(.sf-advanced-mode) #tabResponsive{display:none!important}
.sf-photo-action{border:1px solid #dbe9f5;background:#f6fafd;border-radius:10px;display:grid;gap:7px;padding:12px;margin:14px 0}
.sf-photo-action button{width:100%;justify-content:center}
.sf-photo-action small{font-size:11px;color:#64778b;line-height:1.5}
.sf-close-inspector{display:none;background:white;border:1px solid #d8e1ea;border-radius:8px;padding:7px 10px;color:#385872;cursor:pointer;font-size:11px}
.sf-steps{justify-content:flex-start}.sf-steps .sf-guide-link{margin-left:0}
@media(max-width:1060px){.sf-close-inspector{display:inline-flex}.sf-next-action{display:none}}
@media(max-width:720px){.sf-steps .sf-guide-link{margin-left:auto}.sf-inspector-mode{padding:8px 11px}}
.sf-photo-action strong{font-size:13px;color:#28516e}.sf-direct-upload{display:grid;gap:7px;color:#506b82;font-size:12px;font-weight:650}.sf-direct-upload input{font-size:11px;width:100%;max-width:100%}
+30 -8
View File
@@ -13,6 +13,7 @@ let libraryOpen=false;
let sharedPreview=null;
let siteNavDraft=[],siteDesignReviewToken=null;
let selectedChildId=null;
let advancedMode=false,themePreviewSeq=0,themePreviewAbort=null;
let inspectorTab='content',paletteSearch='',blockSearch='',previewRequest=0,previewController=null,savePromise=null,lastContinuousEdit=0,insertAfterId=null;
let snapshots=[],snapshotCursor=-1,draggedChild=null,draftRevision=0,siteDesign=null,siteDesignRevision=0,siteDesignPreview=null;
@@ -284,24 +285,40 @@ const Studio={
filterPalette(value){paletteSearch=String(value||'').slice(0,70);Studio.renderPalette()},
clearPaletteSearch(){paletteSearch='';const inp=$('#paletteSearch');if(inp){inp.value='';inp.focus()}Studio.renderPalette()},
showThemePreview(){const dialog=$('#themePreviewModal');if(!dialog)return;const current=['automotive','industrial','corporate','minimal'].includes(site?.theme)?site.theme:'corporate';$('#themePreviewTheme').value=current;if(!dialog.open)dialog.showModal();Studio.themeDevice('desktop');Studio.updateThemePreview()},
closeThemePreview(){const dialog=$('#themePreviewModal');if(dialog?.open)dialog.close();const frame=$('#themePreviewFrame');if(frame)frame.removeAttribute('src')},
closeThemePreview(){themePreviewSeq++;themePreviewAbort?.abort();const dialog=$('#themePreviewModal');if(dialog?.open)dialog.close();const frame=$('#themePreviewFrame');if(frame){frame.removeAttribute('src');frame.removeAttribute('srcdoc')}},
themeDevice(device){
const width={desktop:'100%',tablet:'780px',phone:'390px'}[device];if(!width)return;
$('#themePreviewFrameShell').style.width=width;
document.querySelectorAll('[data-theme-device]').forEach(btn=>btn.setAttribute('aria-pressed',String(btn.dataset.themeDevice===device)));
},
updateThemePreview(){
async updateThemePreview(){
const theme=$('#themePreviewTheme')?.value,page=$('#themePreviewPage')?.value;
const url=window.SiteForgeStudioExperience.previewUrl(theme,page);
if(!url)return;
const frame=$('#themePreviewFrame'),status=$('#themePreviewStatus');if(!frame)return;
status.textContent='正在加载主题示例…(不影响已保存的页面)';
frame.onload=()=>{if($('#themePreviewModal')?.open)status.textContent='已加载示例。可切换主题、页面类型和设备继续比较。'};
frame.onerror=()=>{status.textContent='示例加载失败,请稍后重试。'};
frame.src=url;
const frame=$('#themePreviewFrame'),status=$('#themePreviewStatus'),retry=$('#themePreviewRetry');if(!frame)return;
const seq=++themePreviewSeq;
themePreviewAbort?.abort();themePreviewAbort=new AbortController();
status.textContent='正在检查主题预览…';retry.hidden=true;frame.removeAttribute('src');frame.removeAttribute('srcdoc');
try{
const response=await fetch(url,{credentials:'same-origin',signal:themePreviewAbort.signal,cache:'no-store'});
if(!response.ok||!(response.headers.get('content-type')||'').includes('text/html'))throw Error(response.status===401?'登录已过期':`服务返回 ${response.status}`);
const content=await response.text();
if(seq!==themePreviewSeq||!$('#themePreviewModal')?.open)return;
// srcdoc keeps the preview in an opaque sandboxed origin: no allow-same-origin,
// no repeated HTML request, no frame-header conflict, and no access to editor data.
const base=new URL(url,['http:','https:'].includes(location.protocol)?location.href:'https://siteforge.invalid').href;
const safeBase=html(base);
const withBase=/<head(?:\s[^>]*)?>/i.test(content)?content.replace(/<head(\s[^>]*)?>/i,(match)=>match+`<base href=\"${safeBase}\">`):`<base href=\"${safeBase}\">`+content;
status.textContent='正在渲染主题页面…';
frame.onload=()=>{if(seq===themePreviewSeq&&$('#themePreviewModal')?.open)status.textContent='预览已显示 · 可切换主题和设备';};
frame.srcdoc=withBase;
}catch(error){
if(seq!==themePreviewSeq||error.name==='AbortError')return;
status.textContent='预览失败:'+error.message+'。请点击重新加载或在完整主题图库中打开。';retry.hidden=false;
}
}, toggleLibrary(force){libraryOpen=typeof force==='boolean'?force:!libraryOpen;const el=$('#sfLibraryPanel'),btn=$('#sfAddBlock');if(el)el.hidden=!libraryOpen;if(btn){btn.setAttribute('aria-expanded',String(libraryOpen));btn.textContent=libraryOpen?'× 收起区块库':'+ 添加内容区块'};Studio.updateGuidance()},
showHelp(){const dlg=$('#sfGuideDialog');if(dlg&&!dlg.open)dlg.showModal()},
updateGuidance(){const one=$('#sfStep1'),two=$('#sfStep2'),three=$('#sfStep3');if(!one||!two||!three)return;one.classList.toggle('active',selected<0||libraryOpen);two.classList.toggle('active',selected>=0&&!dirty&&!libraryOpen);three.classList.toggle('active',dirty);},
updateGuidance(){const one=$('#sfStep1'),two=$('#sfStep2'),three=$('#sfStep3');if(!one||!two||!three)return;one.classList.toggle('active',selected<0||libraryOpen);two.classList.toggle('active',selected>=0&&!dirty&&!libraryOpen);three.classList.toggle('active',dirty);const note=$('#sfNextAction');if(note)note.textContent=dirty?'更改尚未保存:点击右上角「保存草稿」':libraryOpen?'选择一种内容,添加后直接修改文字或图片':selected>=0?'正在编辑「'+(schema?.blocks?.[blocks[selected]?.type]?.name||'当前区块')+'」,右侧可直接修改':'先在左侧添加区块或选中页面内容';},
draw(){
const list=$('#blockList');$('#blockCount').textContent=blocks.length+' 区块';
const found=editorTools.filterBlocks(blocks,blockSearch,schema.blocks);
@@ -315,6 +332,8 @@ const Studio={
blockKeydown(event,i){if(event.target!==event.currentTarget)return;if(event.key==='Enter'||event.key===' '){event.preventDefault();Studio.select(i)}else if(event.altKey && (event.key==='ArrowUp'||event.key==='ArrowDown')){event.preventDefault();Studio.moveBlock(i,event.key==='ArrowUp'?-1:1)}},
changeInspectorTab(tab){if(!['content','style','responsive'].includes(tab))return;inspectorTab=tab;Studio.applyInspectorTab()},
applyInspectorTab(){for(const key of ['content','style','responsive']){const active=key===inspectorTab;const btn=$('#tab'+key.charAt(0).toUpperCase()+key.slice(1));btn?.setAttribute('aria-selected',String(active));const pane=$('[data-inspector-section="'+key+'"]');if(pane)pane.hidden=!active}},
toggleAdvanced(){advancedMode=!advancedMode;document.body.classList.toggle('sf-advanced-mode',advancedMode);const button=$('#studioAdvancedButton');button?.setAttribute('aria-pressed',String(advancedMode));if(button)button.textContent=advancedMode?'收起高级设置':'更多设计设置';const detail=$('#sfModeExplanation');if(detail)detail.textContent=advancedMode?'可调整布局、样式和手机显示':'当前显示常用设置';if(!advancedMode){inspectorTab='content';Studio.applyInspectorTab()}},
closePane(){document.body.classList.remove('show-inspector','show-structure')},
togglePane(which){const type=which==='left'?'show-structure':'show-inspector';document.body.classList.toggle(type);if(type==='show-structure')document.body.classList.remove('show-inspector');else document.body.classList.remove('show-structure');},
updateHistoryButtons(){const state=editorTools.historyState(snapshots,snapshotCursor);for(const id of ['#studioUndo','#quickUndo']){const el=$(id);if(el)el.disabled=!state.canUndo}for(const id of ['#studioRedo','#quickRedo']){const el=$(id);if(el)el.disabled=!state.canRedo}},
@@ -362,6 +381,9 @@ const Studio={
if(['title','subtitle','description','image','body'].includes(field))out+='<small class="prop-tip">可输入 {{site.name}}、{{page.title}} 等变量。</small>';
out+='</div>';
}
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(b.type==='layout')out+=Studio.childQuickEditor(b);
out+='</div><div data-inspector-section="responsive">';
if(b.type==='layout'){
+1 -1
View File
@@ -123,7 +123,7 @@ function content(){if(!currentSite())return noSite();let arr=state.entries.filte
(arr.length?`<div class="table-wrap"><table class="data-table"><thead><tr><th>名称</th><th>内容类型</th><th>页面地址</th><th>状态</th><th>操作</th></tr></thead><tbody>${arr.slice(0,200).map(e=>`<tr><td><strong>${esc(e.title)}</strong></td><td>${esc(typeName[e.type]||e.type)}</td><td class="muted">${esc(e.route)}</td><td><span class="pill ${e.status==='draft'?'warn':'success'}">${e.status==='draft'?'草稿':'已保存'}</span></td><td class="action-cell"><button class="btn btn-tiny" data-action="edit-entry" data-id="${esc(e.id)}">修改内容</button> <a class="btn btn-tiny" href="/editor/${encodeURIComponent(state.site.id)}/${encodeURIComponent(e.id)}">设计页面 ↗</a></td></tr>`).join('')}</tbody></table></div>`:emptyState('没有找到内容','可以清除筛选条件,或者添加新内容。',actionButton('+ 添加内容','new-entry')))+
`${arr.length>200?'<p class="muted">当前显示前 200 条,请使用搜索缩小范围。</p>':''}</div>`;
}
function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`<div class="theme-card"><div class="theme-preview ${id}">${name}</div><div class="theme-desc"><h3>${name} ${s.theme===id?'<span class="pill success">当前使用</span>':''}</h3><p>${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象','trade-pro':'适合外贸 B2B 英文站点与询盘'})[id]}</p><button class="btn btn-tiny ${s.theme===id?'btn-muted':'btn-primary'}" ${s.theme===id?'disabled':''} data-action="set-theme" data-theme="${id}">${s.theme===id?'正在使用':'使用此主题'}</button></div></div>`).join('');
function design(){if(!currentSite())return noSite();const s=state.site;const cards=Object.entries(themeName).map(([id,name])=>`<div class="theme-card"><div class="theme-preview ${id}">${name}</div><div class="theme-desc"><h3>${name} ${s.theme===id?'<span class="pill success">当前使用</span>':''}</h3><p>${({automotive:'适合汽车出口、车型及品牌展示',industrial:'适合制造业、产品与技术参数',corporate:'适合集团公司、服务和案例',minimal:'适合作品展示和品牌形象','trade-pro':'适合外贸 B2B 英文站点与询盘'})[id]}</p><button class="btn btn-tiny ${s.theme===id?'btn-muted':'btn-primary'}" ${s.theme===id?'disabled':''} data-action="set-theme" data-theme="${id}">${s.theme===id?'正在使用':'使用此主题'}</button><a class="btn btn-tiny btn-muted" href="/theme-gallery?theme=${encodeURIComponent(id)}" target="_blank" rel="noopener">预览页面效果</a></div></div>`).join('');
return htmlHeading('外观与设计','先选一套网站风格,再打开可视化编辑器修改图文、导航和手机端布局。')+
`<div class="section-heading"><div><h2>第一步 · 选择行业主题</h2><p>更换主题不会删除您已经录入的业务内容和 URL。</p></div><a href="/theme-gallery" class="btn btn-tiny">查看完整主题预览 ↗</a></div><div class="choice-cards">${cards}</div>`+
`<div class="section-heading"><div><h2>第二步 · 设计网页</h2><p>挑选要修改的页面;可在设计器中编辑导航、页眉、页脚及各设备布局。</p></div></div><div class="panel"><div class="panel-head"><div><h2>选择要设计的页面</h2><p>修改先保存为草稿,正式发布设计后才会参与下一次静态构建。</p></div></div><div class="grid-2 grid-equal">${state.entries.filter(e=>e.type==='page').slice(0,8).map(e=>`<a class="site-card" href="/editor/${encodeURIComponent(s.id)}/${encodeURIComponent(e.id)}"><span class="site-icon">▤</span><span><h3>${esc(e.title)}</h3><p>${esc(e.route)}</p></span><span class="arrow">›</span></a>`).join('')||'<div class="muted">尚无页面,请先添加内容。</div>'}</div></div><div class="help-hint" style="margin-top:19px"><strong>提示</strong>:网站设置用于公司信息和域名;可视化编辑器用于调整页面外观。两者不会混在同一表单里。</div>`;
+5 -5
View File
@@ -4,7 +4,7 @@
<header class="studio-top sf-easy-top">
<div class="brand"><a href="/" title="返回网站管理">◈</a><div><b>SiteForge</b><small>页面编辑</small></div></div>
<div class="file-name sf-easy-file"><small id="siteName">加载中</small><strong id="pageName">页面编辑器</strong></div>
<button type="button" id="studioThemePreviewButton" class="ui-btn ghost sf-theme-preview-entry" onclick="Studio.showThemePreview()" title="不影响当前网站,先比较四套主题">◫ 主题效果</button>
<button type="button" id="studioThemePreviewButton" class="ui-btn ghost sf-theme-preview-entry" onclick="Studio.showThemePreview()" title="不影响当前网站,先比较四套主题">主题预览</button>
<div class="sf-pane-controls"><button type="button" onclick="Studio.togglePane('left')">☰ 内容</button><button type="button" onclick="Studio.togglePane('right')">⚙ 编辑</button></div>
<div class="top-center sf-easy-device" role="group" aria-label="预览设备">
<button id="deviceDesktop" class="icon-btn on" onclick="Studio.device('desktop')" title="电脑预览" aria-label="电脑预览">电脑</button>
@@ -26,7 +26,7 @@
<button class="ui-btn ghost" onclick="Studio.discardDraft()">放弃草稿</button>
</div></details>
</header>
<div class="sf-steps" aria-label="编辑操作指引"><div class="sf-steps-label">只需 3 步</div><span class="sf-step" id="sfStep1">① 点击页面中的内容</span><span class="sf-step-separator">→</span><span class="sf-step" id="sfStep2">② 直接编辑或调整布局</span><span class="sf-step-separator">→</span><span class="sf-step" id="sfStep3">③ 保存并发布</span><button type="button" class="sf-guide-link" onclick="Studio.showHelp()">新手使用指南 ?</button></div>
<div class="sf-steps" aria-label="编辑操作指引"><div class="sf-steps-label">只需 3 步</div><span class="sf-step" id="sfStep1">① 点击页面中的内容</span><span class="sf-step-separator">→</span><span class="sf-step" id="sfStep2">② 直接编辑或调整布局</span><span class="sf-step-separator">→</span><span class="sf-step" id="sfStep3">③ 保存并发布</span><span id="sfNextAction" class="sf-next-action" role="status" aria-live="polite">点击左侧区块或画布内容开始编辑</span><button type="button" class="sf-guide-link" onclick="Studio.showHelp()">使用帮助</button></div>
<div class="studio-body">
<aside class="left-pane" aria-label="页面内容和区块">
<div class="pane-heading"><div><span class="overline">页面内容</span><h3>编辑页面</h3></div><span class="chip" id="blockCount">0 个区块</span></div>
@@ -47,8 +47,8 @@
</aside>
<section class="preview-workspace"><div class="preview-head"><span class="live-dot" id="previewDot"></span><span id="previewState" role="status" aria-live="polite">实时预览</span><code id="routeDisplay">/</code><span class="preview-note">单击区块或列内组件即可编辑 · 拖动 ⠿ 可排序 · 拖动列间分隔线可调宽</span></div><div class="stage"><div class="device-frame" id="deviceFrame"><iframe id="sitePreview" title="页面预览" sandbox="allow-scripts" referrerpolicy="no-referrer"></iframe></div></div>
<div class="stage-footer"><span id="deviceDetails">桌面端 · 自适应宽度</span><span>当前是草稿预览,不是线上网站</span></div></section>
<aside class="right-pane"><div class="pane-heading"><div><span class="overline">步骤 2 · 修改内容</span><h3>编辑选中的区块</h3></div><span class="chip" id="blockTypeLabel">页面</span></div>
<div class="inspector-nav" role="tablist" aria-label="属性编辑分类"><button type="button" role="tab" id="tabContent" aria-selected="true" onclick="Studio.changeInspectorTab('content')">内容</button><button type="button" role="tab" id="tabStyle" aria-selected="false" onclick="Studio.changeInspectorTab('style')">样式</button><button type="button" role="tab" id="tabResponsive" aria-selected="false" onclick="Studio.changeInspectorTab('responsive')">手机/平板</button></div>
<aside class="right-pane"><div class="pane-heading"><div><span class="overline">当前内容</span><h3>修改内容</h3></div><button type="button" id="sfCloseInspector" class="sf-close-inspector" onclick="Studio.closePane()" aria-label="关闭编辑面板">关闭</button><span class="chip" id="blockTypeLabel">页面</span></div>
<div class="sf-inspector-mode"><span id="sfModeExplanation">当前显示常用设置</span><button type="button" id="studioAdvancedButton" aria-pressed="false" onclick="Studio.toggleAdvanced()">更多设计设置</button></div><div class="inspector-nav" role="tablist" aria-label="属性编辑分类"><button type="button" role="tab" id="tabContent" aria-selected="true" onclick="Studio.changeInspectorTab('content')">内容</button><button type="button" role="tab" id="tabStyle" aria-selected="false" onclick="Studio.changeInspectorTab('style')">样式</button><button type="button" role="tab" id="tabResponsive" aria-selected="false" onclick="Studio.changeInspectorTab('responsive')">手机/平板</button></div>
<div class="inspector" id="inspector"><div class="no-selection">直接点击中间画布的任意区块,即可开始编辑。</div></div>
<details class="right-foot sf-advanced-foot"><summary>高级:动态数据变量</summary><p>支持 <code>{{site.name}}</code>、<code>{{page.title}}</code> 等变量;动态内容列表可以按网站内容类型绑定。</p></details>
</aside>
@@ -57,7 +57,7 @@
<div class="sf-showcase-head"><div><small>仅预览 · 不修改网站内容</small><h2 id="themePreviewTitle">选择适合网站的主题效果</h2><p>先比较布局和手机显示效果,再决定是否到「网站设计」中应用主题。</p></div><button type="button" class="ui-btn ghost" onclick="Studio.closeThemePreview()" aria-label="关闭主题预览">关闭 ×</button></div>
<div class="sf-showcase-tools"><label>主题风格<select id="themePreviewTheme" onchange="Studio.updateThemePreview()"><option value="automotive">汽车与出行</option><option value="industrial">工业与制造</option><option value="corporate">企业品牌</option><option value="minimal">简约展示</option></select></label><label>参考页面<select id="themePreviewPage" onchange="Studio.updateThemePreview()"><option value="home">首页</option><option value="listing">产品列表</option><option value="product">产品详情</option><option value="article">文章详情</option><option value="about">关于我们</option><option value="contact">联系我们</option></select></label><div class="sf-showcase-device" role="group" aria-label="主题参考设备"><button type="button" data-theme-device="desktop" aria-pressed="true" onclick="Studio.themeDevice('desktop')">电脑</button><button type="button" data-theme-device="tablet" aria-pressed="false" onclick="Studio.themeDevice('tablet')">平板</button><button type="button" data-theme-device="phone" aria-pressed="false" onclick="Studio.themeDevice('phone')">手机</button></div></div>
<p class="sf-showcase-status" id="themePreviewStatus" role="status" aria-live="polite">示例使用主题最新版本,可能与你网站锁定的主题版本不同。不会修改当前页面。</p><div class="sf-showcase-stage"><div id="themePreviewFrameShell" class="sf-showcase-frame"><iframe id="themePreviewFrame" title="主题示例页面" sandbox="allow-scripts" referrerpolicy="no-referrer" loading="lazy"></iframe></div></div>
<div class="sf-showcase-footer"><span>预览不会保存、更换或发布主题。确认效果后请到网站设计里操作。</span><a class="ui-btn ghost" href="/theme-gallery" target="_blank" rel="noopener">打开完整主题图库 ↗</a></div>
<div class="sf-showcase-footer"><span>编辑器仅查看外观;完整图库支持交互。预览不会保存或发布主题。<button type="button" id="themePreviewRetry" class="ui-btn ghost" onclick="Studio.updateThemePreview()" hidden>重新加载</button></span><a class="ui-btn ghost" href="/theme-gallery" target="_blank" rel="noopener">打开完整主题图库 ↗</a></div>
</dialog>
<dialog id="sfGuideDialog" class="sf-help-dialog" aria-labelledby="sfGuideTitle">
<div class="sf-help-head"><div><span class="overline">新手指南</span><h2 id="sfGuideTitle">3 分钟学会编辑页面</h2></div><button type="button" class="ui-btn ghost" onclick="document.querySelector('#sfGuideDialog').close()">关闭 ×</button></div>
+41 -6
View File
@@ -1,15 +1,50 @@
<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>SiteForge · 完整主题预览中心</title>
<style>*{box-sizing:border-box}body{margin:0;background:#eef1f5;font:14px/1.6 system-ui,'Microsoft YaHei',sans-serif;color:#172c42}header{display:flex;align-items:center;justify-content:space-between;padding:14px 26px;background:#fff;border-bottom:1px solid #dce3eb;gap:24px;flex-wrap:wrap}header strong{font-size:17px}header strong span{background:#15253e;color:#fff;border-radius:6px;padding:6px 8px;margin-right:8px}header a{color:#255b99;text-decoration:none}.controls{display:flex;align-items:end;flex-wrap:wrap;gap:12px;padding:18px 26px;border-bottom:1px solid #dce3eb;background:white}.controls label{font-weight:700;color:#5c7083;font-size:11px;display:grid;gap:6px}.controls select{border:1px solid #ccd5df;background:#fff;color:#192d41;padding:10px 14px;border-radius:7px;min-width:132px;font-size:13px}.devices{display:flex;gap:4px;margin-left:auto;background:#f0f3f8;border-radius:8px;padding:4px}.devices button{border:0;background:none;padding:9px 14px;border-radius:6px;cursor:pointer;color:#52697e}.devices button.active{background:#1e446c;color:white}.stage{padding:22px 18px 80px;display:flex;justify-content:center;overflow:auto}.frame{width:100%;height:calc(100vh - 210px);min-height:520px;background:white;box-shadow:0 16px 50px #12304720;border-radius:10px;overflow:hidden;transition:width .25s}.frame iframe{border:0;width:100%;height:100%;background:white}.help{text-align:center;color:#6d8296;font-size:12px;margin:-55px auto 28px}.error{color:#c14f45;display:none}@media(max-width:700px){header,.controls{padding:12px}header strong{font-size:14px}.devices{margin-left:0}.frame{min-height:450px}}</style><link rel="stylesheet" href="/ui/professional-ui.css"><script defer src="/ui/professional-ui.js"></script></head>
<style>*{box-sizing:border-box}body{margin:0;background:#eef1f5;font:14px/1.6 system-ui,'Microsoft YaHei',sans-serif;color:#172c42}header{display:flex;align-items:center;justify-content:space-between;padding:14px 26px;background:#fff;border-bottom:1px solid #dce3eb;gap:24px;flex-wrap:wrap}header strong{font-size:17px}header strong span{background:#15253e;color:#fff;border-radius:6px;padding:6px 8px;margin-right:8px}header a{color:#255b99;text-decoration:none}.controls{display:flex;align-items:end;flex-wrap:wrap;gap:12px;padding:18px 26px;border-bottom:1px solid #dce3eb;background:white}.controls label{font-weight:700;color:#5c7083;font-size:11px;display:grid;gap:6px}.controls select{border:1px solid #ccd5df;background:#fff;color:#192d41;padding:10px 14px;border-radius:7px;min-width:132px;font-size:13px}.devices{display:flex;gap:4px;margin-left:auto;background:#f0f3f8;border-radius:8px;padding:4px}.devices button{border:0;background:none;padding:9px 14px;border-radius:6px;cursor:pointer;color:#52697e}.devices button.active{background:#1e446c;color:white}.stage{padding:22px 18px 80px;display:flex;justify-content:center;overflow:auto}.frame{width:100%;height:calc(100vh - 210px);min-height:520px;background:white;box-shadow:0 16px 50px #12304720;border-radius:10px;overflow:hidden;transition:width .25s}.frame iframe{border:0;width:100%;height:100%;background:white}.help{text-align:center;color:#6d8296;font-size:12px;margin:-55px auto 28px}.sf-preview-status{font-size:12px;color:#566d83;max-width:350px}.sf-preview-status.error{color:#aa3434}.sf-preview-retry{padding:8px 12px;border:1px solid #cbd7e4;border-radius:7px;background:white;color:#28547f;cursor:pointer;text-decoration:none;font-size:12px}@media(max-width:700px){header,.controls{padding:12px}header strong{font-size:14px}.devices{margin-left:0}.frame{min-height:450px}}</style><link rel="stylesheet" href="/ui/professional-ui.css"><script defer src="/ui/professional-ui.js"></script></head>
<body data-sf-page="theme"><header><strong><span>SF</span> SiteForge · 主题预览中心</strong><a href="/factory-studio">返回网站工厂 →</a></header>
<section class="controls"><label>主题<select id="theme"><option value="automotive">Automotive · 汽车与出行</option><option value="industrial">Industrial · 工业制造</option><option value="corporate">Corporate · 企业品牌</option><option value="minimal">Minimal · 极简设计</option><option value="trade-pro">Trade Pro · International B2B</option></select></label>
<label>页面类型<select id="page"><option value="home">首页</option><option value="about">关于我们</option><option value="listing">产品 / 分类列表</option><option value="category">分类详情</option><option value="product">产品详情</option><option value="brand">品牌详情</option><option value="article">新闻详情</option><option value="case">案例详情</option><option value="service">服务详情</option><option value="contact">联系我们</option><option value="faq">常见问题</option><option value="standard">标准页面</option><option value="notfound">404</option></select></label>
<label>主题版本<select id="version"><option value="">最新</option></select></label><div class="devices"><button data-size="100%" class="active">PC</button><button data-size="820px">平板</button><button data-size="390px">手机</button></div><span class="error" id="error">预览失败,请确认已登录。</span></section>
<label>主题版本<select id="version"><option value="">最新</option></select></label><div class="devices"><button data-size="100%" class="active">PC</button><button data-size="820px">平板</button><button data-size="390px">手机</button></div><span id="previewStatus" role="status" aria-live="polite" class="sf-preview-status">准备加载预览…</span><button type="button" id="retryPreview" class="sf-preview-retry" hidden>重新加载</button><a id="openPreview" target="_blank" rel="noopener" class="sf-preview-retry" href="#">单独打开预览</a></section>
<div class="stage"><div class="frame" id="frame"><iframe id="preview" title="主题真实页面预览" loading="eager"></iframe></div></div>
<div class="help">预览使用真实页面模板与演示数据。实际网站构建后将使用你的品牌信息和内容。</div>
<script>
const theme=document.querySelector('#theme'),page=document.querySelector('#page'),version=document.querySelector('#version'),frame=document.querySelector('#frame'),iframe=document.querySelector('#preview');
function show(){iframe.src=`/api/factory/themes/${encodeURIComponent(theme.value)}/preview?page_type=${encodeURIComponent(page.value)}&version=${encodeURIComponent(version.value)}`;}
async function loadVersions(){const response=await fetch(`/api/factory/themes/${encodeURIComponent(theme.value)}/versions`,{credentials:'same-origin'});if(!response.ok)throw Error('无法读取主题版本');const arr=await response.json();version.replaceChildren();arr.forEach(x=>{const op=new Option('v'+x.version,x.version);version.append(op);});show();}
theme.addEventListener('change',()=>loadVersions().catch(()=>document.querySelector('#error').style.display='inline'));
[page,version].forEach(x=>x.addEventListener('change',show));document.querySelectorAll('.devices button').forEach(x=>x.addEventListener('click',()=>{document.querySelectorAll('.devices button').forEach(y=>y.classList.remove('active'));x.classList.add('active');frame.style.width=x.dataset.size;}));loadVersions().catch(()=>document.querySelector('#error').style.display='inline');
const status=document.querySelector('#previewStatus'),retry=document.querySelector('#retryPreview'),open=document.querySelector('#openPreview');
let sequence=0,controller=null;
function link(){return `/api/factory/themes/${encodeURIComponent(theme.value)}/preview?page_type=${encodeURIComponent(page.value)}&version=${encodeURIComponent(version.value)}`;}
async function show(){
const seq=++sequence;
controller?.abort();controller=new AbortController();
const url=link();open.href=url;retry.hidden=true;status.classList.remove('error');status.textContent='正在加载预览…';
iframe.removeAttribute('src');
try{
// A successful iframe load event can also mean a browser error page, so first verify HTTP.
const response=await fetch(url,{credentials:'same-origin',signal:controller.signal,cache:'no-store'});
if(!response.ok||!(response.headers.get('content-type')||'').includes('text/html'))throw Error(response.status===401?'登录已过期,请重新登录':`页面返回 ${response.status},无法预览`);
if(seq!==sequence)return;
iframe.onload=()=>{if(seq===sequence)status.textContent='预览已打开 · 可以切换页面或设备';};
iframe.src=url;
}catch(error){
if(seq!==sequence||error.name==='AbortError')return;
status.textContent=`加载失败:${error.message}。请重试或单独打开预览。`;
status.classList.add('error');retry.hidden=false;
}
}
async function loadVersions(){
const seq=++sequence;controller?.abort();
version.replaceChildren(new Option('最新版本',''));version.disabled=true;
try{
const response=await fetch(`/api/factory/themes/${encodeURIComponent(theme.value)}/versions`,{credentials:'same-origin'});
if(!response.ok)throw Error(`主题版本返回 ${response.status}`);
const versions=await response.json();
if(seq!==sequence)return;
versions.forEach(item=>version.add(new Option(`v${item.version}`,item.version)));
}catch(error){if(seq!==sequence)return;status.textContent=`版本列表暂时无法读取,将预览最新版本:${error.message}`;}
finally{if(seq===sequence){version.disabled=false;show();}}
}
theme.addEventListener('change',loadVersions);[page,version].forEach(control=>control.addEventListener('change',show));
retry.addEventListener('click',show);
document.querySelectorAll('.devices button').forEach(button=>button.addEventListener('click',()=>{document.querySelectorAll('.devices button').forEach(other=>other.classList.remove('active'));button.classList.add('active');frame.style.width=button.dataset.size;}));
const requestedTheme=new URLSearchParams(location.search).get('theme');
if(['automotive','industrial','corporate','minimal'].includes(requestedTheme))theme.value=requestedTheme;
loadVersions();
</script></body></html>
+1 -5
View File
@@ -21,11 +21,7 @@ LOADER=Environment(loader=FileSystemLoader(str(ROOT/'themes')),autoescape=select
_ENVS={}
def env_for(theme):
"""Per-theme Jinja env: theme dir first, shared themes/ root as fallback.
Lets a theme override shared macros (_professional_components.html) while
inheriting everything else. Cached per theme; degraded envs are not cached.
"""
"""Per-theme Jinja env: theme dir first, shared themes/ root as fallback."""
if theme not in THEMES:raise ValueError('未知主题')
env=_ENVS.get(theme)
if env is None:
+1 -1
View File
@@ -1,5 +1,5 @@
"""Single source of truth for the SiteForge platform release identity."""
import os
VERSION = "8.31.1-dev.1"
VERSION = "8.32.0-dev.1"
PLATFORM_NAME = "SiteForge Factory"
BUILD_SHA = os.environ.get("SITEFORGE_GIT_SHA", "unknown").strip()[:40] or "unknown"