Files
cms-SiteForge/themes/trade-pro/pages/listing.html
T
浅念 0ec826bd4b feat: port cms case layout into trade-pro
Task 10g: rewrite pages/case.html from the thin 1-line variant to the cms
projects/[slug].astro design (pixel baseline dist/hesupipe/en/projects/):
PageHero (breadcrumb Home > Cases > title, cover bg) + 1.2fr/0.8fr detail
grid (overview block with location/industry rows + rich body via the 10d
typography layer; sticky side column with main photo, 3-col gallery tiles
and Results card) + Recommended Products + Other Projects (groups['case']
minus self), all empty-data sections hidden. Results points come from the
open fields.results convention (semicolon/newline separated); cms
dictionary fallback copy is never ported (fake data across sites).

Listing case branch (10c evaluation): core form (hero + cols-3 cards +
pagination) already matches cms projects list; only the hero copy voice
differed, so listing.html gains a case-aware eyebrow/lead fallback
(OUR PROJECTS). cms-specific tabs (no data source) and the FEATURED CASE
STUDY band (HESU-static dictionary content) stay unported, documented in
the template header.

Adds test_trade_pro_case_layout_structure (build-flow: gallery/results
structure, related-case dedup, empty-data hiding, /cases/ hero voice).

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-23 16:59:52 +08:00

145 lines
9.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 栏目占位页的空集)。
7. 10g 评估 cms projects 列表([...page].astro)vs 本分支:核心形态
(hero + cols-3 卡栅格 + 分页)一致,仅补案例口径 hero 文案
(tp_case_like);cms 专属的 Tab 行(数据驱动分类,无数据源,同 3
不移植)与 FEATURED CASE STUDY 双栏带(HESU 特定项目的静态词典
内容,跨站即假数据)不移植。cms 卡片链到 /news/<slug>/ 是老站
/progect/ 同源遗留,平台链到案例详情(正确行为)。 #}
{% set tp_post_like = (listing[0].type == 'post') if listing else (kind == 'listing' and ('news' in route or 'post' in route)) %}
{# 10g:case 类列表走 cms projects 列表口径(OUR PROJECTS),其余非文章类
保持产品目录口径;判定与 select_listing 的路由推断同构(首项类型优先)#}
{% set tp_case_like = (listing[0].type == 'case') if listing else (kind == 'listing' and 'case' in route) %}
{% set tp_eyebrow = 'NEWS & INSIGHTS' if tp_post_like else ('OUR PROJECTS' if tp_case_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 ('Project references and delivery stories from export customers worldwide.' if tp_case_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 %}