feat: port cms product detail layout into trade-pro (gallery/specs/inquiry)

Rewrite pages/product.html from the 3-row skeleton to the cms
[slug].astro design: breadcrumb -> gallery (main + scrolling thumbs,
reusing the platform pro.js data-gallery-image hooks, zero new JS) +
summary column (eyebrow/h1/lead/brand+model/actions) -> flattened
Product details panel -> Specifications table from fields (hidden when
empty) -> related products on the home card grid -> inquiry section
(left contact info, right external form only when form_endpoint is
configured, CTA to /contact/ otherwise).

Component CSS lands in pro.css under .sf-theme-trade-pro with
900/620 breakpoints; stale .pro-specs tint rules removed (other
themes keep their pro-base skeleton); cross-theme skeleton assertion
in test_theme_pro_v3.py now excludes trade-pro; new
test_trade_pro_product_layout_structure covers the build output.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 10:17:31 +08:00
parent c318829ff3
commit 5ea0c8c5e8
4 changed files with 269 additions and 8 deletions
+4 -1
View File
@@ -19,7 +19,10 @@ def test_complete_theme_families_have_distinct_markup(factory):
assert soup.find('footer',class_='pro-footer')
assert len(soup.find_all('h1'))==1,(theme,page_type,len(soup.find_all('h1')))
assert 'pro-base.css' in r.text and 'pro.js' in r.text
if page_type=='product':assert soup.select_one('.pro-product-layout .pro-specs')
# trade-pro 的产品详情自 Task 10b 起为 cms 移植布局(sf-tp-product-*),
# 骨架断言只覆盖其余沿用 pro-base 骨架的主题(test_theme_registry.py
# 的 test_trade_pro_product_layout_structure 覆盖 trade-pro 侧)。
if page_type=='product' and theme!='trade-pro':assert soup.select_one('.pro-product-layout .pro-specs')
if page_type=='listing':assert soup.select_one('[data-pro-filter-search]')
if page_type=='article':assert soup.select_one('.pro-article-hero')
main.append(factory.get(f'/api/factory/themes/{theme}/preview?page_type=home').text)
+50
View File
@@ -178,6 +178,56 @@ def test_trade_pro_home_layout_structure(factory):
assert len(soup.find_all('h1')) == 1
def test_trade_pro_product_layout_structure(factory):
# Task 10b:product.html 移植 cms 产品详情设计([slug].astro)后的结构契约。
# 构建流(/preview/ HTTP)+ 带 fields/gallery 的产品条目驱动断言:
# 画廊(主图封面 + data-gallery-image 缩略图,复用平台 pro.js 增强钩子)、
# 标题区、参数表渲染 fields 键值(品牌等管理字段不进表)、询盘 CTA、
# 相关产品复用首页卡片网格;旧骨架 .pro-product-layout/.pro-specs 无残留。
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': 'product', 'title': 'Hydraulic Pump HP-200', 'route': '/product/hp-200/',
'cover': 'https://imgs.example/hp-200.jpg', 'summary': 'High-pressure hydraulic pump for export.',
'meta': {'fields': {'brand': 'Quanta', 'range': '500 km', 'pressure': '210 bar',
'gallery': ['https://imgs.example/hp-200.jpg', 'https://imgs.example/hp-200-b.jpg']}}})
assert made.status_code in (200, 201), made.text
other = factory.post(f'/api/sites/{sid}/entries', json={
'type': 'product', 'title': 'Companion Valve', 'route': '/product/valve/',
'summary': 'Matching control valve.'})
assert other.status_code in (200, 201), other.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}/product/hp-200/')
assert r.status_code == 200, r.text[:400]
html = r.text
soup = BeautifulSoup(html, 'html.parser')
# 画廊:主图为封面;多图时缩略图带 data-gallery-image(pro.js 切换/灯箱钩子)
media = soup.select_one('.sf-tp-product-grid .sf-tp-product-media img')
assert media is not None and 'hp-200.jpg' in media.get('src', '')
thumbs = soup.select('.sf-tp-product-thumbs [data-gallery-image]')
assert len(thumbs) == 2
# 标题区(唯一 h1)+ 询盘 CTA 指向联系页(/preview/ 响应会把根相对
# href 重写为 /preview/{sid}/{bid}/contact/,故用后缀匹配)
assert soup.select_one('.sf-tp-product-summary h1.sf-tp-product-title')
assert soup.select_one('.sf-tp-product-actions a.sf-tp-btn[href$="/contact/"]')
# 参数表:fields 键值渲染(下划线转空格 + 首字母大写),空表整段隐藏
spec_table = soup.select_one('table.sf-tp-spec-table')
assert spec_table is not None
spec_text = spec_table.get_text()
assert 'Pressure' in spec_text and '210 bar' in spec_text and '500 km' in spec_text
assert 'Quanta' not in spec_text # brand 等管理字段不进参数表
# 相关产品:复用 sf-tp-card-grid;自身不出现在相关列表
cards = soup.select('.sf-tp-card-grid .sf-tp-card')
assert cards and any('Companion Valve' in c.get_text() for c in cards)
assert all('Hydraulic Pump' not in c.get_text() for c in cards)
# 询盘板块存在(左联系信息 + 右 CTA/表单位)
assert soup.select_one('.sf-tp-product-quote') is not None
# 旧骨架清理:无 .pro-product-layout/.pro-specs 残留类依赖
assert 'pro-product-layout' not in html and 'pro-specs' not in html
assert len(soup.find_all('h1')) == 1
def test_frontend_entries_include_trade_pro():
from pathlib import Path
base = Path(__file__).resolve().parent.parent