浏览代码

优化工单,预警流程

zengy 6 天之前
父节点
当前提交
36b3b5d0d9
共有 31 个文件被更改,包括 321 次插入 和 223 次删除
  1. 0 138
      .feisuan/rules/project_rule.md
  2. 1 1
      CONTEXT.md
  3. 2 2
      docs/domain/work-order-lifecycle.md
  4. 1 0
      memory/index.md
  5. 8 0
      memory/long-term.md
  6. 7 0
      memory/sessions/2026-10-13.md
  7. 142 0
      memory/sessions/2026-10-14.md
  8. 1 1
      src/components/ImageUpload/index.vue
  9. 6 0
      src/router/alarmMenuView.js
  10. 2 2
      src/router/waterHomeRouteResolver.js
  11. 2 1
      src/store/modules/permission.js
  12. 8 0
      src/utils/alarmLabels.js
  13. 3 2
      src/utils/waterAlarmManagementModel.js
  14. 8 7
      src/utils/waterHomeModel.js
  15. 3 2
      src/views/subSystem/alarm/AlarmDetail.vue
  16. 6 3
      src/views/subSystem/alarm/AlarmHistory.vue
  17. 14 13
      src/views/subSystem/alarm/common/audit.vue
  18. 1 1
      src/views/subSystem/alarm/common/js/alarmHistoryModel.js
  19. 3 2
      src/views/subSystem/alarm/common/list.vue
  20. 2 1
      src/views/subSystem/alarm/common/remind.vue
  21. 2 1
      src/views/subSystem/drainage/alarm/analysis.vue
  22. 1 1
      src/views/subSystem/pipeNetwork/ops/inspectTask/index.vue
  23. 42 9
      src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue
  24. 3 4
      src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js
  25. 2 2
      src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue
  26. 1 1
      src/views/subSystem/waterSupply/components/WaterAlarmPage.vue
  27. 12 2
      src/views/system/dept/index.vue
  28. 19 18
      src/views/system/user/index.vue
  29. 4 4
      tests/waterAlarmAuditWorkbench.test.mjs
  30. 4 4
      tests/waterHomeModel.test.mjs
  31. 11 1
      tests/waterSupplyWorkOrderStatusDict.test.mjs

+ 0 - 138
.feisuan/rules/project_rule.md

@@ -1,138 +0,0 @@
-
-# 城市生命线项目开发规范指南
-
-为保证代码质量、可维护性、安全性与可扩展性,结合项目实际技术栈与目录结构,请在开发过程中严格遵循以下规范。
-
-## 一、项目基本信息
-
-- **项目名称**:city-life-line
-- **工作目录**:`E:\work\city-life-line`
-- **代码作者**:Administrator
-- **操作系统**:Windows 10
-- **构建工具**:Maven (后端), Vite (前端)
-- **SDK版本**:JDK 11.0.27
-
-## 二、目录结构
-
-项目采用前后端分离架构,以下为当前项目目录结构:
-
-```text
-city-life-line
-├── html
-├── public
-├── src
-│   ├── api                  # 接口定义层
-│   │   ├── monitor          # 监控相关接口
-│   │   ├── pipeNetwork      # 管网相关接口
-│   │   ├── smxyjldczxt      # 业务模块接口
-│   │   ├── system           # 系统管理接口
-│   │   ├── tool             # 工具接口
-│   │   ├── warning          # 预警接口
-│   │   └── work             # 工单接口
-│   ├── assets               # 静态资源
-│   ├── components           # 公共组件
-│   ├── directive            # 自定义指令
-│   ├── layout               # 布局组件
-│   ├── plugins              # 插件
-│   ├── router               # 路由配置
-│   ├── store                # 状态管理
-│   ├── utils                # 工具类
-│   └── views                # 页面视图
-│       ├── error            # 错误页面
-│       ├── monitor          # 系统监控
-│       ├── portal           # 门户
-│       ├── subSystem        # 子系统业务模块
-│       ├── system           # 系统管理
-│       └── tool             # 系统工具
-└── vite                     # Vite 配置
-```
-
-## 三、技术栈要求
-
-根据项目环境与目录特征,确定技术栈如下:
-
-- **前端框架**:Vue 3 + Vite
-- **UI 组件**:Element Plus (推测,基于 RuoYi-Vue 结构)
-- **状态管理**:Vuex / Pinia (位于 `store` 目录)
-- **后端环境**:JDK 11 + Maven
-- **后端框架**:Spring Boot (推荐版本 2.5+ 以适配 JDK 11)
-
-## 四、分层架构规范
-
-### 前端分层规范
-
-| 目录        | 职责说明                         | 开发约束与注意事项                                               |
-|-------------|----------------------------------|----------------------------------------------------------------|
-| **views**   | 页面结构与展示逻辑               | 仅负责页面渲染,业务逻辑应抽离至 API 或 Utils 中               |
-| **api**     | 接口请求定义                     | 按业务模块划分文件(如 `system/user.js`);统一使用封装后的请求工具 |
-| **components** | 可复用 UI 组件                | 遵循单一职责原则;组件命名采用 PascalCase (如 `UserInfo.vue`)    |
-| **store**   | 全局状态管理                     | 模块化管理状态;避免存储临时性或非共享数据                     |
-| **utils**   | 通用工具函数                     | 纯函数开发,避免副作用;添加清晰的 JSDoc 注释                  |
-
-### 后端分层规范
-
-| 层级        | 职责说明                         | 开发约束与注意事项                                               |
-|-------------|----------------------------------|----------------------------------------------------------------|
-| **Controller** | 处理 HTTP 请求,响应前端调用    | 不包含业务逻辑,负责参数校验与结果封装                         |
-| **Service**    | 核心业务逻辑处理                | 复杂业务需拆分方法;事务控制在此层处理                         |
-| **Repository/Mapper** | 数据库交互                | 禁止在代码中拼接 SQL;优先使用 ORM 框架特性                    |
-| **Entity/DO**  | 数据库实体映射                  | 禁止直接暴露给前端,需转换为 DTO/VO                             |
-
-## 五、代码风格规范
-
-### 命名规范
-
-| 类型       | 命名方式             | 示例                  | 适用范围 |
-|------------|----------------------|-----------------------|----------|
-| 文件/组件  | PascalCase           | `UserProfile.vue`     | 前端     |
-| 变量/函数  | lowerCamelCase       | `getUserList()`       | 全栈     |
-| 常量       | UPPER_SNAKE_CASE     | `MAX_RETRY_COUNT`     | 全栈     |
-| CSS 类名   | kebab-case           | `user-container`      | 前端     |
-| 包名       | 全小写               | `com.city.lifeline`   | 后端     |
-
-### 注释规范
-
-- **语言要求**:注释内容必须使用**中文**(用户第一语言)。
-- **类/方法注释**:必须添加 Javadoc 或 JSDoc 注释,说明功能、参数及返回值。
-  ```java
-  /**
-   * 获取用户信息列表
-   * @param query 查询条件
-   * @return 用户列表
-   */
-  public List<UserVO> selectUserList(UserQuery query);
-  ```
-  ```javascript
-  /**
-   * 获取用户列表
-   * @param {Object} params - 查询参数
-   * @returns {Promise} - 异步结果
-   */
-  export function listUser(params) {
-    return request({ url: '/system/user/list', method: 'get', params })
-  }
-  ```
-
-## 六、安全与性能规范
-
-### 前端安全
-
-- **路由守卫**:在 `router` 中配置权限校验,防止未授权访问。
-- **XSS 防护**:禁止使用 `v-html` 渲染用户输入内容,确需使用必须先进行转义。
-- **敏感信息**:禁止在前端代码中硬编码密钥、Token 等敏感信息。
-
-### 后端安全
-
-- **输入校验**:使用 JSR-303 注解进行参数校验 (`javax.validation.constraints.*`)。
-- **SQL 注入**:严禁拼接 SQL 字符串,必须使用预编译语句或 ORM 框架。
-- **权限控制**:关键接口必须添加权限注解(如 `@PreAuthorize`)。
-
-## 七、编码原则总结
-
-| 原则       | 说明                                       |
-|------------|--------------------------------------------|
-| **组件化** | 前端页面拆分为独立组件,提高复用性与可维护性 |
-| **模块化** | API、Store、Utils 按功能模块划分文件       |
-| **RESTful**| 后端接口遵循 RESTful 设计风格              |
-| **DRY**    | 避免重复代码,公共逻辑封装为 Utils 或 Service |
-| **KISS**   | 保持代码简洁,避免过度设计                 |

+ 1 - 1
CONTEXT.md

@@ -55,7 +55,7 @@ _Avoid_: 设备报警状态、报警记录状态、已驳回状态、延期审
 _Avoid_: 已驳回状态、工单驳回终态
 
 **已取消**:
-管理员取消工单后进入的终态;任意中间态均可进入,进入后不可复活,改派需另建新单。
+管理员仅可在待派单或待接单时取消工单,取消后进入终态;责任人接单后不可取消,取消后不可复活,改派需另建新单。
 _Avoid_: 已驳回、终止
 
 **处理流水**:

+ 2 - 2
docs/domain/work-order-lifecycle.md

@@ -1,7 +1,7 @@
 # 统一工单处置生命周期 · 管理端口径
 
 状态:accepted
-更新日期:2026-09-10
+更新日期:2026-10-14
 后端权威文档:`pipe-network-service/docs/domain/work-order-lifecycle.md`
 
 > 本文是前端 `city-life-line` 渲染与操作工单状态的唯一业务依据。状态名是业务权威;调用方只能按所属接口契约解释状态编码,不得跨接口复用裸数字。2026-09-10 起,管理端业务工单采用数据库与前端一致的 `0–7` 契约。
@@ -45,7 +45,7 @@
 | 提交验收 | 责任人 | 处理中 / 延期中 | 待验收 | 必传处理后证据与描述 |
 | 验收通过 | 管理员 | 待验收 | 已结案 | 填验收结果 |
 | 验收驳回 | 管理员 | 待验收 | 处理中 | 必填驳回原因,交同一责任人继续整改 |
-| 取消工单 | 管理员 | 待派单 / 待接单 / 已接单 / 处理中 / 待验收 | 已取消 | 建议填取消原因 |
+| 取消工单 | 管理员 | 待派单 / 待接单 | 已取消 | 必填取消原因 |
 | 申请延期 | 责任人 | 处理中 | 待延期审核 | 写入延期申请状态,不改主状态 |
 | 延期审核 | 管理员 | 待延期审核 | 处理中 / 延期中 | 通过进入延期中,驳回回到处理中 |
 

+ 1 - 0
memory/index.md

@@ -66,6 +66,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
+- [2026-10-14:故障通知筛选与两端核实命名、工单取消限制、本人登录日志、照片按组切换、性别单选、移动端接口分层及统一前缀](sessions/2026-10-14.md)
 - [2026-10-13:移动端工单管理、待办、详情与操作接入](sessions/2026-10-13.md)
 - [2026-10-12:工单派单增加核验人](sessions/2026-10-12.md)
 - [2026-10-10:报警内容固化到 alarm_data](sessions/2026-10-10.md)

