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))