2026-10-28 11:30:19 +08:00
{# trade-pro product detail — cms 产品详情精确对齐重做(Task 12a,重做 10b)。
设计源(唯一权威依据)D:/project/cms/runtimes/astro-site-runtime/src/pages/
[locale]/products/[slug].astro,区块序列逐块对照:
detail-hero(面包屑 + 1.08/0.92 双栏:画廊/标题区,cms 产品名 h2 全页无 h1)
→ product-tabbed( sticky 选项卡:Product Details 正文恒有 + Specifications
参数表有数据才显)→ related-detail( sf-tp-card 卡片网格)→ quote-section
(左联系信息 + 右 CTA/外部表单)。
与 cms 的三处结构决策:
1. 选项卡系统照搬 cms:容器 data-sf-tp-tabbed + .sf-tp-panel/.active 面板 +
尾部内联 < script > 切换 ( 运行时给容器加 sf - tp - tabs - enabled , CSS 只显示
active 面板并隐藏 panel - title )—— 无 JS 时默认形态即 cms 回退形态 : 面板
全部平铺可读 、 panel - title 可见 , 静态可抓取 。
2. 正文不拆表 : WordPress 规格表 < table > 直接在 body HTML 里 , 正文整体作为
Product Details 面板渲染 ( . sf - tp - product - body rich - body 表格样式 : 全宽 /
发丝框 / 斑马纹 / 覆盖内联宽高 / 单元格 div 净空 ); specs 变量有数据时另设
Specifications 面板 ( 干净 spec - table , 每行两对键值 , 从 fields 读 )。
3. 画廊交互复用平台 pro . js 既有钩子 ( . pro - product - media /
. pro - gallery - thumbs / [ data - gallery - image ] ): 缩略图点击换主图 + 主图灯箱 ,
零新增 JS ; 无 JS 时全部图片仍平铺可读 。 无图回退封面 , 再无则占位渐变
( 空值不破版 )。 # }
2026-10-23 10:17:31 +08:00
< section class = "sf-tp-product-hero" >
2026-10-28 11:30:19 +08:00
< div class = "wrap" >
< nav class = "sf-tp-breadcrumb" aria - label = "Breadcrumb" data - reveal >
< a href = "/" > Home < /a><a href="/products/">Products</a><span class="sf-tp-breadcrumb-current">{{ page.title }}</span>
< /nav>
< div class = "sf-tp-product-grid" >
{ # pro . js 经 . pro - gallery - thumbs 的 parentElement 定位 . pro - product - media img ——
缩略图容器必须保持为 . pro - product - media 的兄弟节点 ( 不可外包滚动容器 ) # }
< div class = "sf-tp-product-gallery" data - reveal >
{ % if gallery % }
< div class = "sf-tp-product-media pro-product-media" >
< img class = "sf-tp-product-main-img" src = "{{ gallery[0] }}" alt = "{{ page.title }}" width = "800" height = "600" loading = "eager" decoding = "async" >
< /div>
{ % if gallery | length > 1 % }
< div class = "sf-tp-product-thumbs pro-gallery-thumbs" aria - label = "Product image gallery" >
{ % for photo in gallery % }
< button type = "button" class = "sf-tp-product-thumb" data - gallery - image = "{{ photo }}" aria - label = "View image {{ loop.index }}" aria - pressed = "{{ 'true' if loop.first else 'false' }}" >< img src = "{{ photo }}" alt = "{{ page.title }} image {{ loop.index }}" width = "150" height = "100" loading = "lazy" decoding = "async" >< /button>
{ % endfor % }
< /div>
{ % endif % }
{ % else % }
< div class = "sf-tp-product-media pro-product-media" >
< div class = "sf-tp-photo-plain" aria - hidden = "true" >< span > PRODUCT < /span></div>
< /div>
{ % endif % }
< /div>
< div class = "sf-tp-product-summary" data - reveal style = "--reveal-i: 1" >
< span class = "eyebrow" > PRODUCT DETAILS / {{ fields . get ( 'brand' ) or 'CATALOGUE' }} < /span>
2026-10-28 11:42:51 +08:00
{ # cms 产品名为 h2 量级 ( 2 xl 非巨型 ); 平台全站单 h1 契约
( test_theme_pro_v3 全页 h1 == 1 ) 保留 h1 标签 、 字号用 cms 2 xl # }
< h1 class = "sf-tp-product-title" > {{ page . title }} < /h1>
2026-10-28 11:30:19 +08:00
< p class = "sf-tp-product-lead" > {{ page . summary or 'Detailed product information, specifications and export packaging are published on this page.' }} < /p>
{ % if fields . get ( 'brand' ) or fields . get ( 'model' ) % }
< p class = "sf-tp-product-meta" > { % if fields . get ( 'brand' ) % } < span > Brand · {{ fields . get ( 'brand' ) }} < /span>{% endif %}{% if fields.get('model') %}<span>Model · {{ fields.get('model') }}</span>{% endif %}</p>
{ % endif % }
< div class = "sf-tp-product-actions" >
< a class = "sf-tp-btn" href = "/contact/" > Get a quote {{ icon ( 'arrow-right' ) }} < /a>
< a class = "sf-tp-btn sf-tp-btn--ghost" href = "/products/" > All products {{ icon ( 'arrow-right' ) }} < /a>
< /div>
< /div>
< /div>
< /div>
2026-10-23 10:17:31 +08:00
< /section>
2026-10-28 11:30:19 +08:00
{ # 选项卡详情 ( cms . product - tabbed ): Tab 列表按数据动态生成 ——
Product Details ( 正文 ) 恒有 ; Specifications 仅 specs 有数据才显示 。
cms 的每产品自定义区块 / Features 无平台数据源 , 不进 Tab 。 # }
{ % set tp_spec_rows = specs | rejectattr ( 'label' , 'equalto' , 'gallery' ) | list % }
< div class = "sf-tp-product-tabbed" data - sf - tp - tabbed >
< nav class = "sf-tp-product-tabs" aria - label = "Product details" >
< div class = "wrap sf-tp-tabs-row" >
< button class = "sf-tp-tab active" data - panel = "overview" role = "tab" aria - selected = "true" type = "button" > Product Details < /button>
{ % if tp_spec_rows % }
< button class = "sf-tp-tab" data - panel = "specifications" role = "tab" aria - selected = "false" type = "button" > Specifications < /button>
{ % endif % }
< /div>
< /nav>
< section id = "overview" class = "sf-tp-detail-section sf-tp-panel active" >
< div class = "wrap" >
< h2 class = "sf-tp-panel-title" > Product details < /h2>
< div class = "sf-tp-product-body" > {{ body_copy ( content , page ) }} < /div>
< /div>
2026-10-23 10:17:31 +08:00
< /section>
2026-10-28 11:30:19 +08:00
{ % if tp_spec_rows % }
< section id = "specifications" class = "sf-tp-detail-section sf-tp-detail-section--compact sf-tp-panel" >
< div class = "wrap" >
< h2 class = "sf-tp-panel-title" > Specifications < /h2>
< div class = "sf-tp-table-scroll" >
< table class = "sf-tp-spec-table" >
< tbody >
{ # cms spec - table : 每行两对键值 ( batch ( 2 ) 纵向配对 ) # }
{ % for row in tp_spec_rows | batch ( 2 ) % }
< tr > { % for cell in row % } < th scope = "row" > {{ cell . label }} < /th><td>{{ cell.value }}</td>{% endfor %}</tr>
{ % endfor % }
< /tbody>
< /table>
< /div>
< /div>
2026-10-23 10:17:31 +08:00
< /section>
{ % endif % }
2026-10-28 11:30:19 +08:00
< /div>
< script >
/* 详情选项卡(cms [slug].astro 尾部脚本移植):渐进增强——运行时给容器加
sf-tp-tabs-enabled( CSS 只显示 active 面板并隐藏 panel-title);无 JS 时
面板全部平铺可读。 */
( function () {
var tabbed = document . querySelector ( '[data-sf-tp-tabbed]' );
if ( ! tabbed ) return ;
tabbed . classList . add ( 'sf-tp-tabs-enabled' );
var tabs = Array . prototype . slice . call ( tabbed . querySelectorAll ( '.sf-tp-tab' ));
tabs . forEach ( function ( tab ) {
tab . addEventListener ( 'click' , function () {
tabs . forEach ( function ( t ) {
var on = t === tab ;
t . classList . toggle ( 'active' , on );
t . setAttribute ( 'aria-selected' , on ? 'true' : 'false' );
});
Array . prototype . slice . call ( tabbed . querySelectorAll ( '.sf-tp-panel' )). forEach ( function ( p ) {
p . classList . toggle ( 'active' , p . id === tab . getAttribute ( 'data-panel' ));
});
tab . scrollIntoView ({ inline : 'center' , block : 'nearest' , behavior : 'smooth' });
});
});
})();
</ script >
2026-10-23 10:17:31 +08:00
{% set tp_related = (groups['product'] + groups['vehicle']) | rejectattr('route', 'equalto', page.route) | list %}
{% if tp_related %}
< section class = "section" >
2026-10-28 11:30:19 +08:00
< div class = "wrap" >
< div class = "sf-tp-section-head" data-reveal >
< div >
< span class = "eyebrow" > RELATED PRODUCTS</ span >
< h2 > You may also need</ h2 >
</ div >
< a class = "sf-tp-more" href = "/products/" > Browse all products {{ icon('arrow-right', cls='sf-tp-icon--sm') }}</ a >
</ div >
< div class = "sf-tp-card-grid" >
{% for tp_item in tp_related[:4] %}
< article class = "sf-tp-card" data-reveal style = "--reveal-i: {{ loop.index0 % 4 }}" >
< div class = "sf-tp-card-media" > {{ cover(tp_item, 'product') }}</ div >
< div class = "sf-tp-card-body" >
< h3 > {{ tp_item.title }}</ h3 >
< p > {{ (tp_item.summary or 'Published specifications available on request.').split('.')[0] }}.</ p >
< a class = "sf-tp-more" href = "{{ safe_href(tp_item.route) }}" > View details {{ icon('arrow-right', cls='sf-tp-icon--sm') }}</ a >
</ div >
</ article >
{% endfor %}
</ div >
</ div >
2026-10-23 10:17:31 +08:00
</ section >
{% endif %}
< section class = "section section--soft sf-tp-product-quote" >
2026-10-28 11:30:19 +08:00
< div class = "wrap sf-tp-quote-grid" >
< div class = "sf-tp-quote-info" data-reveal >
< span class = "eyebrow" > REQUEST A QUOTE</ span >
< h2 > Inquire about this product</ h2 >
< p > Send the specifications you need for < strong > {{ page.title }}</ strong > and our trade desk replies with pricing, lead time and export packaging within one business day.</ p >
< div class = "sf-tp-quote-list" >
{% if footer_email %}
< div class = "sf-tp-quote-item" >
< div class = "icon-circle" aria-hidden = "true" > {{ icon('mail') }}</ div >
< div >< strong > Email</ strong >< p >< a href = "mailto:{{ footer_email }}" > {{ footer_email }}</ a ></ p ></ div >
</ div >
{% endif %}
< div class = "sf-tp-quote-item" >
< div class = "icon-circle" aria-hidden = "true" > {{ icon('check') }}</ div >
< div >< strong > Direct sourcing desk</ strong >< p > One contact from inquiry to container loading.</ p ></ div >
</ div >
</ div >
</ div >
< div class = "sf-tp-quote-form" data-reveal style = "--reveal-i: 1" >
{% if form_endpoint %}
< h3 > Get a quote for {{ page.title }}</ h3 >
< form action = "{{ form_endpoint }}" method = "POST" >
< label > Name < input name = "name" autocomplete = "name" required maxlength = "100" ></ label >
< label > Email < input name = "email" type = "email" autocomplete = "email" required maxlength = "180" ></ label >
< label > Message < textarea name = "message" required minlength = "6" maxlength = "5000" placeholder = "I would like a quotation for {{ page.title }}..." ></ textarea ></ label >
< input name = "website" tabindex = "-1" autocomplete = "off" class = "pro-honeypot" aria-hidden = "true" >
< button type = "submit" class = "sf-tp-btn" > Submit enquiry {{ icon('arrow-right') }}</ button >
< p class = "sf-tp-quote-note" > Submissions are handled by the external form service configured by the site administrator.</ p >
</ form >
{% else %}
< h3 > Get a quote for {{ page.title }}</ h3 >
< p > Share your specifications and target market through the contact page; our trade desk replies within one business day.</ p >
< a class = "sf-tp-btn" href = "/contact/" > Contact us {{ icon('arrow-right') }}</ a >
{% endif %}
</ div >
</ div >
2026-10-23 10:17:31 +08:00
</ section >