Files
cms-SiteForge/themes/trade-pro/pages/listing.html
T
浅念 8f8dfd19e5 fix: listing review round — responsive grid collapse, branch fallback, test assertions
Quality review fixes for Task 10c (2 Important + 5 Minor):
- [I1] sf-tp-card-grid--listing now collapses to 2 cols at 900px and
  compact gap at 620px: the 3-col declaration sits after the 10a media
  blocks in source order, so the fold rules must live in the new 10c
  media blocks to win at equal specificity
- [I2] tp_post_like trusts the first listing item's content type when
  the list is non-empty; the route-substring fallback now only applies
  to empty listings of kind=='listing' — /categories/post-sales-service/
  style slugs can no longer flip a product category page into the news
  branch
- [M3] empty-state negative assertions check the rendered attribute
  string, not the selector literal (previously always true)
- [M4] catalogue section-head intro is now an item/page count instead
  of duplicating the hero lead
- [M5] Popular reads entries link to the posts (same data as Latest,
  no analytics source on the platform)
- [M6] both /categories/pumps/ pagination pages assert exactly one h1
- [M7] side-panel sticky top uses --sf-tp-space-24

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 14:26:34 +08:00

136 lines
8.8 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 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 只定义了这两种列表设计,品牌/案例/服务映射到产品卡形态
(与 brand.html 相关产品、product.html 相关产品一致)。非空列表只信
首项内容类型;路由子串仅作空列表 fallback 且排除 category(否则
/categories/post-sales-service/ 这类 slug 会被误判成新闻分支)。
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 栏目占位页的空集)。 #}
{% set tp_post_like = (listing[0].type == 'post') if listing else (kind == 'listing' and ('news' in route or 'post' in route)) %}
{% 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>
<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>
<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>
{# 导语不与 hero 重复:主列头行改条目/页数计数(cms SectionHead 的
intro 位在平台无独立文案来源,用真实数据代替装饰性复述) #}
<p class="sf-tp-intro">{{ listing|length }} items{% if pagination and pagination.pages > 1 %} · {{ pagination.pages }} pages{% endif %}</p>
</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 %}