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>
This commit is contained in:
浅念
2026-10-28 11:30:19 +08:00
parent df2a183dbf
commit 85e6d4aeca
3 changed files with 242 additions and 150 deletions
+175 -133
View File
@@ -1,145 +1,187 @@
{# trade-pro product detail — cms 产品详情设计移植(Task 10b)。
区块序列对照 D:/project/cms/runtimes/astro-site-runtime/src/pages/[locale]/products/[slug].astro
(像素基准 dist/hesupipe/en/products/alloy-resin-system-window/):
面包屑 → 画廊(主图 + 单行横滚缩略图)+ 标题区(眉题/h1/导语/品牌行/
CTA 对)→ 正文 Product Details → 参数表 Specifications(fields 键值,
空则整段隐藏)→ 相关产品(卡片网格)→ 询盘板块(左联系信息 + 右 CTA/
外部表单)。
与 cms 的两处结构决策:
1. Tab 导航平铺:cms 的 Tab 序列承载每产品自定义区块,平台正文是单一
富文本 + 参数表,仅两个面板不值得引入 JS Tab;cms 无 JS 回退形态
本就是面板平铺,这里固定为平铺(无 JS 依赖、静态可抓取)。
2. 画廊交互复用平台 pro.js 既有钩子(.pro-product-media /
.pro-gallery-thumbs / [data-gallery-image]):缩略图点击换主图 +
主图灯箱,零新增 JS;无 JS 时全部图片仍平铺可读。无图回退封面,
再无则占位渐变(空值不破版)。 #}
{# 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>
<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>
<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>
<section class="section sf-tp-product-overview">
<div class="wrap">
<span class="eyebrow">OVERVIEW</span>
<h2 class="sf-tp-panel-title">Product details</h2>
<div class="sf-tp-product-body" data-reveal>{{ body_copy(content, page) }}</div>
</div>
{# 选项卡详情(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 specs %}
<section class="section section--soft sf-tp-product-specs">
<div class="wrap">
<span class="eyebrow">SPECIFICATIONS</span>
<h2 class="sf-tp-panel-title">Specifications</h2>
<div class="sf-tp-table-scroll" data-reveal>
<table class="sf-tp-spec-table">
<tbody>
{# theme_pro excludes management fields; 'gallery' is a media list, not a spec #}
{% for row in specs if row.label != 'gallery' %}
<tr><th scope="row">{{ row.label }}</th><td>{{ row.value }}</td></tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% 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>
<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>
<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>