@@ -0,0 +1,668 @@
# 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