feat: trust_bar + applications_band blocks; homepage now block-driven

- blueprints: register trust_bar (icon/title/text items) and applications_band
  (2x2 dark-band tiles) block types; render with escaped items, ordinal icon
  fallback, 8-item cap; auto-exposed in editor via /api/editor/blocks and
  grouped under business in the studio palette
- blocks.css: platform base layer for both blocks (app/static + trade-pro
  Layer A kept verbatim in sync) + trade-pro bridge vocabulary (icon-circle,
  hairline dividers, navy->brand gradient band, glass tiles, 900/620
  breakpoints, reduced-motion) without touching canvas hooks
- tests: test_trade_pro_home_block_driven covers the full studio-draft
  PUT -> publish -> build chain; 8 block types on the homepage, template
  markers absent under layout_mode=replace, clean build report
- scripts/setup_home_blocks.py: production write path for site 483295c29ea0
  (login -> draft -> publish -> build -> verify); real catalogue counts feed
  the stats block; run after the Docker image picks up the new block types

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-28 18:11:32 +08:00
parent c4d9b1075c
commit 45c39b1695
6 changed files with 405 additions and 1 deletions
+13
View File
@@ -27,6 +27,8 @@ BLOCKS = {
'pricing':{'name':'服务方案','icon':'$','fields':['title','items'],'defaults':{'title':'服务方案','items':[{'name':'标准方案','price':'联系我们','features':['专业咨询','售后服务'],'href':'/contact/'}]}},
'map':{'name':'位置导航','icon':'⌖','fields':['title','address','url'],'defaults':{'title':'欢迎来访','address':'请填写实际办公地址','url':''}},
'form_link':{'name':'询盘表单入口','icon':'✉','fields':['title','description','button','href'],'defaults':{'title':'提交询盘','description':'欢迎联系我们获取报价和资料','button':'联系销售','href':'/contact/'}},
'trust_bar':{'name':'信任条','icon':'✓','fields':['title','items'],'defaults':{'title':'','items':[{'icon':'✓','title':'Certified quality','text':'Inspected before every shipment'},{'icon':'◎','title':'Global export','text':'Delivering to worldwide markets'},{'icon':'▤','title':'Full documentation','text':'Specs and export files on hand'},{'icon':'✉','title':'Responsive service','text':'Replies within one business day'}]}},
'applications_band':{'name':'应用展示带','icon':'⊞','fields':['title','subtitle','items'],'defaults':{'title':'Built for real-world projects','subtitle':'APPLICATIONS','items':[{'icon':'◉','title':'Water supply','text':'Municipal piping and treatment networks.'},{'icon':'◍','title':'Drainage','text':'Gravity and pressure sewer systems.'},{'icon':'✦','title':'Gas','text':'PE pipelines for fuel gas distribution.'},{'icon':'◆','title':'Mining','text':'Abrasion-resistant slurry and vent lines.'}]}},
}
# Property contracts shared with the no-code editor. Unknown executable types
# are not supported; all values are still verified by validate_blocks.
@@ -308,6 +310,17 @@ def render_blocks(blocks, site, page, entries):
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=='trust_bar':
# Horizontal trust strip; item icons are free-text glyphs, empty ones
# fall back to ordinal markers like the features block.
items=block.get('items') if isinstance(block.get('items'),list) else []
cells=''.join(f'<div class="sf-trust-item"><span class="sf-trust-icon" aria-hidden="true">{escape(str(item.get("icon") or (i+1)))}</span><strong>{escape(str(item.get("title","")))}</strong><span class="sf-trust-text">{escape(str(item.get("text","")))}</span></div>' for i,item in enumerate(items[:8]) if isinstance(item,dict))
head=f'<div class="sf-section-head"><h2>{title}</h2></div>' if title else ''
parts.append(start+head+f'<div class="sf-trust-bar">{cells}</div>'+end)
elif typ=='applications_band':
items=block.get('items') if isinstance(block.get('items'),list) else []
tiles=''.join(f'<div class="sf-app-tile"><span class="sf-app-icon" aria-hidden="true">{escape(str(item.get("icon") or (i+1)))}</span><h3>{escape(str(item.get("title","")))}</h3><p>{escape(str(item.get("text","")))}</p></div>' for i,item in enumerate(items[:8]) if isinstance(item,dict))
parts.append(start+f'<div class="sf-section-head"><div><span class="sf-kicker">{subtitle}</span><h2>{title}</h2></div></div><div class="sf-applications-band">{tiles}</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))
+20
View File
@@ -69,3 +69,23 @@
@media(prefers-reduced-motion:reduce){.sf-hero-track{scroll-behavior:auto}}
.sf-hero-panel .sf-hero-copy h2{font-size:clamp(38px,5vw,76px);font-weight:790;letter-spacing:-.052em}
/* Trust bar: horizontal credibility strip under the hero (icon + title + note). */
.sf-section.sf-trust_bar{padding:38px 0}
.sf-trust-bar{display:grid;grid-template-columns:repeat(var(--sf-trust-cols,4),minmax(0,1fr))}
.sf-trust-item{display:flex;flex-direction:column;align-items:center;text-align:center;gap:6px;padding:10px 20px;border-inline-end:1px solid #dce3eb}
.sf-trust-item:last-child{border-inline-end:0}
.sf-trust-icon{display:grid;place-items:center;width:46px;height:46px;border-radius:50%;background:#eef2f8;color:var(--accent,#315ee2);font-size:18px;font-weight:800;margin-bottom:6px}
.sf-trust-item strong{font-size:15px}
.sf-trust-text{font-size:13px;opacity:.68;line-height:1.55}
@media(max-width:820px){.sf-trust-bar{grid-template-columns:repeat(2,minmax(0,1fr));row-gap:24px}.sf-trust-item:nth-child(2n){border-inline-end:0}}
@media(max-width:540px){.sf-trust-bar{grid-template-columns:minmax(0,1fr)}.sf-trust-item{border-inline-end:0}}
/* Applications band: dark gradient strip with a 2x2 tile grid. */
.sf-section.sf-applications_band{background:linear-gradient(115deg,#101828,#1d3a6e 78%);color:#f5f7fa}
.sf-applications-band{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
.sf-app-tile{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.14);border-radius:10px;padding:26px 24px}
.sf-app-icon{display:grid;place-items:center;width:44px;height:44px;border-radius:50%;background:rgba(255,255,255,.12);font-size:17px;font-weight:800;margin-bottom:12px}
.sf-app-tile h3{font-size:20px;margin:0 0 6px}
.sf-app-tile p{font-size:14px;line-height:1.65;opacity:.75;margin:0}
@media(max-width:540px){.sf-applications-band{grid-template-columns:minmax(0,1fr)}}
+1 -1
View File
@@ -5,7 +5,7 @@
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']),
business:new Set(['collection','products','product_list','brand_wall','brands','features','stats','testimonials','faq','contact','form','cta','map']),
business:new Set(['collection','products','product_list','brand_wall','brands','features','stats','testimonials','faq','contact','form','cta','map','trust_bar','applications_band']),
advanced:new Set(['layout','columns','tabs','accordion','html-embed','embed','spacer','divider'])
};
function categoryOf(key){for(const [name,ids] of Object.entries(categories))if(ids.has(key))return name;return 'basic'}