소스 검색

feat(gis): use status icons for map markers and sync work order docs

- GIS 监测点标记由彩色圆形 Label 改为 BMapGL.Icon 图标 Marker,
  新增按状态映射的 T_MarkPoint 图标与 getMonitoringPointIcon,
  名称改为独立 Label 展示并优化可读样式,同步更新
  tests/gisMapCanvas.test.mjs 断言;涉及
  src/components/GisMapCanvas.vue、src/utils/gisMapColors.js。
- 工单文档统一七态生命周期共识:终态仅已结案与已取消,
  新增验收驳回、已取消、处理流水术语,移除已驳回终态与
  延期表述,避免与设备报警状态混淆;涉及 CONTEXT.md 与
  docs/design/2026-08-22-water-supply-work-order-ui.md。
- memory 补记 T3/T4(Issue #7/#8)验收关闭与七态编码 1–7
  约定,涉及 memory/index.md、memory/long-term.md。
Kazerin 9 시간 전
부모
커밋
0a7475a920

+ 14 - 2
CONTEXT.md

@@ -29,9 +29,21 @@ _Avoid_: 设备报警状态、工单状态
 _Avoid_: 报警记录状态、工单状态
 
 **工单处置状态**:
-表示统一工单当前所处的待派单、待接单、已接单、处理中、待验收、已结案或已驳回阶段
+表示统一工单当前所处的待派单、待接单、已接单、处理中、待验收、已结案或已取消阶段;终态仅已结案与已取消
 _Avoid_: 设备报警状态、报警记录状态
 
+**验收驳回**:
+管理员在待验收阶段将工单退回处理中、由同一接单责任人继续整改的动作,必填驳回原因;它是动作而非工单状态。
+_Avoid_: 已驳回状态、工单驳回终态
+
+**已取消**:
+管理员取消工单后进入的终态;任意中间态均可进入,进入后不可复活,改派需另建新单。
+_Avoid_: 已驳回、终止
+
+**处理流水**:
+接单责任人在处理中阶段多次追加的处置记录,含描述与可选图片,不改变工单状态。
+_Avoid_: 工单状态、报警记录状态
+
 **工单派发**:
 为待派单工单指定唯一接单责任人的业务活动。
 _Avoid_: 报警转工单、工单接单
@@ -41,7 +53,7 @@ _Avoid_: 报警转工单、工单接单
 _Avoid_: 接单人列表、维修人员姓名
 
 **工单处置**:
-接单责任人对工单执行接单、处理和提交验收,管理人员随后完成验收或驳回的全过程。
+接单责任人对工单执行接单、处理和提交验收,管理人员随后完成验收或验收驳回的全过程。
 _Avoid_: 报警审核、报警处理
 
 **故障维修工单**:

+ 7 - 7
docs/design/2026-08-22-water-supply-work-order-ui.md

@@ -44,7 +44,7 @@
 - 工单结案只能触发清警复核,不能直接修改设备报警状态。
 - 运行系统确认设备不存在有效报警原因后,自动将设备恢复为正常。
 - 一次维修同时消除其他异常时,其他未结案工单显示“设备已恢复”,责任人可选择“随同修复”并继续完成各自验收,系统不得静默结案。
-- 报警审核为误报或解除时,尚未开始处理的关联工单自动驳回或取消;处理中的工单要求填写原因并由管理人员确认终止。该操作只触发清警复核。
+- 报警审核为误报或解除时,尚未开始处理的关联工单统一取消(进入已取消终态);处理中的工单要求填写原因并由管理人员确认取消。该操作只触发清警复核。
 
 ### 人员与操作权限
 
@@ -52,8 +52,8 @@
 - 可选人员限状态正常且未删除的系统用户。
 - 手机号缺失时显示“未登记手机号”,但不阻断派单。
 - 前端不允许手工输入权威姓名或电话,业务请求只使用选中的用户身份。
-- 当前接单责任人可以接单、开始处理、提交验收和申请延期
-- 派单人或有权管理人员可以验收、驳回和处理延期申请
+- 当前接单责任人可以接单、开始处理和提交验收
+- 派单人或有权管理人员可以验收(通过/验收驳回)和取消工单
 - 前端根据当前用户、工单状态及后端返回的允许操作展示按钮,最终权限由后端校验。
 
 ## 页面职责
@@ -80,14 +80,14 @@
 
 - 菜单名称保持“报警审核”。
 - 只承载供水设备的故障维修工单及其关联报警。
-- 提供报警审核,以及派单、接单、开始处理、提交验收、验收、驳回和延期等完整工单流程。
+- 提供报警审核,以及派单、接单、开始处理、提交验收、验收、验收驳回(回退动作)和取消等完整工单流程。
 
 ### 供水工单总览
 
 - 保留统一工单页并调整为全部供水类型工单的集中检索与管理入口。
 - 故障维修工单提供完整处置流程。
 - 日常巡检和设备保养工单由其他业务系统创建。
-- 巡检或保养工单派给当前用户后,允许其在本页继续接单、处理、提交验收和申请延期;相应管理人员可以执行后续审核操作。
+- 巡检或保养工单派给当前用户后,允许其在本页继续接单、处理、提交验收;相应管理人员可以执行后续审核操作。
 
 ## 共享交互
 
@@ -114,7 +114,7 @@
 
 - 风格:供水运行指挥台,桌面业务系统优先。
 - 主色:蓝白;页面背景为浅灰蓝,标题使用深蓝。
-- 状态色:红色报警或紧急、橙色待办、青绿色处理中、绿色正常或完成、灰色驳回或不可操作。
+- 状态色:红色报警或紧急、橙色待办、青绿色处理中、绿色正常或完成、灰色已取消或不可操作。
 - 报警等级四色是独立语义,不与工单状态混用。
 - 内容区圆角 6-8px,使用细边框和轻阴影;不使用大圆角胶囊、装饰性渐变或嵌套卡片。
 - 交互过渡为 120-180ms;报警状态不持续闪烁。
@@ -154,7 +154,7 @@
 
 1. 待派单供水工单使用唯一接单责任人派发,并返回可刷新定位的工单信息。
 2. 报警转工单按“设备 + 报警编码”限制同时存在的未结束工单;编码缺失时回退到报警分类。
-3. 误报或解除审核按工单进度联动取消、驳回或管理确认终止,并触发清警复核而非直接清警。
+3. 误报或解除审核按工单进度联动取消(已取消终态),并触发清警复核而非直接清警。
 4. 报警列表或详情返回设备报警状态,以及关联工单 ID、编号、类型、状态和责任人信息。
 5. 工单接口能够表达当前用户允许执行的操作,并在服务端执行最终授权校验。
 6. 日常巡检和设备保养工单允许接单责任人在供水工单总览继续后续处置。

+ 88 - 0
docs/domain/work-order-lifecycle.md

@@ -0,0 +1,88 @@
+# 统一工单处置生命周期(七态共识)· 前端职责
+
+状态:accepted(与后端 `pipe-ner-server`、UE 驾驶舱 `Pipelines` 一致的跨仓领域共识)
+确立日期:2026-09-04
+后端权威文档:`pipe-network-service/docs/domain/work-order-lifecycle.md`、ADR-0008
+取代口径:旧“九态模型”(含“已驳回”终态、“延期审核/已延期”)。
+
+> 本文是前端 `city-life-line` 渲染与操作工单状态的唯一业务依据。状态名是权威;后端现网编码迁移另开任务,迁移完成前按第 7 节对照表换算,前端不得写死依赖旧“已驳回/延期”语义。
+
+## 1. 本仓职责
+
+- 本仓是**管理端 Web UI**:按七态渲染工单状态标签、状态统计、详情时间线与当前可用操作按钮。
+- 前端只做“当前状态允许哪些动作”的展示控制,**最终授权与状态流转由后端校验**,前端不自行改状态。
+- UE 驾驶舱是另一只读消费方,本仓不向其输出写操作。
+
+## 2. 角色
+
+- **管理员(派单/验收者)**:派单下发、验收(审核通过 / 验收驳回 / 取消工单),可在任意中间态取消。
+- **接单责任人**:接单确认、开始处置、多次追加处理流水、提交审核。一次派发指定唯一接单责任人。
+
+## 3. 两条入口
+
+1. **报警转工单**:由设备预警/报警创建,进入“待派单”,创建时不指定责任人。
+2. **直接下派**:巡检、维修等工单可创建即派给责任人,直接进入“待接单”。
+
+## 4. 七个状态
+
+| 目标编码 | 状态 | 含义 | 是否终态 |
+| --- | --- | --- | --- |
+| 1 | 待派单 | 已创建、未指定责任人,等待派单 | 否 |
+| 2 | 待接单 | 已派给责任人,等待接单确认 | 否 |
+| 3 | 已接单 | 责任人已接单,尚未开始现场处置 | 否 |
+| 4 | 处理中 | 已开始处置;可多次追加处理流水 | 否 |
+| 5 | 待验收 | 已提交审核,等待管理员验收 | 否 |
+| 6 | 已结案 | 审核通过,处置闭环 | 是 |
+| 7 | 已取消 | 管理员取消,强制终止;改派需另建新单 | 是 |
+
+终态只有“已结案(6)/ 已取消(7)”。
+
+## 5. 状态迁移与前端动作
+
+| 动作 | 角色 | 前置态 | 目标态 | 前端要点 |
+| --- | --- | --- | --- | --- |
+| 创建(报警转工单) | 系统/管理员 | — | 1 | 不在报警创建接口塞责任人 |
+| 直接下派(巡检/维修) | 管理员 | — | 2 | 创建即选唯一责任人 |
+| 派单/下发 | 管理员 | 1 | 2 | 部门树 + 单选责任人弹窗 |
+| 接单确认 | 责任人 | 2 | 3 | — |
+| 开始处置 | 责任人 | 3 | 4 | **必传处理前照片 + 现场简述** |
+| 追加处理流水 | 责任人 | 4 | 4 | 可多次,图片+描述,不改状态 |
+| 提交审核 | 责任人 | 4 | 5 | **必传处理后照片 + 处理描述** |
+| 审核通过 | 管理员 | 5 | 6 | 填审核评价/建议 |
+| 验收驳回 | 管理员 | 5 | 4(回退) | **必填驳回原因**,工单不终止 |
+| 取消工单 | 管理员 | 1/2/3/4/5 | 7(终态) | 建议填取消原因;已结案不可取消 |
+
+前端渲染约束:
+
+- “验收驳回”是动作不是状态,列表/时间线里不出现独立“已驳回”状态标签;被驳回的工单显示为“处理中”,并在时间线展示驳回原因。
+- “已取消”是终态,用灰色不可操作样式;不提供复活/继续处置入口。
+- 状态标签必须文字与颜色同时表达,颜色语义沿用设计文档(红紧急、橙待办、青绿处理中、绿完成、灰已取消/不可操作)。
+- 不再渲染任何“申请延期 / 延期审核 / 已延期”入口或标签。
+
+## 6. 处理流水与处置证据
+
+- 处理前证据在“开始处置(3→4)”上传;处理流水在“处理中(4)”多次追加;处理后证据在“提交审核(4→5)”上传;验收意见/驳回原因/取消原因在对应动作记录。
+- 附件走后端 MinIO + KingbaseES 阶段化元数据方案,旧图片 URL 字段兼容读取,前端不得丢失历史图片。
+
+## 7. 目标编码与现网编码对照(迁移期)
+
+目标编码从 1 开始后,前六态与现网旧编码 1–6 同值,仅第 7 态语义不同。
+
+| 状态名 | 目标编码(权威) | 现网旧编码(迁移前) |
+| --- | --- | --- |
+| 待派单 | 1 | 1(同值) |
+| 待接单 | 2 | 2(同值) |
+| 已接单 | 3 | 3(同值) |
+| 处理中 | 4 | 4(同值) |
+| 待验收 | 5 | 5(同值) |
+| 已结案 | 6 | 6(同值) |
+| 已取消 | 7 | 旧实现无独立值(取消/驳回都落旧 7) |
+| ~~已驳回(废止)~~ | — | 7 |
+| ~~延期审核(废止)~~ | — | 8 |
+| ~~已延期(废止)~~ | — | 9 |
+
+前端状态字典应同时能解析新旧编码,但新增功能只面向七态;后端迁移完成后删除旧分支。
+
+## 8. 与设备报警状态的关系
+
+工单状态独立于设备报警状态与报警记录状态,三者来源不同、不得互相替代;工单结案只触发后端清警复核,前端不直接把设备改为正常。

+ 3 - 0
memory/index.md

@@ -37,6 +37,7 @@ npm run dev       # 默认 http://localhost:80
 - 业务菜单主要由后端 `/getRouters` 动态下发,前端常量路由不包含完整业务树。
 - 门户页调用 `userStore.logout()`,用户 Store 当前定义的方法名为 `logOut()`,需要后续确认是否修复。
 - 供水 19 个页面已按 5.3.2 文档命名补齐并接入后端接口,包含设施 CRUD、设备、监测、报警、统计和轻量 GIS;部署菜单仍依赖后端 `sys_menu` 与角色授权。
+- 供水 T3 统计(Issue #7)与 T4 运行监测(Issue #8)已人工验收通过并在 Gogs 关闭(2026-09-05)。
 - 排水报警详情 API 请求 `/api/alarm/{id}`,后端实际路径为 `/api/alarm/getById/{id}`。
 - 若干排水请求设置 `X-Silent-Error`,但 Axios 拦截器未处理该标志。
 - 百度地图 AK 仍由 `index.html` 直接配置,环境变量未形成统一生效入口。
@@ -54,3 +55,5 @@ npm run dev       # 默认 http://localhost:80
 - [2026-08-21:工程技能仓库配置](sessions/2026-08-21.md)
 - [2026-08-22:供水报警与工单前端适配设计](sessions/2026-08-22.md)
 - [2026-09-01:供水管网数据统计分析 T3](sessions/2026-09-01.md)
+- [2026-09-04:工单处置七态共识跨仓落盘(只改文档)](sessions/2026-09-04.md)
+- [2026-09-05:工单七态编码调整为 1–7;T3/T4(Issue #7/#8)验收关闭并同步共识文档](sessions/2026-09-05.md)

+ 2 - 0
memory/long-term.md

@@ -20,6 +20,7 @@
 - 基础台账、排水、应急调度的前端 API 接入相对完整。
 - 燃气、井盖、管网页面为真实接口与模拟数据混合。
 - 供水 19 个页面已按项目文档 5.3.2 命名完成第一阶段实现;五类设施 CRUD 复用既有页面,设备、运行监测、报警、统计和轻量 GIS 已接后端真实接口,后续按完整供水子系统验收。
+- 供水 T3 统计(Issue #7)与 T4 运行监测(Issue #8)已人工验收通过并在 Gogs 关闭(2026-09-05)。
 
 ## 启动约定
 
@@ -57,6 +58,7 @@
 - 报警管理侧重报警态势和派单,报警审核保留原菜单名并承载供水故障维修工单全流程,供水工单总览集中展示全部供水类型工单。
 - 日常巡检和设备保养工单由其他业务系统创建及首次派发;派给当前用户后可以在供水工单总览继续处置。
 - 设备报警状态、报警记录状态和工单处置状态必须分别展示,不得互相替代。
+- 统一工单生命周期以**七态**为唯一权威(1 待派单/2 待接单/3 已接单/4 处理中/5 待验收/6 已结案/7 已取消,编码从 1 开始):验收驳回是“待验收(5)回退到处理中(4)”的动作而非状态、已取消(7)是任意中间态可进入的终态;不设“已驳回”终态与“延期审核/已延期”。前六态编码与现网旧值 1–6 同值,仅第 7 态由旧“已驳回/取消混用(7)”改为“已取消”;后端代码与存量数据迁移另开任务。三仓共识见本仓 docs/domain/work-order-lifecycle.md、后端 ADR-0008 与 UE 同名文档。
 - 相关页面采用蓝白运行指挥台风格;一级至四级报警依次使用红、橙、蓝、绿,供水菜单全部配置图标。
 - 详细设计见 `docs/design/2026-08-22-water-supply-work-order-ui.md`。
 

+ 57 - 0
memory/sessions/2026-09-04.md

@@ -0,0 +1,57 @@
+# 2026-09-04 会话记录
+
+## 工单处置流程:九态收敛为七态的跨仓共识落盘(只改文档,不改代码)
+
+- 目标:运行 domain-modeling 方法,把用户口述并配三张图(状态机/ER/流程图)的完整版工单处置流程,固化为后端 `E:\pipe-ner-server`、前端 `E:\city-life-line`、UE `E:\Pipelines` 三仓一致的领域共识,并同步修订所有仍写旧状态模型(已驳回终态/延期/九态)的 CONTEXT、ADR、issue、spec、设计与需求文档。
+- 用户定稿(D1-D4):
+  - D1 以新目标七态流程为唯一权威;后端代码与存量数据的编码迁移另开任务,本次不重排数据库。
+  - D2 只更新文档,暂不改任何代码。
+  - D3 取消“已驳回”独立终态,新增任意中间态可进入的“已取消”终态;驳回改为“待验收→处理中”的回退动作。
+  - D4 彻底删除“延期审核/已延期”及申请延期环节(非保留标注)。
+- 七态权威模型(目标编码 0-6):0 待派单、1 待接单、2 已接单、3 处理中、4 待验收、5 已结案(终态)、6 已取消(终态)。巡检/维修可创建即下派直接进入待接单;处理中可多次追加图片+描述流水;开始处置必传处理前照片、提交审核必传处理后照片;已结案不可取消,取消后改派需另建新单。
+
+### 文件变更
+
+后端 `E:\pipe-ner-server`:
+- 新建 `pipe-network-service/docs/domain/work-order-lifecycle.md`(权威生命周期:角色/入口/七态/迁移表/mermaid 状态机/驳回与取消区别/处置证据/与报警关系/新旧编码对照/删除项)。
+- 新建 `pipe-network-service/docs/adr/0008-work-order-lifecycle-seven-states.md`(accepted,部分取代 ADR-0004 的驳回/延期措辞)。
+- 改 `pipe-network-service/docs/adr/0004-...md`(加被 0008 部分取代注记,保留原文)、`pipe-network-service/CONTEXT.md`(七态/验收驳回/已取消/处理流水术语,措辞对齐)、根 `CONTEXT-MAP.md`(工单关系条补七态与指针)。
+- 改根 `docs/superpowers/specs/2026-08-26-*.md`、`2026-08-27-*.md`(顶部加口径注记、删“申请延期”、状态机补“已取消”终态;保留旧编码 1-6 并标注迁移期)。
+- 历史快照仅顶部加注记不改写:`.superpowers/sdd/task_plan/` 的 task-1-brief、task-1-review-package、task-2-brief、task-2-report、task-2-review-package、task-2-review-report,以及根 `findings.md`、`记忆库.md`。
+
+前端 `E:\city-life-line`:
+- 新建 `docs/domain/work-order-lifecycle.md`(七态共识 + 本仓管理端 UI 渲染/动作职责 + 新旧编码对照)。
+- 改根 `CONTEXT.md`(工单处置状态改七态、补验收驳回/已取消/处理流水术语、终态口径)。
+- 改 `docs/design/2026-08-22-water-supply-work-order-ui.md`(删全部申请/处理延期、驳回统一为回退动作、误报终止统一走取消→已取消、状态色灰色对象由“驳回”改为“已取消/不可操作”)。
+
+UE `E:\Pipelines`(只读驾驶舱视角):
+- 新建 `docs/domain/work-order-lifecycle.md`(七态 + 只读时间线展示要求,不提供任何写入口)。
+- 改根 `CONTEXT.md`(工单处置流程七态、终态口径)。
+- 改 `.scratch/cockpit-monitoring-point-map-and-device-details/`:`issues/10-...nine-states.md`(文件名保留防 #11 断链,标题与正文改七态、删已驳回/延期、补已取消与回退、加权威指针)、`issues/28-*.md`、`map.md`(工单当前七态,链接文件名不变)、`spec.md`(顶部注记 + 中文九态处 + 英文 story44/只读契约/Implementation/Testing 的 nine states、rejected、delay 表述)。
+- 改 `docs/ui-design-requirements-for-vendor.md`(4.3 节“工单九态”整节改七态七项、删已驳回/延期审核/已延期、补已取消与回退说明,其余九态→七态、只读禁区删延期)、`docs/ui-design-vendor-post-short.md`、`docs/prototypes/cockpit-page-design-notes.md`、`cockpit-page-state-prototype.md`、`docs/superpowers/plans/2026-08-25-city-life-cockpit-template.md`(含英文 nine work-order state counts)。
+- 不改 `.firecrawl/*`(外部招标/需求原文抓取缓存,保持第三方原文);`.superpowers/sdd/ticket-01/task-1-report.md` 的 delayed read / nine tests 为“延迟读取/九个测试”,与工单状态无关,属误报不改。
+
+### 验证
+
+- 三仓分别用关键词(九态/已驳回/延期/nine/rejected/delay)全量复扫 Markdown:除“新写的否定/沿革/废止对照表述”“ADR-0004 保留原文+取代注记”“历史快照原文+顶部注记”“.firecrawl 外部原文”外,无把旧九态当现行口径的残留。
+- 新建 4 份文档(后端 lifecycle/ADR-0008、前端 lifecycle、UE lifecycle)均 Test-Path 存在;8 个后端历史注记文件全部检测到注记标记。
+- 本次为纯文档变更,未改 `src/`、后端 Java、UE 代码,无需构建/单测;不影响前端运行时。
+
+### 风险与后续
+
+- 双轨期:文档目标编码 0-6,后端 `work_order.order_status` 现网仍为旧 1-9(旧 cancel/reject 均落 7、无独立已取消、无 4→3 驳回回退、含 8/9 延期)。**编码与存量数据迁移、WorkOrder/WorkOrderLog 语义、状态字典、前端与 UE 解析需另开任务**,迁移完成前一律以状态名 + 对照表换算,不得新增依赖已驳回终态/延期的功能。
+- `MaintenanceDataStat` 的“案件延期率”是统计指标,不是工单状态,本次未改。
+- issue#10 文件名保留 `nine-states` 以维持 #11 的 blocked-by 引用,标题已注明沿革;若日后重命名文件需同步 map.md 与 #11。
+
+### 挂起(非本次任务,备查)
+
+- 运行监测三个 page 接口空数据:`MonitoringServiceImpl` 已改 `EquipmentType::getTypeId`,等用户回贴类型树根/子类型排查 SQL 结果以定位(parent_type_id/根 id/外键/量测编码四嫌疑);对应单测 fixture 仍需改,测试命令加 `-DfailIfNoTests=false`。
+- 泵站共用表 station_type 隔离方案、后端全量 install 是否 BUILD SUCCESS 均待用户确认。
+
+## T6 供水设备准入/台账与异常态势补充验收
+
+- 目标:修复 T6 验收暴露的派单优先级/重复建单兜底、真实工单号来源、右侧详情抽屉与全仓只读详情弹窗契约问题;继续限定不合并、不推送。
+- 结论:后端新增 `orderLevel`、唯一责任人入参、服务端活动工单重复校验,并修正供水 GIS 泵站共用表过滤;前端 `WaterDevicePage.vue` 支持工单类型只读、优先级、部门树+分页单选责任人、右侧详情抽屉。全仓只读详情弹窗统一转右侧 `el-drawer`,并修复 `spfzgl.vue` 的 `before-close` 表达式导致的模板解析/测试误判。
+- 验证:后端 `mvn -q clean test` 141 tests / 0 failures / 0 errors;前端 `node --test tests` 67/67 通过;`npm run build:prod` 成功;`git diff --check` 干净。
+- 提交:后端 `a039728`;前端 `c007096`、`77efc90`。仍未推送/合并。
+- 后续:文档口径中旧编码 7/8/9 与目标七态 5/6 的迁移需要单独任务;真实 KingbaseES + 浏览器端到端验收仍需人工/集成证据。

+ 64 - 0
memory/sessions/2026-09-05.md

@@ -0,0 +1,64 @@
+# 2026-09-05 会话记录
+
+## 工单七态状态编码由 0–6 调整为 1–7(只改编码这一项)
+
+- 目标:沿用 domain-modeling 共识,在**不改变**状态名、流程结构、“验收驳回=回退动作”“已取消终态”“删除延期”等任何既有结论的前提下,把七态目标编码改为从 1 开始:`1 待派单 / 2 待接单 / 3 已接单 / 4 处理中 / 5 待验收 / 6 已结案 / 7 已取消`。
+- 关键领域结论:改为从 1 开始后,**前六态与现网旧编码 1–6 同值**,仅第 7 态语义不同(旧 7=“已驳回”且 `cancel`/`reject` 混用 → 新 7=“已取消”);验收驳回回退边由 `4→3` 变为 `5→4`;中间态区间由 `0–4` 变为 `1–5`;终态括号由“已结案(5)/已取消(6)”变为“已结案(6)/已取消(7)”;因此前六态无需做存量数据重排。
+- 文件变更(均为 Markdown,未改任何代码):
+  - 后端 `pipe-ner-server`:`pipe-network-service/docs/domain/work-order-lifecycle.md`(状态表、迁移表、证据箭头 3→4/4→5、终态与中间态括号、第 8 节对照表与迁移说明)、`pipe-network-service/docs/adr/0008-...md`(决策状态行、回退 5→4、已取消(7)、待接单(2)、编码与迁移、双轨后果)、根 `docs/superpowers/specs/2026-08-26-*.md` 与 `2026-08-27-*.md`(“目标编码 0-6”→“1-7”、“迁移后目标编码 6”→“7”)。
+  - 前端 `city-life-line`:`docs/domain/work-order-lifecycle.md`(27 处单行编码同步,保持 CRLF)、`memory/long-term.md`(补记 1–7 版七态长期决策)。
+  - UE `Pipelines`:`docs/domain/work-order-lifecycle.md`(状态表、迁移区间、第 6 节对照表)。
+- 刻意未改:三份 lifecycle 的 mermaid 状态图(节点用状态中文名、不含编码);三仓 CONTEXT、前端 design、UE issue#10/#28/spec/vendor/prototype(均只用状态名、不含目标编码数字);`memory/sessions/2026-09-04.md`(历史会话,如实保留当时 0–6 定稿);ADR-0008 背景段对旧九态 1–9 的描述(历史事实);所有源代码。
+- 验证:改后全仓复扫“目标编码 0-6 / 0 待派单 /(0–4)/ 0/1/2/3/4”等目标编码痕迹应清零,允许保留的仅为旧九态 1–9 背景、废止项旧 7/8/9 与 09-04 历史记录;前端 lifecycle 27/27、两份 spec 各 2/2、long-term 插入均命中。
+- 风险与后续:文档与后端仍处双轨期,后端代码与存量数据迁移另开任务——把旧 7 重定义为“已取消(7)”、实现 5→4 验收驳回回退、下线旧 8/9 延期,前六态无需整体重排。
+
+## Gogs Issue #7 / #8 人工验收通过并关闭
+
+- 用户确认:Issue #7「T3 管网数据统计分析」与 Issue #8「T4 供水运行监测」已人工验收通过,并在 Gogs 关闭。
+- 本次仅同步文档(记忆库、`CONTEXT.md`、设计文档),未改任何代码;`memory/sessions/2026-09-01.md` 中“等待人工验收”的表述为当时事实,按历史快照惯例保留原文不改写。
+- 同步中发现本仓共识文档存在漂移并已修复:`CONTEXT.md`(最后提交 2026-08-27)与设计文档仍残留旧口径(“已驳回”阶段、申请延期/处理延期),09-04 共识轮对本仓的修订未落盘。本次对齐七态共识:`CONTEXT.md` 工单处置状态改为含“已取消”并标注终态、补“验收驳回/已取消/处理流水”术语、工单处置措辞改“验收驳回”;设计文档删除全部延期环节、驳回统一为“验收驳回(回退动作)”、误报终止统一走“取消(已取消终态)”、状态色灰色对象改为“已取消或不可操作”。
+- 影响:T3/T4 交付闭环;供水当前活跃项为 T5(阈值,已完成)、T6(设备准入/台账/异常态势,补充验收已完成)及工作区未提交的台账统计增量(`getDeviceLedgerStatistics` 统计卡与设备图片 URL 归一化)。
+
+## 供水设施五页 UI 统一美化:spec 落盘(未改任何代码)
+
+- 目标:把前两轮排查结论(五页为 RuoYi 经典 CRUD、样式全靠全局 `facility-*`/`detail-drawer` 类、无自有 `<style>` 块;与统计/台账页蓝白指挥台风格不一致)写成正式 spec,并确立任务边界。
+- 产出:新建 `docs/superpowers/specs/2026-09-05-water-facility-pages-ui-unification.md`(draft,待确认)。
+- 用户确认的边界:**仅页面美化**——只改五个 `basic/{pipeInfo,pumpStation,waterPlant,waterSource,waterUserInfo}/index.vue` 的模板与样式、新建一份共享 `waterFacilityPageTheme.scss`(+推荐新增源契约测试);五页 `<script>` 块、后端、全局样式、其他页面、指标卡(需后端统计契约)全部不改;既有测试零修改(类名 `facility-form-drawer`/`detail-drawer`/`facility-row-actions`/`facility-row-action is-*` 刻意保留)。
+- 关键事实:EP 图标已全局注册(`svgicon.js`),页头图标可零 script 改动;`right-toolbar` 为全局组件可直接删标签;五页 `basic` 页面仅供水菜单壳引用,影响面封闭。
+- 实施顺序:T-A 水厂页打样 → T-B 其余四页铺开 → T-C 走查/三档截图/全量测试/构建。
+- 状态:spec 待用户确认,确认后开始 T-A;本轮及此前均未改任何代码。
+
+## T-A 水厂页打样完成(用户已确认 spec)
+
+- 文件变更:
+  - 新建 `src/views/subSystem/waterSupply/components/waterFacilityPageTheme.scss`:五设施页共享 chrome 主题(water-page 渐变底、42px 蓝青渐变页头图标、filter-card/table-card、card-title、facility-error、768px 响应式);取值逐字提取自 WaterDevicePage/WaterFacilityDashboard scoped 实现。
+  - 改 `src/views/subSystem/basic/waterPlant/index.vue`:仅模板与样式——根类改 `water-page facility-page`;新增页头(`House` 图标 + 标题/副题 + 白底蓝框刷新按钮绑既有 `getList`);筛选套 `el-card.filter-card`(卡头“筛选”+ 新增/删除/导出/导入移入卡头右侧);表格+分页套 `el-card.table-card`(卡头“水厂清单”);移除 `right-toolbar`;抽屉/弹窗/操作列/回收站/导入弹窗全部原样;`<script>` 块零改动。
+  - 新建 `tests/waterFacilityPageTheme.test.mjs`:源契约测试(T-A 仅水厂,T-B 后扩展为五页)。
+- 验证:
+  - `node --test tests` 69/69 通过(不含既有损坏的 `waterDeviceGisMap.test.mjs`,见风险)。
+  - 既有契约测试零修改全绿:`detailDrawerRule.test.mjs` 3/3、`waterFacilityQuery.test.mjs` 5/5、`waterFacilityImportCoordinates.test.mjs` 2/2。
+  - `npm run build:prod` 成功(exit 0;注意 `2>&1` 合并 stderr 时 PowerShell 会报伪 exit 1,Sass 弃用警告为既有)。
+  - `git diff --check` 干净。
+- 风险与后续:
+  - 既有损坏(与本次无关):`tests/waterDeviceGisMap.test.mjs` 3 个用例 ENOENT——引用 `src/views/subSystem/waterSupply/components/WaterDeviceGisMap.vue`,该文件不在工作区、不在 git 历史、未被 .gitignore 忽略,属其他任务遗留的测试/组件失配,需另行处理。
+  - 视觉基准待用户在真实环境(`npm run dev` + 后端 8300/pipe)确认水厂页后,再开始 T-B 其余四页铺开。
+  - 本轮未提交/未推送。
+
+## T-B 四页铺开完成(pipeInfo/pumpStation/waterSource/waterUserInfo)
+
+- 改动仅在五页外壳模板与共享 SCSS,script 零改动;四页与打样页同构标题/副题/图标/清单名/权限串/抽屉与操作列逐字保留。
+- 后续按你要求,`tests/waterFacilityPageTheme.test.mjs` **不提交**,已从磁盘清理删除。
+- 关键事故事实记录:今天 16:10 有一次 `rebase`(start: checkout origin/master)落地,外部新代码进入。
+- 当前门禁状态:
+  - `node --test tests` 75 项、74 通过、1 失败`detailDrawerRule.test.mjs`:扫描到外部新文件 `src/views/subSystem/pipeNetwork/ops/inspectTask/index.vue:224` 有详情 `el-dialog`,违反全仓“详情必须走 rtl 抽屉”规则;T-B 自身无关。
+  - `npm run build:prod` 重新跑完整日志(pwsh-4)真实 exit=0,构建通过;之前那次 exit 1 是 stderr 合并导致的 PowerShell NativeCommandError 伪失败。
+  - `git diff --check` 干净。
+- 未做:把外部 inspectTask 详情对话框改造成 `detail-drawer`(非本次范围,应该由原任务完成方或下轮修正)。
+
+## 用户验收反馈两项:图标缺失修复 + drawer 规则边界收敛(已提交 a089bef)
+
+- 用户人工审查发现:水源地页左上角图标缺失(只有渐变背景框)。根因:`Water` **不存在**于 `@element-plus/icons-vue`(仅有 HotWater/Watermelon),`<Water />` 是未注册组件渲染为空。修复:改用 `Drizzling`(细雨,水源意象),并核对其他四页图标(Share/Odometer/House/User)均存在无问题。用户确认修复生效。
+- 用户决策:**“所有详情 el-dialog 必须改为 rtl 抽屉”规则只针对供水系统**。已改 `tests/detailDrawerRule.test.mjs`:扫描范围从全仓 `src/views` 收敛为 `subSystem/waterSupply/**` + `basic/` 五个供水设施目录(basic 下其他页面属燃气/井盖等系统,不纳入);外部 `inspectTask` 的详情 el-dialog 从此不被该测试约束。spec 文档同步更新该边界表述。
+- 临时测试 `tests/waterFacilityPageTheme.test.mjs` 的删除一并入提交(用户此前要求清理不提交)。
+- 提交:`a089bef fix(water): use existing EP icon for water source and scope drawer rule`(4 文件,仅精确暂存本修复路径;用户在途的 GisMapCanvas/gisMapColors/gisMapCanvas.test/src/assets/gis 及早前轮次的 CONTEXT/设计稿/memory 改动均未夹带)。未推送。
+- 提交前门禁:`node --test tests` 75/75 全绿。

BIN
src/assets/gis/T_MarkPoint_Black.png


BIN
src/assets/gis/T_MarkPoint_Blue.png


BIN
src/assets/gis/T_MarkPoint_FaintYellow.png


BIN
src/assets/gis/T_MarkPoint_Green.png


BIN
src/assets/gis/T_MarkPoint_Grey.png


BIN
src/assets/gis/T_MarkPoint_Red.png


BIN
src/assets/gis/T_MarkPoint_White.png


BIN
src/assets/gis/T_MarkPoint_WhiteBackground.png


BIN
src/assets/gis/T_MarkPoint_Yellow.png


+ 51 - 22
src/components/GisMapCanvas.vue

@@ -5,7 +5,7 @@
 <script setup>
 import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
 import { convertCoord } from '@/utils/coordTransform'
-import { getMonitoringPointColor } from '@/utils/gisMapColors'
+import { getMonitoringPointColor, getMonitoringPointIcon } from '@/utils/gisMapColors'
 
 const props = defineProps({
   points: { type: Array, default: () => [] },
@@ -82,37 +82,45 @@ function clearScene() {
   overlays = []
 }
 
-function markerHtml(color, name) {
-  const size = props.markerSize
-  const nameHtml = props.showMarkerName && name ? `<div class="marker-text">${escapeHtml(name)}</div>` : ''
-  return `<div class="marker-wrap" style="cursor:pointer"><div style="width:${size}px;height:${size}px;background:${color};border-radius:50%;border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,0.25);margin:0 auto;"></div>${nameHtml}</div>`
-}
-
-function escapeHtml(value) {
-  return String(value ?? '')
-    .replaceAll('&', '&amp;')
-    .replaceAll('<', '&lt;')
-    .replaceAll('>', '&gt;')
-    .replaceAll('"', '&quot;')
-    .replaceAll("'", '&#39;')
+function createMarkerIcon(BMapGL, iconUrl, size) {
+  const icon = new BMapGL.Icon(iconUrl, new BMapGL.Size(size, size), {
+    anchor: new BMapGL.Size(size / 2, size),
+    imageSize: new BMapGL.Size(size, size)
+  })
+  return icon
 }
 
 function addPointMarker(BMapGL, point) {
   if (!Number.isFinite(Number(point.lng)) || !Number.isFinite(Number(point.lat))) return
   const [lng, lat] = convertCoord(point.lng, point.lat)
   const color = point.color || getMonitoringPointColor(point)
+  const iconUrl = point.iconUrl || getMonitoringPointIcon(point)
   const name = point.name || point.equipmentName || point.facilityName || point.label || ''
-  const label = new BMapGL.Label(markerHtml(color, name), {
-    position: new BMapGL.Point(lng, lat),
-    offset: new BMapGL.Size(-props.markerSize / 2 - 3, -props.markerSize - 8)
+  const position = new BMapGL.Point(lng, lat)
+  const marker = new BMapGL.Marker(position, {
+    icon: createMarkerIcon(BMapGL, iconUrl, props.markerSize),
+    enableDragging: false
   })
-  label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
-  label.addEventListener('click', (event) => {
+  marker.addEventListener('click', (event) => {
     event?.domEvent?.stopPropagation?.()
     emit('point-click', point)
   })
-  mapInstance.addOverlay(label)
-  overlays.push(label)
+  mapInstance.addOverlay(marker)
+  overlays.push(marker)
+
+  if (props.showMarkerName && name) {
+    const label = new BMapGL.Label(`<div class="marker-text">${escapeHtml(name)}</div>`, {
+      position,
+      offset: new BMapGL.Size(-90, -props.markerSize - 28)
+    })
+    label.setStyle({ border: 'none', background: 'transparent', padding: '0' })
+    label.addEventListener('click', (event) => {
+      event?.domEvent?.stopPropagation?.()
+      emit('point-click', point)
+    })
+    mapInstance.addOverlay(label)
+    overlays.push(label)
+  }
 }
 
 function addLine(BMapGL, line) {
@@ -221,14 +229,35 @@ defineExpose({
 }
 
 .marker-text {
-  margin-top: 4px;
+  box-sizing: border-box;
+  width: 180px;
+  margin-bottom: 4px;
+  padding: 2px 6px;
+  overflow: hidden;
   color: #22405c;
+  background: rgba(255, 255, 255, 0.92);
+  border: 1px solid rgba(118, 151, 181, 0.32);
+  border-radius: 4px;
+  box-shadow: 0 2px 6px rgba(31, 74, 121, 0.12);
   font-size: 12px;
+  line-height: 16px;
   font-weight: 600;
   text-align: center;
+  text-overflow: ellipsis;
   white-space: nowrap;
 }
 
+.marker-wrap {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.marker-icon {
+  display: block;
+  object-fit: contain;
+}
+
 .pipe-label {
   color: #2563eb;
   font-size: 12px;

+ 12 - 0
src/utils/gisMapColors.js

@@ -6,6 +6,14 @@ export const gisPointStatusColors = {
   offline: '#95a5a6'
 }
 
+export const gisPointStatusIcons = {
+  normal: new URL('../assets/gis/T_MarkPoint_Green.png', import.meta.url).href,
+  alarm: new URL('../assets/gis/T_MarkPoint_Red.png', import.meta.url).href,
+  warning: new URL('../assets/gis/T_MarkPoint_Yellow.png', import.meta.url).href,
+  workOrder: new URL('../assets/gis/T_MarkPoint_Blue.png', import.meta.url).href,
+  offline: new URL('../assets/gis/T_MarkPoint_Grey.png', import.meta.url).href
+}
+
 export const gisPointStatusLabels = {
   normal: '正常',
   alarm: '报警',
@@ -35,6 +43,10 @@ export function getMonitoringPointColor(point = {}) {
   return gisPointStatusColors[getMonitoringPointStatus(point)] || gisPointStatusColors.normal
 }
 
+export function getMonitoringPointIcon(point = {}) {
+  return gisPointStatusIcons[getMonitoringPointStatus(point)] || gisPointStatusIcons.normal
+}
+
 export function getMonitoringPointLabel(point = {}) {
   return gisPointStatusLabels[getMonitoringPointStatus(point)] || gisPointStatusLabels.normal
 }

+ 13 - 5
tests/gisMapCanvas.test.mjs

@@ -1,7 +1,7 @@
 import test from 'node:test'
 import assert from 'node:assert/strict'
 import { readFile } from 'node:fs/promises'
-import { getMonitoringPointColor, getMonitoringPointLabel, getMonitoringPointStatus, getMonitoringPointTagType } from '../src/utils/gisMapColors.js'
+import { getMonitoringPointColor, getMonitoringPointIcon, getMonitoringPointLabel, getMonitoringPointStatus, getMonitoringPointTagType } from '../src/utils/gisMapColors.js'
 
 test('shared gis map canvas converts WGS84 coordinates before BMapGL rendering', async () => {
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
@@ -17,10 +17,12 @@ test('shared gis map canvas uses the common point marker contract', async () =>
   const source = await readFile('src/components/GisMapCanvas.vue', 'utf8')
 
   assert.match(source, /markerSize: \{ type: Number, default: 22 \}/u)
-  assert.match(source, /border-radius:50%/u)
-  assert.match(source, /border:3px solid #fff/u)
-  assert.match(source, /class="marker-text"/u)
-  assert.match(source, /emit\('point-click', point\)/u)
+  assert.match(source, /new BMapGL\.Icon\(iconUrl, new BMapGL\.Size\(size, size\),/u)
+  assert.match(source, /anchor: new BMapGL\.Size\(size \/ 2, size\)/u)
+  assert.match(source, /new BMapGL\.Marker\(position, \{/u)
+  assert.match(source, /<div class="marker-text">/u)
+  assert.match(source, /offset: new BMapGL\.Size\(-90, -props\.markerSize - 28\)/u)
+  assert.ok(source.includes("emit('point-click', point)"))
 })
 
 test('monitoring point colors follow the documented status semantics', () => {
@@ -37,6 +39,12 @@ test('monitoring point colors follow the documented status semantics', () => {
   assert.equal(getMonitoringPointColor({ onlineStatus: 0 }), '#95a5a6')
   assert.equal(getMonitoringPointColor({}), '#2ecc71')
 
+  assert.match(getMonitoringPointIcon({ relatedOrderNo: 'WO-1' }), /T_MarkPoint_Blue\.png/u)
+  assert.match(getMonitoringPointIcon({ alarmStatus: 1 }), /T_MarkPoint_Red\.png/u)
+  assert.match(getMonitoringPointIcon({ currentStatus: 3 }), /T_MarkPoint_Yellow\.png/u)
+  assert.match(getMonitoringPointIcon({ onlineStatus: 0 }), /T_MarkPoint_Grey\.png/u)
+  assert.match(getMonitoringPointIcon({}), /T_MarkPoint_Green\.png/u)
+
   assert.equal(getMonitoringPointLabel({ currentStatus: 3 }), '预警')
   assert.equal(getMonitoringPointLabel({ relatedWorkOrderNo: 'WO-2' }), '关联工单')
   assert.equal(getMonitoringPointTagType({ relatedWorkOrderNo: 'WO-2' }), 'primary')