# 12: 工单七态时间线 **What to build:** 只读展示工单七态和状态时间线。验收驳回表现为“待验收回退处理中”的动作轨迹,不展示延期状态。 Blocked by: 11 Status: resolved Labels: ready-for-agent - [x] 工单状态完整展示待派单、待接单、已接单、处理中、待验收、已结案、已取消。 - [x] 每张工单有独立时间线。 - [x] 验收驳回显示为回退动作,不是独立终态。 - [x] 不提供任何工单写操作。 - [x] 有工单状态映射单测。 ## 进度:100% **已完成并关闭(2026-09-18)。** 逻辑闸门 **49/49**、真机走查 **21/21**;回归 15 个既有逻辑闸门全绿, 相邻的两个真机走查(t11 设备详情 29/29、t10 点位详情 28/28)也复跑确认共用详情卡没回退。 ### 交付物 - `domain/workOrderTimeline.js`(新):七态**动作轨迹**的纯函数层。 `stepAction()`(推进/回退/取消/同态/认不出的码)、`trackOf()`(按 `operatedAt` 排序、 记 `reachedCode` 走到过的最远处、数回退次数)、`tracksByOrder()`(每张工单一条)、 `pathView()`(七态路径视图)、`pathOnlyTrack()`(只有路径、没有轨迹的兜底)。 - `components/WorkOrderTimeline.vue`(新):单张工单的**七态路径 + 动作流水**两段。 - `domain/equipmentDetailView.js`:新增 `timelineSource()`(按工单编码配轨迹 + 轨迹分区状态); `DEFERRED_SECTIONS` 只剩 `alarmWarningRelations`(`timelines` 已由本票接手)。 - `components/EquipmentDetailSections.vue`:工单面板内每张工单挂它自己那条时间线。 - `mocks/contractMocks.js`:默认轨迹补成**完整七态 + 一次回退**;新增 `work-order-timeline` (三张工单、三种形状:一路推进 / 待验收回退处理中 / 已取消)与 `work-order-timeline-failed` (工单列表成功、只有轨迹分区失败)两个场景;`/work-orders/*/timeline` 改为**按编码回显** (认不出的编码抛错,与真接口 404 语义一致)。 - `verify/t12-work-order-timeline.mjs`(新,49 项)、`verify/t12-work-order-timeline-walkthrough.mjs`(新,21 项)。 ### 四条值得记下的判断 **1. 「回退是动作」是一条判定,所以它落在纯函数里。** 票面原句「验收驳回显示为回退动作, 不是独立终态」在实现上就是「上一条 5 待验收、这一条 4 处理中 ⇒ rollback」。 `TIMELINE_ACTION` 只有六个取值(start/forward/rollback/cancel/hold/unknown), **没有**「已驳回」「延期」这类伪状态;认不出的码(如后端真给了「已驳回」)落 `UNKNOWN` 且状态名留空 —— 不猜、也不接受第八态。七态路径永远**七个节点**,驳回不多出一个。 **2. 当前态与"走到过的最远处"必须分开,否则回退看不见。** 待验收被驳回后当前态是 4, 但"走到过待验收"是事实。只画当前态的话,回退在界面上会消失(看起来像从没到过 5)。 所以 `trackOf()` 同时给 `stateCode`(工单自己的当前态)与 `reachedCode`(走到过最远), 路径视图按两者分别标 `current` 与 `reached`。 **3. 轨迹分区失败 ≠ 没有动作记录,而七态路径不该跟着一起丢。** 两条独立判断,都是"别对值班人员撒谎": - 后端 `loadTimelines()` 会在时间线读不到时给 `SOURCE_SECTION_FAILED`,**工单列表可能照常成功**, 所以「轨迹没取到」(`timelineSource().placeholder`)与「这张工单没有动作记录」 必须说成两句不同的话,模板上是两条不同分支(F9/F10 两条静态守卫钉住); - 但七态路径所需的**当前状态码就在工单列表分区里**,与轨迹成败无关。 所以轨迹没拿到时界面**仍铺七态路径**(`pathOnlyTrack()`),只是没有动作流水 —— 因为"轨迹没取到"而连"这张工单现在在哪一态"也一起不画,那是丢掉本来已经拿到的信息。 **4.(本轮最值得记的一条)静态守卫与逻辑闸门都盖不住「组件里调了没 import 的函数」。** 真机走查第一轮逮到一个真 bug:`EquipmentDetailSections.vue` 在**脚本体**里写了 `return full || pathOnlyTrack(order)`,却忘了 import —— 结果是 `pathOnlyTrack is not defined` 把整块工单面板打没(C 组 4 条判红)。**三处闸门当时全绿**:静态守卫只查"有没有出现被禁的东西"、 逻辑闸门测的是 domain 层(压根不加载 `.vue`)、而模板级引用检查看不见脚本体。 补上 **F11(模板表达式引用完整性)+ F12(脚本体调用点完整性)** 两条守卫, 并且**用变异测试确认有牙**:去掉那行 import 后 F12 恰好报出 `pathOnlyTrack`。 (F11 第一版只扫模板,变异测试证明它对本案完全无效 —— 那次假绿已修; F12 第一版又被注释里的 `O(n²)` 与 JSDoc 里的 `timelineSource()` 判成假红,已改为先剥注释再扫。) **教训:能加载/能编译 ≠ 能渲染;模板与脚本体要分别守。** ### 与票 11 的接缝 票 11 票面写明「`timelines` 归票 12」,本票接手时**没有新增第七块面板**: 七态轨道是**工单状态面板里每张工单自己的**一条线(验收第 2 条「每张工单有独立时间线」), 所以它挂在 `kind: 'orders'` 那一块上。票 11 的 F5 断言与措辞已同步更新 (`DEFERRED_SECTIONS` 现只剩 `alarmWarningRelations`),t05 的静态守卫面已把本票新增的 一个 domain 与一个组件补进去。 ### 验证 逻辑闸门 **49/49**(六段:A 七态字典 / B 动作判定 / C 轨迹组装 / D 七态路径视图 / E 分区独立(走真契约 mock)/ F 静态守卫,含 F11/F12 两条引用完整性守卫)。回归 15 个既有 逻辑闸门全绿,其中 t05 从 178 增至 194(守卫面扩容)、t11 仍 61/61。 node .scratch/cockpit-shell-panel-layout/verify/t12-work-order-timeline.mjs 真机走查 **21/21**(`yarn dev` 已在 8000/8001 运行,Chrome 152 headless): node .scratch/cockpit-shell-panel-layout/verify/t12-work-order-timeline-walkthrough.mjs 四组,内容**从 DOM 读**(证明界面真画出来了,而不是 store 里有值): - **A 每张工单独立时间线(4 项)**:工单面板画出 3 张(推进 5 步 / 回退 6 步 / 取消 3 步), 且每张的 `data-wo-code` 与工单编码一一对应;三张步数各不相同(证明不是同一份轨迹抄三遍); 轨迹分区 success、无占位说明。 - **B 七态路径与回退动作(9 项,本票核心)**:七态节点**七个一个不少**地画出来 (待派单→待接单→已接单→处理中→待验收→已结案→已取消);路径里**没有**「已驳回」这种第八个节点; 回退是一行带可见文字标记的记录(「回退 · 自待验收」,不靠颜色也读得出); 当前态=处理中、待验收标 `reached`(两者可区分);对照那张一路推进的**零回退** (故"恰一次回退"不是恒真);已取消标 `offPath` 且是当前态(不接在已结案后面冒充第八步); 详情里按钮只有「返回点位 / 关闭」(零写操作);无页面错误。 - **C 轨迹失败(5 项)**:说「工单动作轨迹:该分区加载失败(其它分区不受影响)」, **没有**画成「这张工单没有动作记录」(该措辞出现 0 次);七态路径**照常**画(7 节点); 工单列表照常 success(1 张工单仍在);无页面错误。 - **D 回归(3 项)**:整卡可见文本零内部代号、零字段路径;打开详情不重建地图;无页面错误。 (四组共 4+9+5+3 = 21 项。) 另复跑相邻两个真机走查确认共用详情卡未回退:t11 设备详情 **29/29**、t10 点位详情 **28/28**。 ### 验收对照 | 验收项 | 证据 | | --- | --- | | 工单状态完整展示七态 | 逻辑 A1/A3 + D1;走查 B1(DOM 上七个节点) | | 每张工单有独立时间线 | 逻辑 C8/E1/E2;走查 A2/A3(三张各自一条、步数不同) | | 验收驳回是回退动作、不是独立终态 | 逻辑 B2/C2/C3/C4/D1/D2;走查 B2/B3/B4/B5 | | 不提供任何工单写操作 | 逻辑 F6;走查 B8(详情里只有返回/关闭) | | 有工单状态映射单测 | 逻辑闸门 A–D 四段(49 项中的 22 项) | ## Answer **工单七态与状态时间线只读展示完毕:七态路径(七个节点,驳回不多出第八态)与动作轨迹分成两段, `domain/workOrderTimeline.js` 是纯函数判定层 —— 「验收驳回」由状态码 5→4 判成 `rollback` **动作**而不是终态;当前态与"走到过的最远处"分开记,否则回退在界面上会消失; 「轨迹没取到」与「这张工单没有动作记录」是两句不同的话(模板上是两条不同分支), 但七态路径**不跟着轨迹一起丢**(当前状态码来自工单列表分区,走 `pathOnlyTrack()` 照铺)。 不新增第七块面板:轨道挂在工单面板内每张工单下面。逻辑闸门 49/49、真机走查 21/21。** **本票最值得留给后人的一条**:静态守卫与逻辑闸门都盖不住「组件里调了没 import 的函数」—— 真机走查第一轮逮到 `pathOnlyTrack is not defined`(三处闸门当时全绿,因为守卫只查"有没有 出现被禁的东西"、逻辑闸门测的是 domain 层不加载 `.vue`、而模板级引用检查看不见脚本体)。 已补 F11/F12 两条引用完整性守卫,并用变异测试确认有牙。**能加载 ≠ 能渲染;模板与脚本体要分别守。** ## Comments ### 2026-09-18 认领与阶段判定 已认领(`Status: claimed`)。本票无 `needs-triage` 标签、`Blocked by: 11` 且票 11 已 resolved, 故不经过诊断阶段,直接进入实施。 ### 2026-09-18 真机走查与收口 用户同意继续后跑了真机走查。**第一轮 17/21**:C 组 4 条判红,根因是 `EquipmentDetailSections.vue` 调了 `pathOnlyTrack(order)` 却忘了 import (`pathOnlyTrack is not defined`,整块工单面板打没)。补上 import 并新增 F11(模板表达式引用完整性)+ F12(脚本体调用点完整性)两条守卫、变异测试确认有牙后, **第二轮 21/21 通过**,逻辑闸门同时升到 49/49。相邻两个走查复跑无回退 (t11 29/29、t10 28/28)。本票据此 close(100%)。 走查需要 Chrome(puppeteer 以 piped stdio 启动子进程),当前沙箱模式会以 `spawn EPERM` 拒绝;已按一次沙箱升级重试的路径取得授权后跑通。后续他人复跑若遇同一报错, 那是沙箱边界而不是脚本问题。 ## 重开记录(2026-09-29,由票 20 触发)— 需要维护者复核 > **本票被重开复核,不是要重做,而是因为票 20 按后端权威枚举改动了本票的交付面。** > 维护者 2026-09-29 裁定:**以后端 `WorkOrderStatusEnum`(0–7)为唯一真理,删除其他不必要的字典。** > 该裁定落在票 20 里执行,外溢到本票,故按 Q15=(a) 重开记录并提醒维护者。 ### 为什么必须改(后端的权威口径) `CockpitWorkOrderStateDictionary` 的类注释原文: > 对外公开的工单状态字典,与运行时 `WorkOrderStatusEnum`(0–7)对齐。 > 0 待派单、1 待接单、2 已接单、3 处理中、4 待验收、5 已结案、6 延期中、7 已取消; > 终态为 5、7,延期中是处理旁路。 `com.zksy.ops.enums.WorkOrderStatusEnum` 逐项与之一致。而本票当初按**旧的 1–7 七态**实现, 于是**每一态都错位一格**:本票把「已结案」标在码 6 上,而后端的 6 是「延期中」、5 才是「已结案」。 ### 本票验收面因此改了三处 | 本票原验收原句 | 现状 | 改为 | | --- | --- | --- | | 「工单状态完整展示待派单…已取消」(**七态**) | 0–7 是 **8 个码** | 0–7 共 8 个状态,「七态」这个说法作废 | | 「**不展示延期状态**」 | 6 延期中是后端**正式状态**(`ACTIVE_CODES` 含它、`FINISHED_CODES` 只含 5/7) | 延期中**要展示**,且是**处理旁路**(路径外节点,与已取消同类) | | 「验收驳回显示为回退动作,不是独立终态」 | 仍然成立 | **一字未改**(回退仍是动作;码从 4→3) | ADR 0003 里那句「工单只读展示七态…不展示延期」已同步更正(见该 ADR 的 2026-09-29 修订节)。 ### 票 20 实际改了什么(逐条) - `domain/workOrder.js`:`WORK_ORDER_STATES` 改为 0–7(新增 `bypass` 标记), `WORK_ORDER_CLOSED_CODE` **6 → 5**;文件头写明旧口径的坑。 - `domain/workOrderTimeline.js`:推进主线 `1→6` 改为 **`0→5`**; 新增 `WORK_ORDER_BYPASS_CODE = 6` 与 `TIMELINE_ACTION.BYPASS`; `stepAction()` 里进出 6 判为旁路(既不冒充主线前进、也不算回退); `pathView()` 输出 `bypass` 标记。**`ROLLBACK` 的语义与判定一字未改。** - `components/WorkOrderTimeline.vue`:文案「七态路径」→「状态路径」, 新增 `data-wo-path-bypass` 与「(处理旁路)」提示。 - `mocks/contractMocks.js`:时间线样例全部按 0–7 重排(每条记录的码都小了一格); 新增 `WO-TL-DELAYED`(延期中旁路)场景,使旁路分支**可演示**而不只写在注释里。 - `verify/t12-work-order-timeline.mjs`:**49 → 52 项**。A1/A2/A3 改按 0–7; `B6-no-delay-action-exists`(守"没有延期档"这条**已被后端推翻**的假设)改为 `B6-delay-is-bypass-action`;新增 `A2b`(6 是旁路非终态)与 `D3b`(延期中是路径外旁路)。 **本票核心的 B2/C2/C3/C4(回退是动作、走到过最远处)逐字保留、仍然全绿。** ### 需要维护者确认的两点 1. **本票验收第 1 条的文字**要不要按 0–7 重写(本票已按新口径实测通过,但票面原句未改); 2. **「不展示延期」这条原验收**是否确认作废 —— 它现在与后端权威枚举直接冲突。 复核通过后本票可再次 close(当前 `Status: resolved` 保持不变,本段只是如实记录跨票改动)。