+ 8 - 0
memory/long-term.md

@@ -40,10 +40,18 @@
 
 ## 用户已确认决策
 
+- 移动端专用接口统一放到后端 `zksy-admin` 的 `com.zksy.web.controller.mobile`,与 PC 共用现有业务 Service,数据权限也留在 Service 校验;不复制移动端业务流程。七类 Controller 的 URL 统一为 `/api/mobile/...`:工单 `/ops/workOrder`、巡检 `/ops/inspectTask`、设备 `/equipment-base`、故障通知 `/alarm`、公告 `/notice`、本人登录日志 `/monitor/logininfor/list`、通讯录 `/contacts`(均接在统一前缀后)。移动端调用同步更新,不保留旧移动端专用双路由;此决定取代同日初版保留旧 URL 的约定。登录、资料、通用上传及工单处置等两端共用接口仍保留原入口;报警不再单设 `MobileAlarmService`(2026-10-14)。
+- 移动端“我的-基本资料”性别仅提供男(0)、女(1)两个 radio 单选项,不再提供保密或下拉选择。历史保密(2)或未设置的资料显示为未选中,保存前需明确选择男或女,避免静默修改性别(2026-10-14)。
+- 移动端“日常-故障通知”按当前用户部门自身或祖先 101/102/103 对应设备类型的直接 parent_id=1/2/3 过滤,不包含共享类型 4,也不扩展类型树;管理员同样按公司范围读取,客户端 rootTypeId 只能收窄。已转工单通过 work_order.device_id 关联设备,未转工单按报警设备编码关联;关联损坏不可回退放宽。列表与 PC 审核页一致只查未处理报警,右上角展示待核实/确认报警/误报警;顶部按全部/待核实/确认报警/误报警分 tab,报警等级下拉放在搜索输入框前,使用工单同款公共搜索栏。详情用“故障信息/处置全流程”左右页签,待核实且未处理并有 alarm:review:audit 权限时提供底部“核实”抽屉。移动端状态0统一显示“待核实”,操作与表单使用“核实”文案,PC 同步使用“待核实/核实”,接口枚举和权限保持原约定。核实结果必选、意见选填最多500字、图片最多5张(JPG/JPEG/PNG、每张10MB),复用 PC 附件票据和审核事务。历史沿用 alarm_log,不复制工单日志(2026-10-14)。
+- PC 与移动端报警业务统一展示“待核实/核实”:PC 共享列表、详情、提醒、研判、供水首页及台账导出同步;菜单旧标题“报警审核”由前端按报警组件映射为“报警核实”,兼容后端旧状态名,不修改数据库菜单、接口枚举、路由或权限。工单验收与延期仍使用审核文案(2026-10-14)。
+- 移动端照片预览按同一业务组连续切换,维修前、维修后、每条工单日志附件和巡检现场照片分别成组;工单详情无可执行操作时不展示底部提示栏(2026-10-14)。
+- 移动端“我的-登录日志”仅查询当前登录用户本人,管理员也不例外;服务端从登录态获取账号,按 `sys_logininfor.user_name` 精确匹配,不复用 PC 日志列表的账号模糊查询(2026-10-14)。
+- 工单仅在待派单(0)、待接单(1)时允许取消;移动端、PC 端展示和后端校验统一限制,保留既有取消权限与数据范围(2026-10-14)。
 - 移动端真实接口列表统一采用“首次进入加载 + 用户下拉刷新”,适合分页的业务列表再增加触底上拉分页;不因页面再次显示自动请求,不使用定时轮询。通讯录是明确的全量加载例外,不分页(2026-10-13)。
 - 移动端通讯录不限制部门范围,一次性返回全部状态正常、未删除的用户,并排除当前登录用户和超级管理员;移动端基于全量结果进行本地搜索(2026-10-13,取代部门范围及服务端分页方案)。
 - 移动端工单权限不依赖 PC 的 `ops:workOrder:query`:我的工单按创建人、责任人、核验人以及有派单权的待派单池取数,并叠加用户部门自身/祖先 101、102、103 对应设备父类型 1/2/3(共同包含 4)的公司范围;`device_id` 为空时创建人可见,派单后责任人和核验人按明确指派可见,其他派单人不可见。待办进一步按当前角色与状态收窄。流程权限统一为 `ops:workOrder:process`(接单/处置/提交验收/申请延期)和 `ops:workOrder:audit`(验收/延期审核),派单仍为 `ops:workOrder:dispatch`;派单候选责任人/核验人必须分别具有 process/audit 权限(2026-10-13)。
 - `work_order.dept_id` 已废弃,工单归属和可见性不得再依赖该字段;PC `/api/ops/workOrder` 列表及状态统计对普通用户统一按 `create_by=当前用户 OR receive_user=当前用户 OR verify_user=当前用户` 过滤,超级管理员不受限制(2026-10-13)。
+- 工单处置仅允许“已接单(2)”和“延期中(6)”状态执行并进入“处理中(3)”;已经处于“处理中”的工单不得重复执行工单处置,应继续申请延期或提交验收(2026-10-13)。
 
 - 报警清单顶部展示蓝色“报警总数”、红色“监测异常”、绿色“监测已恢复”三张卡片;仅增加统计区,保留原查询、表格和分页。统计按当前查询范围的全部事件计数,监测恢复不等同于工单处置完成(2026-09-24)。
 

+ 7 - 0
memory/sessions/2026-10-13.md

@@ -21,6 +21,13 @@
 - 后端全量测试:205 项中 1 个既有 `AlarmLifecycleServiceTest` 日志文案断言失败,与工单改动无关;本次初次运行暴露的工单测试均已适配并通过专项测试。
 - 移动端 `npm run type-check`、`npm run build:h5`、`npm run build:mp-weixin`:通过。
 
+## 处理中工单禁止重复处置
+
+- 工单处置状态范围统一为“已接单、延期中”;工单已进入“处理中”后,详情页不再展示“工单处置”。
+- “处理中”工单仍可按既有规则申请延期或提交验收。
+- 后端 `canProcess` 同步排除“处理中”,即使绕过前端直接调用处置接口也会被拒绝;PC 端原有按钮规则已符合,仅同步注释说明。
+- `WorkOrderStatusEnumTest` 与 `WorkOrderServiceImplTest` 共 44 项通过,后端编译、移动端类型检查、H5 和微信小程序构建通过。
+
 ## 部署与联调注意
 
 - 需同步部署管理后端和移动端;权限合并 SQL 沿用后端已有 `migration_work_order_perms_merge.sql`。

+ 142 - 0
memory/sessions/2026-10-14.md

