2026-10-23 14:06:15 +08:00
{# trade-pro listing / category — cms 列表设计移植(Task 10c)。
设计源 D:/project/cms/runtimes/astro-site-runtime/src/pages/[locale]/:
- products/[...page].astro: PageHero + 260px 产品分类侧栏(sticky) +
SectionHead + 3 列卡片栅格(CardGrid cols-3) + 窗口式分页
- news/[...page].astro: PageHero + 1fr/310px 双栏;列表列为横向新闻行
( 290px 封面 + tag/标题/摘要/Read more),侧栏三盒
( Latest / Popular / Categories 计数)
与 cms 的结构决策:
1. kind 分支按列表内容类型:post 类(/news/、/post/ 等文章集合)用
新闻行形态;其余(product/vehicle/brand/case/service)复用产品卡片
栅格——cms 只定义了这两种列表设计,品牌/案例/服务映射到产品卡形态
2026-10-23 14:26:34 +08:00
(与 brand.html 相关产品、product.html 相关产品一致)。非空列表只信
首项内容类型;路由子串仅作空列表 fallback 且排除 category(否则
/categories/post-sales-service/ 这类 slug 会被误判成新闻分支)。
2026-10-23 14:06:15 +08:00
2. 产品分类侧栏仅产品类列表渲染(侧栏链接是产品分类语义,出现在
/brands/ 等页面会误导);文章分支的侧栏是 cms 新闻三盒。空分类
数据时侧栏整段隐藏,主列变单栏。
3. cms news 页的 Tab 行(All/Company News/...)与 products 页 hero 的
“Explore Products”锚点按钮是内容数据驱动 / 页内锚点跳转,平台无
对应数据源与目标锚点,不移植;筛选面板承担列表交互入口。
4. 平台筛选契约(test_theme_pro_v3 对所有主题 listing 断言)保留:
filters() 宏输出 .pro-filter-panel 且必须保持为 .pro-filter-scope
的紧邻前兄弟(pro.js 用 previousElementSibling 定位面板);卡片与
新闻行都携带 .pro-card + data-brand/data-category( pro.js 过滤的
数据源与零结果空态都依赖它们)。
5. 分页器是 cms Pagination 的窗口式页码(首尾必现 + 当前±2,其余折叠
为省略号),上一页/下一页用 arrow-left/arrow-right SVG( Task 9 图标
契约:交互箭头一律 SVG,无裸箭头字符);页码 URL 与构建期静态分页
路由一致(第 1 页 = 栏目根,形如 /products/page/2/ 递增)。
6. 空列表:筛选、栅格、分页整段隐藏,替换为友好空态提示(cms 站点
恒有内容,平台必须处理 noindex 栏目占位页的空集)。 #}
2026-10-23 14:26:34 +08:00
{% set tp_post_like = (listing[0].type == 'post') if listing else (kind == 'listing' and ('news' in route or 'post' in route)) %}
2026-10-23 14:06:15 +08:00
{% set tp_eyebrow = 'NEWS & INSIGHTS' if tp_post_like else 'PRODUCT CATALOGUE' %}
{% set tp_hero_img = safe_image(page.cover) %}
{% set tp_lead = page.summary or ('Industry updates, project stories and exhibition news from our team.' if tp_post_like else 'Browse the full catalogue with specifications, brands and categories.') %}
{% set pg_root = (route.split('/page/')[0]).rstrip('/') ~ '/' %}
{# 分页导航宏:显式传参(pg/base/icon),不依赖模板上下文闭包 #}
{% macro tp_pages(pg, base, icon) -%}
{% if pg and pg.pages > 1 %}
{% set cur = pg.page %}{% set last = pg.pages %}{% set win = [] %}
{% for i in range(1, last + 1) %}
{% if i == 1 or i == last or (i - cur)|abs < = 2 %}{% if win.append(i) %}{% endif %}
{% elif win and win[-1] != '…' %}{% if win.append('…') %}{% endif %}{% endif %}
{% endfor %}
< nav class = "sf-tp-pagination" aria-label = "Pagination" >
{% if pg.previous %}< a class = "sf-tp-page-arrow" href = "{{ pg.previous }}" rel = "prev" aria-label = "Previous page" > {{ icon('arrow-left', size='16') }}</ a >
{% else %}< span class = "sf-tp-page-arrow is-disabled" aria-hidden = "true" > {{ icon('arrow-left', size='16') }}</ span > {% endif %}
{% for n in win %}{% if n == '…' %}< span class = "sf-tp-page-dots" > …</ span >
{% elif n == cur %}< span class = "sf-tp-page-num is-active" aria-current = "page" > {{ n }}</ span >
{% else %}< a class = "sf-tp-page-num" href = "{{ base if n == 1 else base ~ 'page/' ~ n ~ '/' }}" > {{ n }}</ a > {% endif %}{% endfor %}
{% if pg.next %}< a class = "sf-tp-page-arrow" href = "{{ pg.next }}" rel = "next" aria-label = "Next page" > {{ icon('arrow-right', size='16') }}</ a >
{% else %}< span class = "sf-tp-page-arrow is-disabled" aria-hidden = "true" > {{ icon('arrow-right', size='16') }}</ span > {% endif %}
</ nav >
{% endif %}
{%- endmacro %}
< section class = "sf-tp-page-hero" >
{% if tp_hero_img %}< img class = "sf-tp-page-hero-bg" src = "{{ tp_hero_img }}" alt = "" width = "1600" height = "900" fetchpriority = "high" decoding = "async" > {% endif %}
< div class = "sf-tp-page-hero-overlay" aria-hidden = "true" ></ div >
< div class = "wrap" >
< div class = "sf-tp-page-hero-copy" data-reveal >
< span class = "eyebrow" > {{ tp_eyebrow }}</ span >
< h1 > {{ page.title }}</ h1 >
< p class = "sf-tp-page-hero-lead" > {{ tp_lead }}</ p >
</ div >
</ div >
</ section >
{% if listing %}
{% if tp_post_like %}
< section class = "section sf-tp-news-section" >< div class = "wrap" >< div class = "sf-tp-news-layout" >
< div class = "sf-tp-news-list" >
{{ filters() }}
< div class = "pro-filter-scope sf-tp-news-rows" >
{% for item in listing %}
< article class = "sf-tp-news-row pro-card" data-reveal style = "--reveal-i: {{ loop.index0 % 3 }}" data-brand = "{{ fields_of(item).get('brand','') }}" data-category = "{{ fields_of(item).get('category','') }}" >
< div class = "sf-tp-news-row-media sf-tp-card-media" > {{ cover(item, 'post') }}</ div >
< div class = "sf-tp-news-body" >
< span class = "sf-tp-tag" > {{ fields_of(item).get('tag') or fields_of(item).get('category') or 'News' }}</ span >
< h3 >< a href = "{{ safe_href(item.route) }}" > {{ item.title }}</ a ></ h3 >
< p > {{ (item.summary or '')[:200] }}</ p >
< a class = "sf-tp-more" href = "{{ safe_href(item.route) }}" > Read more {{ icon('arrow-right', cls='sf-tp-icon--sm') }}</ a >
</ div >
</ article >
{% endfor %}
</ div >
{{ tp_pages(pagination, pg_root, icon) }}
</ div >
< aside class = "sf-tp-news-aside" >
< div class = "sf-tp-aside-box" data-reveal >< h3 > Latest posts</ h3 > {% for p in listing[:5] %}< p >< a href = "{{ safe_href(p.route) }}" > {{ p.title }}</ a ></ p > {% endfor %}</ div >
2026-10-23 14:26:34 +08:00
< div class = "sf-tp-aside-box" data-reveal style = "--reveal-i: 1" >< h3 > Popular reads</ h3 >< ol > {% for p in listing[:5] %}< li >< a href = "{{ safe_href(p.route) }}" > {{ p.title }}</ a ></ li > {% endfor %}</ ol ></ div >
2026-10-23 14:06:15 +08:00
< div class = "sf-tp-aside-box" data-reveal style = "--reveal-i: 2" >< h3 > Categories</ h3 >< p > All news < strong > {{ groups['post']|length }}</ strong ></ p ></ div >
</ aside >
</ div ></ div ></ section >
{% else %}
{% set tp_kind = listing[0].type %}
{% set tp_show_side = categories and tp_kind in ('product', 'vehicle') %}
< section class = "section sf-tp-catalogue-section" >< div class = "wrap{{ ' sf-tp-catalogue-layout' if tp_show_side else ' sf-tp-catalogue-single' }}" >
{% if tp_show_side %}
< aside class = "sf-tp-side-panel" aria-label = "Product categories" data-reveal >
< h3 > Product categories</ h3 >
< a class = "sf-tp-side-link{{ ' sf-tp-side-link--active' if route == '/products/' }}" href = "/products/" > All products</ a >
{% for cat in categories[:12] %}< a class = "sf-tp-side-link{{ ' sf-tp-side-link--active' if cat.url == route }}" href = "{{ cat.url }}" > {{ cat.name }}</ a > {% endfor %}
</ aside >
{% endif %}
< div class = "sf-tp-catalogue-main" >
< div class = "sf-tp-section-head" data-reveal >
< div >< span class = "eyebrow" > {{ tp_eyebrow }}</ span >< h2 > {{ page.title }}</ h2 ></ div >
2026-10-23 14:26:34 +08:00
{# 导语不与 hero 重复:主列头行改条目/页数计数(cms SectionHead 的
intro 位在平台无独立文案来源,用真实数据代替装饰性复述) #}
< p class = "sf-tp-intro" > {{ listing|length }} items{% if pagination and pagination.pages > 1 %} · {{ pagination.pages }} pages{% endif %}</ p >
2026-10-23 14:06:15 +08:00
</ div >
{{ filters() }}
< div class = "pro-filter-scope sf-tp-card-grid sf-tp-card-grid--listing" >
{% for item in listing %}
< a class = "sf-tp-card pro-card" href = "{{ safe_href(item.route) }}" data-reveal style = "--reveal-i: {{ loop.index0 % 4 }}" data-brand = "{{ fields_of(item).get('brand','') }}" data-category = "{{ fields_of(item).get('category','') }}" >
< div class = "sf-tp-card-media" > {{ cover(item, tp_kind) }}</ div >
< div class = "sf-tp-card-body" >
< h3 > {{ item.title }}</ h3 >
< p > {{ (item.summary or 'Published specifications available on request.').split('.')[0] }}.</ p >
< span class = "sf-tp-more" > View details {{ icon('arrow-right', cls='sf-tp-icon--sm') }}</ span >
</ div >
</ a >
{% endfor %}
</ div >
{{ tp_pages(pagination, pg_root, icon) }}
</ div >
</ div ></ section >
{% endif %}
{% else %}
< section class = "section sf-tp-listing-section" >< div class = "wrap" >
< div class = "sf-tp-listing-empty" data-reveal >
< h2 > Nothing published here yet</ h2 >
< p > Content added to this section in the admin console appears here automatically.</ p >
</ div >
</ div ></ section >
{% endif %}