From 0ec826bd4b6faf3f91ed2dedfb0c668f1950d7ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Fri, 23 Oct 2026 16:59:52 +0800 Subject: [PATCH] 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 --- tests/test_theme_registry.py | 119 ++++++++++++++++++++++++++ themes/trade-pro/pages/case.html | 128 +++++++++++++++++++++++++++- themes/trade-pro/pages/listing.html | 15 +++- themes/trade-pro/pro.css | 42 +++++++++ 4 files changed, 300 insertions(+), 4 deletions(-) diff --git a/tests/test_theme_registry.py b/tests/test_theme_registry.py index c61ea22..f742ab5 100644 --- a/tests/test_theme_registry.py +++ b/tests/test_theme_registry.py @@ -603,6 +603,125 @@ def test_trade_pro_contact_layout_structure(factory): assert len(bsoup.find_all('h1')) == 1 +def test_trade_pro_case_layout_structure(factory): + # Task 10g:case.html 移植 cms 案例详情设计(projects/[slug].astro,像素基准 + # dist/hesupipe/en/projects//)后的结构契约。 + # 区块序列:PageHero(面包屑 Home › Cases › 标题 + 封面底图 + 眉题/h1/导语) + # → detail-grid 双栏(1.2fr 文案列:Project Overview 眉题 + h2 + ● location/ + # industry 行 + 富文本正文;0.8fr sticky 侧列:主图 + 3 列相册缩略图 + + # Results 成果卡)→ Recommended Products(产品卡,空则隐藏)→ Other + # Projects(groups['case'] 排除自身,空则隐藏)。页尾 Cta 由 pro.html 的 + # pro-end-cta 覆盖。cms 的结构化字段(challenge/solution/results 词典兜底) + # 平台无对应数据源:正文分段由富文本自身标题承担,成果卡由 fields.results + # 开放字段驱动(分号/换行分隔要点),空则整段隐藏——绝不渲染词典兜底文案。 + import re + sid = site(factory) + assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200 + made = factory.post(f'/api/sites/{sid}/entries', json={ + 'type': 'case', 'title': 'City Drinking Water Project', 'route': '/case/city-water/', + 'cover': 'https://imgs.example/city-water.jpg', 'summary': 'Municipal supply network upgrade in Jakarta.', + 'body': '

The network demanded reliable joint performance under tropical site conditions.

The Solution

A complete piping system was delivered and supervised on site.

