Files
hesu-JT/docs/superpowers/plans/2026-08-15-hesu-wordpress-theme.md
T
2026-08-27 11:54:26 +08:00

669 lines
39 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.
# HESU WordPress 主题实施计划
> **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:** 把 `d:\wp\demojt` 的 10 页静态站迁移为 WordPress 经典主题 `hesu`(原生 Gutenberg 区块 + 结构锁定 + CPT 新闻 + 激活播种),在一次性 SQLite 测试站实跑验收。
**Architecture:** 经典 PHP 主题 + 22 个服务端渲染自定义区块(block.json + render.php + 无构建原生 wp.element 编辑器脚本),内容存 post_content 并用 `templateLock` + 实例锁锁死结构;CSS/JS 从静态站原样迁移、零改写(仅 main.js 小幅扩展);`after_switch_theme` 播种全部页面/菜单/新闻/图片。
**Tech Stack:** PHP ≥7.4、WordPress ≥6.1(测试站 6.x 最新)、Contact Form 7、便携 PHP(测试用,不入库)、wp-cli phar、wp-sqlite-db drop-in。
**规格文档(唯一需求来源):** `docs/superpowers/specs/2026-08-15-hesu-wordpress-theme-design.md` — 实现中所有取舍以 spec 为准;本计划任务与其 §编号 对应。
**重要约定(全任务适用):**
- 静态 HTML 是前端输出的**唯一真相**:每个区块 render.php 的输出必须与对应静态片段**逐 class 逐层级一致**。
- CSS 原样迁移;仅两处例外:main.js 追加 reveal 扩展、contact.css 末尾**追加** CF7 桥接规则(不改既有规则)。contact.js 迁移时**剥离表单校验分支**(CF7 接管),保留其余逻辑。
- RichText 属性一律普通 string(**禁止 `source:'html'`**,动态区块会丢内容)。
- **save 规则**:叶子区块 `save: () => null`;**容器区块(含 InnerBlocks)save 必须返回 `el(wp.blockEditor.InnerBlocks.Content)`**,否则序列化时内层内容全部丢失。
- 所有输出转义(esc_html/esc_attr/esc_url);`stat-num` 内数字文本节点必须是 firstChild。
- 测试环境位于 `d:\wp\demojt\.test-env\`(gitignore),主题位于 `d:\wp\demojt\hesu-theme\hesu\`,测试站以 junction 挂载(`cmd //c mklink /J`)。
- 无单元测试框架。每个任务的"测试"= `php -l` + curl/wp-cli 断言(先写断言命令,再实现,再跑),Task 13 全量验收兜底。
- 每个任务结束必须 commit(Task 0 建立 git)。
---
### Task 0: Git 仓库初始化
**Files:**
- Create: `d:\wp\demojt\.gitignore`
- [ ] **Step 1: 初始化仓库与 .gitignore**
`.gitignore` 内容:
```
.test-env/
*.zip
.playwright-mcp/
```
- [ ] **Step 2: 提交静态站基线**
```bash
cd d:/wp/demojt && git init && git add -A && git commit -m "chore: static site baseline (source of truth)"
```
Expected: 初始提交完成,`.test-env` 不入库。
---
### Task 1: 一次性测试环境(便携 PHP + WP + SQLite + CF7)
**Files:**(全部在 `.test-env/`,不 commit)
- `.test-env/php/`(便携 PHP)
- `.test-env/wp-cli.phar`
- `.test-env/wp/`(WordPress)
- `.test-env/wp/wp-content/db.php`(wp-sqlite-db drop-in)
- `.test-env/wp/wp-content/plugins/contact-form-7/`
- [ ] **Step 1: 下载便携 PHP(含 pdo_sqlite)**
从 https://windows.php.net/downloads/releases/ 下载最新 PHP 8.2 "VS16 x64 Non Thread Safe" zip,解压到 `.test-env/php/`。
- [ ] **Step 2: 配置 php.ini**
复制 `php.ini-development` 为 `php.ini`,确保以下扩展行去掉分号:`extension_dir="ext"`、`extension=pdo_sqlite`、`extension=sqlite3`、`extension=mbstring`、`extension=curl`、`extension=openssl`、`extension=fileinfo`、`extension=gd`。curl 需要 cacert:下载 https://curl.se/ca/cacert.pem 到 `.test-env/php/cacert.pem` 并设 `curl.cainfo=` 与 `openssl.cafile=`。
验证:`.test-env/php/php.exe -v` 输出版本;`php -m | grep -i sqlite` 有 pdo_sqlite。
- [ ] **Step 3: 下载 wp-cli 与 WordPress**
```bash
cd d:/wp/demojt/.test-env
curl -sOL https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
curl -sOL https://wordpress.org/latest.zip
unzip -q latest.zip && mv wordpress wp
php/php.exe wp-cli.phar --info --path=wp | head -5
```
- [ ] **Step 4: 安装 wp-sqlite-db drop-in**
下载 https://github.com/aaemnnosttv/wp-sqlite-db/raw/master/db.php(或其 src/db.php)到 `wp/wp-content/db.php`。同时创建空目录 `wp/wp-content/database/`。
- [ ] **Step 5: 安装 WP 与 CF7**
```bash
cd d:/wp/demojt/.test-env
php/php.exe wp-cli.phar core install --path=wp --url=http://127.0.0.1:8090 \
--title="HESU Dev" --admin_user=admin --admin_password=admin123 --admin_email=admin@example.com --skip-email
php/php.exe wp-cli.phar plugin install contact-form-7 --path=wp --activate
php/php.exe wp-cli.phar rewrite structure '/%postname%/' --hard --path=wp
```
- [ ] **Step 6: 启动开发服务器并验证**
```bash
cd d:/wp/demojt/.test-env/wp && ../php/php.exe -S 127.0.0.1:8090
```
(后台运行;后续所有 curl 断言打 `http://127.0.0.1:8090`)
```bash
curl -s http://127.0.0.1:8090/ | grep -o "<title>.*</title>"
```
Expected: 输出 HESU Dev 站点标题。数据库为 `.test-env/wp/wp-content/database/.ht.sqlite`(无 MySQL)。
- [ ] **Step 7: Commit**(仅提交可能的辅助脚本,`.test-env` 已 ignore)
```bash
cd d:/wp/demojt && git add -A && git commit -m "chore: disposable WP test env (ignored)" --allow-empty
```
---
### Task 2: 主题骨架 + 静态资产迁移 + header/footer
**Files:**
- Create: `hesu-theme/hesu/style.css`、`functions.php`、`header.php`、`footer.php`、`index.php`
- Create: `hesu-theme/hesu/inc/setup.php`、`inc/assets.php`
- Create: `hesu-theme/hesu/css/`、`js/`、`fonts/`、`image/`(从根目录原样拷贝,保持相对结构)
- Create: `.test-env/wp/wp-content/themes/hesu`(junction 指向 `d:/wp/demojt/hesu-theme/hesu`)
- [ ] **Step 1: 拷贝静态资产**
```bash
cd d:/wp/demojt && mkdir -p hesu-theme/hesu
cp -r css js fonts image hesu-theme/hesu/
```
注意:`style.css` 会被主题版覆盖(下一步),其余原样。
- [ ] **Step 2: style.css 主题头 + 原样式**
在拷入的 `css/style.css` 基础上,主题根 `style.css` = 以下头部 + `css/style.css` 全文:
```css
/*
Theme Name: HESU
Theme URI: https://example.com
Author: HESU
Description: HESU corporate theme — building materials export, auto export, after-sales & modification, global logistics.
Version: 1.0.0
Requires at least: 6.1
Requires PHP: 7.4
Text Domain: hesu
*/
```
(`6.1` 因 block.json `render` 属性需 WP 6.1+。style.css 中相对引用 `image/`、`fonts/` 的路径层级与静态站一致,路径不变。)
- [ ] **Step 3: index.php(WP 必需,缺则主题 broken)**
```php
<?php get_header(); ?>
<main class="page-content"><?php
while ( have_posts() ) { the_post(); the_content(); }
?></main>
<?php get_footer(); ?>
```
- [ ] **Step 4: functions.php + inc/setup.php**
`functions.php`:
```php
<?php
define( 'HESU_VERSION', '1.0.0' );
define( 'HESU_DIR', get_template_directory() );
define( 'HESU_URI', get_template_directory_uri() );
require HESU_DIR . '/inc/setup.php';
require HESU_DIR . '/inc/assets.php';
// 后续任务追加: cpt-news.php, blocks.php, walker-mega.php, customizer.php, seed.php
```
`inc/setup.php`:
```php
<?php
add_action( 'after_setup_theme', function () {
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails', array( 'post', 'page', 'news' ) );
add_theme_support( 'custom-logo', array( 'height' => 60, 'flex-width' => true ) );
add_theme_support( 'html5' );
add_post_type_support( 'page', 'excerpt' );
register_nav_menus( array(
'primary' => __( 'Primary Menu', 'hesu' ),
'footer' => __( 'Footer Menu', 'hesu' ),
) );
} );
```
- [ ] **Step 5: inc/assets.php(全站层)**
```php
<?php
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style( 'hesu-fonts', HESU_URI . '/css/fonts.css', array(), HESU_VERSION );
wp_enqueue_style( 'hesu-style', get_stylesheet_uri(), array( 'hesu-fonts' ), HESU_VERSION );
wp_enqueue_script( 'hesu-main', HESU_URI . '/js/main.js', array(), HESU_VERSION, true );
} );
```
- [ ] **Step 6: header.php / footer.php**
以 `index.html:17-118`(header)与 `index.html:322-387`(footer)为模板移植:
- Logo:`the_custom_logo()`,无自定义 logo 时回退 `image/logo.png`。
- 导航部分此阶段先输出**静态占位结构**(保持 `.nav-item.has-mega`、`.mega` 全部 class/层级),菜单数据化在 Task 10 实现。
- `<span id="year">` 保留(main.js 用)。
- `wp_head()` / `wp_footer()` / `body_class()` 齐全;`<main>` 由区块渲染,模板不放。
- [ ] **Step 7: junction 挂载主题并激活**
```bash
cd d:/wp/demojt/.test-env/wp/wp-content/themes
cmd //c "mklink /J hesu d:\\wp\\demojt\\hesu-theme\\hesu"
cd d:/wp/demojt/.test-env && php/php.exe wp-cli.phar theme activate hesu --path=wp
```
- [ ] **Step 8: 断言**
```bash
cd d:/wp/demojt/hesu-theme/hesu && ../../.test-env/php/php.exe -l header.php && ../../.test-env/php/php.exe -l footer.php && ../../.test-env/php/php.exe -l index.php
curl -s http://127.0.0.1:8090/ | grep -c "site-header"
```
Expected: 语法通过;首页含 header 结构(此时首页为空内容,正常)。
- [ ] **Step 9: Commit**
```bash
cd d:/wp/demojt && git add hesu-theme && git commit -m "feat(theme): skeleton, assets, header/footer"
```
---
### Task 3: 新闻 CPT + 分类法
**Files:**
- Create: `hesu-theme/hesu/inc/cpt-news.php`(functions.php 追加 require)
- [ ] **Step 1: 实现**
```php
<?php
add_action( 'init', function () {
register_post_type( 'news', array(
'labels' => array(
'name' => __( 'News', 'hesu' ), 'singular_name' => __( 'News Article', 'hesu' ),
'add_new_item' => __( 'Add New News Article', 'hesu' ),
),
'public' => true, 'has_archive' => true, 'menu_icon' => 'dashicons-megaphone',
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'rewrite' => array( 'slug' => 'news' ),
'show_in_rest' => true,
) );
register_taxonomy( 'news_cat', 'news', array(
'labels' => array( 'name' => __( 'News Categories', 'hesu' ) ),
'public' => true, 'hierarchical' => false,
'show_in_rest' => true,
'rewrite' => array( 'slug' => 'news-cat' ),
) );
} );
add_action( 'pre_get_posts', function ( $q ) {
if ( is_admin() || ! $q->is_main_query() ) return;
if ( $q->is_post_type_archive( 'news' ) || $q->is_tax( 'news_cat' ) ) {
$q->set( 'posts_per_page', 5 );
}
} );
```
- [ ] **Step 2: 断言**
```bash
curl -s "http://127.0.0.1:8090/?post_type=news" -o /dev/null -w "%{http_code}"
cd d:/wp/demojt/.test-env && php/php.exe wp-cli.phar taxonomy list --path=wp --format=csv | grep news_cat
```
Expected: 200;taxonomy 存在。刷新固定链接:`wp-cli.phar rewrite flush --path=wp`。
- [ ] **Step 3: Commit** `feat(news): CPT + taxonomy`
---
### Task 4: 区块框架 + 范例区块 section-heading
**Files:**
- Create: `hesu-theme/hesu/inc/blocks.php`(functions.php 追加 require)
- Create: `hesu-theme/hesu/blocks/editor/helpers.js`
- Create: `hesu-theme/hesu/blocks/section-heading/{block.json,render.php,index.js}`
- Create: `hesu-theme/hesu/editor-style.css`(此任务建基础版,Task 12 Step 2 扩充至全部页面 CSS)
- [ ] **Step 1: inc/blocks.php 自动注册**
```php
<?php
// 独立编辑器脚本不做任何构建:block.json 用 file: 引用,WP 自动生成 handle。
add_action( 'init', function () {
wp_register_script( 'hesu-blocks-helpers', HESU_URI . '/blocks/editor/helpers.js',
array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components' ), HESU_VERSION );
foreach ( glob( HESU_DIR . '/blocks/*', GLOB_ONLYDIR ) as $dir ) {
if ( file_exists( $dir . '/block.json' ) ) {
register_block_type( $dir );
}
}
} );
```
- [ ] **Step 2: helpers.js(全区块共用)**
```js
window.HESU = {
el: wp.element.createElement,
RichText: wp.blockEditor.RichText,
MediaUpload: wp.blockEditor.MediaUpload,
useBlockProps: wp.blockEditor.useBlockProps,
InspectorControls: wp.blockEditor.InspectorControls,
TextControl: wp.components.TextControl,
SelectControl: wp.components.SelectControl,
PanelBody: wp.components.PanelBody,
};
```
- [ ] **Step 3: 范例区块 section-heading 完整实现**
`blocks/section-heading/block.json`:
```json
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "hesu/section-heading",
"title": "Section Heading",
"category": "hesu",
"render": "file:./render.php",
"editorScript": ["hesu-blocks-helpers", "file:./index.js"],
"attributes": {
"title": { "type": "string", "default": "" },
"lead": { "type": "string", "default": "" },
"btnLabel": { "type": "string", "default": "" },
"btnUrl": { "type": "string", "default": "" },
"center": { "type": "boolean", "default": true }
}
}
```
`render.php`(输出对齐 `index.html:201-208` 的 `.section-head.center` 结构):
```php
<?php
$center = $attributes['center'] ? ' center' : '';
?>
<header class="section-head<?php echo esc_attr( $center ); ?> reveal">
<?php if ( ! empty( $attributes['lead'] ) ) : ?>
<h2 class="section-title"><?php echo esc_html( $attributes['title'] ); ?></h2>
<p class="section-lead"><?php echo wp_kses( $attributes['lead'], array( 'strong' => array(), 'br' => array(), 'a' => array( 'href' => array() ) ) ); ?></p>
<?php else : ?>
<h2 class="section-title"><?php echo esc_html( $attributes['title'] ); ?></h2>
<?php endif; ?>
<?php if ( ! empty( $attributes['btnLabel'] ) ) : ?>
<a href="<?php echo esc_url( $attributes['btnUrl'] ); ?>" class="btn btn-outline"><?php echo esc_html( $attributes['btnLabel'] ); ?></a>
<?php endif; ?>
</header>
```
`index.js`(**这是所有区块编辑器脚本的模板范例**,后续区块同构):
```js
(function () {
var el = HESU.el, RichText = HESU.RichText, useBlockProps = HESU.useBlockProps,
InspectorControls = HESU.InspectorControls, TextControl = HESU.TextControl;
wp.blocks.registerBlockType('hesu/section-heading', {
edit: function (props) {
var attrs = props.attributes, set = props.setAttributes, bp = useBlockProps();
var panel = el(InspectorControls, {},
el(HESU.PanelBody, { title: 'Settings', initialOpen: true },
el(TextControl, { label: 'Button Label', value: attrs.btnLabel, onChange: function (v) { set({ btnLabel: v }); } }),
el(TextControl, { label: 'Button URL', value: attrs.btnUrl, onChange: function (v) { set({ btnUrl: v }); } })));
var view = el('header', bp,
el(RichText, { tagName: 'h2', className: 'section-title', value: attrs.title, onChange: function (v) { set({ title: v }); }, placeholder: 'Title' }),
el(RichText, { tagName: 'p', className: 'section-lead', value: attrs.lead, onChange: function (v) { set({ lead: v }); }, placeholder: 'Lead' }));
return [panel, view];
},
save: function () { return null; } // 动态渲染
});
})();
```
> **全计划通用模式**:`block.json`(属性表)+ `render.php`(照抄静态片段、属性注入)+ `index.js`(上例结构:Inspector 面板 + 内联预览)。
>
> **save 规则(关键)**:叶子区块(本例)`save: function () { return null; }`;**容器区块(含 InnerBlocks 的 hero-home/stats-bar/page-shell/business-intro/product-cards/feature-cards/bento-grid/hub-carousel/timeline/gp-shell/au-stage 等)save 必须返回 `el(wp.blockEditor.InnerBlocks.Content)`**——save 返回 null 的容器区块序列化时内层内容会全部丢失。
- [ ] **Step 4: editor-style.css 基础版**
`editor-style.css` = `@import url("css/fonts.css"); @import url("css/style.css");` 起步(后续任务把页面 CSS 逐个加入;**注意相对路径基于主题根**)。`inc/assets.php` 增加:
```php
add_action( 'enqueue_block_assets', function () {
if ( is_admin() ) {
wp_enqueue_style( 'hesu-editor', HESU_URI . '/editor-style.css', array( 'wp-edit-blocks' ), HESU_VERSION );
}
} );
add_filter( 'should_load_separate_core_block_assets', '__return_true' );
```
- [ ] **Step 5: 断言**
```bash
curl -s http://127.0.0.1:8090/wp-json/wp/v2/block-renderer/hesu/section-heading \
-H "Content-Type: application/json" --data '{"name":"hesu/section-heading","attributes":{"title":"T","center":true}}' | head -c 300
```
Expected: JSON 含 `rendered` 且输出 `section-head center reveal`。(需登录态——改用后台 wp-cli:`wp-cli.phar eval 'echo do_blocks("<!-- wp:hesu/section-heading {\"title\":\"T\"} /-->");' --path=wp`,输出 HTML 片段即通过。)
- [ ] **Step 6: Commit** `feat(blocks): framework + section-heading exemplar`
---
### Task 5: 通用区块组 + body_class 注入 + 资产映射
**Files:**
- Create: `blocks/hero/`、`blocks/stats-bar/`、`blocks/stat/`、`blocks/view-more/`、`blocks/page-shell/`
- Modify: `inc/assets.php`(has_block 映射表 + body_class 过滤器)
- Modify: `page.php`(新建,渲染 the_content)
**各区块要点(block.json 属性 / render 对齐来源):**
- [ ] **Step 1: hesu/hero** — 属性:`bgId`(number)、`bgUrl`(string)、`title`、`sub`(RichText string, 含换行)、`ctaLabel`、`ctaUrl`、`showScroll`(bool)、`variant`(select: `sub-left|sub-center|home|dh|ct`)。render 对齐:业务页 `building-materials.html:79-95`(`.bp-hero`)、居中 `auto-export.html:79-95`、home `index.html:121-152`(home 变体含 `<InnerBlocks/>` 槽位于 `.hero-stats` 内,接受 stats-bar;DH 用 `development-history.html:79-92` `.dh-hero`;CT 用 `contact.html:78-88` `.ct-hero`)。编辑器 JS:home 变体渲染 `el(wp.blockEditor.InnerBlocks, { allowedBlocks: ['hesu/stats-bar'], templateLock: 'all' })`。
- [ ] **Step 2: hesu/stats-bar + hesu/stat** — stats-bar:容器(`.stats-grid`/`.bp-stats` 按 `variant` home|bp),InnerBlocks 允许 `hesu/stat`,templateLock all。stat 属性:`iconId/iconUrl`(可选)、`num`(string)、`suffix`(string)、`label`。render 对齐 `index.html:134-149`(**数字文本节点必须 firstChild**:`<span class="stat-num" data-count="20">20<span class="plus">+</span></span>`)与 `building-materials.html:107-136`(bp 变体带 icon)。
- [ ] **Step 3: hesu/view-more** — 属性 `label/url`,对齐 `building-materials.html:243-245`。
- [ ] **Step 4: hesu/page-shell** — 属性 `className`(seed 写死)、`tag`(main|div,home 用 div)。render:`<main|div class="$className">InnerBlocks 内容</main|div>`。编辑器:InnerBlocks templateLock **'all'**,不提供任何 Inspector。
- [ ] **Step 5: page.php** — 标准循环 + `the_content()`。
- [ ] **Step 6: body_class 注入 + 资产映射**(inc/assets.php 追加)
用 `parse_blocks()` 解析(勿用正则——className 前有 lock 对象,属性顺序不可依赖):
```php
add_filter( 'body_class', function ( $classes ) {
if ( is_singular( 'page' ) && ( $post = get_post() ) ) {
$map = array( // seed 的 className 是受控集合 → 显式映射
'bp' => 'is-bp', // 4 业务页 + 发展历程(浅色基底)
'ct' => 'is-contact',
'gp-main' => 'is-gp', // 语义标记,CSS 无钩子
'au-main' => 'is-au', // 同上
);
foreach ( parse_blocks( $post->post_content ) as $b ) {
if ( 'hesu/page-shell' === $b['blockName']
&& isset( $b['attrs']['className'], $map[ $b['attrs']['className'] ] ) ) {
$classes[] = $map[ $b['attrs']['className'] ];
}
// 首页 className=page-content → 无 body class(静态站首页 body 无 class)
}
}
if ( is_post_type_archive( 'news' ) || is_singular( 'news' ) || is_tax( 'news_cat' ) ) $classes[] = 'is-news';
return $classes;
} );
```
(实现时按此语义写全映射;seed 的 className 值为受控集合,映射安全。)
资产映射表(`wp_enqueue_scripts` 中,按 `has_block()` 逐条;命中任一业务区块 → 追加加载 4 个业务页 JS):
| 条件 | 加载 |
|---|---|
| hero 或 business-intro | business.css |
| 任一业务区块(hero/business-intro/product-cards/brand-wall/showcase/model-list/feature-cards/large-image) | building-materials.js、auto-export.js、after-sales.js、global-logistics.js(自带守卫) |
| product-cards | building-materials.css |
| brand-wall/showcase/model-list | auto-export.css |
| feature-cards | after-sales.css + auto-export.css + global-logistics.css |
| timeline | development-history.css + js |
| gp-shell | global-presence.css + js |
| au-stage 或 au-gps | about-us.css + js |
| news-grid | news.css |
| contact-form | contact.css + js |
- [ ] **Step 7: main.js 扩展** — 追加对 `[data-reveal]` 与 `.nw-row` 的 IntersectionObserver reveal(复用现有 `.reveal` 逻辑参数 threshold 0.12)。**只追加,不改既有行**。
- [ ] **Step 8: 断言** — wp-cli eval 渲染 hero(含 bgUrl)输出含 `bp-hero__title`;`php -l` 全部新文件。
- [ ] **Step 9: Commit** `feat(blocks): hero/stats/view-more/page-shell, body_class, asset map`
---
### Task 6: 业务页区块组
**Files:**
- Create: `blocks/business-intro/`、`blocks/large-image/`、`blocks/feature-cards/`、`blocks/feature-card/`、`blocks/product-cards/`、`blocks/product-card/`、`blocks/showcase/`、`blocks/brand-wall/`、`blocks/model-list/`
**要点(render 对齐来源):**
- [ ] **Step 1: business-intro** — 属性:`heading`、`p1`、`p2`(RichText)、`imgId/imgUrl/imgAlt`、`withStats`(bool)。`withStats=true` 时文本栏内含 InnerBlocks(只允许 stats-bar,lock all)。对齐 `building-materials.html:100-143`。注意 `data-reveal` 属性照抄。
- [ ] **Step 2: large-image** — 属性 `imgId/imgUrl/imgAlt`、`variant`(stock|port)。对齐 `building-materials.html:148-152` 与 `global-logistics.html:148-152`。
- [ ] **Step 3: feature-cards + feature-card** — 容器属性 `variant`(as-services|ae-region|as-region|gl-services) → 决定容器 class;卡片属性 `iconUrl/iconId`、`title`、`desc`、可选 `link`。对齐 `after-sales.html:145-190`、`auto-export.html:192-219`、`after-sales.html:195-234`、`global-logistics.html:157-210`(实现前先读这四个片段确认每个变体的确切 DOM)。
- [ ] **Step 4: product-cards + product-card** — 容器固定 `.bm-products__grid`,容器属性含 `headTitle` 与 `anchor`(section id,seed 值 `products`,页脚/按钮锚点依赖);卡片属性 `icon`(select: hdpe-pipe|hdpe-fittings|pvc-pipe|pvc-fittings|welding → render.php 内置 5 段 SVG,从 `building-materials.html:164-239` 原样拷贝)、`imgUrl`(自定义图覆盖内置 SVG)、`name`。**所有容器区块统一加 `anchor` 属性**(输出 section id),seed 沿用静态站锚点值。
- [ ] **Step 5: showcase / brand-wall / model-list** — 实现前读 `auto-export.html:136-188` 与 `css/auto-export.css` 对应段;showcase=图+文案组合;brand-wall=容器 + InnerBlocks 子块 `hesu/brand-card`(图+alt,锁定列表);model-list=车型名 chip 列表(属性驱动)。
- [ ] **Step 6: 断言** — 每块 wp-cli eval do_blocks 渲染,grep 关键 class(`bp-intro__grid`、`bm-products__grid`、`ae-brands`…);php -l 全过。
- [ ] **Step 7: Commit** `feat(blocks): business page blocks`
---
### Task 7: 首页区块组
**Files:**
- Create: `blocks/bento-grid/`、`blocks/banner-cell/`、`blocks/media-card/`、`blocks/presence/`、`blocks/hub-carousel/`、`blocks/hub-card/`、`blocks/partner/`、`blocks/news-grid/`
- [ ] **Step 1: bento-grid** — 容器:`.bento-grid`,InnerBlocks 模板锁定为 `banner-cell + media-card ×4`(templateLock all + template 数组)。banner-cell 属性 `kicker/title`(对齐 `index.html:166-170`);media-card 属性 `imgUrl/imgId`、`title`、`link`、`phClass`(占位 class 如 ph-materials,图缺省时用)(对齐 `index.html:172-193`)。
- [ ] **Step 2: presence** — 属性 `title/lead(RichText 含 strong)/btnLabel/btnUrl/mapId/mapUrl`。对齐 `index.html:199-215`。
- [ ] **Step 3: hub-carousel + hub-card** — 容器输出 `.network-stage/.network-strip/.network-track`(**卡片必须是 track 直接子元素**,main.js 克隆逻辑依赖);hub-card 属性 `imgUrl/imgId/phClass/label/link`。对齐 `index.html:226-247`。
- [ ] **Step 4: partner** — 属性:slides(InnerBlocks 子块或图列表,输出 `.slide.ph.is-active` 结构)、`overlayTitle`(RichText 含 br)、features ×4(iconUrl+word)。结构复杂,实现前读 `index.html:250-284` 与 `css/style.css` partner 段;**轮播 dots 容器 `#partnerDots` 必须输出**。
- [ ] **Step 5: news-grid** — 动态查询:`new WP_Query(['post_type'=>'news','posts_per_page'=>4])`,卡片对齐 `index.html:294-314`(`news-card/news-img/news-title/news-meta`,meta= 首个 news_cat 词名 + `·` + 年份);属性仅 `moreUrl`。无文章时输出空态注释(seed 前正常)。
- [ ] **Step 6: 断言** — 建 2 篇测试新闻后 eval 渲染 news-grid 含 2 张卡;其余块 eval grep class;php -l。
- [ ] **Step 7: Commit** `feat(blocks): homepage blocks`
---
### Task 8: 特殊页区块组
**Files:**
- Create: `blocks/timeline/`、`blocks/timeline-item/`、`blocks/gp-shell/`、`blocks/gp-screen/`、`blocks/au-stage/`、`blocks/au-screen-1/`、`blocks/au-screen-2/`、`blocks/au-gps/`、`blocks/contact-form/`、`blocks/contact-map/`
- [ ] **Step 1: timeline + timeline-item** — 容器输出轨道结构(对齐 `development-history.html:96-191`,实现前通读);item 属性 `year/title/desc`(如有图则 `imgUrl`)。
- [ ] **Step 2: gp-shell + gp-screen** — shell 输出 `main.gp-main` 内层 `.gp-pagebg`;screen 属性:`variant`(overview|region)、`id`(gp-1..gp-5)、`title`、`lead`、`imgUrl`。**概览屏的 `gp-nav-list` 为锁定的 InnerBlocks 子列表**(spec 修正 #19:nav 是屏内容,每项=文案+`#gp-N` href+`data-i`,用锁定子块或固定 InnerBlocks 模板实现,对齐 `global-presence.html:109-171`,global-presence.js 零适配)。
- [ ] **Step 3: au-stage/au-screen-1/au-screen-2** — stage 输出 `main.au-main` 包裹;screen-1 属性:`bgUrl`(内联 style background-image)、`cornerImgUrl`、`title`、`text`、`slogan`、`sloganCn`;screen-2 属性:`zoomImgUrl`、`secTitle`、`secText`、`logoUrl`(默认主题 logo)、`revealText`。DOM 逐层对齐 `about-us.html:105-140`(`.au-bg/.au-corner-img/.au-inner.au-left/.au-mask/.au-slogan-layer/.au-zoom-img/.au-sec-head/.au-reveal/.au-logo-big`)。
- [ ] **Step 4: au-gps** — 4 层锁定结构(spec §4):`head(title+text)`、`cards×4(imgUrl)`、`wide(imgUrl)`、`split`(data 轮播卡 ×3:`imgUrl/title/desc` + `#auDots 类圆点容器`——实现前读 `about-us.html:143-200` 与 `js/about-us.js` 确认 dots 由 JS 生成还是静态标记)。
- [ ] **Step 5: contact-form** — 属性 `formId`(select,列 `wpcf7_contact_form` 文章);CF7 缺失输出 `.ct-form` 占位引导框(仅编辑者可见文案)。对齐容器 `contact.html:93-146` 的 section 包裹。
- [ ] **Step 6: contact.css 追加 CF7 桥接规则(spec §10)** — 在 `css/contact.css` **末尾追加**(不改既有规则):以 CF7 实际 DOM(`.wpcf7-form`、`.wpcf7-form-control-wrap`、`.wpcf7-not-valid`、`.wpcf7-validation-errors`、`.wpcf7-mail-sent-ok`、`.wpcf7-submit`)为目标复刻 `ct-form/ct-field/ct-label/ct-input/ct-selectwrap/ct-submit/ct-note` 视觉(字段 class 由 seed 挂 `class:ct-input` 等配合)。同时 `js/contact.js` 迁移时**剥离表单校验分支**(`#ctForm` 相关),保留其余。
- [ ] **Step 7: contact-map** — 属性 `embedUrl`(默认静态站 OSM iframe src,`contact.html:151-157`),输出 iframe。
- [ ] **Step 8: 断言** — eval 渲染各块 grep 关键 class(`gp-screen`、`au-pin`、`dh-timeline`、`ct-map__frame`);`grep -c "wpcf7" css/contact.css` ≥ 1;php -l 全过。
- [ ] **Step 9: Commit** `feat(blocks): special page blocks + CF7 bridge`
---
### Task 9: 新闻模板 + Customizer 新闻 Hero 图
**Files:**
- Create: `hesu-theme/hesu/archive-news.php`、`single-news.php`
- Create: `hesu-theme/hesu/inc/customizer.php`(本任务先加新闻 Hero 图设置,Task 10 扩充)
- Modify: `inc/assets.php`(archive/single 直接 enqueue news.css)
- [ ] **Step 1: archive-news.php** — 结构:`.nw-hero`(对齐 `news.html:78-93`,bg=theme mod `hesu_news_hero` 默认 `image/carexport/export-bg.jpg`)→ `.nw-tabs`(`get_terms(['taxonomy'=>'news_cat'])`,当前项由 `get_queried_object()` 判定,链接 `get_term_link`;全量 Tab=归档地址)→ `.nw-list` 循环输出 `.nw-row`(左文右图,对齐 `css/news.css` 的 `.nw-row` 结构与 `js/news.js:84-90` 生成的 DOM:`__body/__title/__excerpt/__more/__media`)→ `the_posts_pagination()`。行元素加 `data-reveal`。
- [ ] **Step 2: single-news.php** — 按设计稿 `demo-html/新闻详情1.jpg`:`.nw-hero`(同上 Hero + 面包屑 Home/News/当前)→ 文章头(分类 chip + 日期 + H1)→ 特色图 → `the_content()` → prev/next(`get_previous_post/get_next_post` 同类型)→ 返回列表按钮。样式复用 news.css,不足处在新文件 `css/news-single.css` 补(**不改 news.css 原有规则**)。
- [ ] **Step 3: 断言** — `curl http://127.0.0.1:8090/news/` 返回 200 且含 `nw-tabs`;建测试文章后 `/news/_slug/` 含 `the_content` 输出。
- [ ] **Step 4: Commit** `feat(news): archive + single templates`
---
### Task 10: Header/Footer 数据化 + Customizer + Mega Menu
**Files:**
- Modify: `header.php`(菜单数据化)、`footer.php`(页脚菜单数据化)
- Create: `inc/walker-mega.php`(自定义渲染函数 `hesu_render_mega( $menu_location, $group )`)
- Modify: `inc/customizer.php`(mega 左侧文案 ×2 组、社交 ×4、页脚标语、版权与底栏链接)
- [ ] **Step 1: Customizer 设置项** — section `hesu_global`:`social_tiktok/facebook/linkedin/whatsapp`(url)、`footer_slogan`、`footer_copyright`、`mega_business_kicker/title/desc/cta`、`mega_about_kicker/title/desc/cta`、`hesu_news_hero`(与 Task 9 的 theme mod **同名**)。
- [ ] **Step 2: mega 渲染** — `wp_get_nav_menu_items('primary')` 按 `menu_item_parent` 分组:顶级项=导航项(含 has-mega 判定:有子项即 mega);每个子项渲染 `.mega-card`:图=目标对象特色图(`get_post_thumbnail_id($item->object_id)`,非 page 对象或无图→输出 `.ph-*` 占位 div 带 `ph-label`)、文案=页面 excerpt 或菜单项 description、链接=`$item->url`。结构对齐 `index.html:28-104`。
- [ ] **Step 3: 菜单 active 类** —
```php
add_filter( 'nav_menu_css_class', function ( $classes, $item ) {
if ( in_array( 'current-menu-item', $classes, true ) ) $classes[] = 'active';
return $classes;
}, 10, 2 );
```
- [ ] **Step 4: footer 数据化** — footer 菜单(顶级=栏目标题 h4,子级=链接);socials 从 theme mods;News 栏链接 seed 时指向分类词。结构对齐 `index.html:322-387`。
- [ ] **Step 5: 断言** — wp-cli 设 theme mod 后 curl 验证:`wp-cli.phar option update social_facebook "https://x.com" --theme --path=wp`(或 `eval 'set_theme_mod(...)'`)→ `curl -s http://127.0.0.1:8090/ | grep -c "footer-social"`。Mega 渲染断言推迟到 Task 11 Step 5(依赖 seed 建菜单)。
- [ ] **Step 6: Commit** `feat(header/footer): data-driven menus, mega renderer, customizer`
---
### Task 11: Seed 播种 + 设置页
**Files:**
- Create: `hesu-theme/hesu/inc/seed.php`、`inc/settings-page.php`
- Create: `hesu-theme/hesu/seed/`(数据文件:15 篇新闻 JSON——从 `js/news.js` 顶部数据数组原样提取、页面区块标记模板 PHP 数组)
- Modify: `functions.php`(require)
- [ ] **Step 1: 图片入库 helper** — `hesu_sideload( $rel_path )`:`copy(HESU_DIR.'/'.$rel_path, $tmp)` + `media_handle_sideload($_FILES 风格数组, 0)`,返回 attachment_id;以 path 为键做静态缓存避免重复。
- [ ] **Step 2: 页面区块标记生成** — 每页一个 PHP 函数返回序列化 block markup(`seed/markup.php`)。示例(建材页骨架,其余同构):
```php
"<!-- wp:hesu/page-shell {\"lock\":{\"move\":true,\"remove\":true},\"className\":\"bp\",\"tag\":\"main\"} -->\n" .
"<!-- wp:hesu/hero {\"lock\":{\"remove\":true},\"variant\":\"sub-left\",\"bgId\":%d,\"title\":\"HESU PIPE\",...} /-->\n" .
"<!-- wp:hesu/business-intro {\"lock\":{\"remove\":true},...} -->\n" .
"<!-- wp:hesu/stats-bar {\"variant\":\"bp\"} -->\n<!-- wp:hesu/stat {...} /--> ×4\n<!-- /wp:hesu/stats-bar -->\n" .
"<!-- /wp:hesu/business-intro -->\n" .
"<!-- wp:hesu/large-image {...} /-->\n" .
"<!-- wp:hesu/product-cards --> ... <!-- /wp:hesu/product-cards -->\n" .
"<!-- wp:hesu/view-more {...} /-->\n" .
"<!-- /wp:hesu/page-shell -->\n"
```
内容默认值全部取静态站对应页面原文。首页 = hero(home,含 stats-bar 槽内 4 个 stat) + page-shell(`page-content`, tag div) 两个顶层块。9 页各一函数;内层子块一律带 `"lock":{"remove":true}`。
- [ ] **Step 3: 主流程** — 守卫 `get_option('hesu_seeded')` → 侧载图片 → 删除 WP 默认 "Sample Page"(`get_page_by_title('Sample Page')` 存在则 `wp_delete_post`)→ `wp_insert_post` 建 9 页(slug: home/building-materials/auto-export/after-sales/global-logistics/global-presence/about-us/development-history/contact;全部设特色图)→ `update_option('show_on_front','page')`/`page_on_front` → 4 分类词 → 15 篇新闻(标题/摘要/内容段落/特色图占位/分类/日期从 news.js 数据)→ 主菜单+页脚菜单(含 mega 卡片子项、News 链接=归档、GP 锚点链接)绑 location → CF7 激活则建默认表单 → `update_option('hesu_seeded', time())`。挂 `after_switch_theme`。
CF7 程序化建表单(机制):CF7 表单 = `post_type=wpcf7_contact_form` 的文章 + post meta:
```php
$id = wp_insert_post( array( 'post_type' => 'wpcf7_contact_form', 'post_title' => 'HESU Inquiry',
'post_status' => 'publish' ) );
$form = '<div class="ct-form__grid">' /* 按 contact.html:95-144 七个字段,形如: */
. '[select* business_type class:ct-input first_as_label "Select business type" "Building Materials Export" ...]'
. '[email* email class:ct-input placeholder "Enter your email"]' /* ... */ '</div>'
. '[submit class:ct-submit "Send Message"]';
update_post_meta( $id, '_form', $form );
update_post_meta( $id, '_mail', array( /* CF7 邮件默认数组,recipient=admin_email */ ) );
```
- [ ] **Step 4: 设置页 + 补建钩子** — `add_theme_page` + 表单按钮(admin-post + nonce + capability `manage_options` → 清守卫重跑 seed);`activated_plugin` 钩子:插件为 CF7 且默认表单缺失 → 补建。
- [ ] **Step 5: 断言(关键节点)**
```bash
cd d:/wp/demojt/.test-env
php/php.exe wp-cli.phar theme activate twentytwentyfour --path=wp # 切走
php/php.exe wp-cli.phar theme activate hesu --path=wp # 触发 seed
for s in home building-materials auto-export after-sales global-logistics global-presence about-us development-history contact; do
php/php.exe wp-cli.phar post list --post_type=page --name=$s --path=wp --format=ids | grep -q . && echo "$s OK" || echo "$s MISSING"
done
php/php.exe wp-cli.phar post list --post_type=news --path=wp --format=ids | wc -w
curl -s http://127.0.0.1:8090/ | grep -c "network-card"
curl -s http://127.0.0.1:8090/building-materials/ | grep -o '<body class="[^"]*"' | grep -c is-bp
```
Expected: 9 个 slug 全 OK;15 篇新闻;首页含 `network-card`;建材页 body 含 `is-bp` class(body_class 注入生效)。
- [ ] **Step 6: Commit** `feat(seed): activation seeding, settings page, CF7 bootstrap`
---
### Task 12: 编辑器加固 + editor-style 完整版
**Files:**
- Create: `blocks/editor/lock.js`(独立小脚本)
- Modify: `inc/blocks.php`(非管理员 enqueue lock.js)、`editor-style.css`(补全部页面 CSS import)
- [ ] **Step 1: 锁定加固(两层,方案固定)** — ① PHP 端:`allowed_block_types_all` 过滤器——非 `manage_options` 用户只返回 `hesu/*` 区块(插入器里看不到任何可插入项,已有内容不受影响);② `blocks/editor/lock.js`(对非管理员 enqueue):注入一段 CSS 隐藏 `.block-editor-inserter__toggle`(插入器按钮)与区块选项菜单中的移动/删除入口。验收两条:editor 角色编辑器无插入器;区块选项菜单无 Move to Trash/Delete。
- [ ] **Step 2: editor-style.css** — 追加 `@import url("css/xxx.css")` 全部页面 CSS(business/auto-export/after-sales/building-materials/global-logistics/development-history/news/contact/global-presence/about-us;**路径基于主题根**)。确认编辑器内业务页预览浅色基底生效(page-shell 容器 class 场景)。
- [ ] **Step 3: 断言** — 建一个 editor 角色测试账号(wp-cli),后台登录该账号打开建材页:无插入器按钮;管理员打开:结构树完整、板块不可删(选项菜单无删除项)。(人工/浏览器验证,记录截图或 curl admin-ajax 不必——用 wp-cli 建 editor:`wp user create ed ed@t.t --role=editor --user_pass=x`,此步允许人工浏览器确认。)
- [ ] **Step 4: Commit** `feat(editor): lock hardening + full editor styles`
---
### Task 13: 全量验收(spec §12 清单)
**Files:** Modify: 发现问题所涉文件;Create: `docs/superpowers/plans/2026-08-15-acceptance-log.md`(记录每条结果)
- [ ] **Step 1: 逐页对照** — 浏览器开 `http://127.0.0.1:8090` 与静态站(`cd d:/wp/demojt && ./.test-env/php/php.exe -S 127.0.0.1:8080`)逐页比对:板块顺序、`.reveal` 滚动动画、数字滚动、Mega Menu 悬停、partner/network 轮播、GP fullpage 翻屏+active 高亮、AU pinned 视屏、时间线横滑、移动端抽屉。每页记录 通过/问题。
- [ ] **Step 2: 功能项** — 新闻 Tab 过滤/分页/首页自动更新;CF7 提交态(邮件 dummy);Customizer 各字段;菜单高亮;editor 角色锁定(Task 12);CF7 停用再激活 → 联系页占位提示。
- [ ] **Step 3: 代码项** — `php -l` 全主题文件循环通过;grep 检查无未转义 echo(抽查 render.php);控制台无 JS 报错(浏览器)。
- [ ] **Step 4: 修复所有未通过项并复测,全部 PASS 后记录** — Commit `test: acceptance round complete`
---
### Task 14: 打包与文档
**Files:**
- Create: `hesu-theme/hesu/readme.txt`(安装说明:装 CF7→传主题→激活→完成;区块编辑器必须;"解锁=破坏页面"警示;seed 重跑按钮说明)
- Create: 打包脚本步骤(不 commit zip)
- [ ] **Step 1: readme.txt 编写**(中英双语要点)
- [ ] **Step 2: 打包验证** — `cd hesu-theme && zip -r hesu.zip hesu -x "*.DS_Store"`;解压到测试站重装走一遍激活流程(干净站点:重建 SQLite 库 `wp-content/database` 删除 + 重跑 wp core install)确认 zip 包自足。
- [ ] **Step 3: 清理测试环境的决定交给用户**(保留供后续开发 or 删除)
- [ ] **Step 4: Commit** `docs: readme, packaging verified`
---
## 任务依赖
0 → 1 → 2 → 3 → 4 → 5 →(6,7,8 可并行)→ 9,10(依赖 5;10 依赖 3 的菜单)→ 11(依赖全部区块)→ 12 → 13 → 14