@@ -0,0 +1,142 @@
+# 2026-10-14:工单取消限制同步
+
+## 目标与结论
+
+- 用户要求移动端与 PC 端仅允许待派单(0)、待接单(1)工单取消;已接单及后续状态不可取消。
+- 移动端详情按钮、PC 公共 `canOperate.cancel` 和后端 `WorkOrderStatusEnum.canCancel` 已统一收窄;既有取消权限与数据范围不变。
+- 后端取消继续使用原状态作为更新条件,更新失败时提示刷新并阻止写入取消成功日志。
+
+## 文件变更
+
+- 移动端:`src/pages/message/workorder-detail.vue`。
+- PC:`src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js`、`tests/waterSupplyWorkOrderStatusDict.test.mjs`。
+- 后端:`WorkOrderStatusEnum.java`、`WorkOrderServiceImpl.java`、`WorkOrderService.java`、`WorkOrderController.java`、`WorkOrderServiceImplTest.java`。
+- 同步两仓生命周期文档、PC `CONTEXT.md` 和长期记忆中的取消规则。新要求取代历史任意中间态可取消口径。
+
+## 验证
+
+- 后端工单专项测试 54 项通过,包含待派单/待接单可取消、其他状态拒绝、并发更新失败不写成功日志;相关模块编译通过。
+- 补齐既有测试中公司范围支持类重构后的模拟返回值,恢复两项验收测试。
+- PC 工单字典专项测试 3 项通过,`npm run build:prod` 通过。
+- 移动端 `npm run type-check`、`npm run build:h5` 通过。
+
+## 风险与后续
+
+- 需同步部署 PC、移动端和管理后端,后端重启后接口限制生效;无需数据库迁移。
+- 未使用真实登录工单执行取消操作。
+
+## 移动端“我的-登录日志”接口接入
+
+- 目标:参考 PC `/monitor/logininfor/list` 接入移动端登录日志,仅查询当前登录用户的记录。
+- 新增 `GET /monitor/logininfor/mobile/list`,支持 `pageNum/pageSize`,只要求已登录,不依赖 PC 日志管理权限;账号从服务端登录态获取,管理员也仅返回本人日志,不接受客户端指定查询账号。
+- 登录日志表按 `user_name` 记录账号,移动端使用必选的精确匹配,避免 PC 模糊查询误带相似账号;按登录时间、日志 ID 倒序展示,PC 原接口保持原行为。
+- 移动端移除示例数据,展示真实 IP、描述、登录时间;支持首次进入加载、下拉刷新、触底分页、空状态及失败重试,不使用页面再次显示自动请求或轮询。失败重试保留原刷新/分页操作。
+- 文件:移动端 `src/services/login-logs.ts`、`src/pages/profile/login-log.vue`、`src/pages.json`;后端 `SysLogininforController`、`ISysLogininforService`、`SysLogininforServiceImpl`、`SysLogininforMapper` 及 XML,新增服务和 Mapper 测试。
+- 验证:登录日志专项测试 5 项通过(当前账号、管理员隔离、无登录/空账号拒绝、精确 SQL 绑定与稳定倒序),管理后端及依赖模块编译通过;移动端类型检查和 H5 生产构建通过。
+- 风险与后续:需部署移动端并更新、重启管理后端,无数据库迁移或菜单授权变更;未使用真实登录态和数据库联调日志列表。
+
+## 移动端照片按组切换与工单底部提示移除
+
+- 目标:工单详情、处理日志、巡检详情等照片预览支持同一组连续查看;移除工单详情“当前没有可执行操作”。
+- 新增公共 `PhotoPreview.vue`:H5 全屏展示上一张/下一张、当前张数、滑动切换,支持缩放和键盘左右键/Esc;非 H5 继续使用原生整组预览。点击时从选中的照片打开,首尾禁用对应切换按钮,单张不展示切换区。
+- 接入工单维修前/维修后、每条处理日志附件、巡检现场照片;各组独立,不跨组切换。公共 `UploadPanel.vue` 已选图片同样支持预览,删除按钮仍独立工作。
+- 工单详情移除无操作提示栏,仅有操作按钮时保留底部操作区域及对应留白。
+- 文件:移动端 `src/components/PhotoPreview.vue`、`src/components/UploadPanel.vue`、`src/pages/message/workorder-detail.vue`、`src/pages/office/patrol-detail.vue`。
+- 验证:类型检查、H5 和微信小程序构建通过;隔离 Chrome 浏览器使用拦截接口及本地测试图片验证选中项、前后切换、首尾禁用、分组隔离、单张、巡检触摸滑动、关闭/Esc、上传表单预览层级及删除、无操作工单底部提示与留白移除,无页面异常。
+- 风险与后续:仅需部署移动端,无后端或数据库变更;未以真实业务照片进行真机验收。
+
+## 巡检列表整张卡片进入详情
+
+- 用户要求巡检列表点击整张任务卡片均可进入详情;已将跳转事件移到卡片容器,“查看详情”文字保留并通过事件冒泡共用跳转,避免重复导航。
+- 文件:移动端 `src/pages/office/patrol.vue`,无需后端变更。
+- 验证:类型检查、H5 构建通过;隔离浏览器使用拦截接口验证标题、字段、卡片空白与“查看详情”均进入对应任务,覆盖待巡检及已完成,每次点击仅一次详情请求。未进行真机验收。
+
+## 移动端“日常-故障通知”列表、详情与审核
+
+- 目标:参考 PC `src/views/subSystem/alarm/common/audit.vue` 接入真实列表和审核,并按当前用户公司限制故障数据;列表右上角展示审核结果,详情底部弹出审核表单,“故障信息/处置全流程”使用左右页签。
+- 后端新增 `/api/alarm/mobile` 列表、`/{id}` 详情、`/{id}/logs` 历史、`/{id}/reviews` 审核和 `/{id}/attachments` 上传。读取仅要求登录,审核/上传保留 `alarm:review:audit`。公司范围在服务端由部门自身及祖先 101/102/103 映射为设备类型直接 parent_id=1/2/3;不含共享类型4、不递归类型树,管理员无范围豁免,rootTypeId 可选但只能收窄。
+- 有关联工单的报警通过 work_order.device_id 取设备;未转工单报警按 device_code 取设备,损坏工单关联不回退。所有入口使用同一公司范围,空范围不返回全系统数据;列表与 PC 审核页一致只展示未处理报警,包括三种审核结果。
+- 审核复用 PC 原事务、并发状态检查及附件票据:确认报警创建待派单工单,误报警仅写结论;均不直接修改监测/报警处理状态。结果必选,意见选填最多500字,JPG/JPEG/PNG图片最多5张、每张不超过10MB。报警附件沿用框架上传目录及 Redis 临时票据(绑定用户与报警,2小时过期),本次未改为工单 MinIO 附件机制。
+- 移动端列表支持等级、关键字、审核结果筛选,整卡点击、首次加载、下拉刷新、触底分页、空状态和失败重试。详情展示实际字段及关联工单,处置历史沿用 alarm_log 倒序日期分组;设备照片与每条历史照片分别成组预览。实际值0有效,无实际值时不展示实际值/阈值。
+- 底部审核抽屉支持上传、预览、删除、失败重试,上传/提交中锁定关闭和重复操作;只有未审核、未处理且有审核权限才显示审核按钮,提交成功刷新详情及历史。
+- 浏览器分页验证发现公共 H5 样式 overflow-x:hidden 将页面外壳变为独立纵向滚动容器,导致 onReachBottom 不触发;改为 overflow-x:clip 保持横向裁剪和页面纵向滚动。已验证连续三页23条及刷新回第一页,并回归工单/巡检/上传表单的照片预览。
+- 文件:移动端 `src/services/alarms.ts`、`src/components/AlarmAuditDrawer.vue`、`src/pages/office/alarms.vue`、`src/pages/message/alarm-detail.vue`、`src/pages.json`、`src/styles/global.scss`;后端 `MobileAlarmQuery`、`MobileAlarmService`、`MobileAlarmController`,扩展 `AlarmOperationsService/Mapper/XML`、`AlarmReviewService/Mapper/XML` 及相应测试,新增公司部门范围及移动端 Controller 测试;更新后端 CONTEXT 和 PC 记忆库。
+- 验证:后端专项及 PC 兼容回归共88项通过,管理后端与依赖编译通过;枚举替换后 Controller 5项复验通过。移动端类型检查、H5与微信小程序构建通过。隔离 Chrome 拦截接口验证三种标签、筛选(含 auditStatus=0)、整卡跳转、页签、分组照片、两种审核结果、必填检查、失败重试、附件格式/大小/数量/票据、权限与状态限制、分页和刷新,无页面异常。
+- 风险与后续:需要同步部署移动端并更新、重启管理后端,无数据库迁移。浏览器使用隔离模拟接口,未用真实登录态、KingbaseES数据或真机执行审核/上传;PC运行源码未改变,原入口行为由专项测试回归。
+
+## 移动端基本资料性别改为单选
+
+- 用户要求“我的-基本资料”去掉保密选项,性别使用男、女两个 radio,不使用下拉。已在 `src/pages/profile/basic.vue` 改为横排原生单选,保留接口编码男0、女1。
+- 历史性别2或未设置时不默认指定性别,显示未选中;保存时提示选择性别,避免无意修改。请求中直接提交选中的0/1,加载或保存时禁用单选。
+- 验证:类型检查、H5与微信小程序构建通过;隔离 Chrome 拦截接口验证两项单选、回显与互斥、点击文字切换、保存提交0/1、旧保密值未选中且提示选择,无页面异常,未写入真实资料。
+- 风险与后续:仅需部署移动端,无后端、PC运行代码或数据库变更;未进行真机验收。
+
+## 移动端专用 Controller 集中,共用 PC 业务 Service
+
+- 用户确认将移动端接口统一放入后端 `zksy-admin/src/main/java/com/zksy/web/controller/mobile`。已迁入 `MobileWorkOrderController`、`MobileInspectTaskController`、`MobileEquipmentController`、`MobileAlarmController`、`MobileNoticeController`、`MobileLogininforController`、`MobileContactsController`,旧 Controller 中对应的移动端专用方法移除。
+- URL、参数、响应、权限和数据范围保持兼容,移动端无需更改接口地址。登录、资料、通用上传及工单处置等 PC/移动端共用同一 URL 的接口继续保留原 Controller。
+- 撤销上一阶段单设的 `MobileAlarmService`,将查询、公司范围及审核分别归入现有 `AlarmOperationsService`、`AlarmReviewScopeService`、`AlarmReviewService`;移动端审核公开方法保留事务,与 PC 共用审核核心。
+- 更新后端 `CONTEXT.md` 并新增 ADR-0011。Maven 定向 `clean package` 通过:14 个测试类、140 项测试、0 失败/错误;覆盖两端同时注册无映射冲突、18 个移动端 URL、公共接口保留、参数绑定、数据范围和审核事务等。确认构建目录旧报警 Controller 和旧 MobileAlarmService 类已清除。
+- 仅需部署并重启管理后端,无前端或数据库变更;未操作真实工单/报警或执行生产部署,真实登录环境联调待更新后端后确认。
+
+## 移动端接口 URL 统一为 /api/mobile
+
+- 用户选择 `/api/mobile/ops/inspectTask` 形式并要求执行。七类移动端 Controller 全部统一到 `/api/mobile` 前缀,工单/巡检继续保留 ops 业务域;故障通知为 `/api/mobile/alarm`,公告为 `/api/mobile/notice`,设备为 `/api/mobile/equipment-base`,本人登录日志为 `/api/mobile/monitor/logininfor/list`,通讯录为 `/api/mobile/contacts`。
+- 同步修改移动端 `src/services` 下 workorders、inspect-tasks、equipment、alarms(含审核图片上传)、notices、login-logs、contacts 七个文件,并更新后端映射测试、CONTEXT 和 ADR-0011。本决定取代上一节保留旧移动端 URL 的约定,旧地址直接迁移。
+- 工单处置、登录/资料、通用 MinIO 上传仍使用两端共用原入口。Service、数据范围、参数、响应和权限未改,PC 运行源码无需调整。
+- 验证:Maven 管理后端打包及 11 项 Controller 回归通过;移动端类型检查、H5及微信小程序生产构建通过。执行真实 Service 模块的隔离请求检查,验证18个移动端调用(含报警附件)和10个共用调用的 URL/方法,未发网络请求。源码扫描确认无旧移动端地址残留。
+- 交付需同步更新移动端和管理后端并重启后端,无数据库变更;未部署或执行真实工单、巡检、审核操作。
+
+## 移动端故障通知筛选布局及核实文案
+
+- 用户要求审核状态改为 tab、报警等级下拉放在搜索框前并参考工单列表,随后确认“未审核”改为“待核实”、审核操作改称“核实”。
+- 顶部 tab 为全部/待核实/确认报警/误报警,等级独立提供全部等级/一级/二级/三级/四级;复用公共 SearchBar,新增可选 prefix 插槽,切换任一条件重置第一页并保留其余筛选。auditStatus=0 正常发送,全部不限制结果或等级。
+- 状态0优先显示“待核实”,覆盖后端旧 auditStatusName;详情按钮、核实结果/意见、抽屉标题、提交按钮及提示同步使用核实文案。接口、枚举与权限沿用现有约定。
+- 文件:移动端 src/pages/office/alarms.vue、src/components/SearchBar.vue、src/services/alarms.ts、src/pages/message/alarm-detail.vue、src/components/AlarmAuditDrawer.vue;同步长期记忆及索引。
+- 验证:类型检查、H5与微信小程序构建通过。隔离 Chrome 拦截接口验证四种 tab、等级选择/取消、组合搜索、分页及筛选重置、320/375/430宽度无横向溢出,并回归工单公共搜索栏。详情验证后端旧文案覆盖、核实按钮和弹窗、结果必选提示、模拟提交原参数及成功后状态更新/按钮隐藏,无页面异常。
+- 风险与后续:仅需更新移动端,无后端或数据库变更;未调用真实核实接口,尚未真机验收。
+
+## PC 报警核实命名同步
+
+- 用户要求 PC 同步移动端命名:报警状态“未审核/待审核”统一展示为“待核实”,菜单、操作、表单及提示使用“核实”。覆盖公共核实页、报警清单、详情、提醒、排水研判、供水首页和报警台账(含 CSV 导出)。工单验收与延期审核不受影响。
+- 新增 `src/utils/alarmLabels.js` 兼容后端旧状态名称,状态0优先显示待核实;供水展示状态1同样优先显示待核实。动态路由按报警组件将旧菜单标题映射为报警核实,首页入口兼容新旧标题;不修改数据库菜单、路由、接口枚举或权限。
+- 文件:上述公共报警页面、`waterSupply/components/WaterAlarmPage.vue`、`waterAlarmManagementModel.js`、`waterHomeModel.js`、`alarmMenuView.js`、`waterHomeRouteResolver.js`、权限 Store 及对应现有回归测试。修正核实页测试中已过期的列表数值快照断言,保留共享详情实际值保护检查。
+- 验证:生产构建及42项相关回归通过;隔离 Chrome 拦截接口验证菜单、待核实 tab/筛选/表格、必选提示、核实表单按原参数模拟提交、详情跳转和提醒,无页面异常;差异格式检查通过。
+- 风险与后续:仅需更新 PC 前端,无后端或数据库变更;浏览器提交为模拟接口,未执行真实报警核实。
+
+## 移动端工单、故障列表查询样式
+
+- 用户要求两个列表参照设备列表,查询按钮紧贴输入框,并移除总数行右侧重复的状态文字。
+- 公共 `SearchBar.vue` 新增可选 attached 样式,将输入框和查询按钮连接,按钮尺寸、直角和配色与设备列表一致;仅工单与故障列表启用,其他页面保持默认样式。故障报警等级下拉仍位于输入框前。
+- 文件:移动端 `src/components/SearchBar.vue`、`src/pages/profile/workorders.vue`、`src/pages/office/alarms.vue`;两页总数行仅保留数量,顶部状态 tab 及卡片状态保留。
+- 验证:类型检查、H5 与微信小程序构建通过;隔离 Chrome 拦截接口检查320/375/430宽度,按钮与设备列表尺寸、颜色及圆角一致,输入框按钮无间隙、页面无横向溢出;按钮查询、回车查询和状态 tab 请求参数正常,总数行无重复状态,等级筛选位置正常。
+- 风险与后续:仅需更新移动端,无后端或数据库变更;未进行真机验收。
+
+## 故障通知等级选项文案
+
+- 用户要求故障通知列表报警等级增加“报警”二字。下拉选项调整为全部等级、一级报警、二级报警、三级报警、四级报警,等级筛选参数和列表字段不变。
+- 文件:移动端 `src/pages/office/alarms.vue`。
+- 验证:移动端类型检查通过。
+- 风险与后续:仅需更新移动端,无接口、后端或数据库变更。
+
+## 列表查询按钮文案完整显示
+
+- 用户反馈多个列表的查询按钮只显示“查”。按钮源文案原本为“查询”,问题是窄按钮保留了平台默认内边距,文字无法完整排布。
+- 公共 `SearchBar.vue` 和设备列表按钮清除默认内边距并禁止换行,保持原有文案、按钮宽高和查询逻辑。
+- 文件:移动端 `src/components/SearchBar.vue`、`src/pages/office/devices.vue`。
+- 验证:类型检查、H5 构建通过;隔离浏览器对设备、工单、故障、预警、处置方案及事件列表在320/375/430宽度检查按钮文案保持单行“查询”、页面无横向溢出或运行异常。
+
+## 巡查管理移除查询输入框
+
+- 用户要求巡查管理列表去掉查询条件输入框。已移除搜索输入框和专属查询按钮,同时删除关键字状态及请求参数;状态 Tab、列表数据、下拉刷新与分页保持不变。
+- 文件:移动端 `src/pages/office/patrol.vue`。
+- 验证:移动端类型检查通过。
+- 风险与后续:仅需更新移动端,无后端或数据库变更。
+
+## 工单、故障核实、巡检图片统一 10MB
+
+- 用户要求故障详情核实表单提示统一为“支持 png / jpg / jpeg,单个文件不超过 10MB”,并将工单、故障、巡检操作涉及的图片上传限制统一为单文件不超过 10MB,PC 端同步调整。
+- 移动端 `UploadPanel` 增加单文件大小校验,工单操作、故障核实、巡检完成均显式使用 10MB,并统一上传提示文案。
+- PC 端工单处置/验收/延期、巡检完成的 `ImageUpload` 显式设置 `file-size=10`;报警核实提示与校验统一为 10MB;公共 `ImageUpload` 允许恰好 10MB 的文件通过(边界改为 `>`)。
+- 非本次业务范围的基础维修、隐患清单等组件仍保留原有 5MB 限制,避免扩大其他上传场景的行为变化。
+- 验证:移动端 `npm run type-check`、`npm run build:h5` 与 PC 端 `npm run build:prod` 均通过;PC `git diff --check` 通过。PC 构建仅输出既有 Sass 弃用警告。

