2026-10-29 13:49:08 +08:00
{# trade-pro product detail — cms 产品详情精确对齐重做(Task 12a,重做 10b);
Task 15 选项卡改锚点 Tab + 每产品自定义区块。
2026-10-28 11:30:19 +08:00
设计源(唯一权威依据)D:/project/cms/runtimes/astro-site-runtime/src/pages/
[locale]/products/[slug].astro,区块序列逐块对照:
detail-hero(面包屑 + 1.08/0.92 双栏:画廊/标题区,cms 产品名 h2 全页无 h1)
2026-10-29 13:49:08 +08:00
→ product-tabbed( sticky 锚点选项卡:Product Details 正文恒有 → 每产品自定义
区块 detail_sections( wppop_sections 迁移,标题来自数据)→ Specifications /
Features 按数据显隐)→ related-detail( sf-tp-card 卡片网格)→ quote-section
2026-10-28 11:30:19 +08:00
(左联系信息 + 右 CTA/外部表单)。
与 cms 的三处结构决策:
2026-10-29 13:49:08 +08:00
1. 选项卡为锚点跳转非面板切换(用户要求,Task 15):Tab 是 #anchor 链接,
区块全部平铺渲染 + panel-title 常显——无 JS 时原生锚点跳转、全部区块可读,
静态可抓取;JS 渐进增强平滑滚动 + scrollspy 高亮当前 Tab 并在横滚行内居中。
2026-10-28 11:30:19 +08:00
2. 正文不拆表:WordPress 规格表 < table > 直接在 body HTML 里,正文整体作为
2026-10-29 13:49:08 +08:00
Product Details 区块渲染(.sf-tp-product-body rich-body 表格样式:全宽/
2026-10-28 11:30:19 +08:00
发丝框/斑马纹/覆盖内联宽高/单元格 div 净空);specs 变量有数据时另设
2026-10-29 13:49:08 +08:00
Specifications 区块(干净 spec-table,每行两对键值,从 fields 读)。
自定义区块数据由 theme_pro.product_detail_sections 供给:fields.sections
结构化优先,旧同步产物的 body H2 段识别拆出(正文 overview 不重复渲染)。
2026-10-28 11:30:19 +08:00
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 量级(2xl 非巨型);平台全站单 h1 契约
( test_theme_pro_v3 全页 h1==1)保留 h1 标签、字号用 cms 2xl #}
< 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-29 13:49:08 +08:00
{# 选项卡详情(cms .product-tabbed, Task 15 改锚点 Tab):Tab 序列完全数据
驱动,对齐 cms detailTabs——Product Details(正文)恒有 → 每产品自定义区块
detail_sections( CMS wppop_sections 迁移:Packing & Delivery / Our Exhibition /
Our Certifications / Our Company / FAQ,标题来自数据不固定;theme_pro 从
fields.sections 读、旧同步产物从 body H2 段识别拆出)→ Specifications( specs
有数据才显)→ Features( fields.features 有数据才显)。
Tab 是锚点跳转非面板切换(用户要求 + 区块全部平铺渲染):无 JS 时原生锚点
跳转 + 全部区块可读、标题可见,静态可抓取;JS 平滑滚动 + scrollspy 高亮。 #}
2026-10-28 11:30:19 +08:00
{% 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" >
2026-10-29 13:49:08 +08:00
< a class = "sf-tp-tab active" href = "#overview" role = "tab" aria-selected = "true" > Product Details</ a >
{% for s in detail_sections %}
< a class = "sf-tp-tab" href = "#{{ s.id }}" role = "tab" aria-selected = "false" > {{ s.title }}</ a >
{% endfor %}
2026-10-28 11:30:19 +08:00
{% if tp_spec_rows %}
2026-10-29 13:49:08 +08:00
< a class = "sf-tp-tab" href = "#specifications" role = "tab" aria-selected = "false" > Specifications</ a >
{% endif %}
{% if detail_features %}
< a class = "sf-tp-tab" href = "#features" role = "tab" aria-selected = "false" > Features</ a >
2026-10-28 11:30:19 +08:00
{% endif %}
</ div >
</ nav >
2026-10-29 13:49:08 +08:00
< section id = "overview" class = "sf-tp-detail-section" >
2026-10-28 11:30:19 +08:00
< div class = "wrap" >
< h2 class = "sf-tp-panel-title" > Product details</ h2 >
2026-10-29 13:49:08 +08:00
< div class = "sf-tp-product-body" > {{ body_copy(detail_overview, page) }}</ div >
2026-10-28 11:30:19 +08:00
</ div >
2026-10-23 10:17:31 +08:00
</ section >
2026-10-29 13:49:08 +08:00
{% for s in detail_sections %}
< section id = "{{ s.id }}" class = "sf-tp-detail-section" >
< div class = "wrap" >
< h2 class = "sf-tp-panel-title" > {{ s.title }}</ h2 >
< div class = "rich-body sf-tp-product-body" > {{ s.content }}</ div >
</ div >
</ section >
{% endfor %}
2026-10-28 11:30:19 +08:00
{% if tp_spec_rows %}
2026-10-29 13:49:08 +08:00
< section id = "specifications" class = "sf-tp-detail-section sf-tp-detail-section--compact" >
2026-10-28 11:30:19 +08:00
< 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-29 13:49:08 +08:00
{% if detail_features %}
< section id = "features" class = "sf-tp-detail-section sf-tp-detail-section--compact" >
< div class = "wrap" >
< h2 class = "sf-tp-panel-title" > Features</ h2 >
< div class = "sf-tp-detail-features" >
{% for f in detail_features %}
< div class = "sf-tp-detail-feature" >
< span aria-hidden = "true" > ✦</ span >
< div >< h3 > {{ f.title }}</ h3 >< p > {{ f.desc }}</ p ></ div >
</ div >
{% endfor %}
</ div >
</ div >
</ section >
{% endif %}
2026-10-28 11:30:19 +08:00
</ div >
< script >
2026-10-29 13:49:08 +08:00
/* 锚点选项卡(Task 15, cms [slug].astro 尾部脚本重做):点击平滑滚动到区块
并在横滚行内居中;scrollspy 滚动高亮当前 Tab。无 JS 时为原生锚点跳转,
区块全部平铺可读。 */
2026-10-28 11:30:19 +08:00
( function () {
var tabbed = document . querySelector ( '[data-sf-tp-tabbed]' );
if ( ! tabbed ) return ;
2026-10-29 13:49:08 +08:00
var nav = tabbed . querySelector ( '.sf-tp-product-tabs' );
2026-10-28 11:30:19 +08:00
var tabs = Array . prototype . slice . call ( tabbed . querySelectorAll ( '.sf-tp-tab' ));
2026-10-29 13:49:08 +08:00
var targets = tabs . map ( function ( t ) {
return document . getElementById (( t . getAttribute ( 'href' ) || '' ). slice ( 1 ));
});
function activate ( i , center ) {
tabs . forEach ( function ( t , j ) {
var on = i === j ;
2026-10-28 11:30:19 +08:00
t . classList . toggle ( 'active' , on );
t . setAttribute ( 'aria-selected' , on ? 'true' : 'false' );
});
2026-10-29 13:49:08 +08:00
if ( center && tabs [ i ] && tabs [ i ]. scrollIntoView ) {
tabs [ i ]. scrollIntoView ({ inline : 'center' , block : 'nearest' , behavior : 'smooth' });
}
}
tabs . forEach ( function ( tab , i ) {
tab . addEventListener ( 'click' , function ( e ) {
var target = targets [ i ];
if ( ! target ) return ;
e . preventDefault ();
target . scrollIntoView ({ behavior : 'smooth' , block : 'start' });
activate ( i , true );
2026-10-28 11:30:19 +08:00
});
});
2026-10-29 13:49:08 +08:00
var current = - 1 ;
function spy () {
var mark = ( nav ? nav . getBoundingClientRect (). bottom : 0 ) + 32 ;
var idx = 0 ;
targets . forEach ( function ( el , i ) {
if ( el && el . getBoundingClientRect (). top <= mark ) idx = i ;
2026-10-28 11:30:19 +08:00
});
2026-10-29 13:49:08 +08:00
if ( idx !== current ) { current = idx ; activate ( idx , true ); }
}
window . addEventListener ( 'scroll' , spy , { passive : true });
spy ();
2026-10-28 11:30:19 +08:00
})();
</ 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 >