', + 'meta': {'fields': {'location': 'Jakarta, Indonesia', 'industry': 'Municipal water supply', + 'results': 'Delivered on schedule;Passed third-party inspection', + 'gallery': 'https://imgs.example/city-water.jpg; https://imgs.example/city-water-b.jpg; https://imgs.example/city-water-c.jpg'}}}) + assert made.status_code in (200, 201), made.text + for i in range(2): + r = factory.post(f'/api/sites/{sid}/entries', json={ + 'type': 'case', 'title': f'Metro Water Supply {i}', 'route': f'/case/metro-{i}/', + 'summary': f'Reference project {i}.'}) + assert r.status_code in (200, 201), r.text + # 1 个产品驱动 Recommended Products 区 + prod = factory.post(f'/api/sites/{sid}/entries', json={ + 'type': 'product', 'title': 'HDPE Pipe PE100', 'summary': 'Pressure pipe for water supply.'}) + assert prod.status_code in (200, 201), prod.text + assert factory.put(f'/api/factory/sites/{sid}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200 + bid = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version'] + r = factory.get(f'/preview/{sid}/{bid}/case/city-water/') + assert r.status_code == 200, r.text[:400] + html = r.text + soup = BeautifulSoup(html, 'html.parser') + # hero:封面底图 + 面包屑(Home › Cases › 标题)+ 眉题 + 唯一 h1 + hero = soup.select_one('section.sf-tp-page-hero.sf-tp-case-hero') + assert hero is not None + bg = hero.select_one('.sf-tp-page-hero-bg') + assert bg is not None and 'city-water.jpg' in bg.get('src', '') + assert hero.select_one('.sf-tp-breadcrumb a[href$="/cases/"]') + current = hero.select_one('.sf-tp-breadcrumb-current') + assert current is not None and current.get_text(strip=True) == 'City Drinking Water Project' + assert hero.select_one('.sf-tp-page-hero-copy[data-reveal] h1') + assert 'Municipal supply network upgrade' in hero.select_one('.sf-tp-page-hero-lead').get_text() + assert len(soup.find_all('h1')) == 1 + # detail 双栏:文案列(概览眉题 + h2 + ● location/industry 行 + 富文本正文) + grid = soup.select_one('.sf-tp-case-grid') + assert grid is not None + copy = grid.select_one('.sf-tp-case-copy') + assert copy is not None and 'project overview' in copy.select_one('.eyebrow').get_text().lower() + assert copy.find('h2') is not None and 'City Drinking Water' in copy.find('h2').get_text() + locations = copy.select('.sf-tp-case-location') + assert len(locations) == 2 # location + industry(blueprints 的 case 字段) + assert 'Jakarta, Indonesia' in locations[0].get_text() + assert 'Municipal water supply' in locations[1].get_text() + body = copy.select_one('.sf-tp-case-body .pro-rich') + assert body is not None and 'The Solution' in body.get_text() + # 侧列:主图 gallery[0] + 全量相册缩略图(cms 同款含首图)+ Results 成果卡 + side = grid.select_one('.sf-tp-case-side') + assert side is not None + photo = side.select_one('.sf-tp-photo img') + assert photo is not None and 'city-water.jpg' in photo.get('src', '') + thumbs = side.select('.sf-tp-case-gallery img') + assert len(thumbs) == 3 # cover 去重 + b + c + results = side.select_one('.sf-tp-case-results') + assert results is not None and 'Results' in results.find('h3').get_text() + points = results.select('li') + assert len(points) == 2 + assert all(p.select_one('.sf-tp-case-check svg') for p in points) # ✓ 为 SVG 图标 + assert 'Passed third-party inspection' in points[1].get_text() + # Recommended Products:产品卡(无产品整段隐藏) + products = soup.select_one('.sf-tp-case-products') + assert products is not None + assert any('HDPE Pipe PE100' in c.get_text() for c in products.select('.sf-tp-card')) + # Other Projects:相关案例去自身 + related = soup.select_one('.sf-tp-case-related') + assert related is not None + cards = related.select('.sf-tp-card') + assert len(cards) == 2 + assert all('City Drinking Water' not in c.get_text() for c in cards) + assert related.select_one('a.sf-tp-more[href$="/cases/"] svg') # View all projects → SVG 箭头 + # 交互箭头一律 SVG(Task 9 契约) + assert not re.search(r'[←→↗↘⌕▾]', ''.join(a.get_text() for a in soup.find_all('a'))) + # 本构建的链接全通(新增 /cases/ 面包屑与相关案例路由不产生断链) + report = factory.get(f'/api/sites/{sid}/builds/{bid}/report').json() + assert not report['errors'] and not report['broken_links'] and not report['missing_local_images'] + + # ── 案例列表分支(10c listing 的 case 类走产品卡栅格;10g 补案例口径 + # hero 文案:cms projects 列表 OUR PROJECTS,非 PRODUCT CATALOGUE)── + cases_html = factory.get(f'/preview/{sid}/{bid}/cases/').text + assert 'OUR PROJECTS' in cases_html + assert 'PRODUCT CATALOGUE' not in cases_html + csoup = BeautifulSoup(cases_html, 'html.parser') + assert len(csoup.select('.sf-tp-card-grid--listing .sf-tp-card')) == 3 + + # ── 空数据案例(无 cover/gallery/body/fields/其他案例/产品):相册、 + # 成果卡、推荐产品、相关案例整段隐藏;占位图保持版式完整 ── + sid2 = site(factory) + bare = factory.post(f'/api/sites/{sid2}/entries', json={ + 'type': 'case', 'title': 'Reference Project', 'route': '/case/reference/'}) + assert bare.status_code in (200, 201), bare.text + assert factory.put(f'/api/factory/sites/{sid2}/theme', json={'theme': 'trade-pro', 'version': '1.0.0'}).status_code == 200 + bid2 = factory.post(f'/api/sites/{sid2}/builds', json={}).json()['version'] + page2 = factory.get(f'/preview/{sid2}/{bid2}/case/reference/') + assert page2.status_code == 200, page2.text[:300] + bsoup = BeautifulSoup(page2.text, 'html.parser') + assert len(bsoup.find_all('h1')) == 1 + assert bsoup.select_one('.sf-tp-page-hero-bg') is None + assert bsoup.select_one('.sf-tp-case-gallery') is None + assert bsoup.select_one('.sf-tp-case-results') is None + assert bsoup.select_one('.sf-tp-case-products') is None + assert bsoup.select_one('.sf-tp-case-related') is None + assert bsoup.select_one('.sf-tp-case-side .sf-tp-photo-plain') is not None # 无图占位 + assert bsoup.select_one('.sf-tp-case-location') is None # 字段空则行隐藏 + + def test_frontend_entries_include_trade_pro(): from pathlib import Path base = Path(__file__).resolve().parent.parent diff --git a/themes/trade-pro/pages/case.html b/themes/trade-pro/pages/case.html index 8ae8407..574a763 100644 --- a/themes/trade-pro/pages/case.html +++ b/themes/trade-pro/pages/case.html @@ -1 +1,127 @@ -
SELECTED WORK / PROJECT