+ 1 - 1
src/components/ImageUpload/index.vue

@@ -105,7 +105,7 @@ function handleBeforeUpload(file) {
     proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
     return false;
   }
-  if (props.fileSize && file.size / 1024 / 1024 >= props.fileSize) {
+  if (props.fileSize && file.size / 1024 / 1024 > props.fileSize) {
     proxy.$modal.msgError(`上传图片大小不能超过 ${props.fileSize} MB!`);
     return false;
   }

+ 6 - 0
src/router/alarmMenuView.js

@@ -1,3 +1,9 @@
+/** 报警核实菜单兼容现有数据库名称,路由和权限不变。 */
+export function resolveAlarmMenuTitle(view, title) {
+  return ['subSystem/alarm/common/audit', 'subSystem/alarm/audit', 'subSystem/waterSupply/monitorAlarm/audit'].includes(view)
+    && title === '报警审核' ? '报警核实' : title
+}
+
 /** 按菜单的数据范围选择供水专属报警管理,不改数据库下发的 path/name。 */
 export function resolveAlarmMenuView(view, rawQuery) {
   let query = rawQuery || {}

+ 2 - 2
src/router/waterHomeRouteResolver.js

@@ -12,7 +12,7 @@ const WATER_ALARM_ENTRY_ALIASES = {
     suffixes: ['/monitorAlarm/index', '/alarm/common/list', '/alarm/list', '/alarm/alarmList', '/alarm/index']
   },
   '/monitorAlarm/audit': {
-    title: '报警审核',
+    title: '报警核实',
     suffixes: ['/monitorAlarm/audit', '/alarm/common/audit', '/alarm/audit', '/alarm/alarmAudit']
   }
 }
@@ -45,7 +45,7 @@ function routeScore(route, candidates, title) {
   }
 
   const suffixIndex = candidates.findIndex(candidate => path.endsWith(normalizePath(candidate)))
-  const titleMatch = route?.meta?.title === title
+  const titleMatch = route?.meta?.title === title || (title === '报警核实' && route?.meta?.title === '报警审核')
   if (suffixIndex < 0 && !titleMatch) return Number.NEGATIVE_INFINITY
   let score = suffixIndex < 0 ? 0 : SCORE.SUFFIX_BASE - suffixIndex
   if (titleMatch) score += SCORE.TITLE

+ 2 - 1
src/store/modules/permission.js

@@ -5,7 +5,7 @@ import Layout from '@/layout/index'
 import ParentView from '@/components/ParentView'
 import InnerLink from '@/layout/components/InnerLink'
 import { ensureUniqueSubsystemRouteNames } from '@/router/routeNameUtils'
-import { resolveAlarmMenuView } from '@/router/alarmMenuView'
+import { resolveAlarmMenuTitle, resolveAlarmMenuView } from '@/router/alarmMenuView'
 import { joinRoutePath } from '@/utils/ruoyi'
 
 // 匹配views里面所有的.vue文件
@@ -65,6 +65,7 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
       route.children = filterChildren(route.children, false, route.path)
     }
     if (route.component) {
+      if (route.meta) route.meta.title = resolveAlarmMenuTitle(route.component, route.meta.title)
       // Layout ParentView 组件特殊处理
       if (route.component === 'Layout') {
         route.component = Layout

+ 8 - 0
src/utils/alarmLabels.js

@@ -0,0 +1,8 @@
+// 兼容现有接口的旧名称,仅统一报警业务的展示文案。
+export const alarmPendingText = text => ['未审核', '待审核'].includes(text) ? '待核实' : text
+
+export function alarmAuditText(alarm = {}) {
+  return alarm.auditStatus != null && alarm.auditStatus !== '' && Number(alarm.auditStatus) === 0
+    ? '待核实'
+    : alarmPendingText(alarm.auditStatusName) || '未知'
+}

+ 3 - 2
src/utils/waterAlarmManagementModel.js

@@ -1,8 +1,9 @@
 import { normalizeAlarmTrend } from './waterHomeModel.js'
+import { alarmPendingText } from './alarmLabels.js'
 
 export const WATER_ALARM_ROOT_TYPE_ID = 1
 
-const displayStatusLabels = { 1: '待审核', 2: '已转工单', 3: '已解除', 4: '误报警' }
+const displayStatusLabels = { 1: '待核实', 2: '已转工单', 3: '已解除', 4: '误报警' }
 const columns = [
   ['报警单号', 'alarmNo'], ['设备编码', 'deviceCode'], ['设备名称', 'deviceName'],
   ['报警类型', 'warningType'], ['报警等级', 'alarmLevelName'], ['报警时间', 'createTime'],
@@ -40,7 +41,7 @@ export function normalizeWaterAlarmRow(row = {}) {
     alarmLevelName: row.alarmLevelName || (level ? String(level) : '未分级'),
     monitorStatusText: row.monitorStatusName || '未获取',
     deviceAlarmStatusText: row.deviceAlarmStatusText || (row.deviceAlarmStatus == null ? '未获取' : Number(row.deviceAlarmStatus) === 1 ? '报警中' : '正常'),
-    displayStatusText: row.displayStatusText || displayStatusLabels[displayStatus] || '未知',
+    displayStatusText: displayStatus === 1 ? '待核实' : alarmPendingText(row.displayStatusText) || displayStatusLabels[displayStatus] || '未知',
     orderStatusText: orderStatus,
     receiveUserName: row.receiveUserName || '未分配'
   }

+ 8 - 7
src/utils/waterHomeModel.js

@@ -1,4 +1,5 @@
 import { normalizeDeviceRow } from './waterSupplyUiModel.js'
+import { alarmPendingText } from './alarmLabels.js'
 import { getFacilityStatusMeta, getMonitoringPointIcon, getMonitoringPointStatus } from './gisMapColors.js'
 
 /**
@@ -35,7 +36,7 @@ export const WATER_DEVICE_ROOT_TYPE_ID = 1
 export const HOME_QUICK_ENTRIES = [
   { key: 'abnormal', label: '设备异常管理', suffix: '/device/abnormal', icon: 'WarningFilled' },
   { key: 'alarm', label: '报警管理', suffix: '/monitorAlarm/index', icon: 'BellFilled' },
-  { key: 'audit', label: '报警审核', suffix: '/monitorAlarm/audit', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'Finished' },
+  { key: 'audit', label: '报警核实', suffix: '/monitorAlarm/audit', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'Finished' },
   { key: 'map', label: '监测报警一张图', suffix: '/waterSupplyAlarmGIS', query: { rootTypeId: WATER_DEVICE_ROOT_TYPE_ID }, icon: 'MapLocation' }
 ]
 
@@ -162,7 +163,7 @@ export function alarmLevelText(level) {
 }
 
 const ALARM_DISPLAY_STATUS_META = {
-  1: { text: '待审核', type: 'warning' },
+  1: { text: '待核实', type: 'warning' },
   2: { text: '已转工单', type: 'primary' },
   3: { text: '已解除', type: 'success' }
 }
@@ -173,11 +174,11 @@ const ALARM_DISPLAY_RELEASED = 3
  * A 区实时报警列表。
  *
  * 实时列表只展示仍需要关注的报警:已解除(displayStatus=3)直接过滤;
- * 待审核 / 已转工单行保留展示状态与关联工单号,供首页按状态分流跳转。
+ * 待核实 / 已转工单行保留展示状态与关联工单号,供首页按状态分流跳转。
  *
  * 后端 5c14625 起实时行已携带 displayStatus;为兼容旧进程/旧产物返回的
  * “只有 orderNo 没有 displayStatus” 的行,这里按工单是否在办兜底推导:
- * 有未取消工单 -> 已转工单,否则 -> 待审核。兜底只影响缺失字段的行,
+ * 有未取消工单 -> 已转工单,否则 -> 待核实。兜底只影响缺失字段的行,
  * 后端明确给出的展示状态始终优先。
  */
 export function normalizeRealtimeAlarmRows(rows = [], limit = 5) {
@@ -198,9 +199,9 @@ export function normalizeRealtimeAlarmRows(rows = [], limit = 5) {
         alarmTime: row?.alarmTime || '-',
         location: row?.equipmentLocation || '-',
         displayStatus,
-        displayStatusText: row?.displayStatusText
+        displayStatusText: displayStatus === 1 ? '待核实' : alarmPendingText(row?.displayStatusText)
           || ALARM_DISPLAY_STATUS_META[displayStatus]?.text
-          || '待审核',
+          || '待核实',
         displayStatusType: ALARM_DISPLAY_STATUS_META[displayStatus]?.type || 'warning',
         orderNo: row?.orderNo || row?.relatedOrderNo || ''
       }
@@ -225,7 +226,7 @@ function derivedDisplayStatus(row = {}) {
 }
 
 /**
- * 实时报警跳转目标:已转工单带工单号 -> 设备维修派单;其余(含待审核)-> 报警审核。
+ * 实时报警跳转目标:已转工单带工单号 -> 设备维修派单;其余(含待核实)-> 报警核实。
  */
 export function realtimeAlarmJumpTarget(row = {}) {
   if (Number(row?.displayStatus) === 2) {

+ 3 - 2
src/views/subSystem/alarm/AlarmDetail.vue

@@ -6,14 +6,14 @@
         <AlarmSummary :alarm="alarm" :level-tone="levelTone(alarm.alarmLevel)" />
         <dl class="alarm-state-grid">
           <div class="alarm-state-item"><dt>监测状态</dt><dd><el-tag size="small" effect="light" :type="monitorStatusTone(alarm.alarmStatus)">{{ alarm.monitorStatusName || '未知' }}</el-tag></dd></div>
-          <div class="alarm-state-item"><dt>审核结果</dt><dd><el-tag size="small" effect="light" :type="auditStatusTone(alarm.auditStatus)">{{ alarm.auditStatusName || '未知' }}</el-tag></dd></div>
+          <div class="alarm-state-item"><dt>核实结果</dt><dd><el-tag size="small" effect="light" :type="auditStatusTone(alarm.auditStatus)">{{ alarmAuditText(alarm) }}</el-tag></dd></div>
           <div class="alarm-state-item"><dt>处置状态</dt><dd><el-tag size="small" effect="light" :type="alarm.workOrderId != null && alarm.orderStatus != null ? orderStatusType(alarm.orderStatus) : 'info'">{{ alarm.orderStatusName || '未转工单' }}</el-tag></dd></div>
         </dl>
         <el-descriptions :column="2" border><el-descriptions-item label="关联工单">{{ alarm.orderNo || '—' }}</el-descriptions-item><el-descriptions-item label="责任人">{{ alarm.receiveUserName || '未分配' }}</el-descriptions-item><el-descriptions-item label="完成期限" :span="2">{{ alarm.planFinishTime || '—' }}</el-descriptions-item></el-descriptions>
         <AlarmHistory :records="timeline" :loading="loading" :error="error" :base-url="baseUrl" />
       </template>
     </div>
-    <template #footer><el-button v-if="alarm && hasActualValue(alarm) && menu('drainageAlarmAnalysis')" @click="navigate('drainageAlarmAnalysis', { alarmId: alarm.id })">进入研判</el-button><el-button v-if="alarm?.alarmStatus === 0 && alarm?.auditStatus === 0 && menu('alarmAudit')" type="primary" @click="navigate('alarmAudit', { alarmId: alarm.id })">前往审核</el-button><el-button v-if="waterShortcuts && alarm?.workOrderId" type="primary" @click="goWaterWorkOrder">前往设备维修派单</el-button><el-button @click="visible = false">关闭</el-button></template>
+    <template #footer><el-button v-if="alarm && hasActualValue(alarm) && menu('drainageAlarmAnalysis')" @click="navigate('drainageAlarmAnalysis', { alarmId: alarm.id })">进入研判</el-button><el-button v-if="alarm?.alarmStatus === 0 && alarm?.auditStatus === 0 && menu('alarmAudit')" type="primary" @click="navigate('alarmAudit', { alarmId: alarm.id })">前往核实</el-button><el-button v-if="waterShortcuts && alarm?.workOrderId" type="primary" @click="goWaterWorkOrder">前往设备维修派单</el-button><el-button @click="visible = false">关闭</el-button></template>
   </el-drawer>
 </template>
 <script setup>
@@ -21,6 +21,7 @@ import { ref, watch, onBeforeUnmount } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { getAlarm, getAlarmTimeline } from '@/api/alarm'
+import { alarmAuditText } from '@/utils/alarmLabels'
 import AlarmSummary from './AlarmSummary.vue'
 import AlarmHistory from './AlarmHistory.vue'
 import { auditStatusTone, levelTone, monitorStatusTone } from './common/js/alarmOperationsModel'

+ 6 - 3
src/views/subSystem/alarm/AlarmHistory.vue

@@ -4,7 +4,7 @@
     <div v-if="groups.length" class="history-hint">按时间倒序分组 · 滚动即可回溯</div>
     <div v-for="group in groups" :key="group.day" class="history-group">
       <div class="history-day" :title="group.day">{{ historyDayLabel(group.day) }}</div>
-      <article v-for="item in group.items" :key="item.logId" class="history-row">
+      <article v-for="(item, index) in group.items" :key="item.logId" class="history-row" :class="{ 'last-log': index === group.items.length - 1 }">
         <time class="history-time" :title="item.operTime">{{ historyTime(item.operTime) }}</time>
         <div class="history-body">
           <div class="history-label"><b>{{ item.operUserName || '系统' }}</b><span class="separator">·</span>{{ item.operTypeName || '历史操作' }}</div>
@@ -24,7 +24,7 @@ import { groupAlarmHistory, historyDayLabel, historyTime } from './common/js/ala
 
 const props = defineProps({
   records: { type: Array, default: () => [] },
-  title: { type: String, default: '处置全流程' },
+  title: { type: String, default: '处置流程' },
   loading: Boolean,
   error: { type: String, default: '' },
   baseUrl: { type: String, default: '' }
@@ -40,7 +40,10 @@ const groups = computed(() => groupAlarmHistory(props.records))
 .history-hint { padding-bottom: 10px; border-bottom: 1px solid #f0f2f4; margin-bottom: 14px; }
 .history-day { font-size: 13px; font-weight: 500; color: #2a3142; margin: 16px 0 4px; }
 .history-row { display: flex; gap: 12px; padding: 8px 0 8px 4px; }
-.history-time { flex: 0 0 60px; font: 12px/1.6 ui-monospace, Consolas, monospace; color: #909399; padding-top: 1px; }
+.history-time { position: relative; align-self: stretch; box-sizing: border-box; flex: 0 0 62px; padding: 1px 22px 0 0; font: 12px/1.6 ui-monospace, Consolas, monospace; color: #909399; }
+.history-time::before { content: ''; position: absolute; top: 5px; right: 8px; z-index: 1; width: 7px; height: 7px; border: 2px solid #fff; border-radius: 50%; background: #3b5bdb; box-shadow: 0 0 0 1px #9bb3d9; transform: translateX(50%); }
+.history-time::after { content: ''; position: absolute; top: 10px; right: 8px; z-index: 0; bottom: -27px; width: 1px; background: #d8e1ec; transform: translateX(50%); }
+.history-row.last-log .history-time::after { display: none; }
 .history-body { flex: 1; min-width: 0; }
 .history-label { font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; }
 .history-label b { font-weight: 700; }

+ 14 - 13
src/views/subSystem/alarm/common/audit.vue

@@ -4,7 +4,7 @@
       <el-form :inline="true" :model="filters" class="search-form" @submit.prevent="search">
         <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码 / 名称" @keyup.enter="search" /></el-form-item>
         <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in alarmLevels" :key="item.value" :label="item.label" :value="Number(item.value)" /></el-select></el-form-item>
-        <el-form-item label="审核结果"><el-select v-model="filters.auditStatus" clearable placeholder="全部结果"><el-option v-for="item in options.auditStatuses" :key="item.key" :label="item.label" :value="item.value" /></el-select></el-form-item>
+        <el-form-item label="核实结果"><el-select v-model="filters.auditStatus" clearable placeholder="全部结果"><el-option v-for="item in options.auditStatuses" :key="item.key" :label="item.key === 'UNREVIEWED' ? '待核实' : item.label" :value="item.value" /></el-select></el-form-item>
         <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" start-placeholder="开始日期" end-placeholder="结束日期" range-separator="至" /></el-form-item>
         <el-form-item><el-button type="primary" :icon="Search" @click="search">查询</el-button><el-button :icon="Refresh" @click="reset">重置</el-button></el-form-item>
       </el-form>
@@ -13,7 +13,7 @@
     <section class="table-card">
       <el-tabs :model-value="activeTab" @tab-change="changeTab">
         <el-tab-pane name="all"><template #label>全部报警 <span class="tab-count">{{ statistics.total }}</span></template></el-tab-pane>
-        <el-tab-pane name="pending"><template #label>待审核 <span class="tab-count">{{ statistics.pending }}</span></template></el-tab-pane>
+        <el-tab-pane name="pending"><template #label>待核实 <span class="tab-count">{{ statistics.pending }}</span></template></el-tab-pane>
       </el-tabs>
       <el-alert v-if="error" :title="error" type="error" show-icon :closable="false" class="load-error"><template #default><el-button link type="primary" @click="load">重新加载</el-button></template></el-alert>
       <el-table v-loading="loading" :data="rows" row-key="id" border stripe style="width: 100%" :header-cell-style="{ background: '#f5f7fa', color: '#606266' }" :default-sort="{ prop: 'createTime', order: 'descending' }" @sort-change="sortChanged">
@@ -24,24 +24,24 @@
         <el-table-column prop="alarmContent" label="报警描述" min-width="275"><template #default="{ row }">{{ row.alarmContent || '—' }}</template></el-table-column>
         <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)" effect="light">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
         <el-table-column prop="createTime" label="报警时间" width="175" sortable="custom" />
-        <el-table-column label="审核结果" width="110" align="center"><template #default="{ row }"><el-tag :type="auditTone(row.auditStatus)" effect="light">{{ row.auditStatusName }}</el-tag></template></el-table-column>
-        <el-table-column label="操作" width="160" align="center" fixed="right"><template #default="{ row }"><el-button v-if="isUnreviewed(row)" v-hasPermi="['alarm:review:audit']" link type="primary" @click="openReview(row)">审核</el-button><el-button link type="primary" @click="openHistory(row)">处置记录</el-button></template></el-table-column>
+        <el-table-column label="核实结果" width="110" align="center"><template #default="{ row }"><el-tag :type="auditTone(row.auditStatus)" effect="light">{{ alarmAuditText(row) }}</el-tag></template></el-table-column>
+        <el-table-column label="操作" width="160" align="center" fixed="right"><template #default="{ row }"><el-button v-if="isUnreviewed(row)" v-hasPermi="['alarm:review:audit']" link type="primary" @click="openReview(row)">核实</el-button><el-button link type="primary" @click="openHistory(row)">处置记录</el-button></template></el-table-column>
         <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配的未处理报警'" /></template>
       </el-table>
       <el-pagination class="pagination" v-model:current-page="pageNum" v-model:page-size="pageSize" :total="total" :page-sizes="[10, 20, 50, 100]" layout="total, sizes, prev, pager, next, jumper" @size-change="search" @current-change="load" />
     </section>
 
-    <el-dialog v-model="reviewVisible" title="报警审核" width="680px" append-to-body destroy-on-close :close-on-click-modal="false" :before-close="closeReview">
+    <el-dialog v-model="reviewVisible" title="报警核实" width="680px" append-to-body destroy-on-close :close-on-click-modal="false" :before-close="closeReview">
       <div v-loading="detailLoading">
         <AlarmSummary v-if="current" :alarm="current" :level-tone="levelTone(current.alarmLevel)" />
         <el-form ref="formRef" :model="form" :rules="rules" label-width="90px" :disabled="submitting || detailLoading">
-          <el-form-item label="审核结果" prop="auditStatus"><el-radio-group v-model="form.auditStatus" class="review-choices"><el-radio v-for="item in reviewChoices" :key="item.key" :label="item.value" border>{{ item.label }}</el-radio></el-radio-group></el-form-item>
-          <el-form-item label="审核意见" prop="opinion"><el-input v-model="form.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="选填,可填写核实情况、判定依据或误报原因" /></el-form-item>
-          <el-form-item label="图片"><div class="attachments"><el-upload v-model:file-list="files" :http-request="uploadFile" :before-upload="beforeUpload" :limit="5" :on-exceed="exceedFiles" :disabled="submitting || detailLoading" accept=".jpg,.jpeg,.png"><el-button :icon="Upload">选择图片</el-button><template #tip><div class="el-upload__tip">JPG、JPEG、PNG,最多 5 张,单张不超过 10 MB。</div></template></el-upload></div></el-form-item>
+          <el-form-item label="核实结果" prop="auditStatus"><el-radio-group v-model="form.auditStatus" class="review-choices"><el-radio v-for="item in reviewChoices" :key="item.key" :label="item.value" border>{{ item.label }}</el-radio></el-radio-group></el-form-item>
+          <el-form-item label="核实意见" prop="opinion"><el-input v-model="form.opinion" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="选填,可填写核实情况、判定依据或误报原因" /></el-form-item>
+          <el-form-item label="图片"><div class="attachments"><el-upload v-model:file-list="files" :http-request="uploadFile" :before-upload="beforeUpload" :limit="5" :on-exceed="exceedFiles" :disabled="submitting || detailLoading" accept=".jpg,.jpeg,.png"><el-button :icon="Upload">选择图片</el-button><template #tip><div class="el-upload__tip">支持 png / jpg / jpeg,单个文件不超过 10MB</div></template></el-upload></div></el-form-item>
         </el-form>
         <el-alert v-if="formError" :title="formError" type="error" :closable="false" show-icon />
       </div>
-      <template #footer><el-button :disabled="submitting || uploading" @click="closeReview()">取消</el-button><el-button type="primary" :loading="submitting" :disabled="detailLoading || uploading || !current" @click="submit">提交审核</el-button></template>
+      <template #footer><el-button :disabled="submitting || uploading" @click="closeReview()">取消</el-button><el-button type="primary" :loading="submitting" :disabled="detailLoading || uploading || !current" @click="submit">提交核实</el-button></template>
     </el-dialog>
 
     <el-drawer v-model="historyVisible" title="处置记录" size="640px" append-to-body destroy-on-close>
@@ -61,6 +61,7 @@ import { useRoute, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { Refresh, Search, Upload } from '@element-plus/icons-vue'
 import { getAlarmReviewOptions, getAlarmReviewPage, getAlarmReviewStatistics, getAlarmReviewDetail, getAlarmLogs, submitAlarmReview, uploadAlarmReviewAttachment } from '@/api/alarmReview'
+import { alarmAuditText } from '@/utils/alarmLabels'
 import AlarmSummary from '../AlarmSummary.vue'
 import AlarmHistory from '../AlarmHistory.vue'
 import { useDict } from '@/utils/dict'
@@ -84,8 +85,8 @@ const reviewChoices = computed(() => options.auditStatuses.filter(item => item.k
 const activeTab = computed(() => filters.auditStatus !== undefined && filters.auditStatus === unreviewed.value ? 'pending' : 'all')
 const uploading = computed(() => files.value.some(file => ['ready', 'uploading'].includes(file.status)))
 const rules = {
-  auditStatus: [{ required: true, message: '请选择审核结果', trigger: 'change' }],
-  opinion: [{ max: 500, message: '审核意见最多500字', trigger: 'blur' }]
+  auditStatus: [{ required: true, message: '请选择核实结果', trigger: 'change' }],
+  opinion: [{ max: 500, message: '核实意见最多500字', trigger: 'blur' }]
 }
 const message = e => e?.response?.data?.msg || e?.message || '请求失败,请重试'
 const isUnreviewed = row => unreviewed.value !== undefined && row.auditStatus === unreviewed.value
@@ -123,7 +124,7 @@ async function openReview(row) {
   try {
     const response = await getAlarmReviewDetail(row.id, { ...scope.value })
     if (request !== detailRequest) return
-    if (!isUnreviewed(response.data)) { reviewVisible.value = false; ElMessage.warning('该报警已审核,请刷新'); return load() }
+    if (!isUnreviewed(response.data)) { reviewVisible.value = false; ElMessage.warning('该报警已核实,请刷新'); return load() }
     current.value = response.data
   } catch (e) { if (request === detailRequest) formError.value = message(e) }
   finally { if (request === detailRequest) detailLoading.value = false }
@@ -142,7 +143,7 @@ async function submit() {
   try {
     await submitAlarmReview(current.value.id, { ...scope.value }, { auditStatus: form.auditStatus, opinion: form.opinion.trim(), attachmentTokens: files.value.map(file => file.response.token) })
     reviewVisible.value = false; files.value = []; Object.assign(form, { auditStatus: undefined, opinion: '' })
-    ElMessage.success('审核已提交'); await load()
+    ElMessage.success('核实已提交'); await load()
   } catch (e) { formError.value = message(e) }
   finally { submitting.value = false }
 }

+ 1 - 1
src/views/subSystem/alarm/common/js/alarmHistoryModel.js

@@ -4,7 +4,7 @@ function historyDate(value) {
 }
 
 export function historyTime(value) {
-  return historyDate(value).slice(11, 19) || '—'
+  return historyDate(value).slice(11, 16) || '—'
 }
 
 export function historyDayLabel(value, now = new Date()) {

+ 3 - 2
src/views/subSystem/alarm/common/list.vue

@@ -30,8 +30,8 @@
         <el-table-column label="报警等级" width="105" align="center"><template #default="{ row }"><el-tag :type="levelTone(row.alarmLevel)">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></template></el-table-column>
         <el-table-column prop="createTime" label="报警时间" width="180" sortable="custom" />
         <el-table-column label="监测状态" width="100" align="center"><template #default="{ row }"><el-tag :type="row.alarmStatus === 0 ? 'danger' : 'success'">{{ row.monitorStatusName }}</el-tag></template></el-table-column>
-        <el-table-column prop="auditStatusName" label="审核结果" width="105" align="center">
-          <template #default="{ row }"><el-tag effect="light" :type="auditStatusTone(row.auditStatus)">{{ row.auditStatusName || '未知' }}</el-tag></template>
+        <el-table-column prop="auditStatusName" label="核实结果" width="105" align="center">
+          <template #default="{ row }"><el-tag effect="light" :type="auditStatusTone(row.auditStatus)">{{ alarmAuditText(row) }}</el-tag></template>
         </el-table-column>
         <el-table-column prop="orderStatusName" label="处置状态" width="100" align="center">
           <template #default="{ row }"><el-tag effect="light" :type="row.workOrderId != null && row.orderStatus != null ? orderStatusType(row.orderStatus) : 'info'">{{ row.orderStatusName || '未转工单' }}</el-tag></template>
@@ -52,6 +52,7 @@ import { hasActualValue, valueText, thresholdText } from './js/alarmReviewModel'
 import { auditStatusTone, levelTone, rangeParams } from './js/alarmOperationsModel'
 import { orderStatusType } from '../../pipeNetwork/ops/workOrder/js/workOrderDict'
 import { useAlarmScope } from './js/useAlarmScope'
+import { alarmAuditText } from '@/utils/alarmLabels'
 import AlarmDetail from '../AlarmDetail.vue'
 import './styles/alarmOperations.scss'
 import { useDict } from '@/utils/dict'

+ 2 - 1
src/views/subSystem/alarm/common/remind.vue

@@ -9,7 +9,7 @@
         <article v-for="row in visibleRows" :key="row.id" class="reminder-message" :style="{ borderLeftColor: levelColor(row.alarmLevel) }">
           <div class="alarm-toolbar"><strong>{{ row.deviceCode }} ({{ row.deviceName || row.deviceCode }}) <el-tag :type="levelTone(row.alarmLevel)">{{ alarmLevels.find(item => Number(item.value) === Number(row.alarmLevel))?.label || row.alarmLevelName || '—' }}</el-tag></strong><span class="muted">{{ row.latestOperationTime || row.createTime }}</span></div>
           <div class="muted">报警单号:{{ row.alarmNo || '—' }} · {{ row.equipmentLocation || '暂无安装位置' }}</div>
-          <div class="alarm-toolbar"><span class="muted">监测:{{ row.monitorStatusName }} · 审核:{{ row.auditStatusName }} · 处置:{{ row.orderStatusName }}</span><el-button link type="primary" @click="detailRef.open(row.id)">事件详情</el-button></div>
+          <div class="alarm-toolbar"><span class="muted">监测:{{ row.monitorStatusName }} · 核实:{{ alarmAuditText(row) }} · 处置:{{ row.orderStatusName }}</span><el-button link type="primary" @click="detailRef.open(row.id)">事件详情</el-button></div>
           <p>{{ row.latestOperation || row.alarmContent || '—' }}</p>
         </article>
         <el-empty v-if="!visibleRows.length && !loading" :description="error ? '数据加载失败' : '暂无报警提醒'" />
@@ -24,6 +24,7 @@ import { VideoPlay, VideoPause } from '@element-plus/icons-vue'
 import { getAlarmReminders, getAlarmOptions, getAlarmStatistics } from '@/api/alarm'
 import { levelTone, levelColor } from './js/alarmOperationsModel'
 import { useAlarmScope } from './js/useAlarmScope'
+import { alarmAuditText } from '@/utils/alarmLabels'
 import AlarmDetail from '../AlarmDetail.vue'
 import './styles/alarmOperations.scss'
 import { useDict } from '@/utils/dict'

+ 2 - 1
src/views/subSystem/drainage/alarm/analysis.vue

@@ -2,7 +2,7 @@
   <div class="alarm-page">
     <el-card shadow="never"><div class="alarm-toolbar"><el-select v-model="selectedId" filterable remote :remote-method="searchEvents" :loading="searching" placeholder="搜索报警单号、设备编码或名称,选择事件" style="width:min(680px,80%)" @change="select"><el-option v-for="row in choices" :key="row.id" :value="row.id" :label="`${row.alarmNo || '历史事件'} · ${row.deviceName || row.deviceCode} · ${row.createTime}`" /></el-select><el-button :disabled="!selectedId" @click="detailRef.open(selectedId)">事件详情</el-button></div>
       <el-alert v-if="error" :title="error" type="error" :closable="false" show-icon />
-      <div v-loading="loading"><AlarmSummary v-if="alarm" :alarm="alarm" :level-tone="levelTone(alarm.alarmLevel)" /><div v-if="alarm" class="muted">监测:{{ alarm.monitorStatusName }} · 审核:{{ alarm.auditStatusName }} · 处置:{{ alarm.orderStatusName }}</div></div>
+      <div v-loading="loading"><AlarmSummary v-if="alarm" :alarm="alarm" :level-tone="levelTone(alarm.alarmLevel)" /><div v-if="alarm" class="muted">监测:{{ alarm.monitorStatusName }} · 核实:{{ alarmAuditText(alarm) }} · 处置:{{ alarm.orderStatusName }}</div></div>
     </el-card>
     <el-card v-if="alarm" shadow="never"><AlarmChart :alarm-id="alarm.id" :scope="scope" /></el-card>
     <el-empty v-else-if="!loading" description="请选择报警事件进行研判" />
@@ -12,6 +12,7 @@
 <script setup name="DrainageAlarmAnalysis">
 import { ref, watch, onBeforeUnmount } from 'vue'
 import { getAlarms, getAlarm } from '@/api/alarm'
+import { alarmAuditText } from '@/utils/alarmLabels'
 import AlarmSummary from '../../alarm/AlarmSummary.vue'
 import AlarmChart from '../../alarm/AlarmChart.vue'
 import AlarmDetail from '../../alarm/AlarmDetail.vue'

+ 1 - 1
src/views/subSystem/pipeNetwork/ops/inspectTask/index.vue

@@ -188,7 +188,7 @@
           <ImageUpload
             v-model="completeForm.photoUrl"
             :limit="9"
-            :file-size="5"
+            :file-size="10"
             :is-show-tip="true"
             :upload-url="imageUploadUrl"
           />

+ 42 - 9
src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue

@@ -120,7 +120,7 @@
           <div class="tabcard">
             <div class="tabs">
               <button class="tab" :class="{ on: activeTab === 'info' }" @click="activeTab = 'info'">工单信息</button>
-              <button class="tab" :class="{ on: activeTab === 'hist' }" @click="activeTab = 'hist'">处理日志</button>
+              <button class="tab" :class="{ on: activeTab === 'hist' }" @click="activeTab = 'hist'">处置流程</button>
             </div>
 
             <!-- 工单信息 -->
@@ -188,13 +188,13 @@
               </div>
             </div>
 
-            <!-- 处理日志 -->
+            <!-- 处置流程 -->
             <div v-show="activeTab === 'hist'" class="pane">
               <div class="hhint">按时间倒序分组 · 滚动即可回溯</div>
               <template v-if="logGroups.length">
                 <div v-for="g in logGroups" :key="g.day">
                   <div class="hday">{{ dayLabel(g.day) }}</div>
-                  <div v-for="(l, i) in g.items" :key="i" class="hrow">
+                  <div v-for="(l, i) in g.items" :key="i" class="hrow" :class="{ 'last-log': i === g.items.length - 1 }">
                     <span class="ht">{{ logTime(l.operTime) }}</span>
                     <div class="hb">
                       <div class="hl">
@@ -218,7 +218,7 @@
                   </div>
                 </div>
               </template>
-              <div v-else class="hint" style="padding: 20px 0">暂无处理记录</div>
+              <div v-else class="hint" style="padding: 20px 0">暂无处置记录</div>
             </div>
           </div>
 
@@ -366,7 +366,7 @@
           <template #label>
             <span v-if="actionAttachRequired" class="req-mark">*</span>{{ actionAttachLabel }}
           </template>
-          <ImageUpload v-model="actionForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
+          <ImageUpload v-model="actionForm.attachUrl" :limit="9" :file-size="10" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -388,7 +388,7 @@
           <el-input v-model="verifyForm.content" type="textarea" :rows="4" maxlength="500" show-word-limit placeholder="请输入验收意见" />
         </el-form-item>
         <el-form-item label="验收附件(可选)">
-          <ImageUpload v-model="verifyForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
+          <ImageUpload v-model="verifyForm.attachUrl" :limit="9" :file-size="10" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -407,7 +407,7 @@
           <el-input v-model="delayForm.delayedReason" type="textarea" :rows="4" maxlength="300" show-word-limit placeholder="请填写延期原因" />
         </el-form-item>
         <el-form-item label="附件">
-          <ImageUpload v-model="delayForm.attachUrl" :limit="9" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
+          <ImageUpload v-model="delayForm.attachUrl" :limit="9" :file-size="10" :is-show-tip="true" :upload-url="imageUploadUrl" relative-path />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -1578,7 +1578,7 @@ onMounted(() => {
 
 .nodelay { font-size: 12.5px; color: var(--txt3); }
 
-/* 处理日志 */
+/* 处置流程 */
 .hhint {
   font-size: 12px;
   color: var(--txt3);
@@ -1592,14 +1592,47 @@ onMounted(() => {
 .hrow { display: flex; gap: 12px; padding: 8px 0 8px 4px; }
 
 .ht {
+  position: relative;
+  align-self: stretch;
   flex: none;
-  width: 40px;
+  width: 62px;
+  padding-right: 22px;
+  box-sizing: border-box;
   font-family: ui-monospace, Consolas, "Courier New", monospace;
   font-size: 12px;
   color: var(--txt3);
   padding-top: 1px;
 }
 
+.ht::before {
+  content: '';
+  position: absolute;
+  top: 5px;
+  right: 8px;
+  z-index: 1;
+  width: 7px;
+  height: 7px;
+  border: 2px solid #fff;
+  border-radius: 50%;
+  background: #3b5bdb;
+  box-shadow: 0 0 0 1px #9bb3d9;
+  transform: translateX(50%);
+}
+
+.ht::after {
+  content: '';
+  position: absolute;
+  top: 10px;
+  right: 8px;
+  z-index: 0;
+  bottom: -27px;
+  width: 1px;
+  background: #d8e1ec;
+  transform: translateX(50%);
+}
+
+.hrow.last-log .ht::after { display: none; }
+
 .hb { flex: 1; min-width: 0; }
 
 .hl {

+ 3 - 4
src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js

@@ -162,15 +162,14 @@ export const canOperate = {
   dispatch: (o) => Number(o?.orderStatus) === WO_STATUS.PENDING_DISPATCH,
   /** 待接单可核查接单 */
   accept: (o) => Number(o?.orderStatus) === WO_STATUS.PENDING_ACCEPT,
-  /** 已接单/延期中可处置(可多次);处理中已开始处置,不再显示「开始处理」 */
+  /** 已接单/延期中可处置;处理中不允许重复处置 */
   process: (o) => [WO_STATUS.ACCEPTED, WO_STATUS.DELAYING].includes(Number(o?.orderStatus)),
   /** 处理中/延期中可提交验收 */
   submitVerify: (o) => [WO_STATUS.PROCESSING, WO_STATUS.DELAYING].includes(Number(o?.orderStatus)),
   /** 待验收可验收 */
   verify: (o) => Number(o?.orderStatus) === WO_STATUS.PENDING_VERIFY,
-  /** 待派单/待接单/已接单/处理中/待验收可取消 */
-  cancel: (o) => [WO_STATUS.PENDING_DISPATCH, WO_STATUS.PENDING_ACCEPT, WO_STATUS.ACCEPTED,
-    WO_STATUS.PROCESSING, WO_STATUS.PENDING_VERIFY].includes(Number(o?.orderStatus)),
+  /** 仅待派单/待接单可取消 */
+  cancel: (o) => [WO_STATUS.PENDING_DISPATCH, WO_STATUS.PENDING_ACCEPT].includes(Number(o?.orderStatus)),
   /** 处理中且无延期申请可申请延期 */
   delay: (o) => Number(o?.orderStatus) === WO_STATUS.PROCESSING
     && Number(o?.delayApplyStatus ?? 0) === DELAY_APPLY_STATUS.NONE,

+ 2 - 2
src/views/subSystem/pipeNetwork/pAlarmMonitor/PEqWarning.vue

@@ -72,7 +72,7 @@
         <el-form-item label="状态">
           <el-select v-model="queryParams.status" placeholder="全部状态" clearable style="width:150px">
             <el-option label="草稿" value="DRAFT"/>
-            <el-option label="待办" value="PENDING"/>
+            <el-option label="待接收" value="PENDING"/>
             <el-option label="已接收" value="RECEIVED"/>
             <el-option label="处理中" value="PROCESSING"/>
             <el-option label="已处置" value="HANDLED"/>
@@ -517,7 +517,7 @@ function renderTypeChart() {
 // ========== 辅助函数 ==========
 const isPending = (status) => status === 'PENDING'
 const isReceived = (status) => status === 'RECEIVED'
-const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待办', RECEIVED:'已接收', PROCESSING:'处理中', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
+const getStatusText = (s) => ({ DRAFT:'草稿', PENDING:'待接收', RECEIVED:'已接收', PROCESSING:'处置中', HANDLED:'已处置', CLOSED:'已解除' }[s]||s)
 const getStatusType = (s) => ({ DRAFT:'info', PENDING:'danger', RECEIVED:'warning', PROCESSING:'warning', HANDLED:'primary', CLOSED:'success' }[s]||'')
 const getLevelText = (lv) => ({ '1':'1 级 (蓝)','2':'2 级 (黄)','3':'3 级 (橙)','4':'4 级 (红)' }[lv]||'-' )
 const getLevelType = (lv) => ({ '1':'','2':'warning','3':'warning','4':'danger' }[lv]||'')

+ 1 - 1
src/views/subSystem/waterSupply/components/WaterAlarmPage.vue

@@ -84,7 +84,7 @@ const detailRef = ref()
 const trendRef = ref(), warningRef = ref(), deviceRef = ref()
 let trendChart, warningChart, deviceChart
 const { alarm_level: levels } = useDict('alarm_level')
-const displayStatuses = [{ label: '待审核', value: 1 }, { label: '已转工单', value: 2 }, { label: '已解除', value: 3 }, { label: '误报警', value: 4 }]
+const displayStatuses = [{ label: '待核实', value: 1 }, { label: '已转工单', value: 2 }, { label: '已解除', value: 3 }, { label: '误报警', value: 4 }]
 const metricCards = computed(() => [
   { key: 'totalCount', label: '报警总数', unit: '条', icon: BellFilled, tone: 'tone-blue' },
   { key: 'alarmingDeviceCount', label: '报警中设备', unit: '台', icon: Monitor, tone: 'tone-red' }

+ 12 - 2
src/views/system/dept/index.vue

@@ -69,9 +69,9 @@
          </el-table-column>
          <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
             <template #default="scope">
-               <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">修改</el-button>
+               <el-button v-if="!protectedDeptIds.includes(scope.row.deptId)" link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['system:dept:edit']">修改</el-button>
                <el-button link type="primary" icon="Plus" @click="handleAdd(scope.row)" v-hasPermi="['system:dept:add']">新增</el-button>
-               <el-button v-if="scope.row.parentId != 0" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">删除</el-button>
+               <el-button v-if="scope.row.parentId != 0 && !protectedDeptIds.includes(scope.row.deptId)" link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['system:dept:remove']">删除</el-button>
             </template>
          </el-table-column>
       </el-table>
@@ -154,6 +154,8 @@ const title = ref("");
 const deptOptions = ref([]);
 const isExpandAll = ref(true);
 const refreshTable = ref(true);
+// 系统内置部门,禁止在前端修改/删除
+const protectedDeptIds = [100, 101, 102, 103];
 
 const data = reactive({
   form: {},
@@ -237,6 +239,10 @@ function toggleExpandAll() {
 
 /** 修改按钮操作 */
 function handleUpdate(row) {
+  if (protectedDeptIds.includes(row.deptId)) {
+    proxy.$modal.msgError("该部门为系统内置,不允许修改");
+    return;
+  }
   reset();
   listDeptExcludeChild(row.deptId).then(response => {
     deptOptions.value = proxy.handleTree(response.data, "deptId");
@@ -271,6 +277,10 @@ function submitForm() {
 
 /** 删除按钮操作 */
 function handleDelete(row) {
+  if (protectedDeptIds.includes(row.deptId)) {
+    proxy.$modal.msgError("该部门为系统内置,不允许删除");
+    return;
+  }
   proxy.$modal.confirm('是否确认删除名称为"' + row.deptName + '"的数据项?').then(function() {
     return delDept(row.deptId);
   }).then(() => {

+ 19 - 18
src/views/system/user/index.vue

@@ -133,7 +133,7 @@
                <el-table-column type="selection" width="50" align="center" />
                <el-table-column label="用户编号" align="center" key="userId" prop="userId" v-if="columns[0].visible" />
                <el-table-column label="用户名称" align="center" key="userName" prop="userName" v-if="columns[1].visible" :show-overflow-tooltip="true" />
-               <el-table-column label="用户昵称" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
+               <el-table-column label="用户姓名" align="center" key="nickName" prop="nickName" v-if="columns[2].visible" :show-overflow-tooltip="true" />
                <el-table-column label="部门" align="center" key="deptName" prop="dept.deptName" v-if="columns[3].visible" :show-overflow-tooltip="true" />
                <el-table-column label="手机号码" align="center" key="phonenumber" prop="phonenumber" v-if="columns[4].visible" width="120" />
                <el-table-column label="状态" align="center" key="status" v-if="columns[5].visible">
@@ -183,8 +183,20 @@
          <el-form :model="form" :rules="rules" ref="userRef" label-width="80px">
             <el-row>
                <el-col :span="12">
-                  <el-form-item label="用户昵称" prop="nickName">
-                     <el-input v-model="form.nickName" placeholder="请输入用户昵称" maxlength="30" />
+                  <el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
+                     <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
+                  </el-form-item>
+               </el-col>
+               <el-col :span="12">
+                  <el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
+                     <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" />
+                  </el-form-item>
+               </el-col>
+            </el-row>
+            <el-row>
+               <el-col :span="12">
+                  <el-form-item label="用户姓名" prop="nickName">
+                     <el-input v-model="form.nickName" placeholder="请输入用户姓名" maxlength="30" />
                   </el-form-item>
                </el-col>
                <el-col :span="12">
@@ -212,18 +224,6 @@
                   </el-form-item>
                </el-col>
             </el-row>
-            <el-row>
-               <el-col :span="12">
-                  <el-form-item v-if="form.userId == undefined" label="用户名称" prop="userName">
-                     <el-input v-model="form.userName" placeholder="请输入用户名称" maxlength="30" />
-                  </el-form-item>
-               </el-col>
-               <el-col :span="12">
-                  <el-form-item v-if="form.userId == undefined" label="用户密码" prop="password">
-                     <el-input v-model="form.password" placeholder="请输入用户密码" type="password" maxlength="20" />
-                  </el-form-item>
-               </el-col>
-            </el-row>
             <el-row>
                <el-col :span="12">
                   <el-form-item label="用户性别">
@@ -371,7 +371,7 @@ const upload = reactive({
 const columns = ref([
   { key: 0, label: `用户编号`, visible: true },
   { key: 1, label: `用户名称`, visible: true },
-  { key: 2, label: `用户昵称`, visible: true },
+  { key: 2, label: `用户姓名`, visible: true },
   { key: 3, label: `部门`, visible: true },
   { key: 4, label: `手机号码`, visible: true },
   { key: 5, label: `状态`, visible: true },
@@ -390,10 +390,11 @@ const data = reactive({
   },
   rules: {
     userName: [{ required: true, message: "用户名称不能为空", trigger: "blur" }, { min: 2, max: 20, message: "用户名称长度必须介于 2 和 20 之间", trigger: "blur" }],
-    nickName: [{ required: true, message: "用户昵称不能为空", trigger: "blur" }],
+    nickName: [{ required: true, message: "用户姓名不能为空", trigger: "blur" }],
     password: [{ required: true, message: "用户密码不能为空", trigger: "blur" }, { min: 5, max: 20, message: "用户密码长度必须介于 5 和 20 之间", trigger: "blur" }, { pattern: /^[^<>"'|\\]+$/, message: "不能包含非法字符:< > \" ' \\\ |", trigger: "blur" }],
+    deptId: [{ required: true, message: "归属部门不能为空", trigger: "change" }],
     email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: ["blur", "change"] }],
-    phonenumber: [{ pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
+    phonenumber: [{ required: true, message: "手机号码不能为空", trigger: "blur" }, { pattern: /^1[3|4|5|6|7|8|9][0-9]\d{8}$/, message: "请输入正确的手机号码", trigger: "blur" }]
   }
 });
 

+ 4 - 4
tests/waterAlarmAuditWorkbench.test.mjs

@@ -9,8 +9,8 @@ test('shared audit page requires a decision but leaves the opinion optional with
   const source = await page()
   assert.ok(source.includes("item.key !== 'UNREVIEWED'"))
   assert.ok(source.includes('v-for="item in reviewChoices"'))
-  assert.ok(source.includes("auditStatus: [{ required: true, message: '请选择审核结果'"))
-  assert.ok(source.includes("opinion: [{ max: 500, message: '审核意见最多500字', trigger: 'blur' }]"))
+  assert.ok(source.includes("auditStatus: [{ required: true, message: '请选择核实结果'"))
+  assert.ok(source.includes("opinion: [{ max: 500, message: '核实意见最多500字', trigger: 'blur' }]"))
   assert.ok(source.includes('placeholder="选填,可填写核实情况、判定依据或误报原因"'))
   assert.ok(!source.includes('请填写审核意见'))
   assert.ok(source.includes('auditStatus: undefined'))
@@ -41,10 +41,10 @@ test('queue and statistics use shared REST endpoints with subsystem scope and se
   assert.ok(!source.includes('list-note'))
 })
 
-test('list and detail omit the entire numeric snapshot for alarms without an actual value', async () => {
+test('list omits numeric snapshots and shared detail guards alarms without an actual value', async () => {
   const source = await page()
   const summary = await readFile('src/views/subSystem/alarm/AlarmSummary.vue', 'utf8')
-  assert.ok(source.includes('<div v-if="hasActualValue(row)" class="snapshot">'))
+  assert.ok(!source.includes('row.actualValue'))
   assert.ok(summary.includes('<p v-if="hasActualValue(alarm)">实际值:'))
 })
 

+ 4 - 4
tests/waterHomeModel.test.mjs

@@ -144,7 +144,7 @@ test('realtime alarm rows expose chinese friendly display fields', () => {
   assert.equal(rows[0].deviceName, '一号压力点')
   assert.equal(rows[0].alarmLevelText, '1')
   assert.equal(rows[0].location, '城南')
-  assert.equal(rows[0].displayStatusText, '待审核')
+  assert.equal(rows[0].displayStatusText, '待核实')
   assert.equal(rows[0].displayStatusType, 'warning')
   assert.equal(normalizeRealtimeAlarmRows([], 5).length, 0)
 })
@@ -174,7 +174,7 @@ test('realtime alarm rows derive transferred status from a live order number whe
   assert.equal(transferred.displayStatusType, 'primary')
   const pending = rows.find(item => item.id === 'legacy-pending')
   assert.equal(pending.displayStatus, 1)
-  assert.equal(pending.displayStatusText, '待审核')
+  assert.equal(pending.displayStatusText, '待核实')
 })
 
 test('realtime alarm rows never treat a cancelled order as transferred', () => {
@@ -183,7 +183,7 @@ test('realtime alarm rows never treat a cancelled order as transferred', () => {
   ], 5)
 
   assert.equal(rows[0].displayStatus, 1)
-  assert.equal(rows[0].displayStatusText, '待审核')
+  assert.equal(rows[0].displayStatusText, '待核实')
 })
 
 test('realtime alarm fallback keeps a live order as transferred even when the alarm row is already handled', () => {
@@ -353,7 +353,7 @@ test('home GIS facility points share device status color semantics by facility s
 })
 
 test('home quick entries cover the four workbench jumps', () => {
-  assert.deepEqual(HOME_QUICK_ENTRIES.map(item => item.label), ['设备异常管理', '报警管理', '报警审核', '监测报警一张图'])
+  assert.deepEqual(HOME_QUICK_ENTRIES.map(item => item.label), ['设备异常管理', '报警管理', '报警核实', '监测报警一张图'])
   assert.ok(HOME_QUICK_ENTRIES.every(item => item.suffix.startsWith('/')))
   assert.equal(HOME_QUICK_ENTRIES.find(item => item.key === 'audit').query.rootTypeId, 1)
 })

+ 11 - 1
tests/waterSupplyWorkOrderStatusDict.test.mjs

@@ -2,7 +2,17 @@ import assert from 'node:assert/strict'
 import { readFile } from 'node:fs/promises'
 import test from 'node:test'
 
-import { orderStatusType, orderStatusText } from '../src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
+import { canOperate, orderStatusType, orderStatusText } from '../src/views/subSystem/pipeNetwork/ops/workOrder/js/workOrderDict.js'
+
+test('cancellation is available only before a work order is accepted', () => {
+  for (const status of [0, 1]) {
+    assert.equal(canOperate.cancel({ orderStatus: status }), true)
+    assert.equal(canOperate.cancel({ orderStatus: String(status) }), true)
+  }
+  for (const status of [2, 3, 4, 5, 6, 7, -1, 8]) {
+    assert.equal(canOperate.cancel({ orderStatus: status }), false)
+  }
+})
 
 test('shared work-order dictionary covers the authoritative 0-7 status codes', () => {
   assert.deepEqual(