Task 15: product detail anchor tabs + per-product custom sections
User report: product detail page was wrong — section titles are
per-product custom data (CMS wppop_sections migration: Packing &
Delivery / Our Exhibition / Our Certifications / Our Company / FAQ),
not fixed headings; they must drive an anchor tab system like the
CMS [locale]/products/[slug].astro detailTabs.
Data side (both layers, task option 1 + 2):
- theme_pro.product_detail_sections: fields.sections
[{title, content}] JSON is the structured source; legacy sync
output that appended sections as trailing body H2 blocks is
recognized by known/data titles and split out (overview keeps the
rest; same-title blocks dedupe to the structured version; Features
body block drops when fields.features renders it)
- sync_cms_data.build_product: store sections/features as structured
fields JSON, stop appending H2 blocks to the body; stats now count
entries with a sections field
- Specifications table no longer leaks structural JSON field keys
(sections/features/specs/gallery/categories) — scalar spec keys
only
Template side (trade-pro product.html, replaces 12a panel
switching):
- tab sequence is fully data-driven and matches the CMS: Product
Details (always) -> custom sections (data titles) ->
Specifications / Features (shown only when data exists)
- tabs are #anchor links, not panel toggles: all sections render
flat with visible titles (readable and crawlable without JS; native
anchor jump), JS adds smooth scrolling + scrollspy highlight +
tab centering in the horizontal scroll row
- section ids are slug-derived (sec-packing-delivery), duplicate
titles get an index suffix; features grid ports the CMS
detail-feature-row
Tests: test_trade_pro_product_layout_structure rewritten for the
anchor-tab contract — sections via fields, legacy body-H2 fallback,
spec/features conditional tabs, no-sections product showing only
Product Details + Specifications, spec table free of JSON key leaks,
updated pro.css needles. 30 passed; the 1 failure
(test_trade_pro_blocks_visual_bridge) fails identically on the
previous commit (pre-existing, unrelated).
Co-Authored-By: Claude Code <noreply@example.com>
This commit is contained in:
@@ -1,19 +1,22 @@
|
||||
{# trade-pro product detail — cms 产品详情精确对齐重做(Task 12a,重做 10b)。
|
||||
{# trade-pro product detail — cms 产品详情精确对齐重做(Task 12a,重做 10b);
|
||||
Task 15 选项卡改锚点 Tab + 每产品自定义区块。
|
||||
设计源(唯一权威依据)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
|
||||
→ product-tabbed(sticky 锚点选项卡:Product Details 正文恒有 → 每产品自定义
|
||||
区块 detail_sections(wppop_sections 迁移,标题来自数据)→ Specifications /
|
||||
Features 按数据显隐)→ 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 可见,静态可抓取。
|
||||
1. 选项卡为锚点跳转非面板切换(用户要求,Task 15):Tab 是 #anchor 链接,
|
||||
区块全部平铺渲染 + panel-title 常显——无 JS 时原生锚点跳转、全部区块可读,
|
||||
静态可抓取;JS 渐进增强平滑滚动 + scrollspy 高亮当前 Tab 并在横滚行内居中。
|
||||
2. 正文不拆表:WordPress 规格表 <table> 直接在 body HTML 里,正文整体作为
|
||||
Product Details 面板渲染(.sf-tp-product-body rich-body 表格样式:全宽/
|
||||
Product Details 区块渲染(.sf-tp-product-body rich-body 表格样式:全宽/
|
||||
发丝框/斑马纹/覆盖内联宽高/单元格 div 净空);specs 变量有数据时另设
|
||||
Specifications 面板(干净 spec-table,每行两对键值,从 fields 读)。
|
||||
Specifications 区块(干净 spec-table,每行两对键值,从 fields 读)。
|
||||
自定义区块数据由 theme_pro.product_detail_sections 供给:fields.sections
|
||||
结构化优先,旧同步产物的 body H2 段识别拆出(正文 overview 不重复渲染)。
|
||||
3. 画廊交互复用平台 pro.js 既有钩子(.pro-product-media /
|
||||
.pro-gallery-thumbs / [data-gallery-image]):缩略图点击换主图 + 主图灯箱,
|
||||
零新增 JS;无 JS 时全部图片仍平铺可读。无图回退封面,再无则占位渐变
|
||||
@@ -61,27 +64,47 @@ Specifications 面板(干净 spec-table,每行两对键值,从 fields 读
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{# 选项卡详情(cms .product-tabbed):Tab 列表按数据动态生成——
|
||||
Product Details(正文)恒有;Specifications 仅 specs 有数据才显示。
|
||||
cms 的每产品自定义区块/Features 无平台数据源,不进 Tab。 #}
|
||||
{# 选项卡详情(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 高亮。 #}
|
||||
{% 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>
|
||||
<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 %}
|
||||
{% if tp_spec_rows %}
|
||||
<button class="sf-tp-tab" data-panel="specifications" role="tab" aria-selected="false" type="button">Specifications</button>
|
||||
<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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<section id="overview" class="sf-tp-detail-section sf-tp-panel active">
|
||||
|
||||
<section id="overview" class="sf-tp-detail-section">
|
||||
<div class="wrap">
|
||||
<h2 class="sf-tp-panel-title">Product details</h2>
|
||||
<div class="sf-tp-product-body">{{ body_copy(content, page) }}</div>
|
||||
<div class="sf-tp-product-body">{{ body_copy(detail_overview, page) }}</div>
|
||||
</div>
|
||||
</section>
|
||||
{% 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 %}
|
||||
{% if tp_spec_rows %}
|
||||
<section id="specifications" class="sf-tp-detail-section sf-tp-detail-section--compact sf-tp-panel">
|
||||
<section id="specifications" class="sf-tp-detail-section sf-tp-detail-section--compact">
|
||||
<div class="wrap">
|
||||
<h2 class="sf-tp-panel-title">Specifications</h2>
|
||||
<div class="sf-tp-table-scroll">
|
||||
@@ -97,29 +120,64 @@ cms 的每产品自定义区块/Features 无平台数据源,不进 Tab。 #}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
</div>
|
||||
<script>
|
||||
/* 详情选项卡(cms [slug].astro 尾部脚本移植):渐进增强——运行时给容器加
|
||||
sf-tp-tabs-enabled(CSS 只显示 active 面板并隐藏 panel-title);无 JS 时
|
||||
面板全部平铺可读。 */
|
||||
/* 锚点选项卡(Task 15,cms [slug].astro 尾部脚本重做):点击平滑滚动到区块
|
||||
并在横滚行内居中;scrollspy 滚动高亮当前 Tab。无 JS 时为原生锚点跳转,
|
||||
区块全部平铺可读。 */
|
||||
(function () {
|
||||
var tabbed = document.querySelector('[data-sf-tp-tabbed]');
|
||||
if (!tabbed) return;
|
||||
tabbed.classList.add('sf-tp-tabs-enabled');
|
||||
var nav = tabbed.querySelector('.sf-tp-product-tabs');
|
||||
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;
|
||||
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;
|
||||
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' });
|
||||
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);
|
||||
});
|
||||
});
|
||||
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;
|
||||
});
|
||||
if (idx !== current) { current = idx; activate(idx, true); }
|
||||
}
|
||||
window.addEventListener('scroll', spy, { passive: true });
|
||||
spy();
|
||||
})();
|
||||
</script>
|
||||
{% set tp_related = (groups['product'] + groups['vehicle']) | rejectattr('route', 'equalto', page.route) | list %}
|
||||
|
||||
@@ -104,8 +104,9 @@
|
||||
.sf-theme-trade-pro .sf-tp-cta h2{margin-block-end:var(--sf-tp-space-2);color:var(--sf-tp-color-text-inverse)}
|
||||
.sf-theme-trade-pro .sf-tp-cta p{color:color-mix(in srgb,var(--sf-tp-color-text-inverse) 83%,transparent);margin:0}
|
||||
|
||||
/* ── 产品详情(Task 12a,[slug].astro 精确对齐重做 10b):hero 画廊/标题 +
|
||||
sticky 选项卡(无 JS 平铺回退)+ rich-body 正文表格 + spec-table + 询盘。
|
||||
/* ── 产品详情(Task 12a,[slug].astro 精确对齐重做 10b;Task 15 选项卡改
|
||||
锚点 Tab + 自定义区块/特性栅格):hero 画廊/标题 + sticky 锚点选项卡 +
|
||||
rich-body 正文表格 + spec-table + detail-features + 询盘。
|
||||
断点映射:cms 900 与 960(询盘堆叠)→ 900;cms 620 → 620。 ── */
|
||||
.sf-theme-trade-pro .sf-tp-product-hero{padding-block:calc(var(--sf-tp-space-8) + var(--sf-tp-space-4)) var(--sf-tp-space-8)}
|
||||
.sf-theme-trade-pro .sf-tp-breadcrumb{display:flex;flex-wrap:wrap;align-items:center;color:var(--sf-tp-color-text-muted);font-size:var(--sf-tp-font-size-sm);margin-block-end:var(--sf-tp-space-6)}
|
||||
@@ -137,13 +138,13 @@
|
||||
/* Tab 单行横滚(数量多不换行);隐藏滚动条 + scroll-snap 贴合(cms .tabs-row) */
|
||||
.sf-theme-trade-pro .sf-tp-tabs-row{display:flex;gap:var(--sf-tp-space-6);overflow-x:auto;scroll-behavior:smooth;scrollbar-width:none;scroll-snap-type:x proximity}
|
||||
.sf-theme-trade-pro .sf-tp-tabs-row::-webkit-scrollbar{display:none}
|
||||
.sf-theme-trade-pro .sf-tp-tab{flex:0 0 auto;white-space:nowrap;padding:var(--sf-tp-space-4) var(--sf-tp-space-1);color:var(--sf-tp-color-text-secondary);font-weight:var(--sf-tp-font-weight-semibold);font-size:var(--sf-tp-font-size-sm);background:none;border:none;border-block-end:var(--sf-tp-border-width-strong) solid transparent;cursor:pointer;scroll-snap-align:center}
|
||||
.sf-theme-trade-pro .sf-tp-tab{flex:0 0 auto;white-space:nowrap;padding:var(--sf-tp-space-4) var(--sf-tp-space-1);color:var(--sf-tp-color-text-secondary);font-weight:var(--sf-tp-font-weight-semibold);font-size:var(--sf-tp-font-size-sm);background:none;border:none;border-block-end:var(--sf-tp-border-width-strong) solid transparent;cursor:pointer;scroll-snap-align:center;text-decoration:none}
|
||||
.sf-theme-trade-pro .sf-tp-tab.active,.sf-theme-trade-pro .sf-tp-tab:hover{color:var(--sf-tp-color-text-link);border-block-end-color:var(--sf-tp-color-border-brand)}
|
||||
/* 面板切换(cms [data-tabbed].tabs-enabled):JS 给容器加 sf-tp-tabs-enabled 后
|
||||
仅显示激活面板并隐藏 panel-title;无 JS 时面板全部平铺可读、标题可见 */
|
||||
.sf-theme-trade-pro .sf-tp-product-tabbed.sf-tp-tabs-enabled .sf-tp-panel{display:none}
|
||||
.sf-theme-trade-pro .sf-tp-product-tabbed.sf-tp-tabs-enabled .sf-tp-panel.active{display:block}
|
||||
.sf-theme-trade-pro .sf-tp-product-tabbed.sf-tp-tabs-enabled .sf-tp-panel-title{display:none}
|
||||
.sf-theme-trade-pro .sf-tp-tab:focus-visible{outline:2px solid var(--sf-tp-color-border-brand);outline-offset:-2px}
|
||||
/* 锚点选项卡(Task 15 重做 12a 面板切换):Tab 是页内锚点链接,区块全部平铺
|
||||
渲染、panel-title 常显(无 JS 原生跳转也可读);JS 平滑滚动 + scrollspy
|
||||
高亮当前 Tab(.sf-tp-tab.active)。区块锚点滚动让位 sticky 条见下方
|
||||
.sf-tp-detail-section 的 scroll-margin-top。 */
|
||||
/* 详情面板(cms .detail-section):区块距 + 锚点滚动让位 sticky 选项卡;
|
||||
区块标题品牌色 3px 下划短线(cms .detail-section > .wrap > h2) */
|
||||
.sf-theme-trade-pro .sf-tp-detail-section{padding-block:calc(var(--sf-tp-space-10) + var(--sf-tp-space-1));scroll-margin-top:calc(var(--sf-tp-space-16) + var(--sf-tp-space-16))}
|
||||
@@ -165,6 +166,13 @@
|
||||
.sf-theme-trade-pro .sf-tp-spec-table{width:100%;border-collapse:collapse;font-size:var(--sf-tp-font-size-sm)}
|
||||
.sf-theme-trade-pro .sf-tp-spec-table th,.sf-theme-trade-pro .sf-tp-spec-table td{border:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-default);padding:calc(var(--sf-tp-space-4) - var(--sf-tp-space-1)) var(--sf-tp-space-4);text-align:start}
|
||||
.sf-theme-trade-pro .sf-tp-spec-table th{background:var(--sf-tp-color-surface-muted);font-weight:var(--sf-tp-font-weight-semibold)}
|
||||
/* 特性栅格(cms .detail-feature-row,fields.features 数据驱动):品牌色星标 +
|
||||
标题/描述两行卡;auto-fit 自适应条目数(cms 固定 4 列,数据条目不定) */
|
||||
.sf-theme-trade-pro .sf-tp-detail-features{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--sf-tp-space-3) var(--sf-tp-space-6)}
|
||||
.sf-theme-trade-pro .sf-tp-detail-feature{display:grid;grid-template-columns:auto 1fr;gap:var(--sf-tp-space-3);align-items:start;padding-block:var(--sf-tp-space-2)}
|
||||
.sf-theme-trade-pro .sf-tp-detail-feature>span{color:var(--sf-tp-color-text-brand);font-size:var(--sf-tp-font-size-xl);line-height:var(--sf-tp-line-height-tight)}
|
||||
.sf-theme-trade-pro .sf-tp-detail-feature h3{font-size:var(--sf-tp-font-size-md);margin-block-end:var(--sf-tp-space-1)}
|
||||
.sf-theme-trade-pro .sf-tp-detail-feature p{margin:0;color:var(--sf-tp-color-text-muted);font-size:var(--sf-tp-font-size-sm)}
|
||||
/* 询盘板块(cms quote-grid 同构):左联系信息 + 右表单卡 */
|
||||
.sf-theme-trade-pro .sf-tp-quote-grid{display:grid;grid-template-columns:0.85fr 1.15fr;gap:calc(var(--sf-tp-space-12) + var(--sf-tp-space-4));align-items:start}
|
||||
.sf-theme-trade-pro .sf-tp-quote-info>p{color:var(--sf-tp-color-text-muted)}
|
||||
|
||||
Reference in New Issue
Block a user