Files
cms-SiteForge/themes/trade-pro/pages/product.html
T
浅念 5e8ae60dfe feat: rebuild product detail page to match cms design (tabs/attrs/spec-table)
Task 12a: rebuild product detail to precisely match cms [slug].astro - sticky tab nav (muted bar, brand underline, single-row h-scroll) with .sf-tp-panel/.active switching via inline script (no-JS renders all panels flat); Product Details panel renders body HTML as rich-body with cms table rules (full-width, hairline borders, zebra, inline width/height override, cell div scrub); Specifications panel shows clean spec-table with two key/value pairs per row when specs exist; panel titles get the cms 3px brand underline. Breakpoints mapped cms 900/960 -> platform 900, cms 620 -> 620; product responsive rules consolidated in the 12a block and removed from the shared responsive block. Product title keeps h1 tag at cms h2 scale (2xl) per platform single-h1 contract. Note: most product.html and test edits were swept into concurrent commit 85e6d4a; this commit carries the pro.css 12a block and the h1 refinements.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-28 11:42:51 +08:00

189 lines
9.3 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{# 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 时全部图片仍平铺可读。无图回退封面,再无则占位渐变
(空值不破版)。 #}
<section class="sf-tp-product-hero">
<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>
{# cms 产品名为 h2 量级(2xl 非巨型);平台全站单 h1 契约
(test_theme_pro_v3 全页 h1==1)保留 h1 标签、字号用 cms 2xl #}
<h1 class="sf-tp-product-title">{{ page.title }}</h1>
<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>
</section>
{# 选项卡详情(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>
</section>
{% 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>
</section>
{% endif %}
</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>
{% set tp_related = (groups['product'] + groups['vehicle']) | rejectattr('route', 'equalto', page.route) | list %}
{% if tp_related %}
<section class="section">
<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>
</section>
{% endif %}
<section class="section section--soft sf-tp-product-quote">
<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>
</section>