Review changes batch 5 of 6

This commit is contained in:
2026-07-20 06:52:26 +08:00
parent 0d0645a112
commit db97d3da27
41 changed files with 6672 additions and 1124 deletions
@@ -0,0 +1,275 @@
# G01 Fixed Header And Independent List Scroll Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. The user forbids subagents, worktrees, git add, commit, push, reset, and checkout; execute inline in the existing workspace and preserve every pre-existing change.
**Goal:** Make G01's normal genealogy state keep its header, current genealogy card, shortcut grid, divider, and bottom tabbar fixed while only the three list groups and add action scroll vertically.
**Architecture:** Add one G01-only split-layout modifier for the normal `hasGenealogies` state. Keep the existing header/background/tabbar outside a single vertical `scroll-view`, place the current card/shortcuts/divider in a fixed flex section, and allocate all remaining viewport height to the list scroller. Loading, error, and empty states retain the existing normal-flow layout.
**Tech Stack:** Vue 3 `<script setup>`, uni-app `scroll-view`, SCSS, PowerShell static contracts, Node Chrome DevTools Protocol runtime smoke.
## Global Constraints
- Modify only `pages/genealogy/g01-my-genealogies.vue`, directly related G01 tests, G01 design/acceptance documentation, and new runtime screenshots.
- Do not modify `PageHeader.vue`, `AppTabbar.vue`, `GenealogyPageBackground.vue`, or another G page.
- Do not add APIs, global state, pull-to-refresh, sticky group headings, collapse animations, or native UniApp prompts.
- Preserve the approved `12rpx` spacing between adjacent application cards.
- Verify 320×568, 360×640, 360×800, 412×915, and 412×1000.
- H5 evidence remains internal; Android/HBuilderX is still required.
- Do not use subagents, worktrees, git add, commit, push, reset, or checkout.
---
### Task 1: Lock the split-scroll contract with failing tests
**Files:**
- Modify: `tests/g01-empty-state-contract.ps1`
- Modify: `tests/g01-empty-state-runtime-smoke.js`
- Test: `tests/g01-empty-state-contract.ps1`
- Test: `tests/g01-empty-state-runtime-smoke.js`
**Interfaces:**
- Consumes: the existing G01 selectors `.current-slip`, `.shortcut-grid`, `.section-divider`, `.genealogy-lower`, `.create-action`.
- Produces: required selectors `.genealogy-fixed-zone`, `.genealogy-list-scroll`, `.genealogy-index--split`, plus the runtime behavior contract for fixed and moving regions.
- [ ] **Step 1: Extend the PowerShell static contract**
Require one normal-state vertical scroll view, keep the fixed zone before it, keep `.genealogy-lower` inside it, and require the reset handlers:
```powershell
foreach ($required in @(
'class="genealogy-fixed-zone"',
'class="genealogy-list-scroll"',
'scroll-y',
':scroll-top="listScrollCommand"',
'@scroll="handleListScroll"',
'const isListLayout = computed(',
'const resetListScroll = async () =>'
)) {
if ($g01 -notmatch [regex]::Escape($required)) { throw "Missing G01 split-scroll contract: $required" }
}
Assert-Match -Content $g01 -Pattern '(?s)<view class="genealogy-fixed-zone">.*class="current-slip".*class="shortcut-grid".*class="section-divider".*</view>\s*<scroll-view[^>]*class="genealogy-list-scroll"[^>]*>.*class="genealogy-lower".*</scroll-view>' -Message 'G01 fixed and scrolling regions are not separated correctly'
```
- [ ] **Step 2: Extend the runtime smoke**
For each required viewport, record fixed element rectangles, scroll the inner region, and assert only the lower heading moves:
```js
const before = await valueOf(send, `(() => {
const selectors = ['.page-header', '.current-slip', '.shortcut-grid', '.section-divider', '.app-tabbar']
return Object.fromEntries(selectors.map((selector) => [selector, document.querySelector(selector)?.getBoundingClientRect().top]))
})()`)
await valueOf(send, `(() => {
const host = document.querySelector('.genealogy-list-scroll')
const scroller = host?.querySelector('.uni-scroll-view') || host
scroller.scrollTop = Math.min(180, scroller.scrollHeight - scroller.clientHeight)
scroller.dispatchEvent(new Event('scroll', { bubbles: true }))
})()`)
```
Wait until `.section-heading` moves upward, then assert every fixed selector changes by no more than one pixel. Also assert the inner region has positive height, `scrollHeight > clientHeight`, and the document has no horizontal overflow.
- [ ] **Step 3: Run the static contract and observe RED**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
```
Expected: FAIL because `.genealogy-fixed-zone` and `.genealogy-list-scroll` do not exist.
- [ ] **Step 4: Run the runtime smoke and observe RED**
Run:
```powershell
node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
```
Expected: FAIL because the normal state has no independent list scroller.
---
### Task 2: Implement the G01-only split layout and position rules
**Files:**
- Modify: `pages/genealogy/g01-my-genealogies.vue:3-165`
- Modify: `pages/genealogy/g01-my-genealogies.vue:270-393`
- Modify: `pages/genealogy/g01-my-genealogies.vue:395-563`
- Test: `tests/g01-empty-state-contract.ps1`
- Test: `tests/g01-empty-state-runtime-smoke.js`
**Interfaces:**
- Consumes: `isLoading`, `hasError`, `hasGenealogies`, `selectGenealogy(genealogy)` and existing page components.
- Produces: `isListLayout: ComputedRef<boolean>`, `listScrollCommand: Ref<number>`, `currentListScrollTop: Ref<number>`, `handleListScroll(event): void`, and `resetListScroll(): Promise<void>`.
- [ ] **Step 1: Add the normal-state layout modifier**
Update the page shell and script imports:
```vue
<view
class="page-shell genealogy-index"
:class="{ 'genealogy-index--split': isListLayout }"
>
```
```js
import { computed, nextTick, ref } from "vue";
const isListLayout = computed(
() => !isLoading.value && !hasError.value && hasGenealogies.value,
);
```
- [ ] **Step 2: Separate fixed and scrolling regions**
Inside the existing `v-else-if="hasGenealogies"` branch, wrap only the card, shortcuts, and divider in the fixed zone, and wrap `.genealogy-lower` in the single scroll view:
```vue
<view class="genealogy-fixed-zone">
<view class="current-slip" @click="openSwitcher">...</view>
<view class="shortcut-grid">...</view>
<image class="section-divider" ... />
</view>
<scroll-view
class="genealogy-list-scroll"
scroll-y
:scroll-top="listScrollCommand"
@scroll="handleListScroll"
>
<view class="genealogy-lower">...</view>
</scroll-view>
```
Do not move the background, title bar, tabbar, dialog layers, or non-normal states into the scroll view.
- [ ] **Step 3: Add scroll tracking and explicit reset after genealogy switching**
```js
const listScrollCommand = ref(0);
const currentListScrollTop = ref(0);
const handleListScroll = (event) => {
currentListScrollTop.value = Number(event?.detail?.scrollTop || 0);
};
const resetListScroll = async () => {
listScrollCommand.value = currentListScrollTop.value;
await nextTick();
listScrollCommand.value = 0;
currentListScrollTop.value = 0;
};
const selectGenealogy = async (genealogy) => {
selectedGenealogyId.value = genealogy.id;
closeSwitcher();
await resetListScroll();
};
```
Opening/closing dialogs and returning from a pushed child page do not call `resetListScroll`, so the existing component instance retains its scroll position.
- [ ] **Step 4: Add the split-only flex sizing**
Keep the current default styles for loading/error/empty, and scope viewport locking to the modifier:
```scss
.genealogy-index--split {
display: flex;
height: 100vh;
min-height: 0;
flex-direction: column;
padding-bottom: calc(112rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
}
.genealogy-index--split .genealogy-content {
display: flex;
min-height: 0;
flex: 1;
flex-direction: column;
padding-bottom: 0;
}
.genealogy-fixed-zone { flex: 0 0 auto; }
.genealogy-list-scroll {
width: 100%;
height: 0;
min-height: 0;
flex: 1;
}
```
Do not add a height media query or hide the system scroll indicator as a product contract.
- [ ] **Step 5: Run the focused tests and reach GREEN**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
powershell -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1
```
Expected: all four commands exit 0.
---
### Task 3: Capture evidence, update QA, and protect adjacent G behavior
**Files:**
- Create: `docs/design/screens/runtime/2026-07-16/G01-fixed-list-scroll-320x568.png`
- Create: `docs/design/screens/runtime/2026-07-16/G01-fixed-list-scroll-360x640.png`
- Create: `docs/design/screens/runtime/2026-07-16/G01-fixed-list-scroll-360x800.png`
- Create: `docs/design/screens/runtime/2026-07-16/G01-fixed-list-scroll-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-16/G01-fixed-list-scroll-412x1000.png`
- Modify: `design-qa.md`
- Modify: `docs/验收规划.md`
- Modify: `docs/交接记录.md`
- Test: G01 and adjacent G runtime/static contracts
**Interfaces:**
- Consumes: the implemented `.genealogy-list-scroll` behavior and existing Chrome debug server on port 9222.
- Produces: five internal H5 screenshots, a current QA record, and an accurate handoff state that does not claim Android or whole-page acceptance.
- [ ] **Step 1: Capture five real H5 viewports**
Use Chrome DevTools Protocol to navigate to G01, set each required viewport, scroll the inner list to a representative position, and save the five named PNGs. Capture with `captureBeyondViewport: false` and check browser exceptions plus `console.error`.
- [ ] **Step 2: Visually compare fixed and moving regions**
Open the 320×568, 412×915, and 412×1000 screenshots together. Confirm the fixed area remains complete, the first visible list row is not covered by the divider, the tabbar does not cover the last reachable action, and the long background remains continuous.
- [ ] **Step 3: Update QA and authoritative status documents**
Prepend a G01 split-scroll section to `design-qa.md` with source spec, implementation screenshots, five required fidelity surfaces, comparison history, evidence limits, and `final result: passed` only if no H5 P0/P1/P2 remains.
Update `docs/验收规划.md` and `docs/交接记录.md` to say the fixed/scroll structure is implemented as an internal H5 candidate. Keep G01 `[~]`, explicitly retain Android/HBuilderX and 4GB-device verification, and do not mark the page frozen or accepted.
- [ ] **Step 4: Run final verification**
Run:
```powershell
powershell -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell -ExecutionPolicy Bypass -File tests/genealogy-shared-background-contract.ps1
node tests/g01-empty-state-runtime-smoke.js http://localhost:5173
node tests/g03-create-flow-runtime-smoke.js http://localhost:5173
node tests/g05-overview-runtime-smoke.js http://localhost:5173
node tests/g06-search-flow-runtime-smoke.js http://localhost:5173
node tests/g08-g10-application-flow-runtime-smoke.js http://localhost:5173
node tests/g11-g12-settings-poems-runtime-smoke.js http://localhost:5173
git diff --check
```
Expected: every command exits 0. Report Android/HBuilderX as still pending.
@@ -0,0 +1,235 @@
# G01 添加家谱底部弹层实施计划
> **状态:已废止。** 用户否决了完整卷轴背景方向;不得继续执行本文。当前唯一实施计划为 `docs/superpowers/plans/2026-07-19-g01-add-genealogy-paper-sheet.md`。
> **执行要求:** 使用 `superpowers:executing-plans` 在当前会话内联执行;禁止多代理、worktree、`git add`、`commit`、`push`、`reset` 和 `checkout`。
**目标:** 将 G01 当前居中大卷轴“添加家谱”弹窗改成紧凑、可明确关闭的自定义底部弹层,并保持三个现有跳转行为不变。
**架构:** 只在 `g01-my-genealogies.vue` 内拆分添加弹层与切换弹层的模板选择器和样式责任;添加弹层底部对齐并使用自己的关闭入口,切换弹层继续沿用居中卷轴。先用现有 PowerShell 视觉契约锁定结构、文案、布局和返回键行为,再做最小实现。
**技术栈:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell 契约测试、Chrome DevTools Protocol 9222。
## 全局约束
- 只返工 G01“添加家谱”弹层,不修改切换家谱弹层、其他 G01 状态或其他页面。
- 不对接接口;三个入口继续调用 `applyToJoin``joinByInvite``createGenealogy`
- 继续使用项目自定义组件和现有真实 PNG 资产,不使用原生 UniApp 弹窗、Toast、Loading 或 ActionSheet。
- 只复用当前 9222 Chrome 窗口和唯一项目标签页,不打开第二个浏览器或第二个项目标签页。
- 不覆盖、删除或清理现有修改、未跟踪文件、测试、文档、截图、母版和候选资产。
- 不执行任何 Git 暂存、提交、推送、重置或检出命令。
- H5 截图只作为内部候选证据;Android/HBuilderX 仍标记为未验证。
- 没有用户明确“通过”,不得将 G01 标记为 `[x]` 或冻结。
---
### 任务 1:锁定底部弹层的结构、文案与视觉契约
**文件:**
- 修改:`tests/g01-visual-contract.ps1`
- 修改:`pages/genealogy/g01-my-genealogies.vue` 的添加弹层模板与弹层样式
**接口:**
- 消费:`addDialogVisible``closeAddDialog()``applyToJoin()``joinByInvite()``createGenealogy()`、现有 `AppButton`
- 产出:`.add-dialog-layer``.add-dialog``.add-dialog__heading``.add-dialog__close``.add-dialog__content`;切换弹层选择器和表现保持不变。
- [ ] **步骤 1:先加入会失败的结构与样式契约**
`tests/g01-visual-contract.ps1` 读取 `$page` 后增加:
```powershell
foreach ($token in @('class="add-dialog__heading"', 'class="add-dialog__close"', '>关闭</view>', 'label="继续创建家谱"')) {
if ($page -notmatch [regex]::Escape($token)) { throw "G-01 add sheet is missing $token" }
}
if ($page -match [regex]::Escape('label="确认没有现有家谱,继续创建"')) {
throw 'G-01 add sheet still puts guidance copy inside the create button.'
}
if ($page -notmatch '(?s)\.add-dialog-layer\s*\{[^}]*align-items:\s*flex-end;[^}]*background:\s*rgba\(34,\s*20,\s*12,\s*0\.68\);') {
throw 'G-01 add sheet is not a bottom-aligned layer with the approved mask.'
}
if ($page -notmatch '(?s)\.add-dialog\s*\{[^}]*max-width:\s*650rpx;[^}]*min-height:\s*545rpx;') {
throw 'G-01 add sheet does not use the compact approved proportions.'
}
if ($page -notmatch '(?s)\.add-dialog__close\s*\{[^}]*min-height:\s*72rpx;') {
throw 'G-01 add sheet close control does not reserve the approved hit area.'
}
if ($page -notmatch '(?s)\.genealogy-switcher-layer\s*\{[^}]*align-items:\s*center;[^}]*padding:\s*40rpx;') {
throw 'G-01 switcher must remain a centered dialog.'
}
```
- [ ] **步骤 2:运行契约并确认按预期失败**
运行:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
预期:`FAIL`,第一条失败信息为缺少 `class="add-dialog__heading"`;失败原因必须是新结构尚未实现,而不是脚本语法或文件编码错误。
- [ ] **步骤 3:最小修改添加弹层模板**
将添加弹层模板改为以下结构;切换家谱模板不动:
```vue
<view v-if="addDialogVisible" class="add-dialog-layer" @click="closeAddDialog">
<view class="add-dialog" @click.stop>
<image class="add-dialog__skin" src="/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png" mode="aspectFit" />
<view class="add-dialog__content">
<view class="add-dialog__heading">
<text class="dialog-title">添加家谱</text>
<text class="dialog-copy">建议先搜索已有家谱避免重复创建</text>
</view>
<view class="add-dialog__close" hover-class="action-hover" @click="closeAddDialog">关闭</view>
<AppButton block label="搜索家谱" @click="applyToJoin" />
<AppButton block type="secondary" label="邀请码加入" @click="joinByInvite" />
<AppButton block type="secondary" label="继续创建家谱" @click="createGenealogy" />
</view>
</view>
</view>
```
- [ ] **步骤 4:拆分添加弹层和切换弹层样式**
用以下责任边界替换当前合并规则;保留现有 `.switcher-item` 及其后续规则:
```scss
.add-dialog-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: flex-end; justify-content: center; box-sizing: border-box; padding: 0 20rpx; background: rgba(34, 20, 12, 0.68); }
.add-dialog { position: relative; width: 100%; max-width: 650rpx; min-height: 545rpx; max-height: calc(100vh - 24rpx); }
.add-dialog__skin { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.add-dialog__content { position: relative; z-index: 1; display: flex; min-height: 545rpx; max-height: calc(100vh - 24rpx); flex-direction: column; align-items: stretch; box-sizing: border-box; padding: 48rpx 58rpx calc(36rpx + env(safe-area-inset-bottom)); overflow-y: auto; }
.add-dialog__heading { padding-right: 104rpx; }
.add-dialog .dialog-title,
.add-dialog .dialog-copy { display: block; text-align: left; }
.add-dialog .dialog-title { padding-left: 54rpx; }
.add-dialog__close { position: absolute; z-index: 2; top: 38rpx; right: 120rpx; display: flex; min-width: 88rpx; min-height: 72rpx; align-items: center; justify-content: center; color: $ink-muted; font-size: 26rpx; }
.add-dialog .app-button { min-height: 92rpx; }
.add-dialog__content > .app-button:first-of-type { margin-top: 24rpx; }
.add-dialog__content > .app-button + .app-button { margin-top: 6rpx; }
.genealogy-switcher-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 40rpx; background: rgba(34, 20, 12, 0.58); }
.genealogy-switcher { position: relative; width: 100%; max-width: 670rpx; min-height: 720rpx; }
.genealogy-switcher__skin { position: absolute; inset: 0; width: 100%; height: 100%; }
.genealogy-switcher__content { position: relative; z-index: 1; display: flex; min-height: 720rpx; flex-direction: column; align-items: center; box-sizing: border-box; padding: 78rpx 58rpx 50rpx; }
.dialog-title { color: $brand-red; font-family: "STKaiti", "KaiTi", serif; font-size: 42rpx; font-weight: 700; letter-spacing: 4rpx; }
.dialog-copy { margin-top: 12rpx; color: $ink-muted; font-size: 25rpx; line-height: 1.5; }
.dialog-close { display: flex; min-height: 72rpx; align-items: center; justify-content: center; margin-top: auto; color: $ink-muted; font-size: 24rpx; }
```
- [ ] **步骤 5:运行契约并确认通过**
运行同一步骤 2。预期输出:
```text
PASS G-01 visual contract
```
---
### 任务 2:让 Android 返回键优先关闭添加弹层
**文件:**
- 修改:`tests/g01-visual-contract.ps1`
- 修改:`pages/genealogy/g01-my-genealogies.vue` 的 uni-app 生命周期导入和返回键处理
**接口:**
- 消费:`addDialogVisible: Ref<boolean>``closeAddDialog(): void`
- 产出:`onBackPress` 回调;弹层打开时返回 `true` 并关闭,未打开时不拦截页面返回。
- [ ] **步骤 1:先加入会失败的返回键契约**
`tests/g01-visual-contract.ps1` 增加:
```powershell
if ($page -notmatch 'import\s*\{[^}]*onBackPress[^}]*\}\s*from\s*"@dcloudio/uni-app"') {
throw 'G-01 add sheet does not import onBackPress.'
}
if ($page -notmatch '(?s)onBackPress\(\(\)\s*=>\s*\{\s*if\s*\(!addDialogVisible\.value\)\s*return\s*false;\s*closeAddDialog\(\);\s*return\s*true;\s*\}\);') {
throw 'G-01 add sheet does not consume Android back before page navigation.'
}
```
- [ ] **步骤 2:运行契约并确认按预期失败**
运行视觉契约。预期:`FAIL: G-01 add sheet does not import onBackPress.`
- [ ] **步骤 3:加入最小返回键处理**
将导入改为:
```js
import { onBackPress, onLoad, onShow } from "@dcloudio/uni-app";
```
`closeAddDialog` 定义之后加入:
```js
onBackPress(() => {
if (!addDialogVisible.value) return false;
closeAddDialog();
return true;
});
```
- [ ] **步骤 4:运行契约并确认通过**
预期输出:`PASS G-01 visual contract`
---
### 任务 3:聚焦验证并在唯一标签页恢复审批状态
**文件:**
- 验证:`pages/genealogy/g01-my-genealogies.vue`
- 读取:`tests/g01-empty-state-contract.ps1`
- 读取:`tests/g01-visual-contract.ps1`
- 生成候选截图:`docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-bottom-sheet-412x915.png`
**接口:**
- 消费:5173 H5 服务、9222 唯一 Chrome 项目标签页、当前 G01 默认列表态。
- 产出:412×915 当前添加弹层的候选证据;不改变验收规划勾选状态。
- [ ] **步骤 1:运行最小相关静态检查**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
git diff --check -- pages/genealogy/g01-my-genealogies.vue tests/g01-visual-contract.ps1 docs/superpowers/specs/2026-07-19-g01-add-genealogy-bottom-sheet-design.md docs/superpowers/plans/2026-07-19-g01-add-genealogy-bottom-sheet.md
```
预期:两个测试均输出 `PASS``git diff --check` 无输出且退出码为 0。不修改测试阈值来换取通过。
- [ ] **步骤 2:确认服务、标签页和视口约束**
只读检查 5173、9222 和 `http://127.0.0.1:9222/json/list`。预期:5173 可访问;9222 只有一个 `localhost:5173` 项目页;不得启动第二个 Chrome。
- [ ] **步骤 3:在同一标签页刷新现有 G01 代码并打开添加弹层**
保持 G01 默认列表态,将视口设为 412×915;若热更新已生效则不导航,关闭旧弹层后点击现有 `.create-action` 重新打开。等待 `.add-dialog-layer``.add-dialog__close` 和三个 `.app-button` 同时出现。
- [ ] **步骤 4:检查运行时几何和交互**
通过当前 CDP 连接仅检查同一个“添加家谱”状态,先检查 320×568,再恢复并检查 412×915
- `.add-dialog` 底边与当前视口底边重合,允许 1px 取整误差;
- 弹层没有横向溢出,且高度小于视口高度的 50%;
- `.add-dialog__close` 完全位于弹层可视区域内;
- 三个按钮同时可见,第三个文本为“继续创建家谱”;
- 点击弹层内部不关闭;点击“关闭”会关闭;再次打开后点击遮罩会关闭;
- 关闭和重新打开不改变 G01 独立列表的滚动位置;
- 最后恢复 412×915,并让添加家谱弹层保持打开。
- [ ] **步骤 5:截取并检查候选证据**
使用当前标签页的 `Page.captureScreenshot` 保存指定 PNG,打开原图检查:没有错误页、加载中、裁切、横向溢出、标题压线、关闭入口消失或大块无效留白。若不满足,只返工当前弹层。
- [ ] **步骤 6:交回用户审批**
只报告当前添加弹层的变更和真实验证结果,明确 Android/HBuilderX 尚未验证;等待用户明确“通过”,不得切换到“切换家谱弹层”,不得更新 G01 为 `[x]`
@@ -0,0 +1,216 @@
# G01 添加家谱宣纸底部弹层实施计划
> **执行要求:** 使用 `superpowers:executing-plans` 在当前会话内联执行;禁止多代理、worktree、`git add`、`commit`、`push`、`reset` 和 `checkout`。步骤使用复选框跟踪。
**目标:** 以已选定视觉目标重做 G01“添加家谱”:删除完整卷轴背景和无效装饰,改为宣纸底面、顶部收边、关闭图标,并原样复用现有 `AppButton` 卷轴按钮。
**架构:** `g01-my-genealogies.vue` 继续独占当前弹层结构和交互;现有 `AppButton.vue` 不修改。新增一个真实透明关闭图标 PNG,弹层使用现有 `page-paper.jpg``a01-paper-transition-v1.png`,切换家谱弹层仍使用原居中卷轴结构。
**技术栈:** uni-app、Vue 3 `<script setup>`、SCSS、现有 `AppButton`、PowerShell 契约测试、Chrome DevTools Protocol 9222、内置 Image Gen。
## 全局约束
- 唯一视觉目标:`docs/design/mockups/2026-07-19/g01-add-dialog-paper-sheet-target.png`
- 只修改 G01“添加家谱”弹层,不修改切换家谱弹层、其他 G01 状态或其他页面。
- 三个入口继续调用 `applyToJoin``joinByInvite``createGenealogy`,不对接接口。
- 按钮必须直接复用现有 `AppButton``a01-scroll-primary-v3.png``a01-scroll-secondary-v3.png`;不得生成、重绘或近似实现按钮皮肤。
- 不删除任何现有资产文件、历史截图、测试或文档;只删除当前 G01 添加弹层内已经失去引用的模板节点和专用样式。
- 只复用当前 9222 Chrome 窗口和唯一项目标签页。
- 不执行 Git 暂存、提交、推送、重置或检出。
- H5 截图只作为候选证据;Android/HBuilderX 仍为未验证。
- 没有用户明确“通过”,不得切换下一状态、冻结 G01 或更新为 `[x]`
---
### 任务 1:生成并验证真实关闭图标资产
**文件:**
- 参考:`docs/design/mockups/2026-07-19/g01-add-dialog-paper-sheet-target.png`
- 创建候选:`docs/design/assets/candidates/2026-07-19/g01-dialog-close-source-green.png`
- 创建成品:`static/assets/modules/genealogy/transparent/g01-dialog-close.png`
**接口:**
- 产出:带透明通道的朱砂细线关闭图标 PNG;后续模板只通过 `/static/assets/modules/genealogy/transparent/g01-dialog-close.png` 使用。
- [ ] **步骤 1:用内置 Image Gen 生成单独图标候选**
附加选定视觉目标作为参考,使用以下完整提示:
```text
Use case: background-extraction
Asset type: mobile UI close icon
Primary request: Create one standard close icon matching the selected G01 bottom-sheet mockup: two thin diagonal cinnabar-red ink strokes, balanced and optically centered, no circle, no label, no shadow, no texture inside the strokes.
Scene/backdrop: perfectly flat solid #00ff00 chroma-key background.
Composition: square canvas, icon centered, icon occupies about 34% of canvas width, generous equal padding.
Constraints: one icon only; crisp antialiased edges; no text; no watermark; no extra decoration; do not use #00ff00 in the icon.
```
将工具返回的原始图片复制为 `docs/design/assets/candidates/2026-07-19/g01-dialog-close-source-green.png`,保留原始生成文件。
- [ ] **步骤 2:移除色键并保留真实透明通道**
使用已安装的绝对 Python 解释器和技能脚本:
```powershell
& 'C:\Users\Rain\AppData\Local\Python\bin\python.exe' 'C:\Users\Rain\.codex\skills\.system\imagegen\scripts\remove_chroma_key.py' --input 'docs\design\assets\candidates\2026-07-19\g01-dialog-close-source-green.png' --out 'static\assets\modules\genealogy\transparent\g01-dialog-close.png' --auto-key border --soft-matte --transparent-threshold 12 --opaque-threshold 220 --despill
```
预期:命令退出码为 0,生成文件存在且非空。若依赖缺失,停止并报告,不安装新依赖、不切换到 CLI 图片模型。
- [ ] **步骤 3:检查图标成品**
`view_image` 打开成品,并用 `System.Drawing.Image` 确认 `PixelFormat` 包含 `Alpha`。拒绝绿色边缘、多余文字、圆圈、阴影或多图标输出;不从效果图裁切替代。
---
### 任务 2:用失败契约锁定宣纸结构和现有按钮
**文件:**
- 修改:`tests/g01-visual-contract.ps1`
- 修改:`pages/genealogy/g01-my-genealogies.vue`
- 不修改:`components/AppButton.vue`
**接口:**
- 消费:任务 1 的 `g01-dialog-close.png`;现有 `AppButton``addDialogVisible``closeAddDialog()` 和三个导航函数。
- 产出:`.add-dialog__paper``.add-dialog__edge``.add-dialog__edge-image``.add-dialog__close-icon`;切换弹层选择器和表现保持不变。
- [ ] **步骤 1:先改契约,要求旧实现失败**
在读取 `$page` 后隔离添加弹层模板,并使用 Base64 解码中文断言:
```powershell
$addMarkup = [regex]::Match($page, '(?s)<view v-if="addDialogVisible".*?<view v-if="switcherVisible"').Value
if (-not $addMarkup) { throw 'G-01 add sheet markup could not be isolated.' }
foreach ($token in @(
'class="add-dialog__paper"',
'page-paper.jpg',
'class="add-dialog__edge"',
'a01-paper-transition-v1.png',
'class="add-dialog__close-icon"',
'g01-dialog-close.png',
'aria-label="关闭"'
)) {
if ($addMarkup -notmatch [regex]::Escape($token)) { throw "G-01 paper sheet is missing $token" }
}
if ($addMarkup -match 'a01-scroll-dialog-v3\.png') { throw 'G-01 add sheet still uses the rejected complete dialog frame.' }
if ($addMarkup -match '>关闭</view>|>取消</view>') { throw 'G-01 add sheet still renders a text close control.' }
foreach ($token in @('label="搜索家谱"', 'label="邀请码加入"', 'label="继续创建家谱"')) {
if ($addMarkup -notmatch [regex]::Escape($token)) { throw "G-01 add sheet no longer reuses AppButton action $token" }
}
if ($page -match '(?s)\.add-dialog\s+\.app-button\s*\{') { throw 'G-01 add sheet must not restyle the existing AppButton.' }
```
在实际 PowerShell 文件中将“关闭、取消、搜索家谱、邀请码加入、继续创建家谱”通过 UTF-8 Base64 解码构造,避免 Windows PowerShell 5 脚本编码误读。
- [ ] **步骤 2:运行契约并确认 RED**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
预期:失败信息为 `G-01 paper sheet is missing class="add-dialog__paper"`,不是语法、编码或文件缺失错误。
- [ ] **步骤 3:最小替换添加弹层模板**
```vue
<view v-if="addDialogVisible" class="add-dialog-layer" @click="closeAddDialog">
<view class="add-dialog" @click.stop>
<image class="add-dialog__paper" src="/static/assets/foundation/opaque/page-paper.jpg" mode="aspectFill" />
<view class="add-dialog__edge" aria-hidden="true">
<image class="add-dialog__edge-image" src="/static/assets/modules/auth/transparent/a01-paper-transition-v1.png" mode="widthFix" />
</view>
<view class="add-dialog__content">
<view class="add-dialog__heading">
<text class="dialog-title">添加家谱</text>
<text class="dialog-copy">建议先搜索已有家谱避免重复创建</text>
</view>
<view class="add-dialog__close" role="button" aria-label="关闭" hover-class="action-hover" @click="closeAddDialog">
<image class="add-dialog__close-icon" src="/static/assets/modules/genealogy/transparent/g01-dialog-close.png" mode="aspectFit" />
</view>
<AppButton block label="搜索家谱" @click="applyToJoin" />
<AppButton block type="secondary" label="邀请码加入" @click="joinByInvite" />
<AppButton block type="secondary" label="继续创建家谱" @click="createGenealogy" />
</view>
</view>
</view>
```
- [ ] **步骤 4:最小替换添加弹层专用样式**
保留 `.genealogy-switcher-*``.dialog-close` 规则,只替换 `.add-dialog*`
```scss
.add-dialog-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: flex-end; justify-content: center; box-sizing: border-box; background: rgba(34, 20, 12, 0.68); }
.add-dialog { position: relative; width: 100%; max-height: calc(100vh - 80rpx); }
.add-dialog__paper { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.add-dialog__edge { position: absolute; z-index: 1; top: -50rpx; right: 0; left: 0; height: 92rpx; overflow: hidden; pointer-events: none; }
.add-dialog__edge-image { position: absolute; right: 0; bottom: 0; left: 0; width: 100%; }
.add-dialog__content { position: relative; z-index: 2; display: flex; max-height: calc(100vh - 80rpx); flex-direction: column; align-items: stretch; box-sizing: border-box; padding: 44rpx 52rpx calc(34rpx + env(safe-area-inset-bottom)); overflow-y: auto; }
.add-dialog__heading { padding-right: 96rpx; }
.add-dialog .dialog-title,
.add-dialog .dialog-copy { display: block; text-align: left; }
.add-dialog__close { position: absolute; z-index: 3; top: 22rpx; right: 30rpx; display: flex; width: 80rpx; height: 80rpx; align-items: center; justify-content: center; }
.add-dialog__close-icon { width: 36rpx; height: 36rpx; }
.add-dialog__content > .app-button:first-of-type { margin-top: 20rpx; }
.add-dialog__content > .app-button + .app-button { margin-top: 6rpx; }
```
删除被否决实现专用的 `.add-dialog__skin`、标题左偏移、关闭文字颜色字号、`right: 120rpx`、固定 `545rpx` 高度和 `.add-dialog .app-button` 覆盖;不删除共享或切换弹层样式。
- [ ] **步骤 5:运行契约并确认 GREEN**
运行步骤 2,预期输出:`PASS G-01 visual contract`
---
### 任务 3:聚焦回归与唯一标签页视觉验证
**文件:**
- 验证:`pages/genealogy/g01-my-genealogies.vue`
- 验证:`tests/g01-visual-contract.ps1`
- 验证:`tests/g01-empty-state-contract.ps1`
- 验证:`tests/g01-loading-state-contract.ps1`
- 验证:`tests/g01-error-state-contract.ps1`
- 生成候选:`docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-paper-sheet-412x915.png`
**接口:**
- 消费:5173 H5、9222 唯一项目标签页、G01 当前添加弹层。
- 产出:当前状态候选截图和真实运行时几何/交互证据;不改变审批勾选状态。
- [ ] **步骤 1:运行全部聚焦契约**
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
git diff --check -- pages/genealogy/g01-my-genealogies.vue tests/g01-visual-contract.ps1 docs/superpowers/specs/2026-07-19-g01-add-genealogy-bottom-sheet-design.md docs/superpowers/plans/2026-07-19-g01-add-genealogy-paper-sheet.md
```
预期:四个测试全部 `PASS``git diff --check` 退出码 0。不得放宽任何既有阈值。
- [ ] **步骤 2:确认运行环境约束**
只读检查 5173、9222 和 `/json/list`。预期:5173 HTTP 200;恰好一个 `localhost:5173` 项目页;不启动新 Chrome。
- [ ] **步骤 3:仅检查当前弹层的两档几何**
在同一标签页保持 G01“添加家谱”打开,依次检查 320×568 和 412×915:弹层底边贴合视口;无横向溢出;标题、说明、关闭图标及三个按钮全部可见且互不遮挡;关闭触控区至少 `80rpx × 80rpx`;页面内仍有且只有三个现有 `.app-button`
- [ ] **步骤 4:检查交互并恢复 412×915**
点击弹层内部不得关闭;点击关闭图标应关闭;重新打开后点击遮罩应关闭;再打开后列表滚动位置不变。最后恢复 412×915,并保持添加弹层打开。
- [ ] **步骤 5:截取并检查原图**
通过当前 CDP `Page.captureScreenshot` 保存候选,使用 `view_image` 打开原图。拒绝矩形硬边、顶部收边裁坏、纸面接缝、关闭图标绿色边缘、按钮被重绘、文字裁切、空白过大或后方信息压不住。
- [ ] **步骤 6:交回当前状态审批**
只报告真实测试和 H5 证据,明确 Android/HBuilderX 未验证。等待用户明确“通过”,不得切换到“切换家谱弹层”,不得更新 G01 为 `[x]`
@@ -0,0 +1,287 @@
# G01 Centered Minimum-Height Add Sheet Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Subagents and worktrees are prohibited for this project.
**Goal:** Make the G01 “添加家谱” sheet use a `780rpx` minimum height whose visible upper edge reaches the user-marked position, center the complete title-and-actions group, and separate adjacent buttons by `24rpx` while preserving safe growth and scrolling for longer content.
**Architecture:** Keep the existing single `g01-add-sheet-background-v3.png` nine-slice sheet and existing `AppButton` assets. The `add-dialog__body` flow container uses vertical `auto` margins to remain centered as its two adjacent-button gaps increase from `6rpx` to `24rpx`; the margins collapse when content grows, while the close control follows the heading in every height state.
**Tech Stack:** uni-app Vue single-file component, SCSS with `rpx`, PowerShell visual-contract tests, Chrome DevTools Protocol on the existing port-9222 project tab.
## Global Constraints
- Modify only G01s current “添加家谱” sheet; do not change other G01 states, the genealogy switcher, or other pages.
- Do not use subagents, worktrees, `git add`, `git commit`, `git push`, `git reset`, or `git checkout`.
- Preserve every existing modified, untracked, ignored, test, document, screenshot, master, candidate, and asset file.
- Do not loosen test thresholds and do not connect APIs.
- Keep the single complete background asset and existing project `AppButton` styles.
- Keep `max-height: calc(100vh - 80rpx)` and content scrolling.
- H5 evidence is internal only; Android/HBuilderX remains unverified.
- Do not mark G01 `[x]`, frozen, or accepted without the users explicit “通过”.
---
### Task 1: Tighten the G01 visual contract for the centered body
**Files:**
- Modify: `tests/g01-visual-contract.ps1:78-135`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: the G01 add-sheet template and SCSS as plain UTF-8 source text.
- Produces: a source contract requiring `add-dialog__body`, nested close control, `780rpx` minimum height, symmetric content padding, and flow-based centering.
- [ ] **Step 1: Require the new body structure and nested close control**
Add `class="add-dialog__body"` to the required markup tokens and add this structural check:
```powershell
if ($addMarkup -notmatch '(?s)<view class="add-dialog__body">\s*<view class="add-dialog__heading">.*?class="add-dialog__close".*?</view>\s*</view>\s*<view class="add-dialog__actions">') {
throw 'G-01 add sheet must keep the close control with the centered heading and actions body.'
}
```
- [ ] **Step 2: Replace the obsolete `620rpx` and fixed-close assertions**
Use these exact contracts:
```powershell
if ($page -notmatch '(?s)\.add-dialog\s*\{[^}]*min-height:\s*780rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*80rpx\);') {
throw 'G-01 add sheet does not reach the approved arrow-aligned minimum height.'
}
if ($page -notmatch '(?s)\.add-dialog__content\s*\{[^}]*min-height:\s*780rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*80rpx\);[^}]*padding:\s*96rpx\s+52rpx\s+calc\(96rpx\s*\+\s*env\(safe-area-inset-bottom\)\);') {
throw 'G-01 add sheet does not reserve the approved symmetric centering area.'
}
if ($page -notmatch '(?s)\.add-dialog__body\s*\{[^}]*margin:\s*auto\s+0;') {
throw 'G-01 add sheet body does not center safely with collapsible auto margins.'
}
if ($page -notmatch '(?s)\.add-dialog__heading\s*\{[^}]*position:\s*relative;[^}]*padding-right:\s*96rpx;') {
throw 'G-01 add sheet heading does not own the close-control positioning context.'
}
if ($page -notmatch '(?s)\.add-dialog__close\s*\{[^}]*position:\s*absolute;[^}]*top:\s*0;[^}]*right:\s*-22rpx;') {
throw 'G-01 add sheet close control does not follow the centered heading.'
}
if ($page -notmatch '(?s)\.add-dialog__actions\s*>\s*\.app-button\s*\+\s*\.app-button\s*\{[^}]*margin-top:\s*24rpx;') {
throw 'G-01 add sheet buttons do not keep the approved 24rpx spacing.'
}
```
Remove the assertions requiring `620rpx`, `118rpx 52rpx calc(64rpx...)`, and `top: 116rpx`.
- [ ] **Step 3: Run the contract and observe the intended failure**
Run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: FAIL with `G-01 add sheet must keep the close control with the centered heading and actions body.` or the new `780rpx` minimum-height failure. A script parse error is not an acceptable red state.
---
### Task 2: Implement the centered, safely growing sheet body
**Files:**
- Modify: `pages/genealogy/g01-my-genealogies.vue:235-250`
- Modify: `pages/genealogy/g01-my-genealogies.vue:952-961`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: existing `addDialogVisible`, `closeAddDialog`, `applyToJoin`, `joinByInvite`, and `createGenealogy` behavior.
- Produces: `add-dialog__body`, which centers at the minimum height, grows with content, and becomes top-origin scroll content at the maximum height.
- [ ] **Step 1: Group the heading and actions, and nest close under the heading**
Replace only the inner add-sheet markup with:
```vue
<view class="add-dialog__content">
<view class="add-dialog__body">
<view class="add-dialog__heading">
<text class="dialog-title">添加家谱</text>
<text class="dialog-copy">建议先搜索已有家谱避免重复创建</text>
<view class="add-dialog__close" role="button" aria-label="关闭" hover-class="action-hover" @click="closeAddDialog">
<image class="add-dialog__close-icon" src="/static/assets/modules/genealogy/transparent/g01-dialog-close.png" mode="aspectFit" />
</view>
</view>
<view class="add-dialog__actions">
<AppButton block label="搜索家谱" @click="applyToJoin" />
<AppButton block type="secondary" label="邀请码加入" @click="joinByInvite" />
<AppButton block type="secondary" label="继续创建家谱" @click="createGenealogy" />
</view>
</view>
</view>
```
- [ ] **Step 2: Apply the minimum-height and safe-centering styles**
Use these exact declarations while preserving the current border-image declarations:
```scss
.add-dialog { position: relative; width: 100%; min-height: 780rpx; max-height: calc(100vh - 80rpx); box-sizing: border-box; border: 1px solid transparent; border-image-source: url("/static/assets/modules/genealogy/transparent/g01-add-sheet-background-v3.png"); border-image-slice: 220 0 1 0 fill; border-image-width: 118rpx 0 1rpx; border-image-repeat: stretch; }
.add-dialog__content { position: relative; z-index: 2; display: flex; min-height: 780rpx; max-height: calc(100vh - 80rpx); flex-direction: column; align-items: stretch; box-sizing: border-box; padding: 96rpx 52rpx calc(96rpx + env(safe-area-inset-bottom)); overflow-y: auto; }
.add-dialog__body { margin: auto 0; }
.add-dialog__heading { position: relative; padding-right: 96rpx; }
.add-dialog__close { position: absolute; z-index: 3; top: 0; right: -22rpx; display: flex; width: 80rpx; height: 80rpx; align-items: center; justify-content: center; }
.add-dialog__actions { display: flex; flex-direction: column; margin: 62rpx -32rpx 0; }
.add-dialog__actions > .app-button + .app-button { margin-top: 24rpx; }
```
- [ ] **Step 3: Run the focused contract**
Run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: `PASS G-01 visual contract`.
- [ ] **Step 4: Run the adjacent G01 state contracts**
Run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
git diff --check
```
Expected: all four G01 contracts pass; `git diff --check` exits 0. Existing line-ending warnings may remain, but no whitespace error may be introduced.
---
### Task 3: Verify minimum, growth, overflow, and interactions in the existing Chrome tab
**Files:**
- Create evidence only: `docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-centered-min-412x915.png`
- Create evidence only: `docs/design/screens/runtime/2026-07-19/g01-approval/05-add-dialog-centered-six-buttons-412x915.png`
- No production or test file changes.
**Interfaces:**
- Consumes: the unique existing `http://localhost:5173` page exposed by Chrome debugging port 9222.
- Produces: measured H5 evidence without opening another browser or project tab.
- [ ] **Step 1: Assert the unique project tab and restore 412×915**
Connect through CDP, filter pages with:
```js
const projectPages = pages.filter(page => page.type === 'page' && page.url.startsWith('http://localhost:5173'))
if (projectPages.length !== 1) throw new Error(`Expected one existing project tab, found ${projectPages.length}`)
```
Set device metrics to `412×915`, navigate that same page to G01, and open `.create-action`.
- [ ] **Step 2: Measure the three-button minimum and centering**
Capture the dialog and body rectangles:
```js
const dialog = document.querySelector('.add-dialog').getBoundingClientRect()
const body = document.querySelector('.add-dialog__body').getBoundingClientRect()
const topSpace = body.top - dialog.top
const bottomSpace = dialog.bottom - body.bottom
```
Expected at 412×915:
- `dialog.height` is approximately `429px` (`780rpx`, tolerance ±3px).
- `Math.abs(topSpace - bottomSpace) <= 3` in H5 where the safe-area inset is zero.
- Each adjacent-button visual gap is approximately `13.2px` (`24rpx`, tolerance ±2px).
- Three `.app-button` elements are visible and horizontal overflow is `0`.
- Save `05-add-dialog-centered-min-412x915.png`.
- [ ] **Step 3: Verify 320×568**
Set `320×568`, reopen the same state, and measure again.
Expected:
- Dialog minimum is approximately `333px` (tolerance ±3px).
- Each adjacent-button visual gap is approximately `10.2px` (`24rpx`, tolerance ±2px).
- The last button bottom is not below the viewport.
- No horizontal overflow or heading/close overlap.
- [ ] **Step 4: Verify natural six-button growth**
At 412×915, clone the three existing button nodes once inside `.add-dialog__actions` for pressure evidence only.
Expected:
- Button count is 6.
- Dialog height is greater than the three-button minimum and less than its maximum.
- `content.scrollHeight === content.clientHeight` for this medium pressure state.
- Save `05-add-dialog-centered-six-buttons-412x915.png`.
- [ ] **Step 5: Verify safe maximum-height scrolling**
Continue cloning button nodes until content exceeds the maximum height.
Expected:
- Dialog height does not exceed `calc(100vh - 80rpx)` by more than 2px.
- `content.scrollHeight > content.clientHeight`.
- Initial `content.scrollTop === 0` and the title is reachable at the top.
- Setting `content.scrollTop = content.scrollHeight` produces a positive scroll position and reaches the final button.
- [ ] **Step 6: Verify close behavior and restore approval state**
Verify in order:
- Clicking `.add-dialog__content` keeps the layer open.
- Clicking `.add-dialog__close` closes it.
- Reopening and clicking `.add-dialog-layer` closes it.
- No runtime exceptions or failed 4xx/5xx asset responses occur on a fresh navigation.
Finally restore the same tab to 412×915, three buttons, no injected clones, and keep the add sheet open.
---
### Task 4: Record internal design QA without claiming user or Android acceptance
**Files:**
- Modify: `design-qa.md`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: the user-marked screenshot, three-button capture, six-button capture, measured centering, responsive check, interaction results, and console/network results.
- Produces: an internal H5 QA entry whose final result describes only the candidate evidence.
- [ ] **Step 1: Create a same-viewport comparison image**
Place the previous 412×915 state and `05-add-dialog-centered-min-412x915.png` side by side without rescaling either app viewport. Save it under the existing G01 approval evidence folder.
- [ ] **Step 2: Inspect the combined comparison and both pressure captures**
Check the five required surfaces explicitly: typography, spacing/layout, color/tokens, image quality/assets, and copy. Record any P0/P1/P2 finding before claiming an internal pass.
- [ ] **Step 3: Update `design-qa.md`**
Record:
- Source screenshot and approved arrow target.
- Implementation and comparison paths.
- 412×915 and 320×568 metrics.
- Three-button centering, six-button growth, and maximum-height scrolling.
- Interaction and console/network evidence.
- `final result: passed` only if no actionable H5 P0/P1/P2 issue remains.
- Explicit limits: user acceptance pending; Android/HBuilderX unverified; G01 not frozen.
- [ ] **Step 4: Run the final verification gate**
Run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell.exe -NoProfile -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
git diff --check
```
Expected: all contracts pass and `git diff --check` exits 0. Report line-ending warnings separately from failures.
Do not run any Git write command. Leave the existing 412×915 Chrome tab showing only the three-button add sheet and wait for explicit user approval.
@@ -0,0 +1,250 @@
# G01 可伸缩切换家谱弹层 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. The user explicitly forbids subagents and worktrees, so execution must remain inline in the current workspace. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 将 G01“切换当前家谱”改为紧凑、可读、带关闭图标且背景随内容九宫格伸缩的居中弹层。
**Architecture:** 继续使用现有完整 PNG `a01-scroll-dialog-v3.png`,由弹层根容器通过 `border-image` 九宫格渲染,固定装饰区并仅拉伸中间宣纸。标题和关闭入口固定在安全区,家谱列表按内容自然增高,到视口上限后独立滚动;不抽取全局组件。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell 合同测试、Chrome DevTools Protocol 9222。
## Global Constraints
- 只修改 G01“切换当前家谱”弹层,不修改公共 `AppDialog``AppButton`、其他状态或其他页面。
- 唯一背景资产为 `static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png`,源尺寸 `1860×1560`;不得覆盖、删除、重绘或拆分资产。
- 九宫格源切片为 `300 260 360 260 fill`,显示边宽为 `110rpx 48rpx 132rpx 48rpx`
- 弹层宽度 `670rpx`、最小高度 `600rpx`、最大高度 `calc(100vh - 120rpx)`;内容区内边距为 `120rpx 58rpx 140rpx`
- 关闭入口复用 `g01-dialog-close.png`,热区 `80rpx × 80rpx`,定位 `top: 78rpx; right: 34rpx`,可访问名称为“关闭”。
- 复用 9222 上唯一的 `localhost:5173` 项目标签页,不新开浏览器或第二个项目标签页。
- 不使用多代理、worktree;不执行 `git add``commit``push``reset``checkout`;不删除或清理现有文件。
- H5 截图仅为内部候选;Android/HBuilderX 未验证;未经用户明确“通过”不得标记或冻结 G01。
---
### Task 1: 建立可伸缩切换弹层视觉合同
**Files:**
- Modify: `tests/g01-visual-contract.ps1`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: G01 源文件字符串 `$page` 和切换弹层现有类名。
- Produces: 九宫格、动态高度、安全区、滚动列表、关闭图标和返回键的单一合同。
- [ ] **Step 1: 添加失败合同**
在现有 switcher 断言附近加入:
```powershell
if ($page -match 'class="genealogy-switcher__skin"') {
throw 'G-01 switcher must not render the complete background as a fixed aspectFit image.'
}
if ($page -notmatch '(?s)\.genealogy-switcher\s*\{[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);[^}]*border-image-source:\s*url\("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3\.png"\);[^}]*border-image-slice:\s*300\s+260\s+360\s+260\s+fill;[^}]*border-image-width:\s*110rpx\s+48rpx\s+132rpx\s+48rpx;') {
throw 'G-01 switcher does not use the approved stretchable complete background.'
}
if ($page -notmatch '(?s)\.genealogy-switcher__content\s*\{[^}]*min-height:\s*600rpx;[^}]*max-height:\s*calc\(100vh\s*-\s*120rpx\);[^}]*padding:\s*120rpx\s+58rpx\s+140rpx;') {
throw 'G-01 switcher content does not keep the approved decoration safe area.'
}
if ($page -notmatch 'class="genealogy-switcher__list"[^>]*scroll-y') {
throw 'G-01 switcher does not provide an independent scroll list.'
}
if ($page -notmatch '(?s)class="genealogy-switcher__close"[^>]*aria-label="关闭".*?g01-dialog-close\.png') {
throw 'G-01 switcher does not use the custom accessible close control.'
}
if ($page -match '<view class="dialog-close" @click="closeSwitcher">关闭</view>') {
throw 'G-01 switcher still exposes the obsolete bottom close copy.'
}
if ($page -notmatch '(?s)onBackPress\(\(\)\s*=>\s*\{\s*if\s*\(switcherVisible\.value\)\s*\{\s*closeSwitcher\(\);\s*return\s*true;\s*\}\s*if\s*\(addDialogVisible\.value\)') {
throw 'G-01 switcher does not consume Android back before the add dialog and page navigation.'
}
```
- [ ] **Step 2: 运行合同并确认 RED**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: FAIL,首先输出 `G-01 switcher must not render the complete background as a fixed aspectFit image.`
### Task 2: 最小实现动态背景、紧凑内容和关闭交互
**Files:**
- Modify: `pages/genealogy/g01-my-genealogies.vue:256-276`
- Modify: `pages/genealogy/g01-my-genealogies.vue:389-395`
- Modify: `pages/genealogy/g01-my-genealogies.vue:967-981`
- Test: `tests/g01-visual-contract.ps1`
**Interfaces:**
- Consumes: `availableGenealogies``selectedGenealogyId``selectGenealogy(item)``closeSwitcher()`
- Produces: `.genealogy-switcher__list` 独立滚动区和 `.genealogy-switcher__close` 自定义关闭控件。
- [ ] **Step 1: 替换切换弹层模板**
将当前弹层内部结构替换为:
```vue
<view v-if="switcherVisible" class="genealogy-switcher-layer" @click="closeSwitcher">
<view class="genealogy-switcher" role="dialog" aria-modal="true" aria-label="切换当前家谱" @click.stop>
<view class="genealogy-switcher__content">
<text class="dialog-title">切换当前家谱</text>
<view class="genealogy-switcher__close" role="button" aria-label="关闭" hover-class="action-hover" @click="closeSwitcher">
<image class="genealogy-switcher__close-icon" src="/static/assets/modules/genealogy/transparent/g01-dialog-close.png" mode="aspectFit" />
</view>
<scroll-view class="genealogy-switcher__list" scroll-y>
<view
v-for="item in availableGenealogies"
:key="item.id"
class="switcher-item"
:class="{ 'switcher-item--active': item.id === selectedGenealogyId }"
@click="selectGenealogy(item)"
>
<view>
<text class="switcher-item__name">{{ item.name }}</text>
<text class="switcher-item__meta">{{ item.location }} · {{ item.memberCount }} 位成员</text>
</view>
<text class="switcher-item__state">{{ item.id === selectedGenealogyId ? '当前' : '选择' }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
```
- [ ] **Step 2: 调整 Android 返回键优先级**
将处理器改为:
```js
onBackPress(() => {
if (switcherVisible.value) {
closeSwitcher()
return true
}
if (addDialogVisible.value) {
closeAddDialog()
return true
}
return false
})
```
- [ ] **Step 3: 写入最小页面级样式**
使用以下结构替换旧 switcher 样式:
```scss
.genealogy-switcher-layer { position: fixed; z-index: 40; inset: 0; display: flex; align-items: center; justify-content: center; box-sizing: border-box; padding: 60rpx 40rpx; background: rgba(34, 20, 12, 0.58); }
.genealogy-switcher { position: relative; width: 670rpx; max-width: 100%; min-height: 600rpx; max-height: calc(100vh - 120rpx); box-sizing: border-box; border: 1px solid transparent; border-image-source: url("/static/assets/modules/auth/transparent/a01-scroll-dialog-v3.png"); border-image-slice: 300 260 360 260 fill; border-image-width: 110rpx 48rpx 132rpx 48rpx; border-image-repeat: stretch; }
.genealogy-switcher__content { position: relative; z-index: 1; display: flex; min-height: 600rpx; max-height: calc(100vh - 120rpx); flex-direction: column; align-items: center; box-sizing: border-box; padding: 120rpx 58rpx 140rpx; }
.genealogy-switcher__close { position: absolute; z-index: 3; top: 78rpx; right: 34rpx; display: flex; width: 80rpx; height: 80rpx; align-items: center; justify-content: center; }
.genealogy-switcher__close-icon { width: 80rpx; height: 80rpx; }
.genealogy-switcher__list { width: 100%; min-height: 0; max-height: calc(100vh - 500rpx); margin-top: 24rpx; overflow-y: auto; }
.switcher-item { display: flex; width: 100%; min-height: 112rpx; align-items: center; justify-content: space-between; box-sizing: border-box; padding: 18rpx 16rpx; border: 1rpx solid transparent; border-bottom-color: rgba(181, 138, 75, 0.42); }
.switcher-item__name,
.switcher-item__meta { display: block; }
.switcher-item__name { color: $ink; font-family: "STKaiti", "KaiTi", serif; font-size: 32rpx; font-weight: 700; }
.switcher-item__meta { margin-top: 6rpx; color: #62584c; font-size: 24rpx; }
.switcher-item__state { color: $brand-red; font-size: 25rpx; font-weight: 600; }
.switcher-item--active { border-color: rgba(159, 23, 15, 0.22); background: rgba(159, 23, 15, 0.055); }
.switcher-item--active .switcher-item__name { color: $brand-red; }
```
不得修改公共组件或任何图片资产。
- [ ] **Step 4: 运行聚焦合同并确认 GREEN**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-visual-contract.ps1
```
Expected: `PASS G-01 visual contract`
- [ ] **Step 5: 运行相邻 G01 合同**
Run each command independently:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-empty-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-loading-state-contract.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File tests/g01-error-state-contract.ps1
git diff --check
```
Expected: 三个状态合同均输出 `PASS``git diff --check` 退出码为 0,允许仅出现工作区既有 LF/CRLF 警告。
### Task 3: 在唯一 Chrome 标签页验证四档、伸缩和交互
**Files:**
- Modify: `design-qa.md`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/06-switch-dialog-stretchable-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/06-switch-dialog-stretchable-320x568.png`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/06-switch-dialog-stretchable-six-items-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/g01-approval/06-switch-dialog-before-vs-stretchable.png`
**Interfaces:**
- Consumes: 9222 上唯一的 `localhost:5173` 页面和当前 G01 列表态。
- Produces: 四档两项状态、六项自然增高、十二项封顶滚动及最终审批状态的运行证据。
- [ ] **Step 1: 确认唯一项目标签页并新鲜打开弹层**
读取 `http://127.0.0.1:9222/json/list`,严格确认只有一个 `type: page` 且 URL 以 `http://localhost:5173` 开头的项目页。刷新同一页面,点击 `.current-slip`,等待 `.genealogy-switcher` 可见。
Expected: 项目标签页数量为 1,切换弹层打开,没有添加弹层或临时克隆。
- [ ] **Step 2: 验证四档两项状态**
在同一标签页依次设置 `320×568``360×640``360×800``412×915`
Expected:
```text
dialog min-height = 600rpx 对应像素值
title 不与顶部边框或祥云重叠
close control 完整可见
itemCount = 2
列表不滚动
末项下方纯内容空白 <= 72rpx 对应像素值
horizontalOverflow = false
```
- [ ] **Step 3: 验证六项自然增高**
只在浏览器运行时向 `.genealogy-switcher__list` 临时克隆四项,并标记 `data-cdp-clone="1"`
Expected: 弹层高度大于两项状态、低于最大高度;背景中间宣纸伸长,顶部祥云和底部山水尺寸不变;列表无需滚动即可看全六项。
- [ ] **Step 4: 验证十二项封顶滚动**
清除六项克隆后临时补足十二项。
Expected: 弹层高度等于最大安全高度;标题和关闭图标固定;列表 `scrollHeight > clientHeight`、初始 `scrollTop = 0`,滚到底后末项可见;后方页面不滚动。
- [ ] **Step 5: 验证关闭和选择交互**
依次验证:弹层内部点击保持打开;关闭图标关闭;遮罩关闭;Android 返回处理器合同;点击第二项更新当前家谱并关闭。每次通过 `.current-slip` 在同一标签页重新打开。
Expected: 不出现原生 Toast、Loading、Modal 或 ActionSheet;控制台无异常,无 4xx/5xx 资源响应。
- [ ] **Step 6: 清理运行时克隆并恢复审批状态**
移除所有 `[data-cdp-clone]`,将视口恢复为 `412×915`,恢复两条真实数据并保持切换弹层打开。
Expected: 项目标签页仍为 1;添加弹层关闭;切换弹层打开;两项、无克隆、无横向溢出。
- [ ] **Step 7: 生成对比并更新设计质检**
把审查前截图 `audit-switch-dialog/01-switch-dialog-412x915.png` 与新 412×915 截图合成同尺寸横向对比;在 `design-qa.md` 记录标题安全区、空白量、九宫格装饰尺寸、文本可读性、关闭入口、四档、压力滚动、交互、控制台和证据限制。
Expected: 若 H5 内部候选不存在可执行 P0/P1/P2,则当前小节 `final result: passed`;同时明确用户尚未通过、Android/HBuilderX 未验证。
- [ ] **Step 8: 最终新鲜验证**
重新运行四个 G01 合同与 `git diff --check`,再读取浏览器最终状态。
Expected: 所有合同退出码为 0;同一项目标签页为 `412×915`、两项、切换弹层打开、无临时克隆、无横向溢出。
@@ -0,0 +1,494 @@
# T07 模块基准页实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. 本项目明确禁止多代理与 worktree,因此只允许当前会话内联执行。
**Goal:** 把 T07“成员目录”完善为 T 模块常规页面的视觉基准,并在用户明确通过前停留在 T07,不修改 T01、F01 或其他页面。
**Architecture:** T07 保留现有搜索与成员列表业务结构,补齐当前家谱上下文、失败恢复入口、可访问操作语义和小屏自然滚动。页面继续消费 `ModulePageBackground``PageHeader``AppLoading``AppButton` 及现有真实位图资产,不抽取新全局组件;新增聚焦静态合同与 Chrome CDP 运行检查作为后续 T 页面推广的基准合同。
**Tech Stack:** uni-app、Vue 3 `<script setup>`、SCSS、PowerShell 合同测试、Node.js Chrome DevTools Protocol 9222、现有 Chrome 5173 H5。
## Global Constraints
- 全程只处理 T07,用户指出问题时不得跨页修改。
- 不对接接口,不把 Mock 或 H5 截图描述为功能通过。
- 只复用 9222 上唯一的 `localhost:5173` Chrome 标签页,不新开浏览器或第二个项目标签页。
- 不使用多代理,不使用 worktree。
- 不执行 `git add``commit``push``reset``checkout`
- 保留全部现有修改、未跟踪文件、测试、文档、截图、母版和候选资产。
- 不为测试通过而放宽阈值。
- 所有弹窗、Toast、Loading 和 ActionSheet 使用项目自定义组件。
- T07 只有全部适用状态、四档尺寸和用户明确结论均通过后,才能成为 T 模块冻结基准。
- Android/HBuilderX 真机或模拟器仍未验证,最终报告必须保留此限制。
---
### Task 1: 固化 T07 基准页视觉与状态合同
**Files:**
- Create: `tests/t07-module-baseline-contract.ps1`
- Read: `pages/tree/t07-member-directory.vue`
- Read: `docs/superpowers/specs/2026-07-19-module-baseline-accelerated-visual-review-design.md`
**Interfaces:**
- Consumes: T07 当前 `directoryState``filteredMembers``searchMembers()``openMember(item)`
- Produces: T07 唯一的聚焦静态基准合同,约束 `.directory-context`、搜索语义、四个状态、恢复动作和滚动规则。
- [ ] **Step 1: 写入失败合同**
创建 `tests/t07-module-baseline-contract.ps1`,完整内容如下:
```powershell
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
$page = Get-Content -LiteralPath (Join-Path $root 'pages/tree/t07-member-directory.vue') -Raw -Encoding utf8
function Assert-Match([string]$Pattern, [string]$Message) {
if ($page -notmatch $Pattern) { throw $Message }
}
Assert-Match 'class="directory-context"' 'T07 must show the current genealogy context.'
Assert-Match '汤氏家谱' 'T07 baseline must identify the current genealogy in the H5 mock state.'
Assert-Match '''directory-state--loading'': directoryState === ''loading''' 'T07 loading state must have an explicit root class.'
Assert-Match 'v-if="directoryState === ''list'' \|\| directoryState === ''empty''"' 'T07 search must remain available for list and empty states only.'
Assert-Match 'class="directory-search__action"[^>]*role="button"[^>]*aria-label="查找成员"' 'T07 search action must expose button semantics and an accessible label.'
Assert-Match '<AppButton\s+v-if="directoryState === ''error''"[^>]+type="secondary"[^>]+label="重新查看"[^>]+@click="retryDirectory"' 'T07 error state must provide a custom recovery action.'
Assert-Match 'const retryDirectory = \(\) => \{ directoryState\.value = ''list'' \}' 'T07 retry action must return to the list state.'
Assert-Match '(?s)\.directory-page\s*\{[^}]*overflow-x:\s*hidden;[^}]*overflow-y:\s*auto;' 'T07 must allow natural vertical scrolling without horizontal overflow.'
Assert-Match '(?s)\.directory-search__action\s*\{[^}]*min-width:\s*88rpx;[^}]*min-height:\s*72rpx;' 'T07 search action must preserve a usable touch target.'
foreach ($state in @('loading', 'list', 'empty', 'error')) {
Assert-Match $state "T07 must preserve the $state state."
}
Write-Output 'T07-MODULE-BASELINE-CONTRACT PASS'
```
- [ ] **Step 2: 运行合同确认旧实现失败**
Run:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -File tests/t07-module-baseline-contract.ps1
```
Expected: FAIL,首个失败信息为 `T07 must show the current genealogy context.`;不得先修改断言。
---
### Task 2: 最小化完善 T07 模板、状态恢复与视觉层级
**Files:**
- Modify: `pages/tree/t07-member-directory.vue`
- Test: `tests/t07-module-baseline-contract.ps1`
- Test: `tests/t07-t08-all-states-visual-contract.ps1`
- Test: `tests/module-app-loading-contract.ps1`
- Test: `tests/t03-t08-member-flow-contract.ps1`
**Interfaces:**
- Consumes: Task 1 的静态合同;现有 `AppLoading``AppButton``PageHeader``ModulePageBackground`
- Produces: T07 常规基准结构:当前谱上下文、列表/空态搜索、错误恢复、自然纵向滚动。
- [ ] **Step 1: 调整模板但保留原有业务结构**
`PageHeader` 后增加当前谱上下文;搜索只在列表和空态显示;把搜索文字改为具有按钮语义的命名元素;在失败状态卡后增加自定义恢复按钮:
同时在页面根节点的状态 class 对象中补入:
```vue
'directory-state--loading': directoryState === 'loading'
```
```vue
<view class="directory-context">
<text class="directory-context__name">汤氏家谱</text>
<text class="directory-context__meta">主支 · {{ members.length }} 位成员</text>
</view>
<view v-if="directoryState === 'list' || directoryState === 'empty'" class="directory-search">
<image src="/static/assets/modules/genealogy/opaque/g06-search-input-wide.png" mode="scaleToFill" />
<input v-model="keyword" aria-label="成员搜索关键词" placeholder="按姓名、字辈或支系查找" placeholder-class="directory-placeholder" @confirm="searchMembers" />
<view class="directory-search__action" role="button" aria-label="查找成员" hover-class="action-hover" @click="searchMembers"><text>查找</text></view>
</view>
```
失败卡片后加入:
```vue
<AppButton
v-if="directoryState === 'error'"
block
type="secondary"
label="重新查看"
@click="retryDirectory"
/>
```
- [ ] **Step 2: 增加最小脚本逻辑**
只增加现有组件导入和恢复函数,不新增接口、计时器或抽象:
```js
import AppButton from '@/components/AppButton.vue'
const retryDirectory = () => { directoryState.value = 'list' }
```
- [ ] **Step 3: 收敛 T07 基准样式**
保留现有位图和卡片尺寸,只增加上下文、触控区和自然滚动样式;把旧 `.directory-search > text` 规则替换为 `.directory-search__action`
```scss
.directory-page { position: relative; min-height: 100vh; overflow-x: hidden; overflow-y: auto; background: $paper; }
.directory-page__header, .directory-context, .directory-search, .directory-content { position: relative; z-index: 2; }
.directory-context { display: flex; align-items: baseline; justify-content: space-between; box-sizing: border-box; padding: 22rpx 32rpx 0; }
.directory-context__name { color: $ink; font-family: 'STKaiti', 'KaiTi', serif; font-size: 32rpx; font-weight: 700; }
.directory-context__meta { color: #62584c; font-size: 24rpx; font-weight: 500; }
.directory-search__action { position: absolute; top: 6rpx; right: 8rpx; z-index: 1; display: flex; min-width: 88rpx; min-height: 72rpx; align-items: center; justify-content: center; color: $brand-red; font-size: 24rpx; font-weight: 700; }
.directory-content > .app-button { margin: 20rpx auto 0; }
```
不得修改共享组件、T08 或其他页面。
- [ ] **Step 4: 运行聚焦与既有合同**
Run:
```powershell
$tests = @(
'tests/t07-module-baseline-contract.ps1',
'tests/t07-t08-all-states-visual-contract.ps1',
'tests/module-app-loading-contract.ps1',
'tests/t03-t08-member-flow-contract.ps1'
)
foreach ($test in $tests) {
powershell -NoProfile -ExecutionPolicy Bypass -File $test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
```
Expected: 四个合同均输出 `PASS`,退出码为 0。
---
### Task 3: 增加唯一 Chrome 标签页的 T07 响应式与交互检查
**Files:**
- Create: `tests/t07-module-baseline-runtime-smoke.js`
- Create: `docs/design/screens/runtime/2026-07-19/t07-baseline/01-list-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/t07-baseline/02-loading-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/t07-baseline/03-empty-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/t07-baseline/04-error-412x915.png`
- Create: `docs/design/screens/runtime/2026-07-19/t07-baseline/05-responsive-contact-sheet.png`
**Interfaces:**
- Consumes: 9222 上唯一 `localhost:5173` 页面;T07 查询状态 `state=loading|empty|error`
- Produces: 四状态、四尺寸、搜索和错误恢复的运行证据;最终把当前标签页停在 `412×915` T07 列表态。
- [ ] **Step 1: 编写 CDP 运行检查**
创建 `tests/t07-module-baseline-runtime-smoke.js`,完整内容如下。运行脚本不得打开新标签页、不得调用 Playwright、不得清除 Chrome 用户数据。
```js
const fs = require('fs')
const path = require('path')
const origin = 'http://localhost:5173'
const outputDirectory = path.resolve('docs/design/screens/runtime/2026-07-19/t07-baseline')
const sizes = [
{ width: 320, height: 568 },
{ width: 360, height: 640 },
{ width: 360, height: 800 },
{ width: 412, height: 915 }
]
const sleep = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds))
const connect = async () => {
const pages = await (await fetch('http://127.0.0.1:9222/json/list')).json()
const projectPages = pages.filter((page) => page.type === 'page' && page.url.startsWith(origin))
if (projectPages.length !== 1) throw new Error(`Expected one project page, found ${projectPages.length}`)
const socket = new WebSocket(projectPages[0].webSocketDebuggerUrl)
await new Promise((resolve, reject) => {
socket.addEventListener('open', resolve, { once: true })
socket.addEventListener('error', reject, { once: true })
})
let id = 0
const pending = new Map()
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data)
const request = pending.get(message.id)
if (!request) return
pending.delete(message.id)
message.error ? request.reject(new Error(message.error.message)) : request.resolve(message.result)
})
const send = (method, params = {}) => new Promise((resolve, reject) => {
id += 1
pending.set(id, { resolve, reject })
socket.send(JSON.stringify({ id, method, params }))
})
return { projectPageCount: projectPages.length, socket, send }
}
const valueOf = async (send, expression) => {
const result = await send('Runtime.evaluate', { expression, returnByValue: true })
if (result.exceptionDetails) throw new Error(result.exceptionDetails.text || 'Runtime evaluation failed')
return result.result?.value
}
const waitFor = async (send, expression, message) => {
for (let attempt = 0; attempt < 60; attempt += 1) {
try {
if (await valueOf(send, expression)) return
} catch (error) {
if (!String(error.message).includes('Inspected target navigated or closed')) throw error
}
await sleep(100)
}
throw new Error(message)
}
const setSize = (send, size) => send('Emulation.setDeviceMetricsOverride', {
...size,
deviceScaleFactor: 1,
mobile: true,
screenWidth: size.width,
screenHeight: size.height
})
let navigationId = 0
const openState = async (send, query = '') => {
navigationId += 1
const url = `${origin}/?t07Baseline=${navigationId}#/pages/tree/t07-member-directory${query}`
await send('Page.navigate', { url })
await waitFor(send, `location.href === ${JSON.stringify(url)} && Boolean(document.querySelector('.directory-page'))`, `T07 did not render ${query || 'list'}`)
await sleep(250)
}
const capture = async (send, filename) => {
const screenshot = await send('Page.captureScreenshot', { format: 'png', fromSurface: true, captureBeyondViewport: false })
fs.mkdirSync(outputDirectory, { recursive: true })
fs.writeFileSync(path.join(outputDirectory, filename), Buffer.from(screenshot.data, 'base64'))
}
const metrics = (send) => valueOf(send, `(() => {
const rect = (selector) => {
const node = document.querySelector(selector)
if (!node) return null
const box = node.getBoundingClientRect()
return { top: box.top, right: box.right, bottom: box.bottom, left: box.left, width: box.width, height: box.height }
}
return {
viewport: { width: innerWidth, height: innerHeight },
context: rect('.directory-context'),
search: rect('.directory-search'),
firstCard: rect('.directory-card'),
lastCard: rect('.directory-card:last-child'),
cardCount: document.querySelectorAll('.directory-card').length,
state: document.querySelector('.directory-state--loading') ? 'loading' : document.querySelector('.directory-state--empty') ? 'empty' : document.querySelector('.directory-state--error') ? 'error' : 'list',
horizontalOverflow: document.documentElement.scrollWidth > innerWidth || document.body.scrollWidth > innerWidth,
documentScrollHeight: document.documentElement.scrollHeight
}
})()`)
const assert = (condition, message) => { if (!condition) throw new Error(message) }
const run = async () => {
const { projectPageCount, socket, send } = await connect()
const runtimeErrors = []
const resourceErrors = []
socket.addEventListener('message', (event) => {
const message = JSON.parse(event.data)
if (message.method === 'Runtime.exceptionThrown') runtimeErrors.push(message.params.exceptionDetails?.text || 'runtime exception')
if (message.method === 'Network.responseReceived' && message.params.response.status >= 400) resourceErrors.push(`${message.params.response.status} ${message.params.response.url}`)
})
try {
await send('Page.enable')
await send('Runtime.enable')
await send('Network.enable')
const responsive = []
for (const size of sizes) {
await setSize(send, size)
await openState(send, '?genealogyId=1001')
const current = await metrics(send)
assert(!current.horizontalOverflow, `${size.width}x${size.height}: horizontal overflow`)
assert(current.context && current.search && current.firstCard, `${size.width}x${size.height}: missing baseline content`)
assert(current.cardCount === 3, `${size.width}x${size.height}: expected three members`)
await valueOf(send, 'scrollTo(0, document.documentElement.scrollHeight)')
await sleep(50)
const lastBottom = await valueOf(send, "document.querySelector('.directory-card:last-child').getBoundingClientRect().bottom")
assert(lastBottom <= size.height + 1, `${size.width}x${size.height}: last member is not reachable`)
await valueOf(send, 'scrollTo(0, 0)')
responsive.push(current)
await capture(send, `responsive-${size.width}x${size.height}.png`)
}
await setSize(send, { width: 412, height: 915 })
await openState(send, '?state=loading&genealogyId=1001')
assert((await metrics(send)).state === 'loading', 'Loading state did not render')
await capture(send, '02-loading-412x915.png')
await openState(send, '?state=empty&genealogyId=1001')
assert((await metrics(send)).state === 'empty', 'Empty state did not render')
await capture(send, '03-empty-412x915.png')
await openState(send, '?state=error&genealogyId=1001')
assert((await metrics(send)).state === 'error', 'Error state did not render')
assert(!await valueOf(send, "Boolean(document.querySelector('.directory-search'))"), 'Error state must not expose search')
await capture(send, '04-error-412x915.png')
await valueOf(send, "document.querySelector('.directory-content .app-button').click()")
await waitFor(send, "document.querySelectorAll('.directory-card').length === 3 && Boolean(document.querySelector('.directory-search'))", 'Error retry did not restore the list')
const inputSearch = async (value) => {
await valueOf(send, `(() => {
const input = document.querySelector('.directory-search input')
input.value = ${JSON.stringify(value)}
input.dispatchEvent(new Event('input', { bubbles: true }))
return true
})()`)
await sleep(50)
await valueOf(send, "document.querySelector('.directory-search__action').click()")
}
await inputSearch('不存在')
await waitFor(send, "Boolean(document.querySelector('.directory-state--empty'))", 'No-result search did not render the empty state')
await inputSearch('')
await waitFor(send, "document.querySelectorAll('.directory-card').length === 3", 'Cleared search did not restore all members')
await openState(send, '?genealogyId=1001')
await capture(send, '01-list-412x915.png')
const final = await metrics(send)
assert(final.viewport.width === 412 && final.viewport.height === 915 && final.state === 'list', 'Final approval state is invalid')
assert(runtimeErrors.length === 0, `Runtime errors: ${runtimeErrors.join(' | ')}`)
assert(resourceErrors.length === 0, `Resource errors: ${resourceErrors.join(' | ')}`)
process.stdout.write(`${JSON.stringify({ projectPageCount, responsive, final, runtimeErrors, resourceErrors }, null, 2)}\n`)
process.stdout.write('PASS T07 module baseline runtime smoke\n')
} finally {
socket.close()
}
}
run().catch((error) => {
process.stderr.write(`${error.stack || error.message}\n`)
process.exit(1)
})
```
- [ ] **Step 2: 运行 CDP 检查**
Run:
```powershell
node tests/t07-module-baseline-runtime-smoke.js
```
Expected: 输出四档几何数据、四状态结果、`projectPageCount: 1``runtimeErrors: []``resourceErrors: []`,最后输出 `PASS T07 module baseline runtime smoke`
- [ ] **Step 3: 打开并检查全部截图**
先生成四档联系表:
```powershell
powershell -NoProfile -ExecutionPolicy Bypass -Command "& '.\scripts\create-horizontal-contact-sheet.ps1' -Output '.\docs\design\screens\runtime\2026-07-19\t07-baseline\05-responsive-contact-sheet.png' -InputPaths @('.\docs\design\screens\runtime\2026-07-19\t07-baseline\responsive-320x568.png','.\docs\design\screens\runtime\2026-07-19\t07-baseline\responsive-360x640.png','.\docs\design\screens\runtime\2026-07-19\t07-baseline\responsive-360x800.png','.\docs\design\screens\runtime\2026-07-19\t07-baseline\responsive-412x915.png')"
```
使用本地图片查看工具逐张打开四状态和四尺寸证据。拒绝以下问题:
- 当前谱名缺失或与搜索框挤压。
- 320×568 出现横向溢出、卡片文字裁切或末项不可达。
- 空态仍显示旧列表,失败态仍显示可误操作搜索框。
- 恢复按钮不是项目卷轴按钮。
- 背景、标题栏、文字层级与 T 模块候选不一致。
发现问题时只返工 T07,并重新执行 Task 1–3 的相关步骤。
---
### Task 4: 完成 T07 内部设计 QA 并交给用户逐态审批
**Files:**
- Modify: `design-qa.md`
- Create: `docs/design/screens/runtime/2026-07-19/t07-baseline/06-before-vs-after.png`
**Interfaces:**
- Consumes: 审查前 `module-baseline-audit/04-t07.png`、Task 3 新鲜运行截图。
- Produces: T07 基准页内部 QA 记录和唯一 Chrome 审批现场。
- [ ] **Step 1: 生成同尺寸前后对比**
使用 `scripts/create-horizontal-contact-sheet.ps1` 合并:
```text
docs/design/screens/runtime/2026-07-19/module-baseline-audit/04-t07.png
docs/design/screens/runtime/2026-07-19/t07-baseline/01-list-412x915.png
```
输出 `06-before-vs-after.png`,必须打开原图检查,而不是只确认文件存在。
- [ ] **Step 2: 更新 `design-qa.md`**
新增 T07 基准页小节,明确记录:
- source visual truth:已批准的模块基准规范与审查前截图。
- implementation screenshotT07 新鲜 412×915 列表态。
- 字体、间距、颜色、图片资产、文案五项检查。
- 四状态、四尺寸、搜索交互、失败恢复、控制台和资源错误检查。
- 仍未验证 Android/HBuilderX、系统字体、软键盘和读屏。
- 没有 P0/P1/P2 时写入精确的 `final result: passed`;有问题则写 `final result: blocked` 并继续返工。
- [ ] **Step 3: 在同一标签页按顺序交付用户审批**
依次只展示一个状态并等待用户结论:
1. 正常列表态。
2. 搜索无结果空态。
3. 加载态。
4. 失败态与“重新查看”。
5. 320×568、360×640、360×800、412×915 四档。
没有用户明确“通过”,不得把 T07 标记为模块基准或冻结。
---
### Task 5: 用户通过后记录 T07 基准状态并准备 F01
**Files:**
- Modify: `docs/验收规划.md`
- Modify: `docs/交接记录.md`
- Modify: `docs/新会话审批交接_2026-07-19.md`
- Modify: `docs/superpowers/specs/2026-07-19-module-baseline-accelerated-visual-review-design.md`
**Interfaces:**
- Consumes: 用户对 T07 全部适用状态与四档尺寸的明确通过结论。
- Produces: T07 成为 T 模块基准的权威记录;下一审核页为 F01。
- [ ] **Step 1: 只在用户整页通过后更新文档**
记录以下事实,不扩大结论:
- G01 已是 G 模块 H5 视觉基准。
- T07 已通过 H5 基准页审核,成为 T 模块常规页面基准。
- 下一基准页为 F01T01 必须等 T07、F01、R01、N01、M01 全部通过后处理。
- Android/HBuilderX、真实接口和真机性能仍未完成。
若用户指出问题,保持 T07 当前审核状态,只返工 T07,不执行本任务。
- [ ] **Step 2: 最终新鲜验证**
Run:
```powershell
$tests = @(
'tests/t07-module-baseline-contract.ps1',
'tests/t07-t08-all-states-visual-contract.ps1',
'tests/module-app-loading-contract.ps1',
'tests/t03-t08-member-flow-contract.ps1'
)
foreach ($test in $tests) {
powershell -NoProfile -ExecutionPolicy Bypass -File $test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
}
node tests/t07-module-baseline-runtime-smoke.js
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
git diff --check
```
Expected: 所有合同和运行检查通过;`git diff --check` 退出码为 0,仅允许工作区既有 LF/CRLF 警告;Chrome 最终保持唯一项目标签页。