upgrade: adopt V8.33.1 — final merge, our patches upstreamed natively

V8.33.0 absorbed our entire platform patch set as native features
(discovery/env_for macro override/blocks.css priority/type-mapping fix/
frontend entries/locale override) and synced trade-pro from our remote;
V8.33.1 adds HESU PIPE industrial-theme home fixes (1.3.1).

Zero patch re-application this round — all verified native. Theme
suites 35/35; full run 428 passed + same 10 known Windows env fails
(FAILED set unchanged). Live: container 8.33.1-dev.1 healthy, hesupipe
192 entries intact, trade-pro build 203 pages 0 errors.

Patch registry retired to archive — future upgrades are plain overlay.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-26 14:27:17 +08:00
parent b5996bf2b1
commit e21dbb6518
66 changed files with 2137 additions and 24 deletions
+7 -2
View File
@@ -254,7 +254,9 @@ def render_blocks(blocks, site, page, entries):
elif typ=='features':
items=block.get('items') if isinstance(block.get('items'),list) else []
cards=''.join(f'<article class="sf-feature"><span>{i+1:02}</span><h3>{escape(str(item.get("title","")))}</h3><p>{escape(str(item.get("text","")))}</p></article>' for i,item in enumerate(items[:12]) if isinstance(item,dict))
parts.append(start+f'<div class="sf-section-head"><h2>{title}</h2></div><div class="sf-features">{cards}</div>'+end)
visible_items=sum(1 for item in items[:12] if isinstance(item,dict))
feature_cols=visible_items if 1<=visible_items<=4 else 3
parts.append(start+f'<div class="sf-section-head"><h2>{title}</h2></div><div class="sf-features" style="--sf-feature-cols:{feature_cols}">{cards}</div>'+end)
elif typ=='stats':
items=block.get('items') if isinstance(block.get('items'),list) else []
cards=''.join(f'<div><strong>{escape(str(item.get("value","")))}</strong><span>{escape(str(item.get("label","")))}</span></div>' for item in items[:12] if isinstance(item,dict))
@@ -269,7 +271,10 @@ def render_blocks(blocks, site, page, entries):
bound=[r for r in bound if brand_of(r)==str(page.get('title','')).casefold().strip()]
count=max(1,min(48,int(block.get('limit') or 6)));cols=max(2,min(4,int(block.get('columns') or 3)))
cards=''.join(f'<a class="sf-item" data-category="{escape(str((json.loads(r.get("meta_json") or "{}").get("fields") or {}).get("category", "")),quote=True)}" data-brand="{escape(str((json.loads(r.get("meta_json") or "{}").get("fields") or {}).get("brand", "")),quote=True)}" href="{safe_href(r.get("route") or "/")}"><div class="sf-item-img">'+(f'<img src="{safe_media(r.get("cover"))}" alt="{escape(r.get("title",""),quote=True)}" loading="lazy">' if safe_media(r.get('cover')) else '<div class="sf-placeholder">'+escape(r.get('title','')[:2])+'</div>')+f'</div><div class="sf-item-copy"><small>{escape(r.get("type",""))}</small><h3>{escape(r.get("title",""))}</h3><p>{escape((r.get("summary") or "")[:140])}</p><span>查看详情 →</span></div></a>' for r in bound[:count])
parts.append(start+f'<div class="sf-section-head"><div><span class="sf-kicker">{subtitle}</span><h2>{title}</h2></div><a href="{safe_href(resolve("href","/product/"))}">{escape(resolve("button","查看全部"))} →</a></div><div class="sf-collection" style="--sf-cols:{cols}">{cards if cards else "<p class=\"sf-muted\">当前暂无相关内容,请导入或添加数据。</p>"}</div>'+end)
# Homepage highlights are not catalogues: avoid injecting search/filters before featured cards.
# Other listing pages may opt into filters for sufficiently large result sets.
filterable=(page.get('route')!='/' and (page.get('type') in ('category','brand') or count>=9))
parts.append(start+f'<div class="sf-section-head"><div><span class="sf-kicker">{subtitle}</span><h2>{title}</h2></div><a href="{safe_href(resolve("href","/product/"))}">{escape(resolve("button","查看全部"))}</a></div><div class="sf-collection" data-sf-filterable="{str(filterable).lower()}" style="--sf-cols:{cols}">{cards if cards else "<p class=\"sf-muted\">当前暂无相关内容,请导入或添加数据。</p>"}</div>'+end)
elif typ=='image_text':
im=safe_media(resolve('image')); img=f'<img src="{im}" alt="{title}" loading="lazy">' if im else '<div class="sf-placeholder sf-large">✧</div>'
side=' sf-reverse' if block.get('image_side')=='left' else ''
+11 -1
View File
@@ -269,6 +269,16 @@ def theme_preview_css(theme_id:str,version:str=''):
except ValueError as exc:raise HTTPException(404,str(exc))
return Response((dir/'theme.css').read_text(encoding='utf-8'),media_type='text/css')
@router.get('/themes/{theme_id}/preview.blocks.css')
def theme_preview_blocks_css(theme_id:str,version:str=''):
"""Serve the identical block stylesheet used by a static theme build."""
from fastapi.responses import Response
try:folder=theme_store.resolve(theme_id,version or None)
except ValueError as exc:raise HTTPException(404,str(exc))
chosen=folder/'blocks.css'
if not chosen.is_file():chosen=theme_store.BASE.parent/'app'/'static'/'blocks.css'
return Response(chosen.read_text(encoding='utf-8'),media_type='text/css',headers={'Cache-Control':'no-store'})
@router.get('/themes/{theme_id}/preview.pro.css')
def theme_preview_pro_css(theme_id:str,version:str=''):
from fastapi.responses import Response
@@ -367,6 +377,6 @@ def theme_preview(theme_id:str,version:str='',page_type:str='home'):
seo_description='SiteForge 主题模板预览')
html=static_site.shell(html,site,page,records,[])
html=html.replace('assets/theme.css',f'/api/factory/themes/{theme_id}/preview.css?version={version or theme_store.latest(theme_id)}')
html=html.replace('assets/blocks.css','/ui/blocks.css').replace('assets/factory.css','/ui/factory.css')
html=html.replace('assets/blocks.css',f'/api/factory/themes/{theme_id}/preview.blocks.css?version={version or theme_store.latest(theme_id)}').replace('assets/factory.css','/ui/factory.css')
html=html.replace('assets/custom.css','/ui/factory.css').replace('assets/site-runtime.js','/ui/site-runtime.js')
return HTMLResponse(html)
+1 -1
View File
@@ -540,7 +540,7 @@ def editor_preview(site_id:str,entry_id:str,payload:BlueprintPayload):
styles='\n'.join([
(ROOT/'app'/'static'/'pro-base.css').read_text(encoding='utf-8'),
(ROOT/'app'/'static'/'commercial.css').read_text(encoding='utf-8'),
(ROOT/'app'/'static'/'blocks.css').read_text(encoding='utf-8'),
((theme_dir/'blocks.css') if (theme_dir/'blocks.css').is_file() else (ROOT/'app'/'static'/'blocks.css')).read_text(encoding='utf-8'),
(ROOT/'app'/'static'/'factory.css').read_text(encoding='utf-8'),
(theme_dir/'pro.css').read_text(encoding='utf-8') if (theme_dir/'pro.css').exists() else '',
(theme_dir/'theme.css').read_text(encoding='utf-8'),
+3 -2
View File
@@ -97,7 +97,7 @@ def set_site_locale(site_id:str,data:LocaleIn,request:Request):
enterprise.require(request,site_id,'owner')
try:locale=locale_value(data.locale)
except ValueError as exc:raise HTTPException(400,str(exc))
site=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,))
site=db.one('SELECT settings_json,theme FROM sites WHERE id=?',(site_id,))
if not site:raise HTTPException(404,'网站不存在')
settings=json.loads(site['settings_json'] or '{}')
settings['locale']=locale
@@ -195,12 +195,13 @@ def site_locale(site_id):
row=db.one('SELECT locale FROM language_sites WHERE site_id=?',(site_id,))
if row:return row['locale']
# Per-site locale override (e.g. standalone English sites outside any language group)
site=db.one('SELECT settings_json FROM sites WHERE id=?',(site_id,))
site=db.one('SELECT settings_json,theme FROM sites WHERE id=?',(site_id,))
try:override=json.loads(site['settings_json'] or '{}').get('locale') if site else None
except ValueError:override=None
if override:
try:return locale_value(override)
except ValueError:pass
if site and site['theme']=='trade-pro':return 'en'
return 'zh-CN'
def assert_build_safe(site_id):
+6
View File
@@ -27,3 +27,9 @@
.sf-map-address{font-style:normal;font-size:18px;white-space:pre-line;line-height:1.8}
@media (prefers-reduced-motion:reduce){.sf-slides{scroll-behavior:auto}.sf-item{transition:none}}
@media(max-width:540px){.sf-slide{flex-basis:92%}}
/* V8.33.1 grid-class collision fix: block section and inner collection/features
share a class. The section must NOT become a grid container itself.
Restrict grid layout to its dedicated inner element. */
.sf-section.sf-features,.sf-section.sf-collection,.sf-section.sf-stats,.sf-section.sf-gallery,.sf-section.sf-attributes{display:block;grid-template-columns:none}
.sf-section.sf-faq{max-width:none}
+1 -1
View File
@@ -284,7 +284,7 @@ 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()},
showThemePreview(){const dialog=$('#themePreviewModal');if(!dialog)return;const current=['automotive','industrial','corporate','minimal','trade-pro'].includes(site?.theme)?site.theme:'corporate';$('#themePreviewTheme').value=current;if(!dialog.open)dialog.showModal();Studio.themeDevice('desktop');Studio.updateThemePreview()},
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;
+1 -1
View File
@@ -3,7 +3,7 @@
const $=s=>document.querySelector(s), $$=s=>[...document.querySelectorAll(s)];
const menu=$('.sf-menu-toggle');if(menu){menu.addEventListener('click',()=>{let open=menu.getAttribute('aria-expanded')!=='true';menu.setAttribute('aria-expanded',String(open));$('#sf-primary-links')?.classList.toggle('is-open',open);});}
// Filter actual static content cards, no API is needed at runtime.
$$('.sf-collection').forEach(grid=>{
$$('.sf-collection[data-sf-filterable="true"]').forEach(grid=>{
const cards=$$('.sf-item',grid);
if(cards.length<2)return;
const toolbar=document.createElement('div');toolbar.className='sf-filters';
+3 -2
View File
@@ -1,7 +1,7 @@
/* SiteForge V8.29: presentation-only studio helpers. No persisted-schema changes. */
'use strict';
(function(root){
const themes=new Set(['automotive','industrial','corporate','minimal']);
const themes=new Set(['automotive','industrial','corporate','minimal','trade-pro']);
const pages=new Set(['home','listing','product','article','about','contact']);
const categories={
visual:new Set(['hero','banner','carousel','gallery','image','image_text','image-grid','video','slider']),
@@ -29,7 +29,8 @@
automotive:['#ffffff','#14283d','#f3f6f9','#102e47'],
industrial:['#ffffff','#193f59','#eef5f8','#104a6d'],
corporate:['#ffffff','#262d58','#f4f3fb','#34397b'],
minimal:['#faf9f5','#292924','#f0efe9','#30302c']
minimal:['#faf9f5','#292924','#f0efe9','#30302c'],
'trade-pro':['#ffffff','#17365c','#eef4fc','#0b3d91']
};
function stylePreset(theme,preset){
const [base,ink,soft,dark]=designPalettes[themes.has(theme)?theme:'corporate'];
+1 -1
View File
@@ -55,7 +55,7 @@
</div>
<dialog id="themePreviewModal" class="sf-theme-showcase" aria-labelledby="themePreviewTitle">
<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>
<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><option value="trade-pro">Trade Pro · 外贸 B2B</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>编辑器仅查看外观;完整图库支持交互。预览不会保存或发布主题。<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>
+1 -1
View File
@@ -45,6 +45,6 @@ theme.addEventListener('change',loadVersions);[page,version].forEach(control=>co
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;
if(['automotive','industrial','corporate','minimal','trade-pro'].includes(requestedTheme))theme.value=requestedTheme;
loadVersions();
</script></body></html>
+1 -1
View File
@@ -1,5 +1,5 @@
"""Single source of truth for the SiteForge platform release identity."""
import os
VERSION = "8.32.0-dev.1"
VERSION = "8.33.1-dev.1"
PLATFORM_NAME = "SiteForge Factory"
BUILD_SHA = os.environ.get("SITEFORGE_GIT_SHA", "unknown").strip()[:40] or "unknown"