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>
This commit is contained in:
浅念
2026-10-23 16:59:52 +08:00
parent 882b8cb1ec
commit 0ec826bd4b
4 changed files with 300 additions and 4 deletions
+127 -1
View File
@@ -1 +1,127 @@
<section class="pro-inner-hero"><div class="pro-wrap"><span class="pro-eyebrow">SELECTED WORK / PROJECT</span><h1>{{page.title}}</h1><p>{{page.summary}}</p></div></section><section class="pro-section"><div class="pro-wrap pro-case-detail">{% if page.cover %}<img src="{{safe_image(page.cover)}}" alt="{{page.title}}">{% endif %}<div class="pro-case-meta">{% if fields.get('location') %}<span>Location<br><strong>{{fields.get('location')}}</strong></span>{% endif %}{% if fields.get('industry') %}<span>Industry<br><strong>{{fields.get('industry')}}</strong></span>{% endif %}</div>{{body_copy(content,page)}}<a class="pro-text-link" href="/cases/">Browse more projects {{ icon('arrow-right') }}</a></div></section>
{# trade-pro case detail — cms 案例详情设计移植(Task 10g)。
区块序列对照 D:/project/cms/runtimes/astro-site-runtime/src/pages/[locale]/projects/[slug].astro
(像素基准 dist/hesupipe/en/projects/<slug>/):
PageHero(面包屑 Home › Cases › 标题 + 封面底图 + 眉题/h1/导语)→
detail-grid 双栏(1.2fr 文案列:Project Overview 眉题 + h2 + ● location/
industry 行 + 富文本正文;0.8fr sticky 侧列:主图 + 3 列相册缩略图 +
Results 成果卡)→ Recommended Products(soft 底产品卡)→ Other Projects
(相关案例卡,排除自身)。
与 cms 的结构决策:
1. cms 文案列是 customerBackground / challenge / solution 三个结构化字段
(空时回退 HESU 词典段落);平台案例只有单一富文本 body——概览块后
正文一整块渲染(10d sf-tp-article-body 排版层双挂),Challenge/
Solution 分段由正文自身的 h2/h3 标题承担;不移植词典兜底段落
(那是 HESU 站点文案,跨站即假数据)。
2. 侧列数据映射:相册 = make_context 的 gallery(cover + fields.gallery
去重,与 10b 产品画廊同源);主图 gallery[0],缩略图全量含首图
(cms 同款,>1 张才渲染缩略图行);无图回退 sf-tp-photo-plain 占位。
Results 成果卡 = fields.results 开放字段(分号/换行分隔要点;cms 的
resultsPoints 词典兜底不移植),空则整段隐藏——绝不编造成果;
✓ 圆徽用 Task 9 check SVG 图标,非文本字符。
3. ● location 行 = blueprints 为 case 定义的字段 location + industry
(cms 的 item.text 单行对应物);均空时行隐藏,概览块(眉题 + h2)
保留 cms 结构位。
4. Recommended Products = 产品/车型前 4(cms fetchProducts 前 4 同构),
空则整段隐藏;Other Projects = groups['case'] 排除自身前 4,动作位
回 /cases/(cms 回 /projects/,平台栏目根)。
5. 页尾 Cta 横幅由 pro.html 的 .pro-end-cta 承担(10b/10d 同决策);
cms 的 CreativeWork JSON-LD 不移植(pro.html 的 og:type=article 已覆盖
分享语义)。 #}
{% set tp_case_others = groups['case']|rejectattr('route','equalto',page.route)|list %}
{% set tp_case_products = (groups['product'] + groups['vehicle'])[:4] %}
{% set tp_results = ((fields.get('results') or '')|string|replace('\r\n','\n')|replace('\r','\n')|replace(';','\n')|trim).split('\n')|map('trim')|reject('equalto','')|list %}
<section class="sf-tp-page-hero sf-tp-case-hero">
{% if safe_image(page.cover) %}<img class="sf-tp-page-hero-bg" src="{{ safe_image(page.cover) }}" alt="" width="1600" height="900" fetchpriority="high" decoding="async">{% endif %}
<div class="sf-tp-page-hero-overlay" aria-hidden="true"></div>
<div class="wrap">
<nav class="sf-tp-breadcrumb" aria-label="Breadcrumb" data-reveal>
<a href="/">Home</a><a href="/cases/">Cases</a><span class="sf-tp-breadcrumb-current">{{ page.title }}</span>
</nav>
<div class="sf-tp-page-hero-copy" data-reveal style="--reveal-i: 1">
<span class="eyebrow">OUR PROJECTS</span>
<h1>{{ page.title }}</h1>
{% if page.summary %}<p class="sf-tp-page-hero-lead">{{ page.summary }}</p>{% endif %}
</div>
</div>
</section>
<section class="section sf-tp-case-detail">
<div class="wrap sf-tp-case-grid">
<div class="sf-tp-case-copy" data-reveal>
<div class="sf-tp-case-overview">
<span class="eyebrow">PROJECT OVERVIEW</span>
<h2>{{ page.title }}</h2>
{% if fields.get('location') %}<p class="sf-tp-case-location"><span aria-hidden="true">●</span> {{ fields.get('location') }}</p>{% endif %}
{% if fields.get('industry') %}<p class="sf-tp-case-location"><span aria-hidden="true">●</span> {{ fields.get('industry') }}</p>{% endif %}
</div>
<div class="sf-tp-case-body sf-tp-article-body">{{ body_copy(content, page) }}</div>
</div>
<div class="sf-tp-case-side">
<div class="sf-tp-photo" data-reveal style="--reveal-i: 1">
{% if gallery %}<img src="{{ gallery[0] }}" alt="{{ page.title }}" width="800" height="600" loading="lazy" decoding="async">
{% else %}<div class="sf-tp-photo-plain" aria-hidden="true"><span>CASE STUDY</span></div>{% endif %}
</div>
{% if gallery|length > 1 %}
<div class="sf-tp-case-gallery" data-reveal style="--reveal-i: 2" aria-label="Project photo gallery">
{% for shot in gallery %}<img src="{{ shot }}" alt="{{ page.title }} photo {{ loop.index }}" width="200" height="140" loading="lazy" decoding="async">{% endfor %}
</div>
{% endif %}
{% if tp_results %}
<div class="sf-tp-case-results" data-reveal style="--reveal-i: 2">
<h3>Results</h3>
<ul>
{% for point in tp_results %}<li><span class="sf-tp-case-check" aria-hidden="true">{{ icon('check', size='12') }}</span>{{ point }}</li>{% endfor %}
</ul>
</div>
{% endif %}
</div>
</div>
</section>
{% if tp_case_products %}
<section class="section section--soft sf-tp-case-products">
<div class="wrap">
<div class="sf-tp-section-head" data-reveal>
<div>
<span class="eyebrow">APPLICATIONS</span>
<h2>Recommended products</h2>
</div>
</div>
<div class="sf-tp-card-grid">
{% for tp_item in tp_case_products %}
<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 %}
{% if tp_case_others %}
<section class="section sf-tp-case-related">
<div class="wrap">
<div class="sf-tp-section-head" data-reveal>
<div>
<span class="eyebrow">OUR PROJECTS</span>
<h2>Other projects</h2>
</div>
<a class="sf-tp-more" href="/cases/">View all projects {{ icon('arrow-right', cls='sf-tp-icon--sm') }}</a>
</div>
<div class="sf-tp-card-grid">
{% for tp_item in tp_case_others[:4] %}
<article class="sf-tp-card" data-reveal style="--reveal-i: {{ loop.index0 % 4 }}">
<div class="sf-tp-card-media">{{ cover(tp_item, 'case') }}</div>
<div class="sf-tp-card-body">
<h3>{{ tp_item.title }}</h3>
<p>{{ (tp_item.summary or 'Project reference 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 %}
+12 -3
View File
@@ -28,11 +28,20 @@
契约:交互箭头一律 SVG,无裸箭头字符);页码 URL 与构建期静态分页
路由一致(第 1 页 = 栏目根,形如 /products/page/2/ 递增)。
6. 空列表:筛选、栅格、分页整段隐藏,替换为友好空态提示(cms 站点
恒有内容,平台必须处理 noindex 栏目占位页的空集)。 #}
恒有内容,平台必须处理 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)) %}
{% set tp_eyebrow = 'NEWS & INSIGHTS' if tp_post_like else 'PRODUCT CATALOGUE' %}
{# 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 'Browse the full catalogue with specifications, brands and categories.') %}
{% 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) -%}
+42
View File
@@ -531,3 +531,45 @@
.sf-theme-trade-pro .sf-tp-contact-item{grid-template-columns:40px 1fr;gap:10px}
.sf-theme-trade-pro .sf-tp-contact-form{padding:20px}
}
/* ── Task 10g:case 案例详情(projects/[slug].astro 移植)──
源 src/pages/[locale]/projects/[slug].astro:detail-grid 1.2fr/0.8fr 双栏
(文案列概览块 + 正文;侧列 sticky:主图 + 3 列相册瓦片 + Results 成果卡)。
共享组件复用:sf-tp-page-hero/breadcrumb(10c 内页 hero)、sf-tp-photo/
photo-plain(10a 实景框)、sf-tp-section-head/card-grid/card(10a 卡片系)、
sf-tp-article-body 排版层(10d 双挂)。本块只补 case 专属形态:● location
行、相册瓦片、成果卡与 ✓ 圆徽。 */
.sf-theme-trade-pro .sf-tp-case-grid{display:grid;grid-template-columns:1.2fr 0.8fr;gap:calc(var(--sf-tp-space-12) + var(--sf-tp-space-4));align-items:start}
/* 概览块:眉题 + h2 + ● location 行(cms .location:品牌色半粗) */
.sf-theme-trade-pro .sf-tp-case-overview h2{margin-block:var(--sf-tp-space-2) var(--sf-tp-space-4)}
.sf-theme-trade-pro .sf-tp-case-location{display:flex;align-items:baseline;gap:var(--sf-tp-space-2);color:var(--sf-tp-color-text-brand);font-weight:var(--sf-tp-font-weight-semibold);margin-block-end:var(--sf-tp-space-3);line-height:var(--sf-tp-line-height-normal)}
.sf-theme-trade-pro .sf-tp-case-location span{font-size:var(--sf-tp-font-size-xs)}
/* 正文块与概览块分组(cms .detail-block 发丝线分隔的对应物) */
.sf-theme-trade-pro .sf-tp-case-body{margin-block-start:var(--sf-tp-space-8);padding-block-start:var(--sf-tp-space-6);border-block-start:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-subtle)}
/* 侧列 sticky(cms .detail-side:top = space-16 + space-4) */
.sf-theme-trade-pro .sf-tp-case-side{display:grid;gap:var(--sf-tp-space-6);position:sticky;top:calc(var(--sf-tp-space-16) + var(--sf-tp-space-4))}
/* 相册:3 列瓦片(cms .case-gallery),悬浮轻缩放 */
.sf-theme-trade-pro .sf-tp-case-gallery{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--sf-tp-space-2)}
.sf-theme-trade-pro .sf-tp-case-gallery img{width:100%;aspect-ratio:var(--sf-tp-ratio-landscape);object-fit:cover;border-radius:var(--sf-tp-radius-sm);border:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-subtle);transition:transform var(--sf-tp-duration-fast) var(--sf-tp-easing-standard)}
.sf-theme-trade-pro .sf-tp-case-gallery img:hover{transform:scale(1.03)}
/* 成果卡(cms .results-card):accent 顶边 + ✓ 圆徽要点列表 */
.sf-theme-trade-pro .sf-tp-case-results{background:var(--sf-tp-color-surface-default);border:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-subtle);border-block-start:var(--sf-tp-border-width-strong) solid var(--sf-tp-color-brand-accent);border-radius:var(--sf-tp-card-border-radius);padding:var(--sf-tp-card-padding-md)}
.sf-theme-trade-pro .sf-tp-case-results h3{font-size:var(--sf-tp-font-size-lg);margin-block-end:var(--sf-tp-space-3)}
.sf-theme-trade-pro .sf-tp-case-results ul{list-style:none;margin:0;padding:0;display:grid;gap:var(--sf-tp-space-2)}
.sf-theme-trade-pro .sf-tp-case-results li{display:flex;align-items:center;gap:var(--sf-tp-space-2);font-size:var(--sf-tp-font-size-sm);color:var(--sf-tp-color-text-secondary)}
.sf-theme-trade-pro .sf-tp-case-check{display:inline-flex;align-items:center;justify-content:center;width:calc(var(--sf-tp-space-4) + var(--sf-tp-space-1));height:calc(var(--sf-tp-space-4) + var(--sf-tp-space-1));color:var(--sf-tp-color-text-on-brand);background:var(--sf-tp-color-brand-primary);border-radius:var(--sf-tp-radius-full);flex:0 0 auto}
/* ── case 响应式(cms 断点 900 单列堆叠 + 侧列取消 sticky;620 紧凑。
内页 hero 的 767/620 竖版由 10c 既有块覆盖;卡片栅格折叠由 10a 既有块
提供(复用 sf-tp-card-grid 类名);正文 620 字号由 10d 的
sf-tp-article-body 双挂规则提供。折叠规则收在本任务媒体块内
(10c 教训:同特异性按源顺序取胜)── */
@media (max-width: 900px){
.sf-theme-trade-pro .sf-tp-case-grid{grid-template-columns:1fr;gap:var(--sf-tp-space-8)}
.sf-theme-trade-pro .sf-tp-case-side{position:static}
}
@media (max-width: 620px){
.sf-theme-trade-pro .sf-tp-case-results{padding:16px}
.sf-theme-trade-pro .sf-tp-case-results li{font-size:var(--sf-tp-font-size-xs)}
}