mirror of
https://github.com/motajs/template.git
synced 2026-10-30 04:40:18 +08:00
docs(09-32): add ListPage/readability refactor plan and execution summary (code uncommitted)
This commit is contained in:
parent
422f3d8f0e
commit
507fcc59ef
@ -0,0 +1,48 @@
|
||||
{
|
||||
"head": "422f3d8f0e29ed72177bbe564681fe5d7309eb8a",
|
||||
"hashes": {
|
||||
"packages-user/client-base/src/components/scroll.tsx": "74fb21f5c9d9c7329e99b8345ba6d7ee7b1eae48",
|
||||
"packages-user/client-base/src/components/list.tsx": "a6723454788b8fff22389bc5d19883c90a4eb5b5",
|
||||
"packages-user/client-base/src/components/textbox.tsx": "5b58de65007189314553be0aca48209bd9e25981",
|
||||
"src/client/ui/settings.tsx": "c5cca14cde8ba813ac4ef5e624e8b359470c472d",
|
||||
"src/client/ui/index.ts": "4e3950598b1beee23753a5556d8e8c82eb21bc57",
|
||||
"src/client/misc/hotkey.ts": "6e884ab4f36cdff958a3490697406a00f67d2e7f",
|
||||
"src/client/ui/inputBox.tsx": "3eb246200ae3300f9f221a8e0e9142e5a612a696",
|
||||
"src/client/ui/waitBox.tsx": "3d5ad7f304e1db0e1b1e75fe76801ec37b917c9f"
|
||||
},
|
||||
"dirty": [
|
||||
"docs/advanced/iii/anon-tokyo.md",
|
||||
"docs/advanced/iii/core-state.md",
|
||||
"docs/advanced/iii/event-system.md",
|
||||
"docs/advanced/iii/index.md",
|
||||
"docs/advanced/iii/loading-system.md",
|
||||
"docs/advanced/iii/passability-and-pathfinding.md",
|
||||
"docs/advanced/iii/plugin-internals.md",
|
||||
"docs/advanced/iii/replay-internals.md",
|
||||
"packages-user/client-base/src/components/textbox.tsx",
|
||||
"packages-user/client-base/src/index.ts",
|
||||
"packages-user/client-base/src/map/__test__/rendererGate.test.ts",
|
||||
"packages-user/client-base/src/map/renderer.ts",
|
||||
"packages-user/client-base/src/map/shader/map.frag",
|
||||
"packages-user/client-base/src/map/status.ts",
|
||||
"packages-user/client-base/src/map/types.ts",
|
||||
"packages-user/client-base/src/map/vertex.ts",
|
||||
"packages-user/client-base/src/setting/index.ts",
|
||||
"packages-user/client-base/src/setting/types.ts",
|
||||
"src/client/ui/__test__/uiContainerGate.test.ts",
|
||||
"src/client/ui/book.tsx",
|
||||
"src/client/ui/bookDetail.tsx",
|
||||
"src/client/ui/equipbox.tsx",
|
||||
"src/client/ui/hotkey.tsx",
|
||||
"src/client/ui/toolbox.tsx"
|
||||
],
|
||||
"eslint": [],
|
||||
"typecheck": [
|
||||
"src/client/ui/book.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/equipbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/equipbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/equipbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/toolbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'.",
|
||||
"src/client/ui/toolbox.tsx error TS2322: Type 'number' is not assignable to type 'AllNumbers | keyof IdToNumber'."
|
||||
]
|
||||
}
|
||||
479
.planning/phases/09-minimum-browser-runnable/09-32-PLAN.md
Normal file
479
.planning/phases/09-minimum-browser-runnable/09-32-PLAN.md
Normal file
@ -0,0 +1,479 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 32
|
||||
type: execute
|
||||
# 27:沿用本阶段既有计划的波次编号,使本计划排在 09-01..09-31 之后(D-01 增量派发,计划累积)
|
||||
# depends_on 为空:本计划触碰的 7 个代码 / 测试文件没有任何并发计划在本波次触碰,故结构上独立;
|
||||
# 运行在最后的波次只是「排期在后」,不是「依赖前序计划」
|
||||
wave: 27
|
||||
depends_on: []
|
||||
# 09-32-BASELINE.json 由 Task 1 在收到「可以执行」之后、编辑任何代码之前写出(Task 0 只汇报、不写文件)
|
||||
files_modified:
|
||||
- src/client/ui/hotkey.tsx
|
||||
- src/client/ui/book.tsx
|
||||
- src/client/ui/bookDetail.tsx
|
||||
- src/client/ui/equipbox.tsx
|
||||
- src/client/ui/toolbox.tsx
|
||||
- src/client/ui/__test__/uiContainerGate.test.ts
|
||||
- src/client/ui/__test__/hotkeySettingsGate.test.ts
|
||||
- .planning/phases/09-minimum-browser-runnable/09-32-PLAN.md
|
||||
- .planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md
|
||||
autonomous: false
|
||||
requirements: []
|
||||
estimate:
|
||||
# 未校准估算(confidence: low),SUMMARY 须声明这一点。
|
||||
# 本计划改三件互不新增行为的事:①`hotkey` 改用既有 `ListPage` 承载分组列表(去掉自建左栏 Scroll / 分组行);
|
||||
# ②五个 UI 文件的 14 处多行内联 loc 数组提为常量;③五个 UI 文件的 13 处内联箭头事件处理器提为具名函数 / 工厂。
|
||||
# 不新增入口、不改数据逻辑、不改按键注册、不改 Scroll / ListPage / TextContent 本体。
|
||||
# 主要成本在:读懂 ListPage 的 basis / contentLoc 几何并复刻既有右侧偏移、把 hotkey 的两栏渲染压成一栏
|
||||
# 而保持捕获 / 写回 / 分组切换口径不变、逐处提常量与工厂、重写闸门断言、跑两轮 eslint 与一次整仓 vue-tsc。
|
||||
# tokens 取 raw 的 2 倍,与 09-19..09-31 的换算口径一致;仍属未校准。
|
||||
# tasks 只计 3 个 auto 执行任务(Task 1–3);Task 0 是 blocking-human 汇报关卡,不写文件、不编辑代码,不计入估算。
|
||||
tokens: 70000
|
||||
raw_tokens: 35000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
must_haves:
|
||||
truths:
|
||||
- "**本计划第一个 block 点是 Task 0 的 `blocking-human` 汇报关卡(AGENTS.md 硬要求 / D-03)**:先汇报「用 `ListPage` 承载热键分组列表 + 五个 UI 文件提取内联事件处理器与多行内联 loc」的要做什么 / 为什么 / 大致怎么做 / 影响范围,逐条说明 `ListPage` 的 props 契约与几何换算、右栏偏移如何保持不变、分组选中改由 `ListPage` 内部 `Selection` 表示的可接受性、`nocache` 依赖属性透传的假设、删除既有常量 / 注释的授权请求、14 处多行 loc 与 13 处内联箭头的逐文件清单、`hotkeySettingsGate.test.ts` 的断言改写(`ListPage` / `close` / 内置「关闭」,去掉过时的「返回」要求)、验证方式,以及「不做浏览器 UAT / 执行器不提交任何代码」两条铁律,然后停下等待用户恰好回复「可以执行」"
|
||||
- "**只动 `files_modified` 的 10 个路径**:`hotkey.tsx`、`book.tsx`、`bookDetail.tsx`、`equipbox.tsx`、`toolbox.tsx`、`uiContainerGate.test.ts`、`hotkeySettingsGate.test.ts`,以及 `09-32-PLAN.md` / `09-32-BASELINE.json` / `09-32-SUMMARY.md`;**不改** `packages-user/client-base/src/components/list.tsx`(`List` / `ListPage` 本体)、`scroll.tsx`、`textbox.tsx`、`settings.tsx`、`ui/index.ts`、`src/client/misc/hotkey.ts`、**`inputBox.tsx` / `waitBox.tsx`**、任何其它 UI / 依赖 / 配置"
|
||||
- "**`hotkey` 以自己的根级 `<ListPage>` 承载分组列表**:恰好 1 个 `<ListPage>`,props 为 `loc={FULL_LOC}`、`basis={GROUP_BASIS}`、`list={groupItems}`、`selected={selectedGroup.value}`、`lineHeight={GROUP_ROW_HEIGHT}`、`font={CONTENT_FONT}`、`close`(布尔简写属性,必须显式存在)、`closeLoc={CLOSE_LOC}`、`onUpdate={selectGroup}`、`onClose={close}`,并透传 `nocache`;文件内**不再有**自有 `<container>`(container 计数为 0)、**不再有**自定义左栏 `Scroll`(`<Scroll` 计数恰好 1)、**没有** `Selection`、**没有** key 属性"
|
||||
- "**分组列表内容只由 `ListPage` 渲染**:`groupItems` 由 `groups.map(([id, group]) => [id, group.name])`(显式 `[string, string]` 返回类型)一次算出并在 setup 中定义;`group.name` 在整个文件里恰好出现 1 次;左栏的 `g-rectr` 命中行、`GROUP_TEXT_LEFT`、`RIGHT_LEFT`、`RIGHT_WIDTH`、`selectGroup(id)` 全部消失;`GROUP_WIDTH` / `GROUP_ROW_HEIGHT` / `LIST_TOP` / `LEFT_PAD` / `TOP_PAD` 等仍被使用的常量保留"
|
||||
- "**右栏几何与既有偏移一致**:`GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH`、`CONTENT_WIDTH = MAIN_WIDTH * (1 - GROUP_BASIS)`、`COMMAND_WIDTH = CONTENT_WIDTH - LEFT_PAD * 3`,`COMMAND_SCROLL_LOC = [LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT]`(数值等于改造前的 `[212, 48, 368, 320]` 减去 `ListPage` 内容区原点 180 → `[32, 48, 368, 320]`,绝对位置不变);`BOX_LEFT` 改由 `COMMAND_WIDTH` 定义但取值仍为 172;指令行偏移 `COMMAND_TEXT_LEFT` / `COMMAND_TEXT_TOP` / `KEY_BOX_HEIGHT` / `COMMAND_GAP` / `BOX_FILL_HEIGHT` 逐字不变;关闭按钮 `CLOSE_LOC` 放在画面左下角,文本由 `ListPage` 内置「关闭」渲染"
|
||||
- "**`hotkey` 行为零变化**:`@common/exit` 仍走 `useKey` 且捕获中不关闭(`if (selectedCommand.value) return;` 保留)、`ListPage` 的 `onClose` 复用同一个 `close`;捕获阶段监听 `keydown` / `keyup`、`EVENT_KEY_CODE_MAP`、`KeyCode.Unknown`、`heldKeys`、`selectedCommand`、`selectedIndex`、`setKeyCombination(`、`refreshCombinations`、`formatCombination`(` + ` / `未设置`)、`selectGroup` 的 `cancelCapture` + `selectedGroup.value = id` + `refreshCombinations`、无入口 / 无持久化 / 无冲突检测 / 无恢复默认全部保持;按键框仍是 `g-rectr` + `strokeStyle={isSelected ? …}` + `lineWidth={isSelected ? …}` 的选中边框(各恰好 1 处),**不使用 `Selection`**"
|
||||
- "**五个 UI 文件里不再有多行内联 loc 数组**:改造前实测 14 处(`book` 0 / `bookDetail` 2 / `equipbox` 7 / `toolbox` 2 / `hotkey` 3),改造后五个文件对「loc 左方括号紧跟换行」的正则匹配数均为 0;每处都变成 `const xxxLoc: ElementLocator = […]`(`map` 回调 / setup 内的局部常量)或文件级常量后写成 `loc={xxxLoc}`;单行短 loc 允许保留"
|
||||
- "**五个 UI 文件里不再有内联箭头事件处理器**:改造前实测 13 处(`book` 1 / `bookDetail` 1 / `equipbox` 7 / `toolbox` 2 / `hotkey` 2),改造后五个文件对「on 事件属性直接给箭头函数」的正则匹配数均为 0;每处都变成 setup 里的具名函数或具名工厂(`getEnemyClick` / `getUpdateHeight` / `getSlotEnter` / `getSlotLeave` / `getSlotDown` / `getOwnedDown` / `onMove` / `getItemClick` / `getKeyClick`),JSX 只写 `onXxx={fn}` 或 `onXxx={factory(arg)}`"
|
||||
- "**行为零变化(逐文件口径)**:`book` 的 13 个同级元素与四项临界 0 回退、`bookDetail` 的 `cumsum(hs)` / `ends[i] - hs[i]` / 无手写累加 / `autoHeight` + `onUpdateHeight` + `textHeights` + `SPECIAL_MIN_HEIGHT` 回退与那条「已按 `props.loc` 响应式同步」注释、`equipbox` 的 `hidden` 分流 / 拖拽(`startPress` / `movePress` / `endPress` / 阈值 / 悬停)/ 再点穿戴与 4 个 `*_SCROLL_LOC`、`toolbox` 的 `hidden={!!current}` / 空文本回退 / 再点使用,全部保持 09-31 的日志口径"
|
||||
- "**列表结构保持平铺**:五个文件仍用内联 `.map()` 返回同级元素数组,`Scroll` 的直接子元素仍是平铺元素,`Scroll` 内仍无 `container`、无 `hidden=`;不新增 key 属性、不加 `Fragment` / 包裹容器 / 条件渲染作为替代;`bookDetail` 的 `TextContent` 仍每次求值新建 loc 数组(提为局部常量后仍是每次 `map` 回调新建)"
|
||||
- "**验证是源码形状 + 静态检查,且如实声明未做浏览器验证**:更新 `src/client/ui/__test__/uiContainerGate.test.ts`——hotkey 组改为断言 `ListPage` / 单 `Scroll` / 无自有 `container` / 无 `Selection` / 分组行已删 / 布尔 `close`,新增「五文件无多行内联 loc」「五文件无内联箭头事件处理器」两组,保留 flat-Scroll / 显示开关分流 / 0 回退 / `noevent` / `cumsum` / 无 key 属性断言;每个 `it` 上方紧邻一行 `//` 注释;跑 eslint(只判 NEW,基线采集与比较用**同一组 7 个路径**)、`pnpm check:type`(整仓,只判改动文件内 NEW,过滤范围同样是**同一组 7 个路径**)、`hotkeySettingsGate.test.ts` 改写后仍为绿、**8 个**受保护文件哈希 + 全仓脏文件快照范围闸门;**不做浏览器 UAT,不宣称任何视觉 / 滚动 / 点击 / 拖拽行为已验证**"
|
||||
- "**`hotkeySettingsGate.test.ts` 按用户决定同步更新且仍为绿**:它原本断言 `hotkey.tsx` 含「返回」;改造后该文本被 `ListPage` 的「关闭」取代,因此本计划**修改该测试**(测试文件由 AI 维护):该用例改名并把断言改为 `hotkeySource` 含 `ListPage` / `close` 布尔属性 / `closeLoc={CLOSE_LOC}` / `onClose={close}` / `@common/exit`、不含 `text=\"返回\"` 与 `text=\"关闭\"`,并新增 `list.tsx?raw` 断言 `text=\"关闭\"` 证明关闭文本来自 `ListPage`;**删除**对「返回」的过时要求。该文件因此**退出受保护哈希集合**(受保护文件降为 8 个),并进入 `files_modified`、eslint 与 `pnpm check:type` 的 7 路径 scope;SUMMARY 必须如实登记这条改法与「不再靠注释命中」的口径"
|
||||
- "**范围与纪律**:除 Task 0 中用户明确授权的既有常量(`GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH` 及其 jsDoc)与注释(分组行的 JSX 注释与行注释)之外,不修改任何既有注释或 jsDoc(只允许新增);不改按键注册表;不改设置入口;不新增依赖;不执行构建;不顺手修复任何无关诊断;发现疑似问题只记录、不擅自登记为缺陷"
|
||||
artifacts:
|
||||
- path: src/client/ui/hotkey.tsx
|
||||
provides: "根级 `<ListPage>`(`loc={FULL_LOC}` / `basis={GROUP_BASIS}` / `list={groupItems}` / `selected={selectedGroup.value}` / `lineHeight={GROUP_ROW_HEIGHT}` / `font={CONTENT_FONT}` / `close` / `closeLoc={CLOSE_LOC}` / `onUpdate={selectGroup}` / `onClose={close}` / `nocache`)+ `default` 插槽内唯一一条 `<Scroll loc={COMMAND_SCROLL_LOC}>`;新增 `GROUP_BASIS` / `CONTENT_WIDTH` / `COMMAND_WIDTH` / `COMMAND_SCROLL_LOC` / `CLOSE_LOC`(`: ElementLocator`)与 `groupItems` / `getKeyClick`;`nameLoc` / `boxLoc` / `textLoc` 三个局部 `ElementLocator`;删除左栏 Scroll、分组 `g-rectr` 行、`GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH`;捕获 / 写回 / 分组切换 / 关闭逻辑不变"
|
||||
- path: src/client/ui/book.tsx
|
||||
provides: "新增 `getEnemyClick(view)` 工厂,`onClick={getEnemyClick(view)}`;13 个同级元素、单行 loc、`noevent`、0 值回退、`specialText(base)`、五条基础数值文案、三条既有行注释与 JSX 注释全部不动;无多行 loc、无内联箭头"
|
||||
- path: src/client/ui/bookDetail.tsx
|
||||
provides: "新增 `SPECIAL_SCROLL_LOC: ElementLocator` 文件常量,`Scroll loc={SPECIAL_SCROLL_LOC}`;`map` 内新增 `textLoc: ElementLocator`,`TextContent loc={textLoc}`;新增 `getUpdateHeight(code)` 工厂,`onUpdateHeight={getUpdateHeight(row.special.code)}`;`cumsum(hs)` / `ends[i] - hs[i]` / `textHeights` / `updateTextHeight` / `SPECIAL_MIN_HEIGHT` / 0 回退 / 既有注释全部保持"
|
||||
- path: src/client/ui/equipbox.tsx
|
||||
provides: "新增 `onMove` 具名函数(根 container 的 onMove)、`getSlotEnter` / `getSlotLeave` / `getSlotDown` / `getOwnedDown` 工厂;新增局部 `deltaLoc` / `slotCellLoc` / `slotIconLoc` / `slotNameLoc` / `ownedCellLoc` / `ownedIconLoc` / `dragLoc`(均 `: ElementLocator`);`hidden` 分流、拖拽 / 悬停 / 再点穿戴、4 个 `*_SCROLL_LOC` 与 `ElementLocator` 导入保持;新增 `IActionEvent`(`@motajs/render`)与 `IEquipmentState`(`@user/data-base`)导入"
|
||||
- path: src/client/ui/toolbox.tsx
|
||||
provides: "新增 `getItemClick(item)` 工厂,两处 `onClick={getItemClick(item)}`;两处 `map` 内新增 `cellLoc: ElementLocator` 并写成 `loc={cellLoc}`;`hidden={!!current}`、空文本回退、`clickItem` 的再点使用、两处四元组结构保持;新增 `ElementLocator` 导入"
|
||||
- path: src/client/ui/__test__/uiContainerGate.test.ts
|
||||
provides: "hotkey 组改为 ListPage / 单 Scroll / 无自有 container / 无 Selection / 分组行已删 / 布尔 close / closeLoc / onUpdate / onClose / nocache 断言,并新增 list.tsx 的 `?raw` 导入以断言内置关闭文本;`keeps only the root container` 改为「四文件各 1、hotkey 为 0」;删除分组名 / 分组命中层的 `tagAround` 断言;删除已无用的 `firstContainerTag` 辅助函数及其注释(辅助函数由 5 个降为 4 个);`noevent` 配对改用 `getKeyClick(row.command, j)`,toolbox 行改用 `getItemClick(item)`;`hoists the four equipbox scroll locators with explicit types` 的导入断言改为合并后的 `import { ElementLocator, IActionEvent } from '@motajs/render';`;`bookDetail` 的 `TextContent` 断言改为 `loc={textLoc}` + `textLoc: ElementLocator = [0, row.textTop, SPECIAL_WIDTH, row.textHeight]`;新增「五文件无多行内联 loc」「五文件无内联箭头事件处理器」两组;每个 `it` 上方紧邻一行 `//`"
|
||||
- path: src/client/ui/__test__/hotkeySettingsGate.test.ts
|
||||
provides: "关闭用例改名并把断言从「返回」改为 `ListPage` / `close` 布尔属性 / `closeLoc={CLOSE_LOC}` / `onClose={close}` / `@common/exit`,否定 `text=\"返回\"` 与 `text=\"关闭\"`;新增 `list.tsx?raw` 导入并断言 `text=\"关闭\"`;其余用例与既有 `stripComments` 辅助函数不动;`it` 上方保留一行 `//` 注释"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json
|
||||
provides: "Task 1 在收到「可以执行」之后、编辑任何代码之前写出的机器可读基线:`{ head, hashes, dirty, eslint, typecheck }`;`hashes` 保护 **8 个**文件(`scroll.tsx` / `list.tsx` / `textbox.tsx` / `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx`);`dirty` 是当时 `git status --porcelain -uall` 的全仓脏文件快照(去掉 `.planning/`);只写一次"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md
|
||||
provides: "执行摘要:ListPage 接入方式与几何换算、删除的常量 / 注释清单、14 处多行 loc 与 13 处内联箭头的逐文件清单、nocache 走属性透传的假设、hotkeySettingsGate 断言改写(`ListPage` / `close` / 内置「关闭」,已删除「返回」要求)的口径、门禁结果、未做浏览器 UAT 声明、估算未校准声明、执行器 0 次代码提交声明;**先跑完所有门禁再写本文件,写完后再跑摘要闸门**"
|
||||
- path: .planning/phases/09-minimum-browser-runnable/09-32-PLAN.md
|
||||
provides: "本计划文件本身"
|
||||
key_links:
|
||||
- "`ListPage` 几何 ↔ 右栏偏移 — ListPage 把内容区放在 `[width * basis, 0, width * (1 - basis), height]`,而改造前右栏 Scroll 的绝对左边缘是 `GROUP_WIDTH + LEFT_PAD * 2`;因此 `GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH` 且 `COMMAND_SCROLL_LOC` 的 x 必须是 `LEFT_PAD * 2`(相对内容区),写成 0 会让右栏整体左移 32 像素、写成 `LEFT_PAD * 2 + GROUP_WIDTH` 会右移 180 像素"
|
||||
- "`ListPage` 的 update 事件 ↔ `selectGroup` — `onUpdate={selectGroup}` 是唯一的分组切换入口;一旦写成 `v-model:selected` 或漏掉 `onUpdate`,分组高亮会变但 `selectedGroup` / `commands` / `combinationLists` 不刷新,右栏停留在旧分组"
|
||||
- "`ListPage` 内部 `Selection` ↔ 分组选中态 — ListPage 用自己的 Selection 表示选中行(默认 color / border 为 undefined,仅有脉动 alpha);这取代了原来分组行的金色 2px 边框,属用户已接受的形态变化;hotkey 源码里不得再出现 Selection"
|
||||
- "`nocache` ↔ ListPage 根 container — ListPage 的根是 `<container loc={props.loc}>`,nocache 不在其声明 props 中,按 Vue 属性透传落到该根元素;若执行期发现 nocache 未被透传(整屏被缓存复用),**停下上报**,不得改 list.tsx,也不得擅自加包裹 container"
|
||||
- "`List` 初始选中 ↔ `list[0][0]` — List 的内部 selected 固定从 `props.list[0][0]` 起算,而 ListPage 的 selected 取自 `props.selected`;两者都等于 `groups[0][0]` 时一致。若分组列表为空,List 会读 `list[0][0]` 而抛错,因此空分组属于本计划不处理的边界"
|
||||
- "受保护文件哈希(scroll.tsx / list.tsx / textbox.tsx / settings.tsx / ui/index.ts / misc/hotkey.ts / inputBox.tsx / waitBox.tsx)— 证明本计划没有改渲染器、ListPage 本体、TextContent、设置入口、按键注册,也没有改指定不动的两个 UI;`hotkeySettingsGate.test.ts` 已按用户决定**显式**更新(断言 `ListPage` / `close` / 「关闭」),因此不在受保护集合,只在范围闸门的 7 个允许路径里"
|
||||
- "14 处多行 loc + 13 处内联箭头 ← 源码形状闸门 — 这两条是本计划的可读性验收口径,必须由 `uiContainerGate.test.ts` 用正则守住,避免后续增删时又写回内联形式"
|
||||
assumptions:
|
||||
- "**ListPage 契约(`packages-user/client-base/src/components/list.tsx:161-220`)**:basis 默认 0.3;contentLoc 在非 right 时为 `[width - contentWidth, 0, contentWidth, height]`,`contentWidth = width * (1 - basis)`;close 为真时渲染一个 loc 为 `props.closeLoc`、文本为「关闭」、`onClick` 为 close 的 text;onUpdate 对应 update 事件(选中项 id);default 插槽以 `slots[selected]?.(selected) ?? slots.default?.(selected)` 调用。ListPage / List / Selection 均由 `@user/client-base` 桶导出(`components/index.ts` → `./list` → `./misc`)"
|
||||
- "**ListPage 的既有使用样例**:`src/client/ui/statistics.tsx:66-85` 用 `<ListPage list={…} selected=\"total\" loc={[180, 0, 480, 480]} close onClose={close} lineHeight={24} closeLoc={[10, 470, void 0, void 0, 0, 1]}>` 加对象形式插槽;本计划的写法与之保持一致(对象形式 default 插槽 + loc / basis / list / selected / lineHeight / font / close / closeLoc / onUpdate / onClose)"
|
||||
- "**画面常量实测**:MAP_WIDTH = 416、STATUS_BAR_WIDTH = 180、MAIN_WIDTH = 596、MAIN_HEIGHT = 416;hotkey.tsx 现状 GROUP_WIDTH = 180、LEFT_PAD = 16、TOP_PAD = 16、LIST_TOP = 48、LIST_HEIGHT = 320、RIGHT_LEFT = 212、RIGHT_WIDTH = 368、BOX_LEFT = 172、KEY_BOX_WIDTH = 180、KEY_BOX_HEIGHT = 24、BOX_FILL_HEIGHT = 20、COMMAND_GAP = 12。因此 GROUP_BASIS = 180 / 596、CONTENT_WIDTH = 416、COMMAND_WIDTH = 416 - 48 = 368、COMMAND_SCROLL_LOC = [32, 48, 368, 320]"
|
||||
- "**待提取清单(规划日实测)**:多行内联 loc —— book 0、bookDetail 2(Scroll 的四个特殊属性常量、TextContent 的 `[0, row.textTop, SPECIAL_WIDTH, row.textHeight]`)、equipbox 7(状态区增减文本、槽位 3 个、拥有装备 2 个、拖拽浮层)、toolbox 2(两列 g-rectr)、hotkey 3(指令名、按键框、组合文本);内联箭头事件处理器 —— book 1(openDetail(view))、bookDetail 1(onUpdateHeight)、equipbox 7(onMove、槽位 onEnter 两处、onLeave 两处、onDown 两处)、toolbox 2(clickItem(item))、hotkey 2(selectGroup(id)、selectKey(row.command, j)),合计 14 + 13"
|
||||
- "**hotkeySettingsGate 的「返回」断言改为「关闭」**:原第 100-104 行断言 hotkeySource 含 `@common/exit` 与「返回」。改造后 `text=\"返回\"` 被删除,该断言不再有真实语义(只剩 jsDoc 里的字面命中),因此本计划按用户决定**更新该测试**:用例改名为 `closes through the common exit command and the ListPage close text`,断言 `@common/exit` / `ListPage` / 布尔 `close` / `closeLoc={CLOSE_LOC}` / `onClose={close}`,否定 `text=\"返回\"` 与 `text=\"关闭\"`,并用新增的 `list.tsx?raw` 断言 `text=\"关闭\"`;既有 jsDoc `/** 关闭界面,返回文本是实际的关闭路径 */` 仍按 AGENTS.md 原样保留(不再被任何闸门依赖)"
|
||||
- "**nocache 属性透传**:ListPage 未声明 nocache prop,按 Vue 的状态化组件属性透传规则落到其唯一根元素 `<container loc={props.loc}>`;这是本计划对「整屏不缓存」的唯一依赖,属可验证但未在浏览器验证的假设。若执行期有证据表明未透传,停下上报"
|
||||
- "**受影响但不在范围内的既有失败**:keyAdaptationGate.test.ts 是既存失败(09-30 记录 15 个用例 5 个失败,09-31 未修),本计划不碰、不以它为绿、不声称全部既有闸门为绿;它引用的是 `src/client/misc/hotkey.ts` 与 `ui/inputBox` / `waitBox`,与本计划改的 `ui/hotkey.tsx` 无关"
|
||||
- "**范围闸门会看到 09-29 / 09-30 / 09-31 的未提交改动**:09-31 明确「执行器 0 次代码提交」,5 个 UI 文件与 uiContainerGate.test.ts、textbox.tsx 都还在脏树里。所有闸门一律使用 `09-32-BASELINE.json` 中 Task 1 现场采集的 head / dirty,**不使用任何历史字面量**;因此范围闸门只能证明「没有新增范围外路径」"
|
||||
- "**dev.md 约束**:不得出现 `import type`;尽量避免 `?.`;对象类型必须先声明 interface(本计划不新增对象类型,groupItems 沿用 `[string, string][]` 元组);`//` 后留空格;既有注释只增不改(本次删除的两处注释与三个常量是 Task 0 待授权的例外);导入之间不得夹注释与空行;换行 CRLF;Prettier printWidth 80 / tabWidth 4 / 单引号 / 无尾逗号"
|
||||
- "**无 RESEARCH / 无 VALIDATION**:本 run 不存在 09-32-RESEARCH.md 与 09-32-VALIDATION.md;本阶段无 requirement ID,Nyquist 验证不适用,不创建这两类文件"
|
||||
prohibitions:
|
||||
- "不得在用户恰好回复「可以执行」之前执行任何任务或派发执行子代理(AGENTS.md / D-03)"
|
||||
- "**不得运行任何 git 写命令**:git add / commit / stash / checkout / restore / worktree / reset 一律禁止;代码改动留在工作树,是否提交由用户另行决定;三个 `.planning` 产物可能被编排器按 commit_docs: true 聚合提交,不属于本计划的代码提交"
|
||||
- "**不得修改 `packages-user/client-base/src/components/list.tsx`**(List / ListPage 是本计划的依赖,不是改造对象);不得通过改它来「适配」hotkey"
|
||||
- "不得修改 `packages-user/client-base/src/components/scroll.tsx`、`textbox.tsx`、`src/client/ui/settings.tsx`、`src/client/ui/index.ts`、`src/client/misc/hotkey.ts`(渲染器、设置入口、UI 导出清单、按键注册)"
|
||||
- "**不得修改 `src/client/ui/inputBox.tsx` 与 `waitBox.tsx`**(用户明确要求不动)"
|
||||
- "**不得修改 `src/client/ui/__test__/hotkeySettingsGate.test.ts` 的其它用例**:只允许按 Task 3 指定的那条关闭用例改法(改名 + 断言换成 `ListPage` / `close` / 「关闭」 + 新增 `list.tsx?raw` 导入)改动它,其余用例与 `stripComments` 辅助函数必须原样保留,也不得为了让源码通过而改断言语义"
|
||||
- "不得在 Scroll 内新增任何 container / template / 包裹层 / Fragment;不得新增 key 或索引 key"
|
||||
- "不得改变行为口径:book / bookDetail 的 0 值回退、bookDetail 的 cumsum / SPECIAL_MIN_HEIGHT 回退 / 高度回调、equipbox 的拖拽 / 悬停 / 再点穿戴、toolbox 的再点使用、hotkey 的捕获 / 写回 / 分组切换 / 无入口 / 无持久化 / 无冲突 / 无恢复默认"
|
||||
- "不得把 hotkey 的分组选中改回自建 g-rectr 边框,也不得把 ListPage 的 close 换成自定义「返回」文本"
|
||||
- "不得新增依赖、不得执行安装命令;不得跑 build:game / build:packages / build:lib"
|
||||
- "不得越界处理发现的其它疑似问题;只记录、不擅自修复、不擅自登记为缺陷"
|
||||
open_items:
|
||||
- "**OI-1(命名报备,按 AGENTS.md 在 Task 0 一并报备)**:新增 / 改名的**局部标识符**为 —— hotkey.tsx 的常量 `GROUP_BASIS` / `CONTENT_WIDTH` / `COMMAND_WIDTH` / `COMMAND_SCROLL_LOC` / `CLOSE_LOC`、局部量 `groupItems`、工厂 `getKeyClick`、map 内 `nameLoc` / `boxLoc` / `textLoc`;book.tsx 的 `getEnemyClick`;bookDetail.tsx 的 `SPECIAL_SCROLL_LOC`、`textLoc`、`getUpdateHeight`;equipbox.tsx 的 `getSlotEnter` / `getSlotLeave` / `getSlotDown` / `getOwnedDown` / `onMove`、局部量 `deltaLoc` / `slotCellLoc` / `slotIconLoc` / `slotNameLoc` / `ownedCellLoc` / `ownedIconLoc` / `dragLoc`;toolbox.tsx 的 `getItemClick`、`cellLoc`。**删除** `GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH` 三个文件级常量。**不新增** interface、导出函数或 GameUI id;公共 / 受保护 / 导出命名、`selectGroup` / `selectKey` 签名一律不改。**你若要求其它命名,执行前按你的命名整体替换,方案形态不变**"
|
||||
- "**OI-2(既有注释 / 常量的删除授权)**:改造会删掉三个常量的 jsDoc(GROUP_TEXT_LEFT / RIGHT_LEFT / RIGHT_WIDTH)以及两处描述分组行的注释(JSX 注释「每行点击区与分组名,选中态由同一矩形的描边表示」与行注释「选中分组只由这一行的边框表示,元素始终挂载」)。按 AGENTS.md「不得擅自修改任何已有注释」,本计划在 Task 0 明确请求授权;未获授权则停下,不得先删后报。其余既有注释(含 `/** 关闭界面,返回文本是实际的关闭路径 */`)**一字不改**"
|
||||
- "**OI-3(执行期硬停条件)**:①若 nocache 未透传到 ListPage 根 container,停下上报,不得改 list.tsx / 不得加包裹 container;②若改后的 `hotkeySettingsGate.test.ts` 关闭用例变红(说明 `hotkey.tsx` 没有 `ListPage` / 布尔 `close` / `closeLoc` / `onClose`,或 `list.tsx` 不再含 `text=\"关闭\"`),停下上报并修 `hotkey.tsx`,**不得**把断言改回去迁就源码;③若 groups 为空导致 List 读 `list[0][0]` 抛错,停下上报(属 ListPage 既有边界,不在本计划修);④若 `pnpm check:type` 在改动文件内出现 NEW 诊断且无法在不动 list.tsx 的前提下消除,停下上报"
|
||||
- "**OI-4(已知视觉差异,只记录、不修复)**:①分组选中态由金色 2px 边框变为 ListPage 内部 Selection 的脉动高亮(默认 color / border 为 undefined);②**左栏列表几何变化**:改造前是自建 `Scroll loc={[16, 48, 180, 320]}`,改造后是 `ListPage` 的列表区 `loc={[0, 0, 180, 416]}` —— 行整体左移 16、上移 48,并占满 416 的整高视口(因此行数多时能滚到画面底部);③**分组名文本不再有金色**:原 `fillStyle={isSelected ? 'gold' : '#ffffff'}`,改造后是 `List` 内置 `text`(继承默认 fillStyle),选中只体现在 Selection 上;④**选中矩形宽度为列表宽减 10**:`List` 的 `selectionLoc` 为 `[0, 0, (loc[2] ?? 200) - 10, lineHeight]`,即 180 - 10 = 170,不再与原分组行同宽;⑤分组名文本 x 由 8 变为 `List` 固定的 10;⑥关闭入口由左上角「返回」变为 `ListPage` 左下角 `CLOSE_LOC` 的「关闭」(`CLOSE_LOC = [16, 400, …]`,y = 400);⑦列表占满整高后,最后一行可以滚到与左下角「关闭」同一高度(y = 400 附近)并被其覆盖,属形态副作用;⑧`List` 行点击用 container 包裹而非平铺 g-rectr(ListPage 内部行为,不在 hotkey 源码里)。以上均由用户选择 ListPage 直接带来,不属本计划的改造目标"
|
||||
- "**OI-5(已知遗留,只记录、不修复)**:①keyAdaptationGate.test.ts 为既存失败(15 个用例 5 个失败),不在本计划范围;②浏览器 UAT 全程跳过,布局、点击命中、拖拽手感、捕获时序由用户自行验证;③book.tsx 的过期 TODO(写着「留空」,与 0 值回退矛盾)按 AGENTS.md 原样保留;④bookDetail 的「一次遍历累加出每行位置…」与 book / toolbox / equipbox 中其余既有注释一律原样保留"
|
||||
---
|
||||
|
||||
<!-- planner-discipline-allow: loc={[ -->
|
||||
<!-- planner-discipline-allow: key= -->
|
||||
<!-- planner-discipline-allow: key={ -->
|
||||
<!-- planner-discipline-allow: container -->
|
||||
<!-- planner-discipline-allow: Selection -->
|
||||
<!-- planner-discipline-allow: GROUP_TEXT_LEFT -->
|
||||
<!-- planner-discipline-allow: RIGHT_LEFT -->
|
||||
<!-- planner-discipline-allow: RIGHT_WIDTH -->
|
||||
<!-- planner-discipline-allow: 返回 -->
|
||||
|
||||
<objective>
|
||||
Phase 9 · 第 32 个计划(新增的独立增量)——**用既有 `ListPage` 重写热键设置界面,并把五个列表型 UI 里的内联事件处理器与多行内联 loc 提取成具名函数 / 工厂与 `ElementLocator` 常量**。
|
||||
|
||||
**要解决的问题(可读性):**
|
||||
|
||||
1. `hotkey.tsx` 现在自己用一条 `Scroll` + 逐行 `g-rectr` + 文本模拟「左栏分组列表」,还要手工算每行位置、维护选中边框;项目里已有 `ListPage`(分组列表 + 选中态 + 关闭按钮 + 右侧内容区)却没用上,导致同一套列表逻辑重复实现。
|
||||
2. 五个列表型 UI 里散落着内联箭头事件处理器(13 处)与 Prettier 已经折行的内联 loc 数组(14 处),读起来要现场解析闭包与坐标数组,且 loc 数组无法复用、无法命名、无法单独断言。
|
||||
|
||||
**做法(用户决定,不得扩大):**
|
||||
|
||||
1. **`hotkey.tsx` 改用 `ListPage`**:根 `<ListPage loc={FULL_LOC} basis={GROUP_BASIS} list={groupItems} selected={selectedGroup.value} lineHeight={GROUP_ROW_HEIGHT} font={CONTENT_FONT} close closeLoc={CLOSE_LOC} onUpdate={selectGroup} onClose={close} nocache>`;`default` 插槽里放唯一一条 `Scroll loc={COMMAND_SCROLL_LOC}` 渲染指令行与按键框。删除自建左栏 `Scroll`、分组 `g-rectr` 行与 `GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH`;`ListPage` 内部的 `container` / `Selection` 与内置「关闭」文本可以直接使用。`@common/exit` 仍走 `useKey` 且捕获中不关闭。
|
||||
2. **提取内联事件处理器**:`book` 的 `openDetail`、`bookDetail` 的 `onUpdateHeight`、`equipbox` 的 `onMove` / 槽位进入 / 离开 / 按下 / 拥有装备按下、`toolbox` 的 `clickItem`、`hotkey` 的 `selectKey`,全部改成 setup 里的具名函数或具名工厂(如 `const getEnemyClick = (view: IEnemyView) => () => { openDetail(view); }`),JSX 只写 `onXxx={fn}` / `onXxx={factory(arg)}`。
|
||||
3. **提取多行 loc**:凡 Prettier 已折行的 loc 数组(14 处)一律变成 `const xxxLoc: ElementLocator = […]`(`map` 回调 / setup 局部)或文件级常量,再写 `loc={xxxLoc}`;单行短 loc 保留。
|
||||
4. **验证**:更新 `uiContainerGate.test.ts`——hotkey 组改为 `ListPage` / 单 `Scroll` / 无自有 `container` / 无 `Selection` / 分组行已删 / 布尔 `close` / `closeLoc` / `onUpdate` / `onClose` / `nocache` / 内置「关闭」,删除已无用的 `firstContainerTag`,`noevent` 配对的 toolbox 针改为 `getItemClick(item)`,equipbox 导入断言改为合并后的 `import { ElementLocator, IActionEvent } from '@motajs/render';`,新增「五文件无多行内联 loc」「五文件无内联箭头事件处理器」两组,其余保留项(flat-Scroll / 显示开关分流 / 0 回退 / `noevent` / `cumsum` / 无 key 属性)不动;更新 `hotkeySettingsGate.test.ts` 的关闭用例(断言 `ListPage` / `close` / 「关闭」,去掉「返回」);eslint 只判 NEW(同一组 7 个路径)、`pnpm check:type` 只判改动文件内 NEW(同一组 7 个路径)、`hotkeySettingsGate` 改写后仍为绿、8 个受保护文件哈希 + 全仓脏文件范围闸门;**不做浏览器 UAT,不提交代码**。
|
||||
|
||||
**明确不做:** 不改 `list.tsx` / `scroll.tsx` / `textbox.tsx`;不改 `inputBox.tsx` / `waitBox.tsx`;除上面那条关闭用例外不改 `hotkeySettingsGate.test.ts`;不改按键注册、设置入口、UI 导出清单;不新增 key / 容器 / 条件渲染;不改任何行为口径;不新增依赖。
|
||||
|
||||
**约束:** 只动 `files_modified` 的 10 个路径;除 Task 0 获授权的三个常量与两处注释外不改既有注释与 jsDoc;不新增对象类型;不顺手修复任何无关诊断。
|
||||
|
||||
**修改方式声明(AGENTS.md):** 本计划的全部文件写入(写基线、改 5 个 UI 文件、更新 1 个闸门测试、写摘要)由**派发子代理(`gsd-executor`)执行**,非内联执行;除 `files_modified` 外无任何写入。**Task 0 只汇报、不写任何文件**;`09-32-BASELINE.json` 是 **Task 1 在收到「可以执行」之后、编辑任何代码之前的第一步**写出(只写一次)。「开始执行」的界限仍是「可以执行」四个字。
|
||||
|
||||
**可见跳过:** spec-less probe fallback skipped: phase has no requirement IDs to probe (visible skip)。本阶段无 requirement ID,不创建 RESEARCH.md / VALIDATION.md,Nyquist 验证不适用。
|
||||
</objective>
|
||||
|
||||
## 本计划产出物(Artifacts this plan produces)
|
||||
|
||||
| 类型 | 名称 / 路径 | 说明 |
|
||||
|------|-------------|------|
|
||||
| 修改 | `src/client/ui/hotkey.tsx` | 根级 `ListPage` 承载分组列表 + `default` 插槽内唯一 `Scroll`;新增 5 个常量 / `groupItems` / `getKeyClick` / 3 个局部 loc;删除左栏 Scroll、分组行与 3 个常量;捕获 / 写回 / 分组切换 / 关闭不变 |
|
||||
| 修改 | `src/client/ui/book.tsx` | `getEnemyClick` 工厂替换 `openDetail(view)` 的内联箭头;其余一律不动 |
|
||||
| 修改 | `src/client/ui/bookDetail.tsx` | `SPECIAL_SCROLL_LOC` + `textLoc` + `getUpdateHeight`;`cumsum` / 高度回调 / 0 回退不变 |
|
||||
| 修改 | `src/client/ui/equipbox.tsx` | `onMove` + 4 个工厂 + 7 个局部 loc;`hidden` 分流 / 拖拽 / 悬停不变 |
|
||||
| 修改 | `src/client/ui/toolbox.tsx` | `getItemClick` + 2 个局部 `cellLoc`;显示分流 / 再点使用不变 |
|
||||
| 修改 | `src/client/ui/__test__/uiContainerGate.test.ts` | hotkey 组改写 + 新增两组「无多行 loc / 无内联箭头」;删除 `firstContainerTag`、toolbox 针改 `getItemClick(item)`、equipbox 导入断言改合并导入;其余保留 |
|
||||
| 修改 | `src/client/ui/__test__/hotkeySettingsGate.test.ts` | 关闭用例改名 + 断言换成 `ListPage` / `close` / 「关闭」(新增 `list.tsx?raw` 导入),去掉过时的「返回」要求;其余用例不动 |
|
||||
| 基线 | `.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json` | `head` + **8 个**受保护文件哈希 + 全仓脏文件快照 + eslint / typecheck 基线 |
|
||||
| 交付 | `.planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md` | 执行摘要、门禁结果、未 UAT / 0 次代码提交声明 |
|
||||
| 计划 | `.planning/phases/09-minimum-browser-runnable/09-32-PLAN.md` | 本文件 |
|
||||
|
||||
## 覆盖审计(Multi-Source Coverage Audit)
|
||||
|
||||
| 源 | 条目 | 覆盖位置 |
|
||||
|----|------|----------|
|
||||
| GOAL(ROADMAP §Phase 9) | 「可在浏览器中运行并验证的最小版本」伞目标 | 本增量不新增入口、不做浏览器验证;改善热键界面的实现可读性与五个列表 UI 的源码形状,SUMMARY 如实记录 |
|
||||
| REQ | 本阶段无 requirement ID(`requirements: []`) | Nyquist 不适用 |
|
||||
| RESEARCH | 本 run 无 RESEARCH.md / 本计划无 DISCOVERY.md | 不适用 |
|
||||
| CONTEXT D-01 | 增量派发,一个任务一个计划 | 本计划即该次派发的唯一计划,不拆分 |
|
||||
| CONTEXT D-02 | 计划可累积追加 | plan 32,排在 09-01..09-31 之后(`wave: 27` 只是排期编号) |
|
||||
| CONTEXT D-03 | 执行前必须先汇报并等待「可以执行」 | Task 0(`blocking-human`),`autonomous: false` |
|
||||
| CONTEXT D-04 | 新命名必须先反馈获批 | OI-1(全部为局部标识符 + 三个待删常量) |
|
||||
| CONTEXT D-06 | 范围由用户界定,不擅自扩大 | `files_modified` 10 项 + 范围闸门 + **8 个**受保护文件哈希 |
|
||||
| CONTEXT D-07 | UI 形态由用户给定,AI 不臆造 | 用户给定 `ListPage` 方案与内置关闭文本;本计划不另设形态 |
|
||||
| 用户要求 1 | 用 `ListPage`(不用自建左栏 Scroll),内部 container / Selection 可接受,用内置「关闭」 | Task 1 / Task 3 闸门 |
|
||||
| 用户要求 2 | 内联事件处理器提为具名函数 / 工厂(行数据用工厂) | Task 1 / Task 2 / Task 3 闸门 |
|
||||
| 用户要求 3 | 所有会被 Prettier 折行的 loc 提为 `ElementLocator` 常量 | Task 1 / Task 2 / Task 3 闸门 |
|
||||
| 用户要求 4 | 行为保持(0 回退、cumsum / 响应式 loc / 无重挂载 key、equipbox 拖拽 / 悬停、toolbox use、hotkey 全口径、无 key 属性、Scroll 内无 container) | Task 1 / Task 2 约束 + Task 3 保留断言 |
|
||||
| 用户要求 5 | 右栏保留单一 `Scroll`,宽度按 `MAIN_WIDTH * (1 - basis)`、高度仍为 `LIST_HEIGHT`(320),内容区容器高度为 `MAIN_HEIGHT`(416),保留指令偏移 | Task 1(`COMMAND_SCROLL_LOC` 计算 + 闸门断言数值来源) |
|
||||
| 用户要求 6 | 不改 `inputBox.tsx` / `waitBox.tsx`、`textbox.tsx`、按键注册 | 受保护哈希 8 个(含这四个文件与 `list.tsx`) |
|
||||
| 验证要求 | 更新 `uiContainerGate.test.ts`:hotkey 新结构 + 五文件 loc / handler 源码闸门 + 保留项断言;更新 `hotkeySettingsGate.test.ts` 的关闭用例;eslint、类型 NEW-only(同一组 7 个路径)、`hotkeySettingsGate` 改写后仍绿、哈希 / 范围、无提交、无浏览器 UAT | Task 3 |
|
||||
|
||||
<execution_context>
|
||||
@C:/Users/13194/.config/opencode/gsd-core/workflows/execute-plan.md
|
||||
@C:/Users/13194/.config/opencode/gsd-core/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-CONTEXT.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-31-PLAN.md
|
||||
@.planning/phases/09-minimum-browser-runnable/09-31-SUMMARY.md
|
||||
@AGENTS.md
|
||||
@dev.md
|
||||
@packages-user/client-base/src/components/list.tsx
|
||||
@packages-user/client-base/src/components/scroll.tsx
|
||||
@src/client/uiConstant.ts
|
||||
@src/client/ui/statistics.tsx
|
||||
@src/client/ui/hotkey.tsx
|
||||
@src/client/ui/book.tsx
|
||||
@src/client/ui/bookDetail.tsx
|
||||
@src/client/ui/equipbox.tsx
|
||||
@src/client/ui/toolbox.tsx
|
||||
@src/client/ui/__test__/uiContainerGate.test.ts
|
||||
@src/client/ui/__test__/hotkeySettingsGate.test.ts
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
<task type="checkpoint:human-verify" gate="blocking-human">
|
||||
<name>Task 0: 预执行汇报关卡 —— 汇报「`hotkey` 改用 `ListPage` + 五个 UI 文件提取内联事件处理器与多行 loc」的要做什么 / 为什么 / 怎么做 / 影响范围、逐文件清单、几何换算与验证方式,等待「可以执行」</name>
|
||||
<precondition>工作树仍是 09-31 结束时的状态,可用以下事实自检:`src/client/ui/hotkey.tsx` 共 288 行,根为 `<container loc={FULL_LOC} nocache>`,内有「返回」文本 + 两条 `<Scroll>`(左栏 180 宽的分组行、右栏 `[RIGHT_LEFT, LIST_TOP, RIGHT_WIDTH, LIST_HEIGHT]` = `[212, 48, 368, 320]`),常量 `GROUP_WIDTH = 180` / `LEFT_PAD = 16` / `RIGHT_LEFT = 212` / `RIGHT_WIDTH = 368` / `LIST_TOP = 48` / `LIST_HEIGHT = 320` / `BOX_LEFT = 172` / `GROUP_TEXT_LEFT = 8`;`packages-user/client-base/src/components/list.tsx` 第 161-220 行为 `ListPage`(`basis` 默认 0.3、非 `right` 时内容区为 `[width - width * (1 - basis), 0, width * (1 - basis), height]`、`close` 为真时渲染文本「关闭」的 text);`src/client/ui/statistics.tsx` 第 66-85 行为 `ListPage` 的既有用法;规划日实测五个 UI 文件的多行内联 loc 为 `book 0 / bookDetail 2 / equipbox 7 / toolbox 2 / hotkey 3`(合计 14),内联箭头事件处理器为 `book 1 / bookDetail 1 / equipbox 7 / toolbox 2 / hotkey 2`(合计 13);`src/client/ui/__test__/uiContainerGate.test.ts` 现有 16 组断言,其中 hotkey 相关 4 组依赖「两条 Scroll」「每文件 1 个根 container」「分组命中行 `selectGroup(id)`」「root container 带 nocache」;`src/client/ui/__test__/hotkeySettingsGate.test.ts` 第 100-104 行断言 `hotkey.tsx` 含 `@common/exit` 与「返回」。</precondition>
|
||||
<files>无(本任务不写任何文件,只做只读汇报;`09-32-BASELINE.json` 改由 Task 1 在获批后、改代码前写出)</files>
|
||||
<read_first>
|
||||
- .planning/phases/09-minimum-browser-runnable/09-31-SUMMARY.md(09-31 完成后的源码形状与门禁口径)
|
||||
- .planning/phases/09-minimum-browser-runnable/09-CONTEXT.md(D-01 增量派发、D-03 汇报关卡、D-04 命名批准、D-06 不扩大范围、D-07 形态由用户给)
|
||||
- dev.md(注释 / 类型 / 无 `?.` / 无 `import type` / 每 `it` 一行注释 / CRLF 规范)
|
||||
- AGENTS.md(执行前须获批、命名先反馈、既有注释不得擅改、修改方式须预先声明)
|
||||
- packages-user/client-base/src/components/list.tsx(`List` / `ListPage` 的 props、`basis`、`contentLoc`、内置「关闭」文本、`Selection` 用法)
|
||||
- src/client/ui/statistics.tsx(`ListPage` 的既有调用样例)
|
||||
- src/client/ui/hotkey.tsx(现有两栏结构、常量、捕获逻辑)
|
||||
- src/client/ui/{book,bookDetail,equipbox,toolbox}.tsx(这四个文件的 11 处内联箭头与其余 11 处多行 loc 的位置)
|
||||
- src/client/ui/__test__/uiContainerGate.test.ts 与 src/client/ui/__test__/hotkeySettingsGate.test.ts(本次要更新 / 必须保持不变的既有闸门)
|
||||
- src/client/uiConstant.ts(`MAIN_WIDTH = 596` / `MAIN_HEIGHT = 416` / `FULL_LOC`)
|
||||
</read_first>
|
||||
<what-built>
|
||||
一份待批准的执行计划(`09-32-PLAN.md`):把热键设置界面的分组列表换成项目既有 `ListPage`(内置选中态与「关闭」按钮),右栏指令区保留为内容区里唯一一条 `Scroll`;并把五个列表型 UI 里的 14 处多行内联 loc 与 13 处内联箭头事件处理器全部提成具名常量 / 具名函数 / 具名工厂;行为零变化;用源码形状闸门 + eslint / 类型前后基线验收。
|
||||
</what-built>
|
||||
<action>
|
||||
**停下并向用户汇报,等待恰好回复「可以执行」后才继续;本任务不写任何文件(含 `09-32-BASELINE.json`——它由 Task 1 的第一步写出),不派发执行子代理。**
|
||||
|
||||
① **要做什么(三件事,互不新增行为):**
|
||||
- **`hotkey.tsx` 改用 `ListPage`**:根元素从自建 container + 两条 `Scroll` 换成 `ListPage`,左侧分组列表与「关闭」按钮交给 `ListPage`,右栏指令行 + 按键框仍是唯一一条 `Scroll`。
|
||||
- **提取内联事件处理器(13 处)**:改成 setup 里的具名函数或具名工厂,JSX 不再出现箭头闭包。
|
||||
- **提取多行内联 loc(14 处)**:改成 `const xxxLoc: ElementLocator = […]` 或文件级常量,`loc` 只引用名字。
|
||||
|
||||
② **为什么:**
|
||||
- `ListPage` 已经实现了「左列表 + 选中态 + 关闭按钮 + 右侧内容区」,`hotkey` 却手工重写了同一套东西(逐行算 top、维护金色边框、单独一条 Scroll),既重复又难读;接上 `ListPage` 后左栏不再占 40 行代码。
|
||||
- 内联箭头与内联 loc 数组让「谁在处理这个事件」「这个矩形是什么」必须在 JSX 现场反推;命名后既能一眼看懂,也能被闸门单独断言(这也是本计划能用源码形状验证 hotkey 新结构的前提)。
|
||||
- 这两类改造都是**纯形状变化**:事件仍调同一批函数、矩形仍取同样的数值,不新增入口、不改数据、不改按键注册。
|
||||
|
||||
③ **怎么写(严格按此范围):**
|
||||
- **`hotkey.tsx`(唯一结构改动):**
|
||||
- 导入:`@motajs/render` 增加 `ElementLocator`;`@user/client-base` 增加 `ListPage`(与既有 `Scroll` / `useKey` 同一行)。
|
||||
- 新增文件级常量(均带中文 jsDoc):`GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH`(使左栏仍为 180 宽)、`CONTENT_WIDTH = MAIN_WIDTH * (1 - GROUP_BASIS)`(= 416)、`COMMAND_WIDTH = CONTENT_WIDTH - LEFT_PAD * 3`(= 368)、`COMMAND_SCROLL_LOC: ElementLocator = [LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT]`(相对 `ListPage` 内容区,绝对位置与改造前一致)、`CLOSE_LOC: ElementLocator = [LEFT_PAD, MAIN_HEIGHT - LEFT_PAD, void 0, void 0, 0, 1]`(左下角)。
|
||||
- 删除:`GROUP_TEXT_LEFT`、`RIGHT_LEFT`、`RIGHT_WIDTH` 及其 jsDoc;`BOX_LEFT` 改由 `COMMAND_WIDTH` 定义(取值仍是 172,jsDoc 保留)。
|
||||
- setup 新增:`const groupItems: [string, string][] = groups.map(([id, group]) => [id, group.name]);`(带 jsDoc),以及工厂 `const getKeyClick = (command: IHotkeyCommand, index: number) => () => { selectKey(command, index); };`(带 jsDoc)。
|
||||
- 渲染:根为 `<ListPage loc={FULL_LOC} basis={GROUP_BASIS} list={groupItems} selected={selectedGroup.value} lineHeight={GROUP_ROW_HEIGHT} font={CONTENT_FONT} close closeLoc={CLOSE_LOC} onUpdate={selectGroup} onClose={close} nocache>`;子节点用对象形式插槽(`default: () => <Scroll loc={COMMAND_SCROLL_LOC}>…</Scroll>`,与 `statistics.tsx` 的用法一致)。`Scroll` 内仍是 `commandRows.value.map((row, i) => { … })`:先 `const nameLoc: ElementLocator = [COMMAND_TEXT_LEFT, row.top + COMMAND_TEXT_TOP];`,内层 `.map((keys, j) => { … })` 里 `const boxLoc` / `const textLoc`,然后 `<g-rectr loc={boxLoc} … onClick={getKeyClick(row.command, j)} />`、`<text … loc={textLoc} />`。
|
||||
- 删除左栏 `Scroll`、分组 `g-rectr` 行与分组名 `text`,连带删除它们的两条注释(JSX 注释「每行点击区与分组名,选中态由同一矩形的描边表示」与行注释「选中分组只由这一行的边框表示,元素始终挂载」)——**此删除需你在回复中授权(见 ⑥)**。
|
||||
- 保持不变:`@common/exit` 的 `if (selectedCommand.value) return;` 与 `props.controller.close(props.instance)`、`close` 函数、`selectGroup`(`cancelCapture` + `selectedGroup.value = id` + `refreshCombinations`)、按键捕获流程、`commandRows` / `ICommandRow` / `combinationLists` / `formatCombination` / `refreshCombinations`、按键框的 `strokeStyle` / `lineWidth` 选中边框、`nocache`(透传给 `ListPage` 的根 container)。
|
||||
- **不改** `list.tsx` / `scroll.tsx` / `textbox.tsx`。
|
||||
- **`book.tsx`**:新增 `const getEnemyClick = (view: IEnemyView<IEnemyAttr>) => () => { openDetail(view); };`,JSX 写 `onClick={getEnemyClick(view)}`;其余一切不动(该文件没有多行 loc)。
|
||||
- **`bookDetail.tsx`**:导入增加 `ElementLocator`;新增文件级 `SPECIAL_SCROLL_LOC: ElementLocator = [SPECIAL_LEFT, SPECIAL_TOP, SPECIAL_WIDTH, SPECIAL_HEIGHT]`,`Scroll loc={SPECIAL_SCROLL_LOC}`;`map` 回调内新增 `const textLoc: ElementLocator = [0, row.textTop, SPECIAL_WIDTH, row.textHeight];`,`TextContent loc={textLoc}`;新增工厂 `const getUpdateHeight = (code: number) => (height: number) => { updateTextHeight(code, height); };`,`onUpdateHeight={getUpdateHeight(row.special.code)}`;`cumsum` / 注释 / 高度逻辑不动。
|
||||
- **`equipbox.tsx`**:导入增加 `IActionEvent`(`@motajs/render`)与 `IEquipmentState`(`@user/data-base`);新增 `const onMove = (ev: IActionEvent) => { movePress(ev.absoluteX, ev.absoluteY); };` 并写 `onMove={onMove}`;新增工厂 `getSlotEnter(slot)` / `getSlotLeave(slot)` / `getSlotDown(equip, slot)` / `getOwnedDown(uid)`,JSX 写 `onEnter={getSlotEnter(i)}` / `onLeave={getSlotLeave(i)}` / `onDown={getSlotDown(slotEquip, i)}` / `onDown={getOwnedDown(item.uid)}`;7 处多行 loc 提为局部 `deltaLoc` / `slotCellLoc` / `slotIconLoc` / `slotNameLoc` / `ownedCellLoc` / `ownedIconLoc` / `dragLoc`。
|
||||
- **`toolbox.tsx`**:导入增加 `ElementLocator`;新增工厂 `getItemClick(item)`,两处 `onClick={getItemClick(item)}`;两处 `map` 内新增 `const cellLoc: ElementLocator = [4, y + 2, COLUMN_WIDTH - 8, ROW_HEIGHT - 4];`。
|
||||
- **`uiContainerGate.test.ts`(Task 3 执行,此处只汇报口径)**:hotkey 组改为「1 个 `ListPage` / 1 条 `Scroll` / 0 个自有 container / 无 `Selection` / 分组行已删 / **布尔 `close` 属性** / `closeLoc` / `onUpdate` / `onClose` / `nocache` / `list.tsx` 含内置「关闭」」;`keeps only the root container` 改为「四个文件各 1、hotkey 为 0」;**删除已无用的 `firstContainerTag` 辅助函数及其注释**(辅助函数由 5 个降为 `stripComments` / `flatten` / `getScrollBlocks` / `tagAround` 4 个,避免 eslint no-unused-vars);`noevent` 配对改用 `getKeyClick(row.command, j)`,**toolbox 行针由 `clickItem(item)` 改为 `getItemClick(item)`**(提取后 `clickItem(item)` 只出现在工厂体内,原针会让 `tagAround` 返回空串),并保留 `expect(toolRowTag).not.toContain('noevent')`;**`hoists the four equipbox scroll locators with explicit types` 的导入断言改为合并后的 `import { ElementLocator, IActionEvent } from '@motajs/render';`**(Task 2 会把两条 `@motajs/render` 导入合并,旧字面量断言必红);`bookDetail` 的 `TextContent` 断言改为 `loc={textLoc}` + `textLoc: ElementLocator = [0, row.textTop, SPECIAL_WIDTH, row.textHeight]`;新增「五文件无多行内联 loc」「五文件无内联箭头事件处理器」两组;每个 `it` 上方一行 `//`。
|
||||
- **`hotkeySettingsGate.test.ts`(Task 3 执行,此处只汇报口径)**:关闭用例改名为 `closes through the common exit command and the ListPage close text`,断言 `hotkeySource` 含 `@common/exit` / `ListPage` / 布尔 `close` / `closeLoc={CLOSE_LOC}` / `onClose={close}`,否定 `text="返回"` 与 `text="关闭"`;新增 `list.tsx?raw` 导入并断言 `text="关闭"`;其余 10 个用例与 `stripComments` 一字不动。
|
||||
|
||||
④ **需要你重点核对的行为 / 视觉变化:**
|
||||
- **右栏绝对位置不变**:`ListPage` 的内容区原点在 `MAIN_WIDTH * basis = 180`,所以 `COMMAND_SCROLL_LOC` 的 x 取 `LEFT_PAD * 2 = 32`,加回内容区原点正好是原来的 212;宽 368、高 320 与原来逐字相同。**若执行期发现右栏整体偏移,就是这里算错。**
|
||||
- **左栏列表几何变化(视觉差异,用户已接受)**:改造前是自建 `Scroll loc={[16, 48, 180, 320]}`;改造后是 `ListPage` 列表区的 `loc={[0, 0, 180, 416]}` —— 行整体**左移 16、上移 48**,并且占满 416 的**整高视口**(行数多时能滚到画面底部;底部那一行会贴到左下角「关闭」所在高度 y = 400 附近)。
|
||||
- **分组选中态变化(形态变化,用户已接受)**:由原来的金色 2px 边框换成 `ListPage` 内部 `Selection` 的脉动高亮(默认 color / border 为 undefined);**分组名文本不再变金色**(`List` 内置 `text` 用默认 fillStyle);**选中矩形宽度是列表宽减 10**(`180 - 10 = 170`),不再与原分组行同宽;分组名 x 由 8 变为 `List` 固定的 10。
|
||||
- **关闭入口变化**:左上角「返回」→ 左下角 `CLOSE_LOC` 的「关闭」(`ListPage` 内置文本)。`@common/exit` 快捷键不变,捕获中仍不关闭。
|
||||
- **`nocache` 依赖属性透传**:`ListPage` 未声明 `nocache`,靠 Vue 属性透传落到其根 container。**若你希望显式包一层 `<container loc={FULL_LOC} nocache>`(容器计数仍为 1),请在回复中说明**;默认按用户给定的「`ListPage` 即根」写法。
|
||||
- **边界:分组列表为空**:`List` 的内部选中固定读 `props.list[0][0]`,空列表会抛错;`iterateGroups()` 恒有分组,本计划不处理该边界(属 `ListPage` 既有行为,不改 `list.tsx`)。
|
||||
- **其余行为完全不变**:0 值回退、`cumsum`、高度回调、`equipbox` 拖拽 / 悬停 / 再点穿戴、`toolbox` 再点使用、`hotkey` 捕获 / 写回 / 分组切换 / 无入口 / 无持久化 / 无冲突 / 无恢复默认。
|
||||
|
||||
⑤ **影响范围:**
|
||||
- 修改 5 个 UI 文件:`src/client/ui/{hotkey,book,bookDetail,equipbox,toolbox}.tsx`。
|
||||
- 修改 2 个既有测试:`src/client/ui/__test__/uiContainerGate.test.ts`、`src/client/ui/__test__/hotkeySettingsGate.test.ts`(仅那条关闭用例 + 一行 `list.tsx?raw` 导入)。
|
||||
- 另有 3 个 `.planning` 产物:本计划、`09-32-BASELINE.json`、`09-32-SUMMARY.md`。合计 10 个路径。
|
||||
- **显式不改**:`packages-user/client-base/src/components/list.tsx`、`scroll.tsx`、`textbox.tsx`、`src/client/ui/settings.tsx`、`src/client/ui/index.ts`、`src/client/misc/hotkey.ts`、`src/client/ui/inputBox.tsx` / `waitBox.tsx`、其它任何 UI / 依赖 / 配置。
|
||||
|
||||
⑥ **需要你授权的既有注释 / 常量删除(AGENTS.md 要求先获批):**
|
||||
- 三个文件级常量及其 jsDoc:`GROUP_TEXT_LEFT`、`RIGHT_LEFT`、`RIGHT_WIDTH`(左栏分组行删除后不再有使用点)。
|
||||
- 两处描述分组行的注释:JSX 注释「每行点击区与分组名,选中态由同一矩形的描边表示」与行注释「选中分组只由这一行的边框表示,元素始终挂载」(宿主代码被删除)。
|
||||
- **默认保留、不改**:`/** 关闭界面,返回文本是实际的关闭路径 */`(改后文字已略过时,但按 AGENTS.md「既有注释只增不改」原样保留;它**不再**被任何闸门依赖)。
|
||||
- **需要你知道的测试改动**:`hotkeySettingsGate.test.ts` 原来靠上面那条 jsDoc 里的「返回」字面量蒙过断言,实际关闭文本已是 `ListPage` 内置的「关闭」。按你的指示(测试文件由 AI 维护),本计划把它改成断言 `ListPage` / 布尔 `close` / 内置「关闭」,并**删除过时的「返回」要求**;该文件因此进入 `files_modified`(第 7 个代码 / 测试路径)并退出受保护哈希集合。若你不同意改这个测试、希望保留「靠注释命中」的写法,请在回复中说明,我会改回原口径。
|
||||
|
||||
⑦ **验证方式:**
|
||||
1. `pnpm exec eslint` 跑**同一组 7 个代码 / 测试文件**(5 个 UI + `uiContainerGate.test.ts` + `hotkeySettingsGate.test.ts`),基线采集与最终比较 scope 完全相同,只判 **NEW**;路径归一化口径固定为「反斜杠转正斜杠后,从 `packages-user/` 或 `src/` 开始截取」。
|
||||
2. `pnpm check:type`(整仓 `vue-tsc --noEmit`)前后基线比较:过滤范围同样是**同一组 7 个路径**,只对改动文件内的 **NEW** 诊断判失败;既有基线诊断允许消失。
|
||||
3. `uiContainerGate.test.ts`:hotkey 组按 ③ 改写 + 新增「无多行 loc」「无内联箭头」两组;删除 `firstContainerTag`、toolbox 针改 `getItemClick(item)`、equipbox 导入断言改合并导入;保留 flat-Scroll / Scroll 内无 container 与 `hidden` / 显示开关分流 / 0 回退 / `noevent` / `cumsum` / 无 key 属性断言;每个 `it` 上方一行 `//`。
|
||||
4. `hotkeySettingsGate.test.ts`:按 ③ 的最后一条改关闭用例,改后必须仍为绿(**不再靠 jsDoc 里的「返回」命中**,也不再进入受保护哈希);其余用例不动。
|
||||
5. 受保护文件哈希闸门(**8 个**:`scroll.tsx`、`list.tsx`、`textbox.tsx`、`settings.tsx`、`ui/index.ts`、`misc/hotkey.ts`、`inputBox.tsx`、`waitBox.tsx`)。
|
||||
6. 改动范围闸门:只有 7 个代码 / 测试路径,且用整仓 `git status --porcelain -uall` 与基线 dirty 快照比较,任何「不在快照、不属于这 7 个路径、也不在 `.planning/`」的新路径都判范围外写入。**注意**:09-29 / 09-30 / 09-31 的改动尚未提交,这些路径在基线里本来就是脏的,因此该闸门只能证明「没有新增范围外路径」。
|
||||
7. `keyAdaptationGate.test.ts` 是既存失败,不在本计划范围,不修、不声称全绿。
|
||||
8. **不做浏览器 UAT**:布局、点击命中、拖拽手感、捕获时序、`nocache` 是否生效留给你人工确认。
|
||||
|
||||
⑧ **命名清单(OI-1):** 见 `open_items` OI-1 —— 全部为局部标识符(常量 5 个、局部量 3 个、工厂 9 个、局部 loc 12 个)与三个待删常量;不新增 interface、导出函数、`GameUI` id;`selectGroup` / `selectKey` 签名不改。**你若要求其它命名,请在回复中说明,我会整体替换后再执行。**
|
||||
|
||||
⑨ **两条铁律:** **不做浏览器 UAT、不宣称任何视觉 / 滚动 / 点击 / 拖拽 / 选中态行为已验证**;**执行器不提交任何代码**(禁止 git add / commit / stash / checkout / worktree / reset),代码改动留在工作树供你审查。三个 `.planning` 产物可能由 GSD 编排器按 `commit_docs: true` 聚合提交,不算本计划的代码提交。
|
||||
</action>
|
||||
<how-to-verify>
|
||||
逐条核对 ①–⑨ 是否与你的意图一致,特别是:①`ListPage` 的 props 组合(`basis` / `list` / `selected` / `lineHeight` / `font` / `close`(布尔简写)/ `closeLoc` / `onUpdate` / `onClose` / `nocache`)与「`default` 插槽里一条 `Scroll`」是否符合你要的形态;②`GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH`、`COMMAND_SCROLL_LOC = [32, 48, 368, 320]` 复刻右栏绝对位置是否认可;③左栏从 `[16, 48, 180, 320]` 变为 `ListPage` 列表区 `[0, 0, 180, 416]`(左移 16 / 上移 48 / 占满整高,能贴到左下角「关闭」所在的 y = 400)、选中态由金色边框变为 `ListPage` 内部 `Selection` 脉动高亮、选中矩形宽度为 180 - 10 = 170、分组名文本不再金色、关闭入口改为左下角「关闭」,这些视觉差异是否都可接受;④`nocache` 走属性透传(不额外包 container)是否可接受,还是你要显式包一层;⑤14 处多行 loc 与 13 处内联箭头的分布(book 0/1、bookDetail 2/1、equipbox 7/7、toolbox 2/2、hotkey 3/2)与「单行短 loc 保留」的口径是否就是你要求的;⑥删除三个常量 + 两处分组行注释的授权,以及「关闭界面,返回文本…」这条 jsDoc 默认保留、`hotkeySettingsGate.test.ts` 改为断言 `ListPage` / `close` / 「关闭」是否可接受;⑦`uiContainerGate` 的改法(hotkey 改 4 组、新增 2 组、容器计数「四文件各 1 / hotkey 为 0」、删除 `firstContainerTag`、toolbox 针改 `getItemClick(item)`、equipbox 导入断言改合并导入)是否就是你要的覆盖面;⑧「不做浏览器 UAT / 执行器不提交代码」与 8 个受保护文件(含 `list.tsx`)的闸门口径;⑨`List` 读 `list[0][0]` 导致空分组列表会抛错这一边界登记为「只记录、不修」是否可接受。
|
||||
</how-to-verify>
|
||||
<resume-signal>仅当完全同意时回复恰好四个字「可以执行」;任何修改意见(**尤其是 ③ 的视觉差异、④ 的 nocache 写法、⑥ 的注释删除授权、以及 OI-1 的命名**)请直接说明,本计划会据此改写后再来一次汇报。</resume-signal>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: 写出改动前基线,然后把 `hotkey` 的分组列表换成 `ListPage`、右栏压成内容区里唯一一条 `Scroll`,并提取其内联 handler 与多行 loc</name>
|
||||
<files>.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json(新增,第一步写出), src/client/ui/hotkey.tsx</files>
|
||||
<action>
|
||||
**依赖 Task 0 已收到「可以执行」,且 Task 0 中三类授权(删 3 个常量 / 删 2 处分组行注释 / 命名)均已获批。** **第一步(必须在任何代码编辑之前):** 写出 `.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json`,schema 为 `{ head, hashes, dirty, eslint, typecheck }`:
|
||||
- `head`:当前 `git rev-parse HEAD`。
|
||||
- `hashes`:**8 个**受保护文件的 `git hash-object` —— `packages-user/client-base/src/components/scroll.tsx`、`packages-user/client-base/src/components/list.tsx`、`packages-user/client-base/src/components/textbox.tsx`、`src/client/ui/settings.tsx`、`src/client/ui/index.ts`、`src/client/misc/hotkey.ts`、`src/client/ui/inputBox.tsx`、`src/client/ui/waitBox.tsx`;键用完整仓库相对路径(POSIX 正斜杠),值小写 sha。**`hotkeySettingsGate.test.ts` 不在其中**(Task 3 会按用户决定改它)。
|
||||
- `dirty`:`git status --porcelain -uall`(**不加 pathspec**),取状态列之后的路径、去掉 `.planning/` 开头的条目,按字典序写入。**注意:09-29 / 09-30 / 09-31 的 5 个 UI 文件与 `uiContainerGate.test.ts` / `textbox.tsx` 尚未提交,会出现在这份快照里;这是基线,不是异常。**
|
||||
- `eslint`:对**同一组 7 个路径**(`src/client/ui/hotkey.tsx`、`book.tsx`、`bookDetail.tsx`、`equipbox.tsx`、`toolbox.tsx`、`src/client/ui/__test__/uiContainerGate.test.ts`、`src/client/ui/__test__/hotkeySettingsGate.test.ts`)跑 `pnpm exec eslint -f json`,每条归一化为 `仓库相对路径|规则ID|严重度|消息`,归一化代码必须与 Task 3 逐字相同:`const norm = p => { const s = String(p).replace(/\\/g, '/'); const i = s.search(/(packages-user|src)\//); return i >= 0 ? s.slice(i) : s; };`,条目为 `norm(f.filePath) + '|' + String(m.ruleId || 'null') + '|' + String(m.severity) + '|' + String(m.message).trim()`。基线采集与 Task 3 比较的 scope 必须完全相同。`eslint` 以 `status === 1` 退出属于正常(有告警),必须照常解析 stdout;只有未运行 / stdout 为空 / JSON 不可解析才停下上报。
|
||||
- `typecheck`:整仓跑 `pnpm check:type`,再**原样复制 Task 3 的 norm** —— `const norm = l => l.replace(/\\/g, '/').replace(/\(\d+,\d+\):/, '').replace(/^.*?(?=(src|packages-user)\/)/, '').trim();` —— 并用与 Task 3 **逐字相同的 7 路径 scope** 过滤出这些文件内的诊断,保留重复项与 `src/` / `packages-user/` 前缀。只写一次,此后不得重写。
|
||||
|
||||
**第二步:`src/client/ui/hotkey.tsx`。**
|
||||
1. 导入:把 `import { Font } from '@motajs/render';` 改为 `import { ElementLocator, Font } from '@motajs/render';`;把 `import { IGameUIPropsBase, Scroll, useKey } from '@user/client-base';` 改为 `import { IGameUIPropsBase, ListPage, Scroll, useKey } from '@user/client-base';`(同模块导入合并到同一行,导入之间不夹注释 / 空行)。
|
||||
2. 常量区:**删除** `GROUP_TEXT_LEFT`、`RIGHT_LEFT`、`RIGHT_WIDTH` 三个常量及其 jsDoc(Task 0 已授权);在 `GROUP_WIDTH` 之后新增 `GROUP_BASIS`(`GROUP_WIDTH / MAIN_WIDTH`)、`CONTENT_WIDTH`(`MAIN_WIDTH * (1 - GROUP_BASIS)`)、`COMMAND_WIDTH`(`CONTENT_WIDTH - LEFT_PAD * 3`)三个 number 常量与 `COMMAND_SCROLL_LOC: ElementLocator = [LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT]`、`CLOSE_LOC: ElementLocator = [LEFT_PAD, MAIN_HEIGHT - LEFT_PAD, void 0, void 0, 0, 1]`,每个都带中文 jsDoc 说明用途;`BOX_LEFT` 改为 `COMMAND_WIDTH - KEY_BOX_WIDTH - LEFT_PAD`(取值仍是 172,jsDoc 原样保留);`LEFT_PAD` / `TOP_PAD` / `GROUP_WIDTH` / `GROUP_ROW_HEIGHT` / `LIST_TOP` / `LIST_HEIGHT` / `KEY_BOX_WIDTH` / `KEY_BOX_HEIGHT` / `COMMAND_GAP` / `BOX_FILL_HEIGHT` / `COMMAND_TEXT_LEFT` / `COMMAND_TEXT_TOP` / `CONTENT_FONT` 一律不动。
|
||||
3. setup 新增两个具名成员(都带 jsDoc):`const groupItems: [string, string][] = groups.map(([id, group]) => [id, group.name]);` 与工厂 `const getKeyClick = (command: IHotkeyCommand, index: number) => () => { selectKey(command, index); };`。
|
||||
4. 渲染函数整体替换为单一的 `<ListPage>` 根:`<ListPage loc={FULL_LOC} basis={GROUP_BASIS} list={groupItems} selected={selectedGroup.value} lineHeight={GROUP_ROW_HEIGHT} font={CONTENT_FONT} close closeLoc={CLOSE_LOC} onUpdate={selectGroup} onClose={close} nocache>`,子节点为对象形式的插槽(键为 `default`,值为返回 `<Scroll loc={COMMAND_SCROLL_LOC}>` 的函数,即 `{{ default: () => … }}`,与 `statistics.tsx` 的用法一致)。`Scroll` 的内容沿用现有 `commandRows.value.map((row, i) => { … })` 结构,但:①在回调开头新增 `const nameLoc: ElementLocator = [COMMAND_TEXT_LEFT, row.top + COMMAND_TEXT_TOP];` 并把指令名 text 写成 `loc={nameLoc}`;②内层 `.map((keys, j) => { … })` 里新增 `const boxLoc: ElementLocator = [BOX_LEFT, boxTop, KEY_BOX_WIDTH, BOX_FILL_HEIGHT];` 与 `const textLoc: ElementLocator = [BOX_LEFT + KEY_BOX_WIDTH / 2, boxTop + BOX_FILL_HEIGHT / 2];`,`g-rectr` 写 `loc={boxLoc}` / `onClick={getKeyClick(row.command, j)}`,组合文本写 `loc={textLoc}`;③保留「每条指令名与它的每个按键框、组合文本,展开为同级子项」这条 JSX 注释、「每个组合展开为按键框与文本两个同级子项」与「选中按键框同样只切换这个既有矩形的描边」两条行注释的原文。
|
||||
5. **删除**左栏那条 `Scroll`、分组 `g-rectr` 行与分组名 `text`,连带删除它们的两条注释(JSX 注释「每行点击区与分组名,选中态由同一矩形的描边表示」与行注释「选中分组只由这一行的边框表示,元素始终挂载」,Task 0 已授权);文件内不得再出现 `GROUP_TEXT_LEFT`、`RIGHT_LEFT`、`RIGHT_WIDTH`、`selectGroup(id)`、`group.name`(`group.name` 只允许在 `groupItems` 的 map 里出现 1 次)。
|
||||
6. **不得改动**:`@common/exit` 的 `key.implement`(`if (selectedCommand.value) return;` + `props.controller.close(props.instance)` 逐字保留)、`close`、`cancelCapture` / `removeCapture` / `selectGroup` / `selectKey` / `onKeyDown` / `onKeyUp` / `onUnmounted(cancelCapture)` / `refreshCombinations()` / `formatCombination` / `refreshCombinations` / `commandRows` / `ICommandRow` / `combinationLists` / `formatCombination(showing)` / `selectedCommand` / `selectedIndex` / `heldKeys` / `KeyCode.Unknown` / `EVENT_KEY_CODE_MAP` / 两条 `addEventListener(..., true)` / `strokeStyle={isSelected ? …}` / `lineWidth={isSelected ? …}` / `noevent` / `nocache`;`/** 关闭界面,返回文本是实际的关闭路径 */` 这条 jsDoc **逐字保留**(AGENTS.md 既有注释不改;它不再被任何闸门依赖)。
|
||||
7. 不新增 key 属性、不新增 `Selection`、不新增包裹 container、不改 `list.tsx` / `scroll.tsx`;`<Scroll` 计数必须为 1、`<container` 计数必须为 0、`<ListPage` 计数必须为 1。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const b='.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json';const need=['packages-user/client-base/src/components/scroll.tsx','packages-user/client-base/src/components/list.tsx','packages-user/client-base/src/components/textbox.tsx','src/client/ui/settings.tsx','src/client/ui/index.ts','src/client/misc/hotkey.ts','src/client/ui/inputBox.tsx','src/client/ui/waitBox.tsx'];const bad=[];if(!fs.existsSync(b))bad.push('baseline missing');else{let j={};try{j=JSON.parse(fs.readFileSync(b,'utf8'))}catch(e){bad.push('baseline unparsable')}for(const k of ['head','hashes','dirty','eslint','typecheck'])if(!(k in j))bad.push('missing '+k);const ks=Object.keys(j.hashes||{});if(ks.length!==8)bad.push('hashes must have 8 entries, got '+ks.length);for(const f of need)if(!Object.prototype.hasOwnProperty.call(j.hashes||{},f))bad.push('hashes missing '+f);if(!Array.isArray(j.dirty))bad.push('dirty must be an array');if(!Array.isArray(j.eslint))bad.push('eslint baseline must be an array');if(!Array.isArray(j.typecheck))bad.push('typecheck baseline must be an array')}if(bad.length){console.error('TASK1 BASELINE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: baseline')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const strip=s=>s.replace(/\{[ \t]*\/\*[^]*?\*\/[ \t]*\}/g,'').replace(/\/\*[^]*?\*\//g,'').split(/\r?\n/).map(l=>l.replace(/\/\/.*/,'')).join('\n');const raw=fs.readFileSync('src/client/ui/hotkey.tsx','utf8');const c=strip(raw);const f=c.replace(/\s+/g,' ');const bad=[];const cnt=re=>(c.match(re)||[]).length;const need=['ListPage','ElementLocator','basis={GROUP_BASIS}','list={groupItems}','selected={selectedGroup.value}','lineHeight={GROUP_ROW_HEIGHT}','font={CONTENT_FONT}','closeLoc={CLOSE_LOC}','onUpdate={selectGroup}','onClose={close}','default:','nocache','COMMAND_SCROLL_LOC','GROUP_BASIS','CONTENT_WIDTH','COMMAND_WIDTH','CLOSE_LOC','getKeyClick','onClick={getKeyClick(row.command, j)}','commandRows','ICommandRow','combinationLists','formatCombination','refreshCombinations','setKeyCombination(','KeyCodeUtils.toString','EVENT_KEY_CODE_MAP','KeyCode.Unknown','iterateGroups(','iterateKeyCombinations(','@common/exit','FULL_LOC','未设置','selectedGroup.value = id'];for(const n of need)if(f.indexOf(n)<0)bad.push('missing :: '+n);if(!/<ListPage[^>]*\sclose(?=[\s>])/.test(c))bad.push('missing :: bare close prop on ListPage');if(!/addEventListener\(\s*'keydown'\s*,\s*onKeyDown\s*,\s*true\s*\)/.test(c))bad.push('missing capture-phase keydown listener');if(cnt(/<ListPage(?=[\s>])/g)!==1)bad.push('expected exactly 1 ListPage, got '+cnt(/<ListPage(?=[\s>])/g));if(cnt(/<Scroll(?=[\s>])/g)!==1)bad.push('expected exactly 1 Scroll, got '+cnt(/<Scroll(?=[\s>])/g));if(cnt(/<container(?=[\s>])/g)!==0)bad.push('hotkey must not own a container, got '+cnt(/<container(?=[\s>])/g));if(/Selection/.test(c))bad.push('Selection must not appear in hotkey source');if(c.indexOf('key={')>=0)bad.push('key attribute must not appear');if(/loc=\{\[\s*\r?\n/.test(c))bad.push('multiline inline loc remains');if(/on[A-Z][A-Za-z]*=\{[^{}]*=>/.test(c))bad.push('inline arrow handler remains');if(cnt(/group\.name/g)!==1)bad.push('group.name must appear exactly once, got '+cnt(/group\.name/g));if(cnt(/strokeStyle=\{\s*isSelected\s*\?/g)!==1)bad.push('expected exactly 1 selected border strokeStyle');if(cnt(/lineWidth=\{\s*isSelected\s*\?/g)!==1)bad.push('expected exactly 1 selected border lineWidth');if(/<g-rect[\s>]/.test(c))bad.push('g-rect must not appear');for(const gone of ['GROUP_TEXT_LEFT','RIGHT_LEFT','RIGHT_WIDTH','selectGroup(id)'])if(f.indexOf(gone)>=0)bad.push('must be removed :: '+gone);if(bad.length){console.error('TASK1 HOTKEY GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task1: hotkey on ListPage, handlers and locs extracted')"</automated>
|
||||
<automated>cmd /c "pnpm exec eslint src/client/ui/hotkey.tsx"</automated>
|
||||
<automated>cmd /c "pnpm exec vitest run src/client/ui/__test__/hotkeySettingsGate.test.ts"</automated>
|
||||
<fails_when>基线缺失或字段残缺、`hashes` 不是 8 条、`dirty` / `eslint` / `typecheck` 不是数组;`hotkey.tsx` 缺 `ListPage` / `basis={GROUP_BASIS}` / `list={groupItems}` / `selected={selectedGroup.value}` / `lineHeight={GROUP_ROW_HEIGHT}` / `font={CONTENT_FONT}` / 布尔 `close` / `closeLoc={CLOSE_LOC}` / `onUpdate={selectGroup}` / `onClose={close}` / `default:` / `nocache` / `COMMAND_SCROLL_LOC` / `getKeyClick` / `onClick={getKeyClick(row.command, j)}` 任一,或 `<ListPage>` / `<Scroll>` / `<container>` 计数不是 1 / 1 / 0,或出现 `Selection` / key 属性 / 多行内联 loc / 内联箭头 handler / `<g-rect` / `GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH` / `selectGroup(id)`,或 `group.name` 不是恰好 1 次,或选中边框的 `strokeStyle` / `lineWidth` 不是各 1 次,或丢失捕获阶段监听 / 写回 / 分组切换 / 关闭标识;`hotkeySettingsGate` 变红(此时它仍断言「返回」,说明源码里那条 jsDoc 被删 / 改,须恢复注释而不是改测试);eslint 报错;或任一 node 闸门非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>`09-32-BASELINE.json` 已在改代码前写出且只写一次(8 个受保护哈希 + 7 路径 eslint / typecheck 基线 + 脏文件快照);`hotkey.tsx` 的根为 `<ListPage>`(`loc={FULL_LOC}` / `basis={GROUP_BASIS}` / `list={groupItems}` / `selected={selectedGroup.value}` / `lineHeight={GROUP_ROW_HEIGHT}` / `font={CONTENT_FONT}` / `close` / `closeLoc={CLOSE_LOC}` / `onUpdate={selectGroup}` / `onClose={close}` / `nocache`),`default` 插槽里只有一条 `<Scroll loc={COMMAND_SCROLL_LOC}>`;左栏 Scroll、分组行与 3 个常量已删,源码无 `Selection` / key 属性 / 多行内联 loc / 内联箭头;捕获 / 写回 / 分组切换 / `@common/exit` / 选中边框逐字不变;`hotkeySettingsGate` 全绿;eslint 无报错。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: 提取 `book` / `bookDetail` / `equipbox` / `toolbox` 的 11 处内联 handler 与 11 处多行 loc(+ Task 1 的 hotkey 2 处 handler,合计 13),其余一律不动</name>
|
||||
<files>src/client/ui/book.tsx, src/client/ui/bookDetail.tsx, src/client/ui/equipbox.tsx, src/client/ui/toolbox.tsx</files>
|
||||
<action>
|
||||
**依赖 Task 1 已完成。只做「提具名函数 / 工厂」与「提 `ElementLocator` 常量」两件事,不改任何逻辑体、不改列表结构、不改既有注释。**
|
||||
|
||||
**A. `src/client/ui/book.tsx`(1 处 handler,0 处多行 loc)**
|
||||
1. 在 `openDetail` 之后新增工厂(带 jsDoc):`const getEnemyClick = (view: IEnemyView<IEnemyAttr>) => () => { openDetail(view); };`。
|
||||
2. `Scroll` 内命中层的 `onClick` 改为 `onClick={getEnemyClick(view)}`。
|
||||
3. 其余一律不动:13 个元素的顺序与全部属性(含单行 loc / `anc` / `font` / `fillStyle` / `noevent` / `cursor` / `fill`)、`enemies.value.map((view, i) => …)` 的内联数组返回、四项临界 0 回退、五条基础数值文案、三条既有行注释(含单独成行的「临界信息缺失时回退为 0…」)、既有 JSX 注释、根 container。
|
||||
|
||||
**B. `src/client/ui/bookDetail.tsx`(1 处 handler,2 处多行 loc)**
|
||||
1. 导入改为 `import { ElementLocator, Font } from '@motajs/render';`。
|
||||
2. 新增文件级常量(带 jsDoc):`const SPECIAL_SCROLL_LOC: ElementLocator = [SPECIAL_LEFT, SPECIAL_TOP, SPECIAL_WIDTH, SPECIAL_HEIGHT];`;`Scroll` 写 `loc={SPECIAL_SCROLL_LOC}`。
|
||||
3. `specialRows.value.map(row => [ … ])` 回调内新增 `const textLoc: ElementLocator = [0, row.textTop, SPECIAL_WIDTH, row.textHeight];`(**仍在回调内、每次求值新建**),`TextContent` 写 `loc={textLoc}`。
|
||||
4. 在 `updateTextHeight` 之后新增工厂(带 jsDoc):`const getUpdateHeight = (code: number) => (height: number) => { updateTextHeight(code, height); };`;`TextContent` 写 `onUpdateHeight={getUpdateHeight(row.special.code)}`。
|
||||
5. 不动:`cumsum(hs)` / `ends[i] - hs[i]` / 无手写累加、`textHeights` / `updateTextHeight`(含同值早退)/ `watch(specials, …)` / `SPECIAL_MIN_HEIGHT` / 0 值回退 / 两条临界文案 / `toggleMode` / `close` / 全部既有注释(含「TextContent 已按 props.loc 响应式同步位置,行位置变化时无需重挂载」与「一次遍历累加出每行位置…」);`Scroll` 内仍无 container、无 `hidden=`,文件仍只有 1 个 container。
|
||||
|
||||
**C. `src/client/ui/equipbox.tsx`(7 处 handler,7 处多行 loc)**
|
||||
1. 导入:`import { ElementLocator, IActionEvent } from '@motajs/render';`(替换原来单独一行的 `ElementLocator` 导入);`import { EquipStatus, IEquipmentState, IHeroState } from '@user/data-base';`(新增 `IEquipmentState`)。
|
||||
2. 新增具名函数(带 jsDoc):`const onMove = (ev: IActionEvent) => { movePress(ev.absoluteX, ev.absoluteY); };`;根 container 写 `onMove={onMove}`(`onUp={endPress}` 原样)。
|
||||
3. 新增四个工厂(均带 jsDoc):`getSlotEnter(slot: number)`(`() => { hoverSlot.value = slot; }`)、`getSlotLeave(slot: number)`(`() => { if (hoverSlot.value === slot) hoverSlot.value = -1; }`)、`getSlotDown(equip: IEquipmentState<IHeroAttr> | null, slot: number)`(`(ev: IActionEvent) => { if (!equip) return; startPress(equip.uid, slot, ev.absoluteX, ev.absoluteY); }`)、`getOwnedDown(uid: number)`(`(ev: IActionEvent) => { startPress(uid, -1, ev.absoluteX, ev.absoluteY); }`)。JSX 写 `onEnter={getSlotEnter(i)}` / `onLeave={getSlotLeave(i)}`(槽底与图标两处各一对)、`onDown={getSlotDown(slotEquip, i)}`、`onDown={getOwnedDown(item.uid)}`。
|
||||
4. 7 处多行 loc 提为局部 `const xxxLoc: ElementLocator`:状态区增减文本 `deltaLoc = [STATUS_WIDTH - 8, y + STATUS_ROW_HEIGHT / 2]`、槽底 `slotCellLoc = [cellX + 4, 4, SLOT_CELL_WIDTH - 8, SLOT_HEIGHT - 8]`、槽图标 `slotIconLoc = [cellX + (SLOT_CELL_WIDTH - SLOT_ICON_SIZE) / 2, 14, SLOT_ICON_SIZE, SLOT_ICON_SIZE]`、槽名 `slotNameLoc = [cellX + SLOT_CELL_WIDTH / 2, SLOT_HEIGHT - 12]`、拥有装备行底 `ownedCellLoc = [4, y + 2, RIGHT_WIDTH - 8, OWNED_ROW_HEIGHT - 4]`、拥有装备图标 `ownedIconLoc = [10, y + 6, OWNED_ICON_SIZE, OWNED_ICON_SIZE]`、拖拽浮层 `dragLoc = [dragX.value - DRAG_ICON_SIZE / 2, dragY.value - DRAG_ICON_SIZE / 2, DRAG_ICON_SIZE, DRAG_ICON_SIZE]`(`dragLoc` 定义在渲染闭包内、`return (` 之前)。
|
||||
5. 不动:三处内联 `.map()` 与三元组结构、`hidden={!!equip}` / `hidden={!equip}` / `hidden={!dragging.value}`、三处空文本 / 0 回退、`startPress` / `movePress` / `endPress` 逻辑体、`getTargetSlot` / `selectedSlot` 的单行 `for`、4 个 `*_SCROLL_LOC`、两条既有行注释与三条既有 JSX 注释、根 container。
|
||||
|
||||
**D. `src/client/ui/toolbox.tsx`(2 处 handler,2 处多行 loc)**
|
||||
1. 导入改为 `import { ElementLocator, Font } from '@motajs/render';`。
|
||||
2. 在 `clickItem` 之后新增工厂(带 jsDoc):`const getItemClick = (item: IHeroItemState<IHeroAttr>) => () => { clickItem(item); };`;两处 `g-rectr` 写 `onClick={getItemClick(item)}`。
|
||||
3. 两处 `map` 回调内各新增 `const cellLoc: ElementLocator = [4, y + 2, COLUMN_WIDTH - 8, ROW_HEIGHT - 4];`,对应 `g-rectr` 写 `loc={cellLoc}`。
|
||||
4. 不动:两处内联 `.map()` 与四元组结构、`hidden={!!current}` 提示位置、`TextContent` 的空文本回退、`clickItem` 的再点使用、`collect` / `selectedItem` / `permItems` / `consItems`、单行 loc、两条既有 JSX 注释、根 container。
|
||||
|
||||
**E. 四个文件共同约束:** 不得新增 key 属性、`Fragment`、包裹容器、条件渲染或 `hidden` 开关;不得改既有注释;不得把任何 loc 数组改成共享可变数组(map 内的常量必须每次回调新建)。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>node -e "const fs=require('fs');const strip=s=>s.replace(/\{[ \t]*\/\*[^]*?\*\/[ \t]*\}/g,'').replace(/\/\*[^]*?\*\//g,'').split(/\r?\n/).map(l=>l.replace(/\/\/.*/,'')).join('\n');const bad=[];for(const p of ['src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx']){const c=strip(fs.readFileSync(p,'utf8'));if(/loc=\{\[\s*\r?\n/.test(c))bad.push(p+' :: multiline inline loc remains');if(/on[A-Z][A-Za-z]*=\{[^{}]*=>/.test(c))bad.push(p+' :: inline arrow handler remains');if(c.indexOf('key={')>=0)bad.push(p+' :: key attribute must not appear')}if(bad.length){console.error('TASK2 SHAPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task2: no multiline loc, no inline arrow handler, no key')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const strip=s=>s.replace(/\{[ \t]*\/\*[^]*?\*\/[ \t]*\}/g,'').replace(/\/\*[^]*?\*\//g,'').split(/\r?\n/).map(l=>l.replace(/\/\/.*/,'')).join('\n');const flat=p=>strip(fs.readFileSync(p,'utf8')).replace(/\s+/g,' ');const bad=[];const b=flat('src/client/ui/book.tsx');for(const n of ['getEnemyClick','onClick={getEnemyClick(view)}','enemies.value.map(','critical ? critical.baseInfo.damage : 0','specialText(base)'])if(b.indexOf(n)<0)bad.push('book missing :: '+n);if((b.match(/<container(?=[\s>])/g)||[]).length!==1)bad.push('book container count');const d=flat('src/client/ui/bookDetail.tsx');for(const n of ['SPECIAL_SCROLL_LOC','loc={SPECIAL_SCROLL_LOC}','loc={textLoc}','getUpdateHeight','onUpdateHeight={getUpdateHeight(row.special.code)}','cumsum(hs)','ends[i] - hs[i]','autoHeight','textHeights','SPECIAL_MIN_HEIGHT','getDescription()'])if(d.indexOf(n)<0)bad.push('bookDetail missing :: '+n);if(!/textLoc: ElementLocator = \[\s*0,\s*row\.textTop,\s*SPECIAL_WIDTH,\s*row\.textHeight\s*\]/.test(d))bad.push('bookDetail missing :: textLoc locator array');if(d.indexOf('top +=')>=0)bad.push('bookDetail still has manual accumulation');if((d.match(/<container(?=[\s>])/g)||[]).length!==1)bad.push('bookDetail container count');const e=flat('src/client/ui/equipbox.tsx');for(const n of ['onMove={onMove}','getSlotEnter','getSlotLeave','getSlotDown','getOwnedDown','onEnter={getSlotEnter(i)}','onLeave={getSlotLeave(i)}','onDown={getSlotDown(slotEquip, i)}','onDown={getOwnedDown(item.uid)}','deltaLoc: ElementLocator','slotCellLoc: ElementLocator','slotIconLoc: ElementLocator','slotNameLoc: ElementLocator','ownedCellLoc: ElementLocator','ownedIconLoc: ElementLocator','dragLoc: ElementLocator','loc={dragLoc}','hidden={!!equip}','hidden={!equip}','hidden={!dragging.value}','STATUS_SCROLL_LOC','DESC_SCROLL_LOC','SLOT_SCROLL_LOC','OWNED_SCROLL_LOC','startPress(','onUp={endPress}'])if(e.indexOf(n)<0)bad.push('equipbox missing :: '+n);if((e.match(/<container(?=[\s>])/g)||[]).length!==1)bad.push('equipbox container count');const t=flat('src/client/ui/toolbox.tsx');for(const n of ['getItemClick','onClick={getItemClick(item)}','cellLoc: ElementLocator','loc={cellLoc}','hidden={!!current}','permItems.value.map(','consItems.value.map('])if(t.indexOf(n)<0)bad.push('toolbox missing :: '+n);if((t.match(/onClick=\{getItemClick\(item\)\}/g)||[]).length!==2)bad.push('toolbox expected 2 getItemClick handlers');if((t.match(/cellLoc: ElementLocator/g)||[]).length!==2)bad.push('toolbox expected 2 cellLoc constants');if((t.match(/<container(?=[\s>])/g)||[]).length!==1)bad.push('toolbox container count');if(bad.length){console.error('TASK2 CONTENT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK task2: handlers extracted and behaviour markers retained')"</automated>
|
||||
<automated>cmd /c "pnpm exec eslint src/client/ui/book.tsx src/client/ui/bookDetail.tsx src/client/ui/equipbox.tsx src/client/ui/toolbox.tsx"</automated>
|
||||
<fails_when>四个文件中任一仍含多行内联 loc、内联箭头 `on<Event>={… => …}` 或 key 属性;`book` 缺 `getEnemyClick` / `onClick={getEnemyClick(view)}` / 0 回退标识;`bookDetail` 缺 `SPECIAL_SCROLL_LOC` / `textLoc: ElementLocator` / `getUpdateHeight` / `cumsum(hs)` / `ends[i] - hs[i]` 或仍含手写累加;`equipbox` 缺 `onMove` / 四个工厂 / 七个 `*Loc: ElementLocator` / `hidden` 分流 / 4 个 `*_SCROLL_LOC` / `onUp={endPress}`;`toolbox` 缺 `getItemClick`(2 处)/ `cellLoc`(2 处)/ `hidden={!!current}`;任一文件 container 计数不为 1;eslint 报错;或任一 node 闸门非零退出。</fails_when>
|
||||
</verify>
|
||||
<done>四个 UI 文件的 11 处内联箭头全部变成具名函数 / 工厂(JSX 只剩 `onXxx={fn}` 或 `onXxx={factory(arg)}`),11 处多行内联 loc 全部变成 `ElementLocator` 常量(7 + 2 + 2 个,`book` 无),加上 Task 1 的 hotkey 2 处 handler,五个文件合计 14 处多行 loc 与 13 处内联箭头(11 + 2)清零;0 值回退 / `cumsum` / 高度回调 / `hidden` 分流 / 拖拽 / 悬停 / 再点使用 / `noevent` / 4 个 `*_SCROLL_LOC` / 既有注释全部保持;四文件 eslint 无报错。</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: 更新 `uiContainerGate.test.ts`(hotkey 改 `ListPage` 结构 + 新增两组源码闸门)与 `hotkeySettingsGate.test.ts`(关闭用例断言改 `ListPage` / `close` / 「关闭」),先跑全部门禁、再写执行摘要、最后跑摘要闸门</name>
|
||||
<files>src/client/ui/__test__/uiContainerGate.test.ts, src/client/ui/__test__/hotkeySettingsGate.test.ts, .planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md</files>
|
||||
<action>
|
||||
**依赖 Task 1、2 已完成。**
|
||||
|
||||
**A. 更新 `src/client/ui/__test__/uiContainerGate.test.ts`(保留 `stripComments` / `flatten` / `getScrollBlocks` / `tagAround` **4 个**辅助函数与既有说明注释;**删除** `firstContainerTag` 及其上方那行 `//` 注释——hotkey 断言改用 `ListPage` 开标签后它没有任何使用点,留着会触发 eslint `no-unused-vars`):**
|
||||
1. 顶部新增 `import listSource from '../../../../packages-user/client-base/src/components/list.tsx?raw';`(不导入组件本体)。
|
||||
2. **`keeps only the root container in each list file`**:四个文件(`book` / `bookDetail` / `equipbox` / `toolbox`)仍断言 container 计数为 1;`hotkey` 改为断言 container 计数为 0、`<ListPage` 计数为 1、`<Scroll` 计数为 1;`equipFlat` 的两条否定断言保留。
|
||||
3. **`flattens both hotkey columns and expresses selection with borders` 整组重写**(组名改为 `renders the hotkey groups through ListPage and keeps the key box border`):断言 `getScrollBlocks(source)` 长度为 1;`source` 的 `@user/client-base` 导入行含 `ListPage`(正则 `/import\s*\{[^}]*ListPage[^}]*\}\s*from\s*'@user\/client-base'/`);`source` 含 `ListPage`、`basis={GROUP_BASIS}`、`list={groupItems}`、`selected={selectedGroup.value}`、`lineHeight={GROUP_ROW_HEIGHT}`、`font={CONTENT_FONT}`、布尔 `close`(用 `/<ListPage[^>]*\sclose(?=[\s>])/` 断言,`onClose={close}` 不会误命中)、`closeLoc={CLOSE_LOC}`、`onUpdate={selectGroup}`、`onClose={close}`、`default:`、`COMMAND_SCROLL_LOC`;`not.toMatch(/Selection/)`;`not.toContain('GROUP_TEXT_LEFT')` / `not.toContain('RIGHT_LEFT')` / `not.toContain('RIGHT_WIDTH')` / `not.toContain('selectGroup(id)')`;保留 `commandRows` / `ICommandRow` 与 `getCommandTop` / `getCommandHeight` / `getContentHeight` 的否定断言;用 `tagAround(source, 'g-rectr', 'getKeyClick(row.command, j)')` 取按键框并断言含 `strokeStyle` / `lineWidth` / `isSelected`;`strokeStyle={isSelected ?` 与 `lineWidth={isSelected ?` **各恰好 1 处**(hotkey 源码里只有按键框这一处选中边框;断言必须与实际源码一致,不得为凑数改源码);保留 `not.toContain('if (isSelected)')` / `not.toContain('hidden={!isSelected}')` / `not.toContain('{isSelected &&')` / `not.toContain('isSelected && groupNodes.push')` / `not.toContain('isSelected && commandNodes.push')`。
|
||||
4. **`marks the hotkey root container as nocache` 改为 `marks the hotkey list page as nocache`**:用 `/<ListPage[^>]*>/` 取出 `ListPage` 开标签并断言含 `nocache`(`firstContainerTag` 已在 A 段开头删除,hotkey 上它本来也只能返回空串)。
|
||||
5. **`splits display toggles by position and pairs hit layers with noevent`**:hotkey 段删除 `groupNameTag` / `groupHitTag`(分组名与分组命中层已由 `ListPage` 渲染),保留 `commandNameTag`(`row.command.name`)、`combinationTag`(`formatCombination(showing)`)、`keyHitTag`(改为 `getKeyClick(row.command, j)`)并保留其 `noevent` / 非 `noevent` 断言;**toolbox 段把 `toolRowTag` 的针从 `clickItem(item)` 改为 `getItemClick(item)`**(提取后 `clickItem(item)` 只出现在 `getItemClick` 工厂体内,`tagAround` 会从工厂体起找不到前一个 `<g-rectr` 而返回空串),并保留 `expect(toolNameTag).toContain('noevent')` / `expect(toolCountTag).toContain('noevent')` / `expect(toolRowTag).not.toContain('noevent')`;末尾的 `tags` 数组同步删除 `groupNameTag` / `groupHitTag` 两个变量(`toolRowTag` 保留在数组里)。
|
||||
6. **`renders every list with inline map and short loop variables`**:hotkey 段保留 `groups.map(`(`groupItems` 用的就是它)、`commandRows.value.map(`、`combinationLists.value[i]`、`.map((keys, j)`;把 `selectKey(row.command, j)` 换成 `getKeyClick(row.command, j)`,并新增 `selectKey(command, index)`(工厂体内)。其余文件的断言与 `groupIndex` / `commandIndex` / 折行 `for (` 的否定断言保留。
|
||||
7. **`keeps line comments out of Scroll children`**:`hotkey` 的 `counts` 由 `[1, 1]` 改为 `[1]`(只剩右栏那一条);`hotkeyComments` 只保留「选中按键框同样只切换这个既有矩形的描边」,并新增两条否定断言:`hotkeySource` 不含「选中分组只由这一行的边框表示,元素始终挂载」、不含「每行点击区与分组名」(Task 0 已授权删除);`book` / `equipbox` 的注释断言保留。
|
||||
8. **`renders every list file without keys`**:五文件 `not.toContain('key={')` 保留;`bookDetail` 的 `contentTag` 断言改为 `not.toContain('key=')` + `flatten(contentTag)).toContain('loc={textLoc}')`,并新增对 `flatten(stripComments(bookDetailSource))` 的空白容忍正则 `/textLoc: ElementLocator = \[\s*0,\s*row\.textTop,\s*SPECIAL_WIDTH,\s*row\.textHeight\s*\]/` 断言(`flatten` 会把折行压成空格,故不得用字面量 `toContain`);两条过期注释的否定断言保留。
|
||||
9. **新增 `// hotkey 的分组列表由 ListPage 渲染,关闭按钮使用其内置的关闭文本`**:断言 `hotkeySource` 含 `ListPage` 与 `closeLoc=`,`not.toContain('text="返回"')`、`not.toContain('text="关闭"')`(关闭文本不在 hotkey 里自建);并断言 `listSource` 含 `text="关闭"` 与 `basis`(证明「关闭」来自 `ListPage`)。
|
||||
10. **新增 `// 五个 UI 文件都不保留多行内联 loc 与内联箭头事件处理器`**:对五个 `stripComments(source)` 逐个断言 `not.toMatch(/loc=\{\[\s*\r?\n/)` 与 `not.toMatch(/on[A-Z][A-Za-z]*=\{[^{}]*=>/)`。
|
||||
11. 保留全部原有保留项断言:Scroll 内无 container / 无 `hidden=`、`cumsum` 组、`book` 每行 11 `text` + 1 `icon` + 1 `g-rect`、`book` / `bookDetail` 0 回退、`noevent` 逐元素配对、4 个 `*_SCROLL_LOC` 带 `: ElementLocator`、无预建 `VNode` 数组、内联 `.map()` 与短循环变量。**`hoists the four equipbox scroll locators with explicit types` 里那条导入断言必须改**:Task 2 把 `import { Font } from '@motajs/render';` 与 `import { ElementLocator } from '@motajs/render';` 合并成 `import { ElementLocator, IActionEvent } from '@motajs/render';`,因此把 `expect(source).toContain("import { ElementLocator } from '@motajs/render';")` 改为 `expect(source).toContain("import { ElementLocator, IActionEvent } from '@motajs/render';")`(或等价宽松正则 `/\{\s*ElementLocator,\s*IActionEvent\s*\}\s*from\s*'@motajs\/render'/`);其余三个 `*_SCROLL_LOC` 断言不动。
|
||||
12. 每个 `it` 上方紧邻一行 `//` 注释;不修改与本次无关的既有 `it`;测试不读取 `.planning/`、不导入组件本体。跨包 `?raw`(`textbox.tsx` / `list.tsx`)若运行时报错,**停下上报**;允许用 `node:fs` 的 `readFileSync` 读同一路径并在 SUMMARY 记录为偏离。
|
||||
|
||||
**A2. 更新 `src/client/ui/__test__/hotkeySettingsGate.test.ts`(只动关闭用例与一行导入,其余 10 个用例与 `stripComments` 一字不动):**
|
||||
1. 顶部新增 `import listSource from '../../../../packages-user/client-base/src/components/list.tsx?raw';`(与 `uiContainerGate.test.ts` 同一相对路径,不导入组件本体)。
|
||||
2. 把最后一个用例 `// 关闭路径使用公共退出命令与返回文本` + `it('closes through the common exit command and the back text', …)` 改为 `// 关闭路径使用公共退出命令,关闭文本由 ListPage 内置的「关闭」提供` + `it('closes through the common exit command and the ListPage close text', …)`。
|
||||
3. 断言改为:`expect(hotkeySource).toContain('@common/exit')`、`expect(hotkeySource).toContain('ListPage')`、`expect(/<ListPage[^>]*\sclose(?=[\s>])/.test(hotkeySource)).toBe(true)`(布尔 `close` 属性)、`expect(hotkeySource).toContain('closeLoc={CLOSE_LOC}')`、`expect(hotkeySource).toContain('onClose={close}')`、`expect(hotkeySource).not.toContain('text="返回"')`、`expect(hotkeySource).not.toContain('text="关闭"')`、`expect(listSource).toContain('text="关闭"')`;**删除** `expect(hotkeySource).toContain('返回')`。
|
||||
4. 不得顺手改其它用例、不得改 `stripComments`、不得改 `hotkey.tsx` 来迁就测试。
|
||||
|
||||
**B. 先跑完所有门禁(此时还没有 SUMMARY):** 逐条执行下方 automated 命令并记录**实际**输出。
|
||||
- vitest:`uiContainerGate.test.ts` 与 `hotkeySettingsGate.test.ts`。
|
||||
- eslint:跑**与 Task 1 基线完全相同的 7 个路径**,与 `09-32-BASELINE.json` 的 `eslint` 比较,只判 NEW。
|
||||
- `pnpm check:type`:整仓运行,按**与 Task 1 基线完全相同的 7 路径 scope** 过滤,只判 NEW;基线中存在、执行后消失的诊断允许。
|
||||
- 受保护文件哈希(**8 个**)与基线逐字相等;HEAD 与基线相同,或 `git diff --name-only <baseline>..HEAD` 只含 `.planning/` 路径。
|
||||
- 范围(两条都要):①`git status --porcelain -uall -- <7 个代码 / 测试路径>` 只出现这 7 个路径;②整仓 `git status --porcelain -uall` 的每条路径必须属于这 7 个路径、或出现在基线 dirty 快照、或以 `.planning/` 开头,否则判范围外写入。
|
||||
- **顺序铁律**:先跑门禁,**再**写 SUMMARY;不得在门禁实际运行之前把结果写进 SUMMARY。
|
||||
|
||||
**C. 写 `.planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md`(用 B 步的真实结果):**
|
||||
- `ListPage` 接入方式:完整的 props 组合、`default` 插槽里的唯一 `Scroll`、`GROUP_BASIS` / `CONTENT_WIDTH` / `COMMAND_WIDTH` / `COMMAND_SCROLL_LOC` / `CLOSE_LOC` 的推导与「右栏绝对位置不变」的算式(180 + 32 = 212)。
|
||||
- 删除清单:左栏 Scroll + 分组 `g-rectr` 行 + 分组名 text + 3 个常量(`GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH`)+ 2 处分组行注释(用户授权);`groupItems` / `getKeyClick` / 3 个局部 loc 的新增位置。
|
||||
- 14 处多行 loc 与 13 处内联箭头的逐文件清单与落点(文件名 + 新常量 / 新工厂名)。
|
||||
- `nocache` 走 Vue 属性透传到 `ListPage` 根 container 的假设与本计划「不改 list.tsx」的边界。
|
||||
- `hotkeySettingsGate`:关闭用例已按 A2 改写为断言 `ListPage` / 布尔 `close` / `closeLoc` / `onClose` / `@common/exit` + `list.tsx` 的 `text="关闭"`,并已删除「返回」要求;该文件因此**不再**进入受保护哈希(受保护文件 8 个),只出现在 7 个允许路径里;既有 jsDoc 按 AGENTS.md 原样保留但不再被任何闸门依赖。
|
||||
- 视觉差异登记:左栏列表几何 `[16, 48, 180, 320]` → `ListPage` 列表区 `[0, 0, 180, 416]`(左移 16 / 上移 48 / 占满 416 整高,末行可贴到左下角「关闭」所在的 y = 400);分组选中态(金色边框 → `Selection` 脉动高亮)、分组名文本不再金色、选中矩形宽度 180 - 10 = 170、分组名 x 8→10、关闭入口变为左下角「关闭」、`List` 行用 container 包裹(`ListPage` 内部)。
|
||||
- 记录 B 步的真实门禁结果(两个 vitest 的用例数、eslint NEW-only、`pnpm check:type` NEW-only、8 个受保护哈希、HEAD 政策、全仓脏文件快照范围闸门)。
|
||||
- 声明:`estimate` 未校准(`confidence: low`);**不做浏览器 UAT,不宣称布局 / 点击命中 / 拖拽 / 捕获时序 / `nocache` 已验证**;09-29 / 09-30 / 09-31 改动未提交、这些路径在基线里本来就是脏的,范围闸门只证明「无新增范围外路径」;`keyAdaptationGate.test.ts` 是既存失败、不在本计划范围;**执行器 0 次代码提交**(`.planning` 产物可能由编排器按 `commit_docs: true` 聚合提交),代码工作树留给用户审查。
|
||||
|
||||
**D. 最后跑摘要闸门:** 确认 SUMMARY 存在且包含上述必需声明(见下方最后一条 automated);该闸门必须在 C 步之后运行,失败则补齐 SUMMARY 后重跑,不得只改闸门。
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cmd /c "pnpm exec vitest run src/client/ui/__test__/uiContainerGate.test.ts"</automated>
|
||||
<automated>cmd /c "pnpm exec vitest run src/client/ui/__test__/hotkeySettingsGate.test.ts"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json','utf8'));const files=['src/client/ui/hotkey.tsx','src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx','src/client/ui/__test__/uiContainerGate.test.ts','src/client/ui/__test__/hotkeySettingsGate.test.ts'];const r=cp.spawnSync('cmd',['/c','pnpm exec eslint -f json '+files.join(' ')],{encoding:'utf8',timeout:300000});const text=(r.stdout||'').trim();const bad=[];if(r.status===null||r.status>=2||!text)bad.push('eslint failed to run (status '+r.status+')');let arr=[];try{arr=JSON.parse(text||'[]')}catch(e){bad.push('eslint json unparsable')}const norm=p=>{const s=String(p).replace(/\\/g,'/');const i=s.search(/(packages-user|src)\//);return i>=0?s.slice(i):s};const cur=[];for(const f of arr){for(const m of (f.messages||[]))cur.push(norm(f.filePath)+'|'+String(m.ruleId||'null')+'|'+String(m.severity)+'|'+String(m.message).trim())}const before=b.eslint||[];const count=(a,v)=>{let n=0;for(const x of a)if(x===v)n++;return n};for(const v of new Set(cur)){if(count(cur,v)>count(before,v))bad.push('NEW eslint :: '+v)}if(bad.length){console.error('ESLINT GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK eslint: no new entries (baseline '+before.length+' -> current '+cur.length+')')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json','utf8'));const scope=['src/client/ui/hotkey.tsx','src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx','src/client/ui/__test__/uiContainerGate.test.ts','src/client/ui/__test__/hotkeySettingsGate.test.ts'];const r=cp.spawnSync('cmd',['/c','pnpm check:type'],{encoding:'utf8',timeout:900000});const text=(r.stdout||'')+(r.stderr||'');const diag=text.split(/\r?\n/).filter(l=>/error TS\d+/.test(l));const bad=[];if(r.status===null)bad.push('vue-tsc did not run');else if(r.status!==0&&diag.length===0)bad.push('vue-tsc crashed with no parseable diagnostics');const norm=l=>l.replace(/\\/g,'/').replace(/\(\d+,\d+\):/,'').replace(/^.*?(?=(src|packages-user)\/)/,'').trim();const cur=diag.map(norm).filter(x=>scope.some(p=>x.indexOf(p)===0));const before=(b.typecheck||[]).filter(x=>scope.some(p=>x.indexOf(p)===0));const count=(a,v)=>{let n=0;for(const x of a)if(x===v)n++;return n};for(const v of new Set(cur)){if(count(cur,v)>count(before,v))bad.push('NEW diagnostic :: '+v)}if(bad.length){console.error('TYPECHECK GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK typecheck: no new diagnostics in changed files (baseline '+before.length+' -> current '+cur.length+')')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json','utf8'));const bad=[];const keys=Object.keys(b.hashes||{});if(keys.length!==8)bad.push('baseline must protect 8 files, got '+keys.length);for(const file of keys){const r=cp.spawnSync('git',['hash-object',file],{encoding:'utf8'});if(r.status!==0)bad.push('cannot hash '+file);else if((r.stdout||'').trim()!==b.hashes[file])bad.push(file+' changed')}const head=(cp.spawnSync('git',['rev-parse','HEAD'],{encoding:'utf8'}).stdout||'').trim();if(head!==b.head){const d=cp.spawnSync('git',['diff','--name-only',b.head+'..'+head],{encoding:'utf8'});if(d.status!==0)bad.push('HEAD changed and diff failed');else{const nonPlan=(d.stdout||'').split(/\r?\n/).filter(Boolean).filter(p=>!p.startsWith('.planning/'));if(nonPlan.length)bad.push('HEAD changed with non-planning commits :: '+nonPlan.join(','))}}if(bad.length){console.error('PROTECTED GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK protected files unchanged; HEAD policy satisfied')"</automated>
|
||||
<automated>node -e "const cp=require('child_process'),fs=require('fs');const b=JSON.parse(fs.readFileSync('.planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json','utf8'));const allowed=['src/client/ui/hotkey.tsx','src/client/ui/book.tsx','src/client/ui/bookDetail.tsx','src/client/ui/equipbox.tsx','src/client/ui/toolbox.tsx','src/client/ui/__test__/uiContainerGate.test.ts','src/client/ui/__test__/hotkeySettingsGate.test.ts'];const bad=[];if(!Array.isArray(b.dirty))bad.push('baseline dirty snapshot missing');const out=cp.spawnSync('git',['status','--porcelain','-uall'],{encoding:'utf8'}).stdout||'';const paths=out.split(/\r?\n/).filter(Boolean).map(l=>l.slice(3).replace(/^\"|\"$/g,'')).filter(p=>p.indexOf('.planning/')!==0);const before=new Set((b.dirty||[]).map(p=>String(p).replace(/\\/g,'/')));for(const p of paths)if(!allowed.includes(p)&&!before.has(p))bad.push('out-of-scope write :: '+p);for(const p of allowed)if(!paths.includes(p))bad.push('expected change missing :: '+p);if(bad.length){console.error('SCOPE GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK scope: no out-of-scope writes ('+paths.length+' dirty paths total, '+before.size+' pre-existing)')"</automated>
|
||||
<automated>node -e "const fs=require('fs');const p='.planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md';const bad=[];if(!fs.existsSync(p))bad.push('summary missing');else{const s=fs.readFileSync(p,'utf8');for(const n of ['ListPage','关闭','14','13','uiContainerGate','hotkeySettingsGate','eslint','check:type','confidence: low','UAT','0 次代码提交','nocache','close','Selection','GROUP_BASIS','keyAdaptationGate'])if(s.indexOf(n)<0)bad.push('summary missing :: '+n)}if(bad.length){console.error('SUMMARY GATE FAILED :: '+bad.join(' ; '));process.exit(1)}console.log('OK summary content')"</automated>
|
||||
<fails_when>任一 vitest 用例失败;eslint 出现 NEW 条目(归一化口径与 scope 必须与 Task 1 相同);`pnpm check:type` 在改动文件集合内出现 NEW 诊断(norm 与 scope 必须与 Task 1 逐字相同、保留 `src/` / `packages-user/` 前缀);8 个受保护文件哈希变化(含 `list.tsx` / `textbox.tsx` / `inputBox.tsx` / `waitBox.tsx`);HEAD 变化且 `baseline..HEAD` 含非 `.planning` 路径;7 个允许路径缺失;整仓脏文件里出现既不在基线 dirty 快照也不在 7 个允许路径中的新路径;`09-32-SUMMARY.md` 缺失或缺少必需声明;或任一 node 闸门非零退出。**不要把 `keyAdaptationGate.test.ts` 的既存失败计入本闸门。** **顺序错误(先写 SUMMARY 再跑门禁、或未运行就记录结果)同样判失败。**</fails_when>
|
||||
</verify>
|
||||
<done>`uiContainerGate.test.ts` 通过:hotkey 组断言 `ListPage` / 1 条 `Scroll` / 0 个自有 container / 无 `Selection` / 分组行已删 / 布尔 `close` / `closeLoc` / `onUpdate` / `onClose` / `nocache` / `list.tsx` 的内置「关闭」;新增「五文件无多行内联 loc」「五文件无内联箭头事件处理器」两组;`bookDetail` 改为 `loc={textLoc}` + `textLoc: ElementLocator`;`book` / `bookDetail` 0 回退、flat-Scroll、Scroll 内无 container / 无 `hidden`、`noevent` 配对、`cumsum`、无 key 属性、4 个 `*_SCROLL_LOC` 断言全部保留;`firstContainerTag` 已删、toolbox 针已改 `getItemClick(item)`、equipbox 导入断言已改合并导入;每个 `it` 上方有一行注释;`hotkeySettingsGate` 的关闭用例已按 A2 改写并仍全绿;eslint 无 NEW(7 路径同 scope);`pnpm check:type` 改动文件内无 NEW(7 路径同 scope);8 个受保护文件哈希与基线一致;HEAD 与基线相同或仅含 `.planning` 聚合提交;7 个代码 / 测试路径全部出现且无新增范围外路径;**先跑完门禁、再写 `09-32-SUMMARY.md`、最后通过摘要闸门**,摘要如实声明未做浏览器 UAT、14 + 13 处清理、`ListPage` 几何换算、`nocache` 透传假设、执行器 0 次代码提交、估算未校准、范围闸门局限。</done>
|
||||
</task>
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| `ListPage` props ↔ 热键分组列表 | 分组 id / 名称通过 `list` 传入 `ListPage`,选中通过 `update` 事件回到 `selectGroup`;一旦 props 拼错或漏 `onUpdate`,分组切换会静默失效 |
|
||||
| `ListPage` 内容区几何 ↔ 右栏坐标 | 右栏 `Scroll` 的 loc 是相对内容区的,而内容区原点由 `basis` 决定;偏移算错会让全部指令行错位 |
|
||||
| `ListPage` 根 container ↔ `nocache` | `nocache` 不在 `ListPage` 声明 props 中,靠属性透传落到根元素;透传失效会让整屏被缓存复用 |
|
||||
| 事件处理器提取 ↔ 行为 | 具名工厂必须闭合原行数据(`view` / `slotEquip` / `slot` / `uid` / `item` / `command` / `index`),闭包捕获错变量会让点击作用到错误的目标 |
|
||||
| 用户可见行为 ↔ 未验证声明 | 本计划不做浏览器 UAT,任何「布局 / 点击 / 拖拽已验证」的表述都属于不实声明 |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|----------|-------------|-----------------|
|
||||
| T-09-32-01 | Tampering | `hotkey.tsx` 的 `ListPage` 接入 | high | mitigate | Task 1 闸门断言 `ListPage` / `basis` / `list` / `selected` / `lineHeight` / `font` / `closeLoc` / `onUpdate` / `onClose` / `default:` / `nocache` 逐项存在,并断言 `<ListPage>` / `<Scroll>` / `<container>` 计数为 1 / 1 / 0 |
|
||||
| T-09-32-02 | Tampering | 右栏几何(`GROUP_BASIS` / `COMMAND_SCROLL_LOC`) | high | mitigate | Task 1 只允许通过常量名引用 loc(禁止多行内联 loc),并在 Task 0 与 SUMMARY 记录 `180 + 32 = 212` 的等价算式;`BOX_LEFT` 取值必须仍为 172 |
|
||||
| T-09-32-03 | Denial of Service | `nocache` 透传失效导致整屏缓存复用 | medium | mitigate | OI-3 规定发现未透传必须停下上报;prohibition 禁止改 `list.tsx` 或擅自加包裹 container;`nocache` 进入 Task 3 的 ListPage 标签断言 |
|
||||
| T-09-32-04 | Tampering | 事件处理器提取时闭包捕获错变量(行数据 / 槽位 / 指令) | high | mitigate | Task 2 断言每个工厂与调用点的字面调用形式(`getSlotDown(slotEquip, i)` / `getOwnedDown(item.uid)` / `getEnemyClick(view)` / `getItemClick(item)` / `getKeyClick(row.command, j)`);`selectKey` / `startPress` / `clickItem` / `openDetail` 的逻辑体零改动 |
|
||||
| T-09-32-05 | Tampering | 借「提 loc 常量」之名改坐标数值或共享可变数组 | high | mitigate | Task 2 的 loc 常量逐个断言来源表达式;prohibition 规定 map 内常量必须每次回调新建;`bookDetail` 的 `textLoc` 断言完整数组字面量 |
|
||||
| T-09-32-06 | Repudiation | 声称「Visual / Selection / nocache 已验证」但未做浏览器 UAT | medium | mitigate | Task 0 汇报与 SUMMARY 均必须写明「未做浏览器 UAT」与「`nocache` 为未验证假设」;执行器不提交代码 |
|
||||
| T-09-32-07 | Elevation of Privilege | 改 `list.tsx` / `textbox.tsx` / `inputBox.tsx` / `waitBox.tsx` 来「让闸门变绿」 | high | mitigate | 这 4 个文件进入受保护哈希闸门(共 8 个);prohibition 逐条禁止;范围闸门拒绝任何新增范围外路径;`hotkeySettingsGate.test.ts` 的改动被限制为 A2 指定的那条用例(改写后必须在 Task 3 的 vitest 里跑绿,且不得反向放宽断言语义) |
|
||||
| T-09-32-SC | Tampering | npm / pnpm 安装 | high | mitigate | 本计划**不新增任何依赖、不执行安装命令**;若执行期出现安装需求,必须停下上报 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- **源码形状**:`src/client/ui/__test__/uiContainerGate.test.ts` 全部断言通过(`?raw`,不导入组件):hotkey 的 `ListPage` 结构 / 单 Scroll / 无自有 container / 无 `Selection` / 分组行已删 / `nocache` / 内置「关闭」;`bookDetail` 的 `SPECIAL_SCROLL_LOC` + `textLoc` + `getUpdateHeight`;「五文件无多行内联 loc」「五文件无内联箭头事件处理器」;并保留 flat-Scroll / Scroll 内无 container / 无 `hidden` / 显示开关分流 / 0 回退 / `noevent` / `cumsum` / 无 key 属性 / `book` 每行 11 `text` + 1 `icon` + 1 `g-rect` / 4 个 `*_SCROLL_LOC` / 无预建 `VNode`。
|
||||
- **静态检查**:`pnpm exec eslint` 对**同一组 7 个改动文件**无 NEW(Task 1 基线与 Task 3 比较的 scope 完全相同);`pnpm check:type` 整仓运行后在**同一组 7 个路径**内无 NEW 诊断(既有基线诊断允许消失)。
|
||||
- **既有闸门**:`src/client/ui/__test__/hotkeySettingsGate.test.ts` 按 A2 改写关闭用例后仍为绿(断言 `ListPage` / 布尔 `close` / `closeLoc={CLOSE_LOC}` / `onClose={close}` / `@common/exit` + `list.tsx` 的 `text="关闭"`,已删除「返回」要求;其余 10 个用例未动);它因此不在受保护哈希集合(受保护 8 个),但必须在 7 个允许路径内出现。`keyAdaptationGate.test.ts` 是既存失败(15 个用例 5 个失败),不在本计划范围;不修、不声称全部既有闸门为绿。
|
||||
- **执行顺序**:全部门禁(vitest / eslint / check:type / 哈希 / HEAD / 范围)先跑完 → 再写 `09-32-SUMMARY.md` → 最后跑摘要闸门;不得在门禁运行前记录结果。
|
||||
- **范围**:`git status --porcelain -uall -- <7 个代码 / 测试路径>` 恰好只有这 7 个路径;整仓 `git status --porcelain -uall` 里没有「不在基线 dirty 快照、也不在这 7 个路径、也不在 `.planning/`」的新路径;**8 个**受保护文件(`scroll.tsx` / `list.tsx` / `textbox.tsx` / `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx`)哈希与基线一致。
|
||||
- **无代码提交**:HEAD 与 `09-32-BASELINE.json` 的 `head` 相同,或 `git diff --name-only <baseline>..HEAD` 只含 `.planning/` 路径;任何非 `.planning` 路径的提交都判失败。
|
||||
- **无浏览器验证声明**:SUMMARY 明确写入「未做浏览器 UAT,未宣称布局 / 滚动 / 点击命中 / 拖拽 / 捕获时序 / `nocache` 已验证」;并写明 `nocache` 走属性透传的假设与「不改 `list.tsx`」的边界。
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `src/client/ui/hotkey.tsx`:根为唯一 `<ListPage>`(`loc={FULL_LOC}` / `basis={GROUP_BASIS}` / `list={groupItems}` / `selected={selectedGroup.value}` / `lineHeight={GROUP_ROW_HEIGHT}` / `font={CONTENT_FONT}` / 布尔 `close` / `closeLoc={CLOSE_LOC}` / `onUpdate={selectGroup}` / `onClose={close}` / `nocache`),`default` 插槽里唯一 `<Scroll loc={COMMAND_SCROLL_LOC}>`;左栏 Scroll、分组 `g-rectr` 行、`GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH`、`selectGroup(id)` 全部消失;`<container>` 计数 0、`<Scroll>` 计数 1、无 `Selection`、无 key 属性、无多行内联 loc、无内联箭头。
|
||||
- [ ] `hotkey` 行为保持:捕获阶段监听 + `EVENT_KEY_CODE_MAP` + `KeyCode.Unknown` + `heldKeys`、`setKeyCombination` 写回、`formatCombination` 的 ` + ` / `未设置`、`selectGroup` 的三步、`@common/exit` 捕获中不关闭、按键框 `g-rectr` 选中边框各 1 处、无入口 / 无持久化 / 无冲突 / 无恢复默认。
|
||||
- [ ] 右栏几何:`GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH`、`CONTENT_WIDTH = MAIN_WIDTH * (1 - GROUP_BASIS)`、`COMMAND_WIDTH = CONTENT_WIDTH - LEFT_PAD * 3`、`COMMAND_SCROLL_LOC = [LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT]`、`BOX_LEFT = COMMAND_WIDTH - KEY_BOX_WIDTH - LEFT_PAD`(= 172)。
|
||||
- [ ] `book` / `bookDetail` / `equipbox` / `toolbox`:11 处内联箭头与 11 处多行 loc 全部提取(`getEnemyClick` / `getUpdateHeight` / `onMove` + 4 个槽位工厂 / `getItemClick`;`SPECIAL_SCROLL_LOC` / `textLoc` / 7 个 equipbox loc / 2 个 `cellLoc`),加上 Task 1 的 hotkey 2 处 handler,五文件合计 13 处(11 + 2)清零;逻辑体、属性、0 回退、`hidden` 分流、拖拽 / 悬停、再点使用、`noevent`、4 个 `*_SCROLL_LOC`、既有注释零改动。
|
||||
- [ ] 五个 UI 文件在多行内联 loc 与内联箭头两条源码闸门下均为 0 命中;无新增 key 属性 / Fragment / 容器 / 条件渲染。
|
||||
- [ ] `uiContainerGate.test.ts` 通过(含 hotkey 组改写、`firstContainerTag` 删除、toolbox 针改 `getItemClick(item)`、equipbox 合并导入断言、新增两组、`bookDetail` 的 `loc={textLoc}` 断言、每个 `it` 上方一行注释);`hotkeySettingsGate` 的关闭用例按 A2 改写后仍全绿;eslint 无 NEW(7 路径同 scope);`pnpm check:type` 改动文件内无 NEW(7 路径同 scope);8 个受保护哈希一致;HEAD 政策 / 范围闸门通过。
|
||||
- [ ] **顺序正确**:先跑完所有门禁 → 再写 `09-32-SUMMARY.md` → 最后通过摘要闸门;摘要含 `ListPage` 接入与几何换算、删除的常量 / 注释清单、14 + 13 的逐文件清单、`nocache` 透传假设、`hotkeySettingsGate` 关闭用例改写(`ListPage` / `close` / 「关闭」,已删「返回」)的口径、真实门禁结果、未 UAT 声明、估算未校准声明、执行器 0 次代码提交声明、范围闸门局限说明。
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md` **after all gates have actually run**, recording: the `ListPage` integration in `hotkey.tsx` (full props, the single `Scroll` inside the object-form `default` slot, and the geometry derivation `GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH` / `CONTENT_WIDTH = MAIN_WIDTH * (1 - GROUP_BASIS)` / `COMMAND_WIDTH = CONTENT_WIDTH - LEFT_PAD * 3` / `COMMAND_SCROLL_LOC = [LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT]` keeping the command column at the same absolute x because `180 + 32 = 212`); the removal inventory (left column `Scroll`, per-group `g-rectr` rows, group name `text`, `GROUP_TEXT_LEFT` / `RIGHT_LEFT` / `RIGHT_WIDTH`, and the two authorized group-row comments) plus the additions (`groupItems`, `getKeyClick`, `nameLoc` / `boxLoc` / `textLoc`); the per-file inventory of the 14 multiline inline locs and 13 inline arrow handlers with their new constant / factory names; the `nocache` attribute-fallthrough assumption onto `ListPage`'s root container and that `list.tsx` was not touched; that `hotkeySettingsGate.test.ts` was deliberately updated per the user's decision (its close case now asserts `ListPage` / the boolean `close` prop / `closeLoc={CLOSE_LOC}` / `onClose={close}` plus `list.tsx`'s `text="关闭"`, and the stale 「返回」 assertion was removed — so it is no longer hash-protected, its other 10 cases are untouched, and its existing jsDoc is kept purely for comment discipline); the visual differences (left group list moved from `[16, 48, 180, 320]` to the `ListPage` list region `[0, 0, 180, 416]` — rows shift left by 16 / up by 48 and the viewport becomes full height so the last row can reach the bottom-left 「关闭」 at y = 400; group selection now the `Selection` pulsing highlight instead of the gold border and the selected group text is no longer gold; the selection rect is `listWidth - 10` = 170 wide; group name x 8→10; close entry moved to the bottom-left 「关闭」; `List` rows wrapped in `container` internally); gate results (both vitest suites, eslint NEW-only and `pnpm check:type` NEW-only over the same seven-path scope, the eight protected hashes, HEAD policy, and the full-repo dirty-snapshot scope gate); the uncalibrated estimate (`confidence: low`, `tasks: 3` excluding the Task 0 checkpoint); skipped browser UAT and the unverified `nocache` assumption; that 09-29 / 09-30 / 09-31 changes are still uncommitted (so the scope gate can only prove no new out-of-scope paths); the pre-existing `keyAdaptationGate.test.ts` failures that are out of scope; zero executor code commits; and that the code working tree is left for user review.
|
||||
</output>
|
||||
274
.planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md
Normal file
274
.planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md
Normal file
@ -0,0 +1,274 @@
|
||||
---
|
||||
phase: 09-minimum-browser-runnable
|
||||
plan: 32
|
||||
subsystem: ui
|
||||
tags:
|
||||
[vue, jsx, listpage, element-locator, handler-extraction, source-gate, no-uat]
|
||||
|
||||
# Dependency graph
|
||||
requires:
|
||||
- phase: 09-minimum-browser-runnable
|
||||
provides: "09-29..09-31 的平铺 Scroll、内联 .map()、0 值回退、cumsum 行位置、37 个 key 全删"
|
||||
provides:
|
||||
- "hotkey 的分组列表改由既有 ListPage 承载(分母 basis + 内置选中态 + 内置「关闭」)"
|
||||
- "5 个列表型 UI 的 14 处多行内联 loc 全部提为 ElementLocator 常量"
|
||||
- "5 个列表型 UI 的 13 处内联箭头事件处理器全部提为具名函数 / 工厂"
|
||||
- "18 组源码形状闸门(原 16 组,hotkey 组改写,新增 2 组)"
|
||||
|
||||
# 未校准估算:09-29..09-31 的改动在执行开始时仍未提交,工作树自带它们的 diff,
|
||||
# 无法把本计划自身的改动从工作树里分离,故 tokens 记 null,不虚报。
|
||||
actuals:
|
||||
tokens: null
|
||||
tasks: 3
|
||||
commits: 0
|
||||
note: "工作树自带 09-29..09-31 的未提交改动,无法分离本计划自身 diff,tokens 记 null;tasks 只计 Task 1-3(Task 0 是汇报关卡,不计入);commits 为 0,执行器未做任何代码提交。"
|
||||
|
||||
estimate:
|
||||
tokens: 70000
|
||||
raw_tokens: 35000
|
||||
tasks: 3
|
||||
confidence: low
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "分组列表 + 选中态 + 关闭按钮一律交给 ListPage;调用方只提供 list / selected / basis / closeLoc / onUpdate / onClose"
|
||||
- "ListPage 内容区原点为 width * basis,内容区内的 loc 必须减去该原点才能保持绝对位置"
|
||||
- "事件处理器一律写成 setup 里的具名函数或具名工厂,JSX 只保留 onXxx={fn} / onXxx={factory(arg)}"
|
||||
- "凡会被 Prettier 折行的 loc 数组一律提为 const xxxLoc: ElementLocator"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- .planning/phases/09-minimum-browser-runnable/09-32-BASELINE.json
|
||||
- .planning/phases/09-minimum-browser-runnable/09-32-SUMMARY.md
|
||||
modified:
|
||||
- src/client/ui/hotkey.tsx
|
||||
- src/client/ui/book.tsx
|
||||
- src/client/ui/bookDetail.tsx
|
||||
- src/client/ui/equipbox.tsx
|
||||
- src/client/ui/toolbox.tsx
|
||||
- src/client/ui/__test__/uiContainerGate.test.ts
|
||||
- src/client/ui/__test__/hotkeySettingsGate.test.ts
|
||||
|
||||
key-decisions:
|
||||
- "hotkey 根元素改为 <ListPage loc={FULL_LOC} basis={GROUP_BASIS} list={groupItems} selected={selectedGroup.value} lineHeight={GROUP_ROW_HEIGHT} font={CONTENT_FONT} close closeLoc={CLOSE_LOC} onUpdate={selectGroup} onClose={close} nocache>,default 插槽只返回一条 <Scroll loc={COMMAND_SCROLL_LOC}>"
|
||||
- "GROUP_BASIS = GROUP_WIDTH / MAIN_WIDTH、CONTENT_WIDTH = MAIN_WIDTH * (1 - GROUP_BASIS)、COMMAND_WIDTH = CONTENT_WIDTH - LEFT_PAD * 3、COMMAND_SCROLL_LOC = [LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT];实测取值为 32 / 48 / 368 / 320,内容区原点 180,180 + 32 = 212 与改造前 RIGHT_LEFT 完全一致"
|
||||
- "BOX_LEFT 改由 COMMAND_WIDTH 定义,实测仍等于 172(368 - 180 - 16)"
|
||||
- "分组选中态交由 ListPage 内部的 Selection 表示,hotkey 源码不再出现 Selection,也不再自建分组行"
|
||||
- "关闭入口改用 ListPage 内置「关闭」文本,close 复用原 close(先 cancelCapture 再 controller.close)"
|
||||
- "hotkeySettingsGate.test.ts 按用户决定改写关闭用例(断言 ListPage / 布尔 close / closeLoc / onClose / @common/exit + list.tsx 的 text=\"关闭\"),删除过时的「返回」要求,因此它退出受保护哈希集合(受保护文件 8 个)"
|
||||
- "五个 UI 文件的所有局部 loc 常量在 map 回调内每次求值新建,不做共享可变数组"
|
||||
|
||||
patterns-established:
|
||||
- "ListPage 接入:left 列表宽度由 basis 反推(GROUP_WIDTH / MAIN_WIDTH),右栏定位相对内容区原点"
|
||||
- "行数据工厂:getEnemyClick(view) / getItemClick(item) / getKeyClick(command, index) / getSlotEnter(slot) / getSlotLeave(slot) / getSlotDown(equip, slot) / getOwnedDown(uid) / getUpdateHeight(code)"
|
||||
- "渲染宽度约束:当内联 JSX 嵌套导致具名调用被 Prettier 折行、进而打断源码闸门的连续字面量断言时,把该段 JSX 提为 setup 级渲染函数以降低缩进(renderCommands / getSpecialItems)"
|
||||
|
||||
requirements-completed: []
|
||||
|
||||
duration: "未精确计时(约 50 分钟,含两次整仓 vue-tsc)"
|
||||
completed: 2026-10-28
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 09 Plan 32: `hotkey` 改用 `ListPage` + 5 个 UI 文件提取 14 处内联 loc 与 13 处内联 handler Summary
|
||||
|
||||
**用项目既有 `ListPage` 重写热键设置界面的分组列表(内置选中态与「关闭」文本),并把 `book` / `bookDetail` / `equipbox` / `toolbox` / `hotkey` 里 14 处多行内联 loc 与 13 处内联箭头事件处理器全部提成 `ElementLocator` 常量与具名函数 / 工厂;行为零变化、不新增入口、不做浏览器 UAT、执行器 0 次代码提交**
|
||||
|
||||
## 执行概览
|
||||
|
||||
| 任务 | 结果 | 关键产物 |
|
||||
|------|------|----------|
|
||||
| Task 1 | 完成(基线先写、代码后改) | `09-32-BASELINE.json`、`hotkey.tsx` 的 `ListPage` 重构 |
|
||||
| Task 2 | 完成 | `book` / `bookDetail` / `equipbox` / `toolbox` 的 handler 与 loc 提取 |
|
||||
| Task 3 | 完成(先跑门禁 → 再写本文件 → 再跑摘要闸门) | `uiContainerGate.test.ts` 18 组、`hotkeySettingsGate.test.ts` 12 组 |
|
||||
|
||||
Task 0 是 `blocking-human` 汇报关卡,用户已回复「可以执行」;本计划在执行前未写任何文件,`09-32-BASELINE.json` 是 Task 1 的第一步、且只写一次。
|
||||
|
||||
## `ListPage` 接入方式
|
||||
|
||||
```tsx
|
||||
<ListPage
|
||||
loc={FULL_LOC}
|
||||
basis={GROUP_BASIS}
|
||||
list={groupItems}
|
||||
selected={selectedGroup.value}
|
||||
lineHeight={GROUP_ROW_HEIGHT}
|
||||
font={CONTENT_FONT}
|
||||
close
|
||||
closeLoc={CLOSE_LOC}
|
||||
onUpdate={selectGroup}
|
||||
onClose={close}
|
||||
nocache
|
||||
>
|
||||
{{ default: () => renderCommands() }}
|
||||
</ListPage>
|
||||
```
|
||||
|
||||
- `close` 是布尔简写(`react/jsx-boolean-value: never` 要求不写 `close={true}`)。
|
||||
- `onUpdate={selectGroup}` 是分组切换的唯一入口,`selectGroup` 仍是 `cancelCapture()` + `selectedGroup.value = id` + `refreshCombinations()` 三步,签名未变。
|
||||
- `onClose` 复用同一个 `close`:`cancelCapture()` + `props.controller.close(props.instance)`。
|
||||
- `default` 插槽只返回一条 `<Scroll loc={COMMAND_SCROLL_LOC}>`(源码内 `<ListPage` 1 个、`<Scroll` 1 个、`<container` 0 个)。
|
||||
- `@common/exit` 仍走 `useKey(props.state.hotkey.system)!`,且 `if (selectedCommand.value) return;` 逐字保留,捕获中不关闭。
|
||||
|
||||
### 几何换算(右栏绝对位置不变)
|
||||
|
||||
| 常量 | 定义 | 实测值 |
|
||||
|------|------|--------|
|
||||
| `GROUP_BASIS` | `GROUP_WIDTH / MAIN_WIDTH` | `180 / 596 ≈ 0.302013422818792` |
|
||||
| `CONTENT_WIDTH` | `MAIN_WIDTH * (1 - GROUP_BASIS)` | `416` |
|
||||
| `COMMAND_WIDTH` | `CONTENT_WIDTH - LEFT_PAD * 3` | `368` |
|
||||
| `COMMAND_SCROLL_LOC` | `[LEFT_PAD * 2, LIST_TOP, COMMAND_WIDTH, LIST_HEIGHT]` | `[32, 48, 368, 320]` |
|
||||
| `CLOSE_LOC` | `[LEFT_PAD, MAIN_HEIGHT - LEFT_PAD, void 0, void 0, 0, 1]` | `[16, 400, …]` 左下角 |
|
||||
| `BOX_LEFT` | `COMMAND_WIDTH - KEY_BOX_WIDTH - LEFT_PAD` | `172`(与改造前逐字相同) |
|
||||
|
||||
`ListPage` 把内容区放在 `[width * basis, 0, width * (1 - basis), height]`,内容区原点为 `596 * 180/596 = 180`;因此右栏 Scroll 的 x 取 `LEFT_PAD * 2 = 32`,绝对位置 `180 + 32 = 212`,与改造前的 `RIGHT_LEFT = 212` 完全一致,宽 368、高 320 也逐字相同。`COMMAND_TEXT_LEFT` / `COMMAND_TEXT_TOP` / `KEY_BOX_HEIGHT` / `COMMAND_GAP` / `BOX_FILL_HEIGHT` 均未改动。
|
||||
|
||||
## 删除清单(Task 0 已授权)
|
||||
|
||||
- 左栏自建 `<Scroll loc={[LEFT_PAD, LIST_TOP, GROUP_WIDTH, LIST_HEIGHT]}>` 及其全部分组 `g-rectr` 行与分组名 `text`。
|
||||
- 文件级常量及其 jsDoc:`GROUP_TEXT_LEFT`、`RIGHT_LEFT`、`RIGHT_WIDTH`。
|
||||
- 两处分组行注释:JSX 注释「每行点击区与分组名,选中态由同一矩形的描边表示」、行注释「选中分组只由这一行的边框表示,元素始终挂载」。
|
||||
- `/** 关闭界面,返回文本是实际的关闭路径 */` 按 AGENTS.md **逐字保留**(文字已略过时),且不再被任何闸门依赖。
|
||||
|
||||
## 新增清单
|
||||
|
||||
- 文件级常量:`GROUP_BASIS` / `CONTENT_WIDTH` / `COMMAND_WIDTH` / `COMMAND_SCROLL_LOC` / `CLOSE_LOC`(均带 jsDoc,前三个为 number,后两个为 `ElementLocator`)。
|
||||
- setup 成员:`groupItems: [string, string][]`(`groups.map(([id, group]) => [id, group.name])`,`group.name` 在全文件恰好 1 次)、`getKeyClick(command, index)`。
|
||||
- 三个局部 loc:`nameLoc`(指令名)、`boxLoc`(按键框)、`textLoc`(组合文本)。
|
||||
- 按键框仍是 `<g-rectr>` + `strokeStyle={isSelected ? …}` + `lineWidth={isSelected ? …}`,两处各恰好 1 次,未使用 `Selection`。
|
||||
|
||||
## 14 处多行内联 loc 的逐文件清单
|
||||
|
||||
| 文件 | 处数 | 新常量 / 落点 |
|
||||
|------|------|----------------|
|
||||
| `book.tsx` | 0 | 无(全部 loc 均为单行短数组,按要求保留) |
|
||||
| `bookDetail.tsx` | 2 | 文件级 `SPECIAL_SCROLL_LOC`(`Scroll`);`getSpecialItems` 内 `textLoc`(`TextContent`,每次调用新建) |
|
||||
| `equipbox.tsx` | 7 | 状态区增减文本 `deltaLoc`;`slotCellLoc` / `slotIconLoc` / `slotNameLoc`;`ownedCellLoc` / `ownedIconLoc`;渲染闭包内 `dragLoc` |
|
||||
| `toolbox.tsx` | 2 | 两处 map 内各一个 `cellLoc`(永久 / 可消耗列) |
|
||||
| `hotkey.tsx` | 3 | `renderCommands` 内 `nameLoc` / `boxLoc` / `textLoc` |
|
||||
| **合计** | **14** | |
|
||||
|
||||
## 13 处内联箭头事件处理器的逐文件清单
|
||||
|
||||
| 文件 | 处数 | 新具名函数 / 工厂 | JSX 调用点 |
|
||||
|------|------|--------------------|-------------|
|
||||
| `book.tsx` | 1 | `getEnemyClick(view)` | `onClick={getEnemyClick(view)}` |
|
||||
| `bookDetail.tsx` | 1 | `getUpdateHeight(code)` | `onUpdateHeight={getUpdateHeight(row.special.code)}` |
|
||||
| `equipbox.tsx` | 7 | `onMove`(根 container)、`getSlotEnter(slot)` ×2、`getSlotLeave(slot)` ×2、`getSlotDown(equip, slot)`、`getOwnedDown(uid)` | `onMove={onMove}` / `onEnter={getSlotEnter(i)}` / `onLeave={getSlotLeave(i)}` / `onDown={getSlotDown(slotEquip, i)}` / `onDown={getOwnedDown(item.uid)}` |
|
||||
| `toolbox.tsx` | 2 | `getItemClick(item)` | 两处 `onClick={getItemClick(item)}` |
|
||||
| `hotkey.tsx` | 2 | `getKeyClick(command, index)`(含分组点击一并由 ListPage 取代) | `onClick={getKeyClick(row.command, j)}` |
|
||||
| **合计** | **13** | | |
|
||||
|
||||
行为口径保持:`book` 13 个同级元素与四项临界 0 回退、`bookDetail` 的 `cumsum(hs)` / `ends[i] - hs[i]` / `autoHeight` + `onUpdateHeight` + `textHeights` + `SPECIAL_MIN_HEIGHT` 回退与「已按 `props.loc` 响应式同步」注释、`equipbox` 的 `hidden` 分流与拖拽 / 悬停 / 再点穿戴、`toolbox` 的 `hidden={!!current}` 与再点使用、`hotkey` 的捕获 / 写回 / 分组切换 / 无入口 / 无持久化 / 无冲突 / 无恢复默认,全部保持。逻辑体(`startPress` / `movePress` / `endPress` / `clickItem` / `openDetail` / `updateTextHeight` / `selectKey`)零改动。
|
||||
|
||||
## `nocache` 走属性透传的假设
|
||||
|
||||
`ListPage` 的 `props` 声明里没有 `nocache`,本次依赖 Vue 的「未声明属性透传到唯一根元素」规则,把它落到 `ListPage` 的根 `<container loc={props.loc}>` 上,从而让整屏仍不缓存。**这是未在浏览器中验证的假设**:范围闸门只能证明 `list.tsx` 未被修改(哈希一致),不能证明透传在运行时生效。若后续发现整屏被缓存复用,应停下上报,不得改 `list.tsx`、也不得擅自加包裹 container(本计划未加,hotkey 源码 `<container` 计数为 0)。
|
||||
|
||||
## `hotkeySettingsGate.test.ts` 的改写口径
|
||||
|
||||
- 关闭用例改名为 `closes through the common exit command and the ListPage close text`,断言 `@common/exit`、`ListPage`、布尔 `close`(`/<ListPage[^>]*\sclose(?=[\s>])/`)、`closeLoc={CLOSE_LOC}`、`onClose={close}`,并否定 `text="返回"` 与 `text="关闭"`。
|
||||
- 新增 `list.tsx?raw` 导入,断言 `text="关闭"`,证明关闭文本确实来自 `ListPage` 而非 hotkey 自建。
|
||||
- **删除**了原来 `expect(hotkeySource).toContain('返回')` 的过时要求——它此前只命中 jsDoc 里的字面量,不代表真实关闭路径。
|
||||
- 其余 10 个用例与 `stripComments` 一字未动;该文件因此**退出受保护哈希集合**(受保护文件降为 8 个),只出现在范围闸门的 7 个允许路径里。
|
||||
- 改写前(Task 1 时)与改写后(Task 3)两次运行均为 12 个用例全绿。
|
||||
|
||||
## 视觉差异登记(只记录、不修复)
|
||||
|
||||
1. 分组选中态由金色 2px 边框变为 `ListPage` 内部 `Selection` 的脉动高亮(默认 `color` / `border` 为 `undefined`)。
|
||||
2. 左栏列表几何从自建 `Scroll loc={[16, 48, 180, 320]}` 变为 `ListPage` 列表区 `loc={[0, 0, 180, 416]}`:行整体左移 16、上移 48,并占满 416 整高(末行可贴到左下角「关闭」所在的 y = 400 附近并被其覆盖)。
|
||||
3. 分组名文本不再变金色(`List` 内置 `text` 用默认 `fillStyle`)。
|
||||
4. 选中矩形宽度为 `listWidth - 10 = 170`,不再与原分组行同宽。
|
||||
5. 分组名文本 x 由 8 变为 `List` 固定的 10。
|
||||
6. 关闭入口由左上角「返回」变为左下角 `CLOSE_LOC` 的「关闭」。
|
||||
7. `List` 的行用 `container` 包裹(`ListPage` / `List` 内部行为,不在 hotkey 源码里)。
|
||||
|
||||
以上均为用户选择 `ListPage` 直接带来的形态变化,本计划不修复。
|
||||
|
||||
## 门禁实际结果(全部在写本文件之前跑完)
|
||||
|
||||
| 门禁 | 结果 |
|
||||
|------|------|
|
||||
| Task 1 基线 schema | `OK task1: baseline` |
|
||||
| Task 1 hotkey 形状 | `OK task1: hotkey on ListPage, handlers and locs extracted` |
|
||||
| `pnpm exec eslint src/client/ui/hotkey.tsx` | exit 0,无输出 |
|
||||
| `vitest hotkeySettingsGate.test.ts`(Task 1 时,仍断言「返回」) | 12 passed |
|
||||
| Task 2 形状(无多行 loc / 无内联箭头 / 无 key) | `OK task2: no multiline loc, no inline arrow handler, no key` |
|
||||
| Task 2 内容(handler 与行为标记) | `OK task2: handlers extracted and behaviour markers retained` |
|
||||
| `pnpm exec eslint`(4 个 UI 文件) | exit 0,无输出 |
|
||||
| `vitest uiContainerGate.test.ts` | **18 passed**(原 16 组 + 新增 2 组) |
|
||||
| `vitest hotkeySettingsGate.test.ts` | 12 passed |
|
||||
| `eslint` NEW-only(7 路径,归一化口径与基线逐字相同) | `baseline 0 -> current 0`,无 NEW |
|
||||
| `pnpm check:type` NEW-only(7 路径 scope) | `baseline 6 -> current 6`,无 NEW 诊断(6 条均为既存 `Type 'number' is not assignable to type 'AllNumbers \| keyof IdToNumber'`) |
|
||||
| 8 个受保护文件哈希 + HEAD 政策 | `OK protected files unchanged; HEAD policy satisfied`(HEAD 仍为 `422f3d8`) |
|
||||
| 全仓脏文件范围闸门 | `OK scope: no out-of-scope writes (25 dirty paths total, 24 pre-existing)` |
|
||||
|
||||
范围闸门的局限:09-29 / 09-30 / 09-31 的改动在执行开始时仍未提交,本计划的 6 个代码 / 测试路径在基线快照里本来就是脏的,因此该闸门只能证明「没有新增范围外路径」,不能证明这些文件的 diff 全部来自本计划。
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### 自动修正 / 必要调整
|
||||
|
||||
**1. [Rule 3 - 阻塞] `COMMAND_SCROLL_LOC` / `CLOSE_LOC` 的声明位置后移到 `LIST_HEIGHT` 之后**
|
||||
- **发现于:** Task 1
|
||||
- **问题:** 计划写「在 `GROUP_WIDTH` 之后新增」这两个 `ElementLocator`,但 `COMMAND_SCROLL_LOC` 引用 `LIST_TOP` / `LIST_HEIGHT`,这两个常量在文件中声明更靠后;模块级 `const` 存在 TDZ,按计划的顺序会在模块初始化时抛 `ReferenceError`。
|
||||
- **处理:** `GROUP_BASIS` / `CONTENT_WIDTH` / `COMMAND_WIDTH` 仍紧跟 `GROUP_WIDTH`,两个 loc 常量移到 `LIST_HEIGHT` 之后;取值与语义完全不变。
|
||||
- **文件:** `src/client/ui/hotkey.tsx`
|
||||
|
||||
**2. [Rule 3 - 阻塞] hotkey 右栏 JSX 提为 setup 级 `renderCommands`**
|
||||
- **发现于:** Task 1
|
||||
- **问题:** 若把 `<Scroll>` 直接内联在 `default: () => (…)` 插槽里,`onClick={getKeyClick(row.command, j)}` 位于 44 列缩进、整行 81 列,Prettier 会把调用参数折行,于是「扁平化后连续字面量」断言(计划自带闸门与 `tagAround` 的针)全部失配。
|
||||
- **处理:** 把右栏渲染提为 setup 里的 `renderCommands`,插槽写成 `{{ default: () => renderCommands() }}`,仍在 `default` 插槽内返回唯一一条 `<Scroll loc={COMMAND_SCROLL_LOC}>`;缩进降到 32 列后 `onClick` 保持单行。行为零变化。
|
||||
- **文件:** `src/client/ui/hotkey.tsx`
|
||||
|
||||
**3. [Rule 3 - 阻塞] bookDetail 每行子项提为 setup 级 `getSpecialItems(row)`**
|
||||
- **发现于:** Task 2
|
||||
- **问题:** `map` 回调改成块体后才能声明 `const textLoc`,此时 `onUpdateHeight={getUpdateHeight(row.special.code)}` 落在 32 列缩进、整行 81 列,Prettier 会折行并打断 Task 2 闸门的连续字面量断言。
|
||||
- **处理:** 每行子项提为 `getSpecialItems(row)`,`textLoc` 仍在每次调用时新建(保持「每次求值新建 loc 数组」);JSX 写 `{specialRows.value.map(row => getSpecialItems(row))}`。
|
||||
- **文件:** `src/client/ui/bookDetail.tsx`
|
||||
|
||||
**4. [Rule 1 - 计划内部矛盾] equipbox 的 `@motajs/render` 导入保留两行**
|
||||
- **发现于:** Task 2
|
||||
- **问题:** 计划要求把 `import { Font } from '@motajs/render';` 与 `import { ElementLocator } from '@motajs/render';` 合并为 `import { ElementLocator, IActionEvent } from '@motajs/render';`,但该文件仍用 `Font.defaults(...)` 生成 `titleFont` / `labelFont`,按字面合并会删掉仍在使用的 `Font`。
|
||||
- **处理:** 保留两行——`import { Font } from '@motajs/render';` 与 `import { ElementLocator, IActionEvent } from '@motajs/render';`。这样 Task 3 断言的精确字符串仍然命中,`Font` 也不会丢失。**需要你确认是否接受这个写法**(若要单行,需要同时放宽该断言)。
|
||||
- **文件:** `src/client/ui/equipbox.tsx`
|
||||
|
||||
**5. [格式] 用 `pnpm exec prettier --write` 统一格式与 CRLF**
|
||||
- **发现于:** 全部任务
|
||||
- **处理:** 编辑后用仓库自带 Prettier 配置(`endOfLine: crlf`、printWidth 80)格式化 5 个 UI 文件与 2 个测试文件;无任何语义改动,eslint 前后基线都是 0 条。
|
||||
|
||||
### 未获预先报备的局部命名(需你确认)
|
||||
|
||||
为了绕开上面第 2 / 3 条的宽度约束,新增了两个**不在 OI-1 命名清单里**的 setup 局部标识符:
|
||||
|
||||
- `renderCommands`(hotkey 右栏渲染函数)
|
||||
- `getSpecialItems`(bookDetail 每行子项工厂)
|
||||
|
||||
两者都是 setup 内的局部 `const`,不导出、不进接口、不进 `GameUI`。若你希望换名(例如 `renderCommandColumn` / `getSpecialRow`),我可以整体替换后再跑一次门禁。
|
||||
|
||||
## 未做浏览器 UAT 声明
|
||||
|
||||
**本计划全程未做浏览器 UAT**,不宣称布局、滚动、点击命中、拖拽手感、捕获时序、`Selection` 脉动高亮或 `nocache` 透传中的任何一项已验证。以上全部由源码形状断言与静态检查间接保证,实际观感需人工确认。
|
||||
|
||||
## 其他声明
|
||||
|
||||
- `estimate` 为**未校准估算**(`confidence: low`),`actuals.tokens` 记 `null`:工作树自带 09-29..09-31 的未提交改动,无法把本计划自身的 diff 分离出来。
|
||||
- **执行器 0 次代码提交**:未运行任何 `git add` / `commit` / `stash` / `checkout` / `restore` / `reset` / `worktree`;代码改动全部留在工作树供审查,`.planning` 产物由编排器按 `commit_docs` 决定。
|
||||
- `keyAdaptationGate.test.ts` 是**既存失败**(09-30 记录 15 个用例 5 个失败,09-31 未修),引用的是 `src/client/misc/hotkey.ts` 与 `ui/inputBox` / `waitBox`,与本计划改的 `ui/hotkey.tsx` 无关;本计划不修、不以其为绿、不声称全部既有闸门为绿。
|
||||
- `book.tsx` 的过期 TODO(写着「留空」,与 0 值回退矛盾)与 `bookDetail` 的「一次遍历累加出每行位置…」等既有注释一律原样保留。
|
||||
- 空分组边界(`List` 固定读 `list[0][0]`,空列表会抛错)属 `ListPage` / `List` 既有行为,本计划只记录、不修复,未改 `list.tsx`。
|
||||
|
||||
## Known Stubs
|
||||
|
||||
无。本计划只做结构重构,未引入任何空数据占位、TODO 或未接线组件。
|
||||
|
||||
## Threat Flags
|
||||
|
||||
无新增。8 个受保护文件(`scroll.tsx` / `list.tsx` / `textbox.tsx` / `settings.tsx` / `ui/index.ts` / `misc/hotkey.ts` / `inputBox.tsx` / `waitBox.tsx`)哈希与基线一致,未新增依赖、未执行安装、未执行构建。
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
- 5 个 UI 文件、2 个测试文件与 2 个 `.planning` 产物全部存在且已按计划改写。
|
||||
- 全部门禁(vitest ×2、eslint NEW-only、`check:type` NEW-only、8 个受保护哈希、HEAD 政策、全仓脏文件范围)均以退出码 0 结束。
|
||||
- 摘要闸门在本文件写完后运行,未通过则补齐后重跑。
|
||||
Loading…
Reference in New Issue
Block a user