Files
cms-SiteForge/docs/superpowers/plans/2026-10-22-trade-pro-theme-port.md
T
浅念 8cea7dee20 chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec
- SiteForge Enterprise V8.10.0-dev.1 upstream source (210 tests)
- Fix: v8_workflows._mapped_input now reads both 类型/分类 columns so WXR
  imports keep post/product types instead of collapsing to page
- Docs: trade-pro theme port spec (R3) and implementation plan

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-10-22 16:49:25 +08:00

369 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# trade-pro 第 5 主题移植实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 把 cms Astro 主题移植为 SiteForge V8_10 第 5 套内置主题 trade-pro(英文外贸 B2B),平台适配 + 骨架跑通 + 完整模板 + 验收。
**Architecture:** 三步走——先做平台适配(主题发现单一源、宏覆盖机制、主题资产优先、6 处前端入口),再以骨架主题包在真实 hesupipe 站点(`483295c29ea0`,193 条内容)跑通构建,最后按 cms 设计源(`D:\project\cms\runtimes\astro-site-runtime`)逐模板移植视觉。设计依据:[spec R3](../specs/2026-10-22-trade-pro-theme-design.md)。
**Tech Stack:** Python 3.12 / FastAPI / Jinja2(FileSystemLoader 多目录)/ pytest / 原生 JS。
**环境事实(执行者必读):**
- 工作目录:`C:\Users\30997\Downloads\SiteForge_Factory_V5_Web_Docker\SiteForge_Enterprise_V8_10`(下称 `$ROOT`,bash 里用正斜杠)
- Docker 实例已运行:`http://127.0.0.1:8787`(容器 siteforge-v8-10-siteforge-1/-worker-1),admin 密码在 `.env`
- 测试命令:`python -m pytest -q`(当前 210 项全绿,任何改动后必须保持);JS 检查 `node --check app/static/<file>.js`
- cms 设计源:`D:/project/cms/runtimes/astro-site-runtime/src`(tokens.css 250 行、base.css 445 行、components/*.astro、pages/[locale]/*.astro);像素基准 `dist/hesupipe/en/`
- 改动 Python 后需 `docker compose up -d --build` 重建容器再验证 HTTP 层(构建约 1 分钟,有缓存)
- 契约清单已固化于 spec §2(F1-F13),本计划直接引用
---
## Phase 1+2:平台适配与骨架跑通(Task 1-8)
### Task 1: Git 基线
**Files:** 无新文件(初始化仓库)
- [ ] **Step 1:** `cd $ROOT && git init && git add -A && git status --short | head -20`
- [ ] **Step 2:** 确认 `.env`、`storage/`、`hesupipe_clean2.xml` 未被跟踪(.gitignore 已含 `.env`、`storage/`;`git check-ignore .env` 应有输出;`hesupipe_clean2.xml` 加入 .gitignore 一行)
- [ ] **Step 3:** `git commit -m "chore: baseline V8.10.0-dev.1 with v8_workflows type-mapping fix and trade-pro spec"`(含 Co-Authored-By 行,下同)
### Task 2: 主题发现单一源(F1)
**Files:**
- Modify: `app/core.py:12`
- Modify: `app/theme_pro.py:16-18`
- Modify: `app/theme_store.py:14`
- Test: `tests/test_theme_registry.py`(新建)
- [ ] **Step 1: 写失败测试**
```python
"""Theme discovery must be a single source of truth."""
from app import core, theme_pro, theme_store
def test_discover_returns_builtin_themes_with_manifests():
ids = core.discover_themes()
for tid in ('automotive', 'industrial', 'corporate', 'minimal'):
assert tid in ids
import json
for tid in ids:
manifest = json.loads((core.ROOT / 'themes' / tid / 'manifest.json').read_text(encoding='utf-8'))
assert manifest.get('id') == tid
def test_three_whitelists_agree():
assert set(core.THEMES) == set(theme_pro.THEMES) == set(theme_store.BUILTIN)
assert isinstance(core.THEMES, tuple)
```
注:`core.ROOT` 若不存在,测试里用 `pathlib.Path(app.__file__).parent.parent`。
- [ ] **Step 2:** `python -m pytest tests/test_theme_registry.py -q` → 预期 FAIL(`discover_themes` 不存在)
- [ ] **Step 3: 实现**——`app/core.py:12` 替换为:
```python
ROOT_PATH = __import__('pathlib').Path(__file__).resolve().parent.parent
def discover_themes():
"""Single source of truth: scan themes/*/manifest.json (id must match folder)."""
import json
themes_dir = ROOT_PATH / 'themes'
found = []
for folder in sorted(themes_dir.iterdir()):
if not folder.is_dir() or folder.name.startswith('_'):
continue
manifest = folder / 'manifest.json'
if not manifest.is_file():
continue
try:
if json.loads(manifest.read_text(encoding='utf-8')).get('id') == folder.name:
found.append(folder.name)
except (ValueError, UnicodeError):
continue
return tuple(found)
THEMES = discover_themes()
```
(core.py 顶部已有 `import json`,可复用;实现时去掉重复 import,风格与文件一致。)
`app/theme_pro.py:16-18` 改为:
```python
from .core import sanitize, route_path, THEMES
```
(删除原第 18 行的元组定义。)
`app/theme_store.py:14` 改为:
```python
from .core import THEMES as BUILTIN
```
- [ ] **Step 4:** `python -m pytest tests/test_theme_registry.py -q` → PASS;`python -m pytest -q` 全绿
- [ ] **Step 5:** `git add -A && git commit -m "feat: single-source theme discovery replaces three hardcoded whitelists"`
### Task 3: theme_pro 每主题环境(宏覆盖机制,F4)
**Files:**
- Modify: `app/theme_pro.py:18-20`(LOADER 旁新增 env_for)
- Modify: `app/theme_pro.py:190`(render 用 env_for)
- Test: `tests/test_theme_registry.py`(追加)
- [ ] **Step 1: 追加失败测试**
```python
def test_env_for_theme_prefers_theme_dir_then_shared_root():
from app import theme_pro
env = theme_pro.env_for('industrial')
paths = [str(p) for p in env.loader.searchpaths]
assert paths[0].replace('\\', '/').endswith('themes/industrial')
assert paths[1].replace('\\', '/').endswith('themes')
# 共享宏库必须仍可解析(回落路径)
assert env.get_template('_professional_components.html') is not None
def test_env_for_is_cached_per_theme():
from app import theme_pro
assert theme_pro.env_for('industrial') is theme_pro.env_for('industrial')
```
- [ ] **Step 2:** 跑测试 → FAIL(env_for 不存在)
- [ ] **Step 3: 实现**——`app/theme_pro.py` 在 LOADER 定义后追加:
```python
_ENVS = {}
def env_for(theme):
"""Per-theme Jinja env: theme dir first, shared themes/ root as fallback.
Lets a theme override shared macros (_professional_components.html) while
inheriting everything else. Cached: one env per theme across 5000-page builds.
"""
env = _ENVS.get(theme)
if env is None:
theme_dir = ROOT / 'themes' / theme
search = [str(theme_dir), str(ROOT / 'themes')] if theme_dir.is_dir() else [str(ROOT / 'themes')]
env = Environment(loader=FileSystemLoader(search), autoescape=select_autoescape(['html', 'xml']))
_ENVS[theme] = env
return env
```
`render()` 末行(:190)改为:
```python
return env_for(theme).get_template('pro.html').render(**ctx)
```
- [ ] **Step 4:** `python -m pytest -q` 全绿(现有 test_theme_pro_v3.py 即回归)
- [ ] **Step 5:** commit `feat: per-theme Jinja env enables theme-level macro override`
### Task 4: builder 主题资产优先(F5)
**Files:**
- Modify: `app/builder.py:191`
- Test: `tests/test_theme_registry.py`(追加)
- [ ] **Step 1: 追加失败测试**
```python
def test_theme_blocks_css_overrides_platform(tmp_path):
from app import builder
from pathlib import Path
dist = tmp_path / 'assets'; dist.mkdir()
theme_dir = tmp_path / 'theme'; theme_dir.mkdir()
(theme_dir / 'blocks.css').write_text('/* theme version */', encoding='utf-8')
builder.copy_blocks_css(theme_dir, dist)
assert (dist / 'blocks.css').read_text(encoding='utf-8') == '/* theme version */'
# 无主题版时回落平台版
dist2 = tmp_path / 'assets2'; dist2.mkdir()
builder.copy_blocks_css(tmp_path / 'nonexistent', dist2)
assert (dist2 / 'blocks.css').is_file()
```
- [ ] **Step 2:** 跑 → FAIL
- [ ] **Step 3: 实现**——`app/builder.py:191` 的 `shutil.copy2(ROOT/'app'/'static'/'blocks.css',dist/'assets'/'blocks.css')` 替换为调用新函数,并在文件合适位置(output_path 附近)定义:
```python
def copy_blocks_css(theme_dir, dist_assets):
"""Theme-provided blocks.css wins; platform default is the fallback."""
src = theme_dir / 'blocks.css' if (theme_dir / 'blocks.css').is_file() else ROOT / 'app' / 'static' / 'blocks.css'
shutil.copy2(src, dist_assets / 'blocks.css')
```
- [ ] **Step 4:** `python -m pytest -q` 全绿;commit `feat: theme-level blocks.css takes priority over platform asset`
### Task 5: trade-pro 骨架主题包(Phase 2 核心)
**Files:**
- Create: `themes/trade-pro/manifest.json`、`site.html`、`pro.html`、`_professional_components.html`、`theme.css`、`blocks.css`、`pro.css`、`page-presets.json`、`pages/`(14 个 html)、`visual/.gitkeep`、`versions/.gitkeep`
- Test: `tests/test_theme_registry.py`(追加)
- [ ] **Step 1: 追加失败测试**
```python
def test_trade_pro_registered_and_compatible():
assert 'trade-pro' in core.THEMES
result = theme_store.compatibility('trade-pro')
assert result['valid'], result['errors']
assert not result['errors']
def test_trade_pro_builds_real_site():
# 集成冒烟:真实构建函数对演示站点渲染 home 不抛异常(复用现有测试夹具方式,
# 参考 tests/test_theme_pro_v3.py 中的最小站点夹具)
...
```
(第二个测试复用 test_theme_pro_v3.py 已有的最小站点构建夹具写法,theme 传 'trade-pro'。)
- [ ] **Step 2:** 跑 → FAIL
- [ ] **Step 3: 创建文件**
`manifest.json`:
```json
{
"id": "trade-pro",
"name": "Trade Pro — International B2B",
"description": "English-first international B2B theme ported from the hesupipe Astro design: SVG icon set, responsive trade catalogue, product specs and inquiry CTA.",
"version": "1.0.0",
"style": "international-b2b",
"entry": "pro.html",
"styles": ["theme.css", "blocks.css"],
"page_templates": ["home","about","contact","listing","product","article","case","standard","category"],
"blocks": ["hero","richtext","features","stats","collection","image_text","gallery","faq","cta","attributes","spacer","layout","carousel","video","tabs","pricing","map","form_link"],
"responsive": true,
"auto_binding": true,
"professional_layout": true,
"commercial_ui": true,
"accent": "#0b3d91"
}
```
`site.html`:以 `themes/industrial/site.html` 为底,改 `lang="zh-CN"` → `lang="en"`,其余变量占位不动(它只包 {{content}})。
`_professional_components.html`:先做**透传骨架**(从共享库复制 card/cover 等宏,Task 10 起逐个换成 cms 设计 + SVG 图标)。
`pro.html`:以 industrial 的 pro.html 为底最小化:`lang="en"`;页眉/页脚消费 site_design chrome 但按 spec 4.2.5 值比对输出英文兜底(中文默认值视为未配置);正文区渲染 page content 与 custom_blocks;按钮类名一律 `sf-tp-*`。
`theme.css`:先放 tokens 骨架(Task 9 全量移植):
```css
:root{--sf-tp-primary:#0b3d91;--sf-tp-accent:#f5a623;--sf-tp-bg:#ffffff;--sf-tp-text:#1a2332;--sf-tp-muted:#5a6a7e;--sf-tp-radius:8px;--sf-tp-font:'Inter',system-ui,-apple-system,sans-serif}
```
`blocks.css`:空文件 + 注释 `/* trade-pro component styles: filled in Phase 3 */`。
`pro.css`:空文件(兼容性硬性要求,spec F8)。
`page-presets.json`:**复制 industrial 的同名文件原样**(Phase 4 精修;13 种 key 齐全即可)。
`pages/`:`home.html` 写最小可用骨架(hero + showcase 列表 + cta 引用共享宏);其余 13 个(about/contact/listing/product/brand/article/case/service/faq/category/search/standard/notfound)先各写薄变体(复用 home 的结构、引用同样宏),文件必须存在(spec F7 硬性)。
- [ ] **Step 4:** `python -m pytest -q` 全绿(含新测试)
- [ ] **Step 5:** `docker compose up -d --build` 后:`curl -s http://127.0.0.1:8787/api/health` 版本不变、`curl -s "http://127.0.0.1:8787/api/factory/themes/trade-pro/compatibility"`(带登录 cookie)返回 valid=true
- [ ] **Step 6:** commit `feat: trade-pro skeleton theme package (Phase 2)`
### Task 6: theme_color 映射 + 6 处前端入口(F3/F11)
**Files:**
- Modify: `app/theme_pro.py`(theme_color 字典,make_context 内,约 :171)
- Modify: `app/templates/admin.html:13`(siteTheme select)
- Modify: `app/templates/workspace.html:41`(建站模态框 theme select)
- Modify: `app/templates/theme-gallery.html:4`(theme select)
- Modify: `app/static/workspace.js:5`(themeName)
- Modify: `app/static/admin.js:5`(cfg)
- Modify: `app/static/factory-v2.js:30`(schemes)
- Test: `tests/test_theme_registry.py`(追加)
- [ ] **Step 1: 追加失败测试**
```python
def test_frontend_entries_include_trade_pro():
import pathlib
base = pathlib.Path(__file__).resolve().parent.parent
for rel, needle in [
('app/templates/admin.html', 'value="trade-pro"'),
('app/templates/workspace.html', 'value="trade-pro"'),
('app/templates/theme-gallery.html', 'value="trade-pro"'),
('app/static/workspace.js', "'trade-pro'"),
('app/static/admin.js', "'trade-pro'"),
('app/static/factory-v2.js', "'trade-pro'"),
]:
assert needle in (base / rel).read_text(encoding='utf-8'), rel
def test_theme_color_map_covers_trade_pro():
# theme_pro.make_context 的 theme_color 字典含 trade-pro(渲染 smoke 断言 meta theme-color)
...
```
- [ ] **Step 2:** 跑 → FAIL
- [ ] **Step 3: 实现**——逐处追加(保持原文风格):
- theme_color 字典追加 `'trade-pro':'#0b3d91'`
- 三个 HTML select 各追加:`<option value="trade-pro">Trade Pro · 外贸 B2B(英文)</option>`(theme-gallery 用 `Trade Pro · International B2B`)
- `workspace.js` themeName 追加 `'trade-pro':'Trade Pro'`
- `admin.js` cfg 追加 `trade-pro:{label:'Trade Pro',desc:'英文外贸 · B2B 目录与询盘',prev:'corp',title:'Go global.'}`
- `factory-v2.js` schemes 追加 `'trade-pro':['#0b3d91','#e8f1fb','GO GLOBAL']`
- [ ] **Step 4:** `python -m pytest -q` 全绿 + `for f in app/static/*.js; do node --check "$f"; done` 通过
- [ ] **Step 5:** commit `feat: register trade-pro in theme color map and all six frontend entry points`
### Task 7: E2E 验收——真实站点切主题构建(Phase 2 门槛)
**Files:** 无代码(验证 + 记录)
- [ ] **Step 1:** 登录拿 cookie/CSRF(参照已验证流程:POST /api/auth/login → GET /api/auth/me)
- [ ] **Step 2:** `PUT /api/factory/sites/483295c29ea0/theme` body `{"theme":"trade-pro"}`(确认返回成功)
- [ ] **Step 3:** `POST /api/sites/483295c29ea0/builds`(Idempotency-Key + CSRF)→ 轮询 job 至 done
- [ ] **Step 4:** 断言:pages ≥203、errors 空;预览首页 HTTP 200、含 `lang="en"`、不含"首页/联系我们"(平台中文默认,spec F9 验收);`/products/`、`/product/alloy-resin-system-window/` 200
- [ ] **Step 5:** 断言 6 入口:新工作台/`/classic` 建站表单、theme-gallery、factory-studio 主题卡里均出现 trade-pro(浏览器人工点验,截图存 `docs/superpowers/` 或记录于验收文件)
- [ ] **Step 6:** 把结果写入 `docs/superpowers/specs/2026-10-22-trade-pro-theme-design.md` 附录"Phase 2 验收记录";commit
---
## Phase 3:完整视觉移植(Task 8-12)
> 以下任务的代码无法在计划里全文预写(从 cms 源逐段移植),但每步给出**精确源文件、提取规则与验收断言**。共用纪律:每完成一个 Task 跑全量 pytest + commit。
### Task 8: 设计令牌与基础样式全量移植
- 源:`D:/project/cms/runtimes/astro-site-runtime/src/styles/tokens.css`(250 行)+ `base.css`(445 行)
- [ ] 变量重命名加 `--sf-tp-` 前缀(spec 4.2.6);按钮/交互元素全部 `sf-tp-` 类名(避开平台 `.btn/.primary/.request/.button/.nav-btn`)
- [ ] 验收断言:`themes/trade-pro/theme.css` 覆盖 tokens 全集(颜色/字体/圆角/间距);构建产物 `assets/theme.css` 含 `--sf-tp-` 变量;平台 custom.css 的 `.btn{...}` 与主题按钮类无交集(grep 断言)
- [ ] pytest(现有快照不受影响)+ commit
### Task 9: 图标宏库(替代箭头)
- 源:cms 组件与 dist HTML 中的内联 SVG(`grep -o '<svg[^>]*>' dist/hesupipe/en/index.html` 起逐个提取;参考 `src/components/*.astro` 里的图标用法)
- [ ] `themes/trade-pro/_professional_components.html` 定义 `icon(name)` 宏:内联 SVG 集合(≥8 个:arrow-right/check/phone/mail/location/whatsapp/download/quote 等,从 cms 实际用到的集合提取)
- [ ] 验收:home/product 页渲染输出含 `<svg` 且无裸文本箭头 `→` 作为唯一图标(对照 dist)
- [ ] commit
### Task 10: 九种独立布局逐个移植(每布局一个 commit)
顺序与源对照(cms `src/pages/[locale]/` ↔ dist `en/` ↔ trade-pro `pages/`):
1. `home`:index.astro ↔ dist 首页(Hero/FeatureGrid/CardGrid/CTA 结构)
2. `product`:products/[slug].astro ↔ dist/products/alloy-resin-system-window/(参数表 specs、多图 gallery、询盘 CTA)
3. `listing`:news/[...page].astro + products 列表 ↔ 按 kind 分支(Phase 0 已确认模板可拿到 page/kind 上下文;若不可分支由 category 承担产品列表,spec §7 兜底)
4. `category`:product-category ↔ 分类页
5. `article`:news/[slug] ↔ 文章详情
6. `about` / 7. `contact`(LeadForm 样式 + form_endpoint)/ 8. `case`(projects)/ 9. `standard`(applications、support)
每个布局的统一验收:构建对应真实页面 200;关键结构选择器与 dist 对照(header/hero/卡片/参数表/页脚);英文文案断言(无中文默认);手机宽度可读(人工抽查一次,全量视觉验收在 Task 12)。
### Task 11: 五个变体布局 + search
- brand/service/faq/notfound/search 以 standard/listing 变体补齐为"结构合理"版本(后续迭代精修,spec 4.3)
### Task 12: 预设、编辑器与全量验收(Phase 4)
- [ ] `page-presets.json` 13 种按 cms 首页/产品页实际区块结构重写(编辑器初始布局与真实设计一致)
- [ ] 编辑器验证:hero/cta/features/collection/attributes 在 trade-pro 下可编辑、预览、保存草稿、发布(spec §6)
- [ ] 全站设计草稿验证:页眉/页脚/导航配置生效(显式值渲染、中文默认被值比对替换为英文兜底)
- [ ] 全量回归:`python -m pytest -q`(≥210+新增全绿)、`node --check` 全部 JS
- [ ] 实战验收:站点 483295c29ea0 最终构建 ≥203 页零错误;视觉对照两层标准(spec §6)逐页记录
- [ ] 更新 spec 附录验收记录;commit
## 风险与回退
- 每个 Task 独立 commit,出问题 `git revert` 单点回退
- 平台适配层(Task 2-4)有 4 主题回归保护:任何现有测试变红立即停止
- trade-pro 目录独立,删目录 + revert 白名单改动即完全移除