{{page.title}}

{{page.summary}}

{% if page.cover %}{{page.title}}{% endif %}
{% if fields.get('location') %}Location
{{fields.get('location')}}
{% endif %}{% if fields.get('industry') %}Industry
{{fields.get('industry')}}
{% endif %}
{{body_copy(content,page)}}Browse more projects {{ icon('arrow-right') }}
+{# trade-pro case detail — cms 案例详情设计移植(Task 10g)。 + 区块序列对照 D:/project/cms/runtimes/astro-site-runtime/src/pages/[locale]/projects/[slug].astro + (像素基准 dist/hesupipe/en/projects//): + 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 %} +
+ {% if safe_image(page.cover) %}{% endif %} + +
+ +
+ OUR PROJECTS +

{{ page.title }}

+ {% if page.summary %}

{{ page.summary }}

{% endif %} +
+
+
+
+
+
+
+ PROJECT OVERVIEW +

{{ page.title }}

+ {% if fields.get('location') %}

{{ fields.get('location') }}

{% endif %} + {% if fields.get('industry') %}

{{ fields.get('industry') }}

{% endif %} +
+
{{ body_copy(content, page) }}
+
+
+
+ {% if gallery %}{{ page.title }} + {% else %}{% endif %} +
+ {% if gallery|length > 1 %} + + {% endif %} + {% if tp_results %} +
+

Results

+
    + {% for point in tp_results %}
  • {{ point }}
  • {% endfor %} +
+
+ {% endif %} +
+
+
+{% if tp_case_products %} +
+
+
+
+ APPLICATIONS +

Recommended products

+
+
+
+ {% for tp_item in tp_case_products %} + + {% endfor %} +
+
+
+{% endif %} +{% if tp_case_others %} + +{% endif %} diff --git a/themes/trade-pro/pages/listing.html b/themes/trade-pro/pages/listing.html index b5dc139..264bc9c 100644 --- a/themes/trade-pro/pages/listing.html +++ b/themes/trade-pro/pages/listing.html @@ -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// 是老站 + /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) -%} diff --git a/themes/trade-pro/pro.css b/themes/trade-pro/pro.css index 2843f7f..0c25cf7 100644 --- a/themes/trade-pro/pro.css +++ b/themes/trade-pro/pro.css @@ -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)} +}