Files
cms-SiteForge/themes/trade-pro/pages/product.html
T
浅念 85e6d4aeca feat: workspace UX — nav quick-access, content type tabs, list pagination
- 外观与设计: prominent 导航菜单 card linking to editor's siteDesign
- 内容与页面: type tab pills (全部/产品/文章/页面/...) with counts,
  replaces dropdown; content list paginated 20/page with prev/next
- Search resets page; tab click resets page

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

188 lines
9.2 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 非巨型,[slug].astro 全页无 h1) #}
<h2 class="sf-tp-product-title">{{ page.title }}</h2>
<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>