feat: port cms header/footer chrome visuals + mobile drawer into trade-pro

Task 10j: reshape the platform chrome DOM (sf-site-head/pro-header/
pro-navigation/pro-mega/pro-footer) into the cms Header.astro/Footer.astro
design via a pure CSS layer in trade-pro pro.css (--pro-* token bridge +
scoped 0,2,0 overrides): sticky glass header, right-aligned semibold nav
with brand underline hover, white dropdown cards with desktop hover/focus
reveal (>=1025px only; drawer semantics below), cms button CTA, dark
four-column-style footer with hairline copyright bar, cms wrap geometry.

Mobile drawer reuses the upstream V8.21.3 four-theme mechanism verbatim
(commercial.css locked styles + pro.js generic [data-pro-menu]/
#pro-navigation hooks trade-pro already emits); mobile_tabs is
template-driven so trade-pro gains the render block (English labels,
data-mobile-open-menu opens the shared drawer).

pro.html: +footer_nav-gated <h3>Quick Links</h3> (cms column heading),
+mobile tabs block. No DOM restructuring elsewhere; site_design data flow
(announcement/CTA/search/footer/mobile_tabs) and pro.js hooks intact.

Tests: +test_trade_pro_chrome_layout (structure/hooks/EN fallback nav/
css evidence/drawer assets + safe-area/mobile_tabs publish cycle).
35 passed in theme suite; full run 364 passed + same 10 known infra fails.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-10-23 17:53:14 +08:00
parent 87ce104d7e
commit a63548c645
3 changed files with 265 additions and 1 deletions
+106
View File
@@ -798,6 +798,112 @@ def test_trade_pro_standard_layout_structure(factory):
assert ssoup.select_one('.sf-tp-page-hero-lead') is not None # bootstrap 摘要在导语位
def test_trade_pro_chrome_layout(factory):
# Task 10j:cms 页眉/页脚 chrome 视觉还原 + 手机端抽屉导航。
# 设计源 D:/project/cms/runtimes/astro-site-runtime/src/components/
# Header.astro(玻璃吸顶 + 右对齐半粗导航 + 悬停下划线/下拉卡 + 移动
# 右滑抽屉)/ Footer.astro(深色四栏 + 版权条),10j 以纯样式层把平台
# chrome DOM(sf-site-head/pro-header/pro-navigation/pro-footer)重塑为
# cms 设计——平台数据流(site_design 导航/公告/CTA/页脚选项)与 pro.js
# 交互钩子(data-pro-menu/data-pro-catalog/data-pro-mega/pro-nav-disclose)
# 全部保留。抽屉复用上游 V8.21.3 四主题机制(commercial.css 样式段 +
# pro.js 通用逻辑:选择器寻钩子而非主题分支,trade-pro 输出同款类名
# 结构即自动可用);mobile_tabs 为模板驱动,10j 给 trade-pro 补上
# 渲染块后配置即生效(下方发布前后双构建断言)。
import re
sid = site(factory)
assert factory.post(f'/api/factory/sites/{sid}/bootstrap').status_code == 200
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']
html = factory.get(f'/preview/{sid}/{bid}/').text
soup = BeautifulSoup(html, 'html.parser')
# ── 平台 chrome 结构与交互钩子保留(数据流不破坏的结构证据)──
header = soup.select_one('header.sf-site-head.pro-header')
assert header is not None
assert header.select_one('.pro-wrap.pro-head-inner') is not None
assert header.select_one('a.pro-logo[href$="/"]') is not None # /preview/ 重写根相对 href,用后缀匹配
toggle = header.select_one('button.pro-mobile-toggle[data-pro-menu]')
assert toggle is not None and toggle.get('aria-controls') == 'pro-navigation'
nav = header.select_one('nav#pro-navigation.pro-navigation')
assert nav is not None and nav.get('aria-label')
# catalog/mega 钩子(aria-controls 对应 mega 面板 id;面板本体按 categories 条件渲染)
catalog = header.select_one('button.pro-catalog-toggle[data-pro-catalog]')
assert catalog is not None and catalog.get('aria-controls') == 'pro-mega-content'
# CTA 双挂 sf-tp-btn(cms 蓝按钮形态)+ chrome.header CTA 数据位
cta = header.select_one('a.pro-header-cta.sf-tp-btn')
assert cta is not None and cta.get('href', '').endswith('/contact/')
# 公告条数据位仍在(未发布 site_design 时渲染英文兜底文案)
assert soup.select_one('.pro-utility .pro-wrap') is not None
# ── 英文兜底导航(tp_nav):7 项默认栏目全渲染(尾部可跟 Search 链接——
# chrome.header.show_search 数据位默认开)──
nav_links = nav.select(':scope > a, :scope > .pro-nav-group > a')
nav_text = [a.get_text(strip=True) for a in nav_links]
assert nav_text[:7] == ['Home', 'Products', 'Brands', 'News', 'Cases', 'About', 'Contact']
assert nav_text[7:] in ([], ['Search'])
# ── 页脚:平台 DOM + cms 栏目标题(Quick Links 随 footer_nav 数据流)
footer = soup.select_one('footer.sf-site-foot.pro-footer')
assert footer is not None
assert footer.select_one('.pro-footer-top') is not None
heading = footer.select_one('.pro-footer-links h3')
assert heading is not None and heading.get_text(strip=True) == 'Quick Links'
footer_nav = footer.select('.pro-footer-links ul li a')
assert len(footer_nav) == 7 and footer_nav[0].get_text(strip=True) == 'Home'
assert footer.select_one('.pro-footer-bottom') is not None
# ── 10j 视觉层随构建分发(pro.css:cms chrome 映射的样式证据)──
pro_css = factory.get(f'/preview/{sid}/{bid}/assets/pro.css').text
assert 'Task 10j' in pro_css
# 玻璃拟态吸顶(Header.astro .site-header)
assert re.search(r'\.sf-theme-trade-pro \.pro-header\{[^}]*position:sticky', pro_css)
assert 'backdrop-filter:blur(10px)' in pro_css
# 导航:cms 右对齐半粗 + 悬停下划线(.nav-links a 的 border-block-end 方案)
assert re.search(r'\.sf-theme-trade-pro \.pro-navigation \.pro-nav-group>a[^{]*\{[^}]*border-block-end', pro_css)
assert 'var(--sf-tp-color-border-brand)' in pro_css
# 下拉卡(.sub-menu):白卡 + 发丝边 + radius-sm + shadow-md 的令牌引用
assert re.search(r'\.sf-theme-trade-pro \.pro-nav-children\{[^}]*var\(--sf-tp-color-surface-default\)', pro_css)
# 深色页脚(.site-footer 令牌桥:surface-inverse + 版权条 border-inverse)
assert re.search(r'\.sf-theme-trade-pro \.pro-footer\{[^}]*var\(--sf-tp-color-surface-inverse\)', pro_css)
assert 'var(--sf-tp-color-border-inverse)' in pro_css
# 抽屉品牌化变量桥:平台 drawer 样式段消费 --pro-* 变量,10j 注入 cms 令牌
assert '--pro-background:var(--sf-tp-color-surface-default)' in pro_css
# ── 上游抽屉机制复用(commercial.css/pro.js 随构建分发 + 安全区适配)──
assert 'assets/commercial.css' in html and 'assets/pro.js' in html
commercial = factory.get(f'/preview/{sid}/{bid}/assets/commercial.css').text
assert '.sf-mobile-drawer-backdrop' in commercial and 'sf-drawer-open' in commercial
assert 'env(safe-area-inset-bottom' in commercial # 抽屉/底部 tabs 的安全区适配
pro_js = factory.get(f'/preview/{sid}/{bid}/assets/pro.js').text
assert 'sf-drawer-header' in pro_js and 'sf-drawer-extra' in pro_js and 'Escape' in pro_js
# ── mobile_tabs:发布 site_design 前不渲染,发布后渲染(含 menu 动作开抽屉)──
assert 'data-mobile-tabs' not in html
import copy
from app import site_design_v87
config = copy.deepcopy(site_design_v87.DEFAULTS)
config['mobile_tabs'] = {'enabled': True, 'items': [
{'title': 'Home', 'icon': 'home', 'route': '/', 'action': 'link'},
{'title': 'Products', 'icon': 'products', 'route': '/products/', 'action': 'link'},
{'title': 'Menu', 'icon': 'menu', 'route': '/', 'action': 'menu'}]}
url = f'/api/sites/{sid}/site-design'
assert factory.put(url, json={'config': config, 'expected_revision': 0}).status_code == 200
assert factory.post(url + '/publish', json={'expected_revision': 1}).status_code == 200
bid2 = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
html2 = factory.get(f'/preview/{sid}/{bid2}/').text
assert 'data-mobile-tabs' in html2 and 'data-mobile-open-menu' in html2
tabs = BeautifulSoup(html2, 'html.parser').select('.sf-mobile-tabs .sf-mobile-tab')
assert len(tabs) == 3
assert [t.select_one('.sf-tab-label').get_text(strip=True) for t in tabs] == ['Home', 'Products', 'Menu']
assert tabs[0].select_one('.sf-tab-icon') is not None # 图标位渲染
# 发布后 chrome_is_custom 生效:公告条走配置数据位(announcement 文案 +
# show_announcement 开关)。publish 会清空草稿 → 新 PUT 从 revision 0 重新开始
announced = {**config, 'header': {**config['header'], 'announcement': 'Global sourcing update', 'show_announcement': True}}
assert factory.put(url, json={'config': announced, 'expected_revision': 0}).status_code == 200
assert factory.post(url + '/publish', json={'expected_revision': 1}).status_code == 200
bid3 = factory.post(f'/api/sites/{sid}/builds', json={}).json()['version']
html3 = factory.get(f'/preview/{sid}/{bid3}/').text
assert 'Global sourcing update' in html3
# 本构建链接全通(chrome 改动不产生断链)
report = factory.get(f'/api/sites/{sid}/builds/{bid3}/report').json()
assert not report['errors'] and not report['broken_links']
def test_frontend_entries_include_trade_pro():
from pathlib import Path
base = Path(__file__).resolve().parent.parent