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>
This commit is contained in:
浅念
2026-10-23 14:26:34 +08:00
parent 9ea8369e2b
commit 8f8dfd19e5
3 changed files with 17 additions and 7 deletions
+4 -2
View File
@@ -282,6 +282,7 @@ def test_trade_pro_listing_layout_structure(factory):
cat1 = factory.get(f'/preview/{sid}/{bid}/categories/pumps/') cat1 = factory.get(f'/preview/{sid}/{bid}/categories/pumps/')
assert cat1.status_code == 200, cat1.text[:200] assert cat1.status_code == 200, cat1.text[:200]
s1 = BeautifulSoup(cat1.text, 'html.parser') s1 = BeautifulSoup(cat1.text, 'html.parser')
assert len(s1.find_all('h1')) == 1
nav1 = s1.select_one('nav.sf-tp-pagination') nav1 = s1.select_one('nav.sf-tp-pagination')
assert nav1 is not None assert nav1 is not None
assert len(s1.select('.sf-tp-card-grid--listing .sf-tp-card')) == 100 assert len(s1.select('.sf-tp-card-grid--listing .sf-tp-card')) == 100
@@ -295,6 +296,7 @@ def test_trade_pro_listing_layout_structure(factory):
cat2 = factory.get(f'/preview/{sid}/{bid}/categories/pumps/page/2/') cat2 = factory.get(f'/preview/{sid}/{bid}/categories/pumps/page/2/')
assert cat2.status_code == 200, cat2.text[:200] assert cat2.status_code == 200, cat2.text[:200]
s2 = BeautifulSoup(cat2.text, 'html.parser') s2 = BeautifulSoup(cat2.text, 'html.parser')
assert len(s2.find_all('h1')) == 1
nav2 = s2.select_one('nav.sf-tp-pagination') nav2 = s2.select_one('nav.sf-tp-pagination')
assert len(s2.select('.sf-tp-card-grid--listing .sf-tp-card')) == 5 assert len(s2.select('.sf-tp-card-grid--listing .sf-tp-card')) == 5
prev = nav2.select_one('a[rel="prev"]') prev = nav2.select_one('a[rel="prev"]')
@@ -330,8 +332,8 @@ def test_trade_pro_listing_layout_structure(factory):
# ── 空列表(/cases/ 无案例):整段隐藏换友好空态,不渲染筛选 ── # ── 空列表(/cases/ 无案例):整段隐藏换友好空态,不渲染筛选 ──
cases = factory.get(f'/preview/{sid}/{bid}/cases/').text cases = factory.get(f'/preview/{sid}/{bid}/cases/').text
assert 'sf-tp-listing-empty' in cases assert 'sf-tp-listing-empty' in cases
assert '[data-pro-filter-search]' not in cases assert 'data-pro-filter-search' not in cases
assert 'nav.sf-tp-pagination' not in cases assert 'sf-tp-pagination' not in cases
def test_frontend_entries_include_trade_pro(): def test_frontend_entries_include_trade_pro():
+8 -4
View File
@@ -9,7 +9,9 @@
1. kind 分支按列表内容类型:post 类(/news/、/post/ 等文章集合)用 1. kind 分支按列表内容类型:post 类(/news/、/post/ 等文章集合)用
新闻行形态;其余(product/vehicle/brand/case/service)复用产品卡片 新闻行形态;其余(product/vehicle/brand/case/service)复用产品卡片
栅格——cms 只定义了这两种列表设计,品牌/案例/服务映射到产品卡形态 栅格——cms 只定义了这两种列表设计,品牌/案例/服务映射到产品卡形态
(与 brand.html 相关产品、product.html 相关产品一致)。 (与 brand.html 相关产品、product.html 相关产品一致)。非空列表只信
首项内容类型;路由子串仅作空列表 fallback 且排除 category(否则
/categories/post-sales-service/ 这类 slug 会被误判成新闻分支)。
2. 产品分类侧栏仅产品类列表渲染(侧栏链接是产品分类语义,出现在 2. 产品分类侧栏仅产品类列表渲染(侧栏链接是产品分类语义,出现在
/brands/ 等页面会误导);文章分支的侧栏是 cms 新闻三盒。空分类 /brands/ 等页面会误导);文章分支的侧栏是 cms 新闻三盒。空分类
数据时侧栏整段隐藏,主列变单栏。 数据时侧栏整段隐藏,主列变单栏。
@@ -27,7 +29,7 @@
路由一致(第 1 页 = 栏目根,形如 /products/page/2/ 递增)。 路由一致(第 1 页 = 栏目根,形如 /products/page/2/ 递增)。
6. 空列表:筛选、栅格、分页整段隐藏,替换为友好空态提示(cms 站点 6. 空列表:筛选、栅格、分页整段隐藏,替换为友好空态提示(cms 站点
恒有内容,平台必须处理 noindex 栏目占位页的空集)。 #} 恒有内容,平台必须处理 noindex 栏目占位页的空集)。 #}
{% set tp_post_like = (listing and listing[0].type == 'post') or ('news' in route or 'post' in route) %} {% 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_eyebrow = 'NEWS & INSIGHTS' if tp_post_like else 'PRODUCT CATALOGUE' %}
{% set tp_hero_img = safe_image(page.cover) %} {% 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 'Browse the full catalogue with specifications, brands and categories.') %}
@@ -84,7 +86,7 @@
</div> </div>
<aside class="sf-tp-news-aside"> <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><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>{{ p.title }}</li>{% endfor %}</ol></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> <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> </aside>
</div></div></section> </div></div></section>
@@ -102,7 +104,9 @@
<div class="sf-tp-catalogue-main"> <div class="sf-tp-catalogue-main">
<div class="sf-tp-section-head" data-reveal> <div class="sf-tp-section-head" data-reveal>
<div><span class="eyebrow">{{ tp_eyebrow }}</span><h2>{{ page.title }}</h2></div> <div><span class="eyebrow">{{ tp_eyebrow }}</span><h2>{{ page.title }}</h2></div>
<p class="sf-tp-intro">{{ tp_lead }}</p> {# 导语不与 hero 重复:主列头行改条目/页数计数(cms SectionHead 的
intro 位在平台无独立文案来源,用真实数据代替装饰性复述) #}
<p class="sf-tp-intro">{{ listing|length }} items{% if pagination and pagination.pages > 1 %} · {{ pagination.pages }} pages{% endif %}</p>
</div> </div>
{{ filters() }} {{ filters() }}
<div class="pro-filter-scope sf-tp-card-grid sf-tp-card-grid--listing"> <div class="pro-filter-scope sf-tp-card-grid sf-tp-card-grid--listing">
+5 -1
View File
@@ -290,7 +290,7 @@
/* 产品目录布局:260px 分类侧栏 + 主列(cms layout-sidebar / side-panel) */ /* 产品目录布局:260px 分类侧栏 + 主列(cms layout-sidebar / side-panel) */
.sf-theme-trade-pro .sf-tp-catalogue-layout{display:grid;grid-template-columns:260px 1fr;gap:var(--sf-tp-space-8)} .sf-theme-trade-pro .sf-tp-catalogue-layout{display:grid;grid-template-columns:260px 1fr;gap:var(--sf-tp-space-8)}
.sf-theme-trade-pro .sf-tp-catalogue-single{display:block} .sf-theme-trade-pro .sf-tp-catalogue-single{display:block}
.sf-theme-trade-pro .sf-tp-side-panel{align-self:start;padding:var(--sf-tp-space-5);border-radius:var(--sf-tp-card-border-radius);background:var(--sf-tp-color-surface-muted);position:sticky;top:calc(var(--sf-tp-space-16) + var(--sf-tp-space-16))} .sf-theme-trade-pro .sf-tp-side-panel{align-self:start;padding:var(--sf-tp-space-5);border-radius:var(--sf-tp-card-border-radius);background:var(--sf-tp-color-surface-muted);position:sticky;top:var(--sf-tp-space-24)}
.sf-theme-trade-pro .sf-tp-side-panel h3{font-size:var(--sf-tp-font-size-sm);letter-spacing:var(--sf-tp-letter-spacing-wide);text-transform:uppercase;margin-block-end:var(--sf-tp-space-2)} .sf-theme-trade-pro .sf-tp-side-panel h3{font-size:var(--sf-tp-font-size-sm);letter-spacing:var(--sf-tp-letter-spacing-wide);text-transform:uppercase;margin-block-end:var(--sf-tp-space-2)}
.sf-theme-trade-pro .sf-tp-side-link{display:block;padding:calc(var(--sf-tp-space-4) - var(--sf-tp-space-1)) var(--sf-tp-space-4);border-block-end:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-default);font-weight:var(--sf-tp-font-weight-semibold);color:var(--sf-tp-color-text-secondary);font-size:var(--sf-tp-font-size-sm)} .sf-theme-trade-pro .sf-tp-side-link{display:block;padding:calc(var(--sf-tp-space-4) - var(--sf-tp-space-1)) var(--sf-tp-space-4);border-block-end:var(--sf-tp-border-width-hairline) solid var(--sf-tp-color-border-default);font-weight:var(--sf-tp-font-weight-semibold);color:var(--sf-tp-color-text-secondary);font-size:var(--sf-tp-font-size-sm)}
.sf-theme-trade-pro .sf-tp-side-link:hover{color:var(--sf-tp-color-text-brand)} .sf-theme-trade-pro .sf-tp-side-link:hover{color:var(--sf-tp-color-text-brand)}
@@ -343,6 +343,9 @@
.sf-theme-trade-pro .sf-tp-page-hero-copy{width:100%} .sf-theme-trade-pro .sf-tp-page-hero-copy{width:100%}
.sf-theme-trade-pro .sf-tp-catalogue-layout{grid-template-columns:1fr} .sf-theme-trade-pro .sf-tp-catalogue-layout{grid-template-columns:1fr}
.sf-theme-trade-pro .sf-tp-side-panel{position:static;display:grid;grid-template-columns:repeat(2,1fr);gap:0 var(--sf-tp-space-4)} .sf-theme-trade-pro .sf-tp-side-panel{position:static;display:grid;grid-template-columns:repeat(2,1fr);gap:0 var(--sf-tp-space-4)}
/* listing 栅格折叠规则须落在上方 3 列声明之后(同特异性按源顺序取胜),
既有 10a 的 900px 折叠块在本段之前,覆盖不到 --listing 修饰符 */
.sf-theme-trade-pro .sf-tp-card-grid--listing{grid-template-columns:repeat(2,minmax(0,1fr))}
.sf-theme-trade-pro .sf-tp-news-layout{grid-template-columns:1fr} .sf-theme-trade-pro .sf-tp-news-layout{grid-template-columns:1fr}
.sf-theme-trade-pro .sf-tp-news-row{grid-template-columns:240px 1fr} .sf-theme-trade-pro .sf-tp-news-row{grid-template-columns:240px 1fr}
} }
@@ -355,6 +358,7 @@
.sf-theme-trade-pro .sf-tp-page-hero-copy h1{font-size:28px;line-height:1.05;margin-block-end:8px} .sf-theme-trade-pro .sf-tp-page-hero-copy h1{font-size:28px;line-height:1.05;margin-block-end:8px}
.sf-theme-trade-pro .sf-tp-page-hero-lead{font-size:11px;line-height:1.35;margin-block-end:12px} .sf-theme-trade-pro .sf-tp-page-hero-lead{font-size:11px;line-height:1.35;margin-block-end:12px}
.sf-theme-trade-pro .sf-tp-side-panel{display:block} .sf-theme-trade-pro .sf-tp-side-panel{display:block}
.sf-theme-trade-pro .sf-tp-card-grid--listing{gap:10px}
.sf-theme-trade-pro .sf-tp-news-row{grid-template-columns:1fr} .sf-theme-trade-pro .sf-tp-news-row{grid-template-columns:1fr}
.sf-theme-trade-pro .sf-tp-news-row .sf-tp-card-media{aspect-ratio:16/9} .sf-theme-trade-pro .sf-tp-news-row .sf-tp-card-media{aspect-ratio:16/9}
.sf-theme-trade-pro .sf-tp-news-body h3{font-size:var(--sf-tp-font-size-lg)} .sf-theme-trade-pro .sf-tp-news-body h3{font-size:var(--sf-tp-font-size-lg)}