Переглянути джерело

完善工单管理,预警管理

zengy 4 днів тому
батько
коміт
75a98d0e8e

+ 6 - 2
CONTEXT.md

@@ -67,8 +67,12 @@ _Avoid_: 工单状态、报警记录状态
 _Avoid_: 报警转工单、工单接单
 
 **接单责任人**:
-一次工单派发中唯一负责接收并推进该工单的系统用户。
-_Avoid_: 接单人列表、维修人员姓名
+一次工单派发中唯一负责接收并推进该工单的系统用户,产品文案统一称“处置人”,接口仍为 receiveUser/receiveUserName。工单详情将处置人、核验人集中在设备信息之后的“处置相关人员”区域;故障信息在设备信息之前,故障描述位于首行。
+_Avoid_: 接单人列表、维修人员姓名、在产品界面继续使用责任人
+
+**工单与预警候选人员**:
+两端共用后端的业务 ID、人员角色、设备公司范围与权限筛选。设备类型直接父级 1/2/3 对应部门自身或祖先 101/102/103;工单无设备或任一业务设备父级 4 不限制候选部门。工单责任人/核验人要求 process/audit,预警处置人/核验人要求 handle/audit;用户启用且未删除,提交再次校验,关联损坏不放宽。核验人仅在合格候选中默认当前账户。共享设备的指定人员须能查看并办理本人业务。
+_Avoid_: 用派单人部门或用户管理数据范围代替设备公司范围
 
 **工单处置**:
 接单责任人对工单执行接单、处理和提交验收,管理人员随后完成验收或验收驳回的全过程。

+ 3 - 1
memory/index.md

@@ -34,6 +34,8 @@ npm run dev       # 默认 http://localhost:80
 
 ## 当前已知风险
 
+- 两端预警/工单详情已按内容优先和“处置相关人员”分区调整,工单相关文案统一“处置人”;后端补充 verifyUserName,两端昵称优先展示。工单 65 项、PC 10 项、移动端 2 项回归通过,两端构建和移动端类型检查通过;核验人姓名需更新后端生效,真实页面待部署验收(2026-10-16)。
+- PC/移动端工单派单与预警发布选人已统一按设备公司和角色权限过滤;工单无设备、两类业务父类 4 均不限制候选部门,核验人仅在合格候选中默认当前账户。后端再次校验,并支持共享设备公司外指定人员查看和办理。相关后端 83 项、PC 10 项、移动端 2 项测试通过,两端生产构建和移动端类型检查通过;需同步部署三端并验证真实登录态及 Kingbase 数据,无新增结构脚本(2026-10-16)。
 - 预警查看权限已统一为 `warning:warning:query`;部署管理后端时执行 `pipe-network-service/sql/migration_warning_query_permission.sql`,将原 list 菜单权限改为 query,保留角色关联并重新登录。脚本尚未在目标数据库执行(2026-10-15)。
 - 预警核验已拆分为 `warning:warning:audit`;同步部署前后端时执行后端 `sql/migration_warning_audit_permission.sql`,在角色管理中单独授权管理角色并重新登录。脚本尚未在目标数据库执行,不自动向维修处置角色授予核验权限(2026-10-15)。
 - 权限管理系统菜单迁移脚本已生成但尚未在目标 KingbaseES 执行;部署 PC 前端后执行 `pipe-network-service/sql/migration_permission_management_subsystem.sql`,重新登录以刷新动态路由(2026-10-15)。
@@ -69,7 +71,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
-- [2026-10-16:PC 预警发布、列表、详情与统一详情组件优化](sessions/2026-10-16.md)
+- [2026-10-16:预警统一详情、工单与预警选人及待办闭环](sessions/2026-10-16.md)
 - [2026-10-15:权限管理子系统、移动端工单权限、PC 登录页与门户高保真重构](sessions/2026-10-15.md)
 - [2026-10-14:故障通知筛选与两端核实命名、工单取消限制、本人登录日志、照片按组切换、性别单选、移动端接口分层及统一前缀](sessions/2026-10-14.md)
 - [2026-10-13:移动端工单管理、待办、详情与操作接入](sessions/2026-10-13.md)

+ 4 - 0
memory/long-term.md

@@ -101,6 +101,10 @@
 
 ## 产品级决策
 
+- PC 与移动端详情的故障描述、预警内容使用普通字段内容的字体、字号和颜色,不加粗;保留首行独占布局与文本换行(2026-10-16)。
+- PC 与移动端详情布局统一:预警内容置于“预警信息”首行,人员区名为“处置相关人员”;工单信息按“故障信息 → 设备信息 → 处置相关人员”排列,故障描述独占故障信息首行,人员区展示处置人和核验人。工单责任人的产品文案统一为“处置人”,含列表、详情、派单选择、关联工单及操作提示;接口仍使用 receiveUser/receiveUserName,新增 verifyUserName 展示核验人昵称(2026-10-16)。
+- PC 与移动端工单、预警共用选人原则:设备类型直接父级 1/2/3 对应部门自身或祖先 101/102/103;工单无设备、两类业务设备父级为 4 时均不限制候选部门,有对应权限的公司外或无部门用户也可选。用户须启用且未删除,1/2/3 还要求部门有效;设备或类型关联损坏不得当作无设备放宽。工单责任人/核验人分别要求 `ops:workOrder:process`/`ops:workOrder:audit`,预警处置人/核验人分别要求 `warning:warning:handle`/`warning:warning:audit`,不复用用户管理的数据范围。当前账户仅在合格核验候选中默认选中,可改选;提交后端再次校验。候选查询须携带业务 ID 与人员角色,预警发布人仍默认创建人(2026-10-16)。
+- 预警管理、详情、归档及待办统一应用设备公司范围与参与人可见性;共享父类 4 的指定处置人/核验人可跨公司查看和办理本人预警,此例外不放宽其他草稿或非共享预警。拥有 publish 权限者额外可见公司范围内系统自动创建预警及所有 DRAFT 草稿。待办按 DRAFT+publish、本人处置+handle+PENDING/RECEIVED/PROCESSING、本人核验+audit+HANDLED 计算,不依赖待办表必须存在记录。移动端预警操作成功后返回列表允许定向刷新,普通再次显示仍不重复加载(2026-10-16)。
 - PC 预警详情统一使用 `lifeCompany/components/WarningDetailDrawer.vue` 和 `GET /warning/detail/full/{warningId}`;发布、待办、处置、查询、关联管理、首页/今日/历史概览及管网预警入口不得再各自维护详情抽屉。公共组件负责基础信息、关联事项/设备/报警、处置时间轴、督办、附件和电子存档,页面只保留通知已读、编辑、处置操作等入口特有逻辑。旧简版 `GET /warning/detail/{warningId}` 已移除(2026-10-16)。
 - 预警列表与详情查看权限统一为 `warning:warning:query`,不再使用 `warning:warning:list`;列表、待办、搜索、跟踪、归档和通知已读同步,现有详情、附件、流程与统计沿用 query。菜单迁移仅更新旧权限值,保留菜单 ID 和角色关联,处置 handle、核验 audit 独立不变(2026-10-15)。
 - 预警维修接收、开始和提交处置使用 `warning:warning:handle`;管理核验解除/退回使用独立的 `warning:warning:audit`,包括 `/clear`、`/resolve`、`/return`、`/misreport`。仍须 HANDLED 且为指定 reviewer(历史为空回退 publisher),权限不代替人员和状态限制。PC 待办、预警处置和管网预警入口统一为“核验”表单,必选“核查解除/退回办理”、必填核验意见;退回仍由原处置人办理。迁移脚本只新增菜单,不从处置权限自动继承核验权限(2026-10-15)。

+ 33 - 0
memory/sessions/2026-10-16.md

@@ -77,3 +77,36 @@
 - 新增/编辑预警的关联设备选择器左栏直接使用 `/equipmentType`(`equipment_type`)接口返回的 `id`、`typeName`,不再由前端推导并展示设备根大类。
 - 左栏接口类型与当前预警类型在 `equipment_warning_type` 中的关联 ID 取交集,只展示实际允许的具体设备类型;点击类型后通过 `equipmentTypeId` 精确筛选,选择“全部类型”时仍通过 `equipmentTypeIds` 限制在允许范围内。
 - `LRelease.vue` 模板与脚本编译、11 项预警测试及 `git diff --check` 通过,Vite 热更新无错误。
+
+## 两端发布人员与待办闭环
+
+- 目标:PC/移动端统一按设备所属公司及角色权限筛选处置人、核验人;核验人仅在当前账户具备 audit 权限且属于合格候选集合时默认当前账户,仍可改选其他人。
+- 后端 `WarningParticipantService` 按关联设备的直接父类型与部门自身/祖先公司匹配候选人员,处置 handle、核验 audit 分开;权限过滤在分页前执行,发布再次校验人员资格。新增 `WarningAccessService`/`WarningAccessScope`,统一管理、详情、归档、流程操作及移动端数据范围;公司范围不对管理员豁免。
+- `EarlyWarningMapper.xml` 复用可见性/待办 SQL:公司范围内有 publish 权限可处理所有 DRAFT,不再要求系统自动创建;待办按权限、指定人员与状态计算,不依赖待办记录;发布时间为空时用创建时间过滤。发布人保持创建人。
+- PC `LRelease.vue` 使用 warningId/role 查询候选部门和人员;新增 `components/WarningPublishAction.vue`,接入 `WarningWorkflowActions.vue`,支持草稿直接发布、远程搜索、合格当前账户默认及异步防覆盖。`LwarningDisp.vue` 保留服务端 availableActions。
+- 移动端 `WarningActionDrawer.vue`、`warning-detail.vue`、`services/warnings.ts` 使用相同候选规则;选择器增加占位项,允许选第一个实际人员;操作成功增加版本标记,待办/管理列表返回时仅按预警变更定向刷新,普通 onShow 不重复请求。
+- 验证:后端 MobileControllerMappingTest、WarningParticipantServiceTest、WarningScopeMapperTest 合计 15 项通过;PC `node --test tests/warning*.test.mjs` 15 项通过,`npm run build:prod` 成功(此前 xlsx 构建问题当前已不复现);移动端类型检查、H5 构建及 2 项抽屉行为测试通过。三仓 `git diff --check` 通过。
+- 风险/后续:尚未进行真实 Kingbase 与登录态端到端联调,需同步部署 PC、移动端及后端;候选权限依赖实际角色授权与部门/设备关联数据。无新增数据库结构迁移,保留此前已有修改。
+
+## 工单与预警统一选人及共享设备例外
+
+- 目标/结论:工单选人与预警对齐,设备直接父类型 1/2/3 对应部门自身或祖先 101/102/103;工单没有设备或两类业务父类型为 4 时不按候选部门过滤,仅要求用户启用、未删除且具备角色对应权限。公司外或无部门合格用户可选,关联损坏不作为无设备放宽。
+- 后端 `WorkOrderServiceImpl`、`SysUserMapper` 新增业务候选查询,工单 handler/reviewer 分别要求 process/audit 权限;候选 API 携带 orderId/type,提交派单再次校验。`WarningParticipantMapper` 父类型 4 不限制部门。工单派单后按指定责任人/核验人与状态办理;`EarlyWarningMapper` 为共享设备指定人员增加可见性例外,保证公司外候选能够办理,不扩大其他预警和草稿范围。
+- PC `ops/workOrder/dispose.vue` 与 `api/pipeNetwork/ops/workOrder.js` 携带工单及角色查询部门/人员,合格当前账户才默认核验人,异步结果不覆盖改选或新工单。移动端 `services/workorders.ts`、`workorder-detail.vue` 同步参数,`ActionFormDrawer.vue` 使用核验人统一文案;预警空候选提示兼容共享设备。
+- 验证:后端五类回归合计 83 项通过(64 工单、8 Controller、6 预警人员、3 预警范围、2 Mapper SQL);PC 工单/预警/状态行为测试 10 项、移动端预警抽屉 2 项通过。PC 生产构建、移动端类型检查及 H5 构建通过,三仓 diff 空白检查通过。新增回归覆盖无设备/父类 4、常规公司范围、角色权限、提交复核和公司外人员后续办理。
+- 风险/后续:无结构迁移;工单候选接口新增必填业务参数,需要后端、PC、移动端同步部署。尚未进行真实 Kingbase 与登录态端到端联调,保留已有修改。
+
+## 两端详情内容优先与处置人员分区
+
+- 目标/结论:预警内容放进“预警信息”第一行,人员区域统一为“处置相关人员”。工单详情顺序改为故障信息、设备信息、处置相关人员,故障描述独占首行并保留换行,人员区单独展示处置人、核验人;工单产品文案将责任人统一称为处置人。
+- PC 修改共享 `WarningDetailDrawer.vue`,拆出设备与人员分区,所有预警详情入口同步;`warningDetail.js` 兼容人员昵称的驼峰/下划线字段。工单 `dispose.vue` 重排详情、修改派单及提示,工单 index/list、供水旧工单页和报警关联工单同步改名。
+- 移动端 `warning-detail.vue`、`workorder-detail.vue` 同步内容与分区顺序,`ActionFormDrawer.vue` 和报警关联工单同步改名,`services/workorders.ts` 增加 verifyUserName 类型。
+- 后端 `WorkOrderVO` 新增 verifyUserName,`WorkOrderServiceImpl` 将处置人与核验人 ID 合并批量查询,昵称优先回填;派单及身份校验提示、新派单日志同步使用处置人。接口原有 ID 字段及选人权限不变,历史存储日志不改写。
+- 验证:新增工单详情两人昵称单批回填回归,工单 Service 65 项通过;PC 相关行为 10 项、移动端 2 项通过;PC 生产构建、移动端类型检查/H5 构建通过,最后追加的关联工单文案变更通过 SFC 编译。三仓 diff 空白检查通过。
+- 风险/后续:本次无数据库迁移;需部署后端新增字段以展示核验人姓名,真实登录页面与设备数据尚未端到端验收。保留三仓已有修改。
+
+## 详情描述字体统一
+
+- 按用户要求,PC 与移动端故障描述、预警内容改为普通字重;移动端去掉独立字号、颜色,PC 预警内容继承普通字段字号和颜色,保持首行位置及换行能力。
+- 修改两端预警与工单详情四个组件;仅涉及 CSS,无接口或后端改动。
+- 验证:四个 Vue 组件样式编译通过,两端 diff 空白检查通过;无需后端更新,真实页面待用户验收。

+ 4 - 4
src/api/pipeNetwork/ops/workOrder.js

@@ -88,11 +88,11 @@ export function delWorkOrder(orderId) {
 }
 
 /** 可指派部门树 */
-export function workOrderDeptTree() {
-  return request({ url: `${BASE_URL}/assignable/deptTree`, method: 'get' })
+export function workOrderDeptTree(params) {
+  return request({ url: `${BASE_URL}/assignable/deptTree`, method: 'get', params })
 }
 
 /** 可指派用户 */
-export function workOrderUsers(deptId) {
-  return request({ url: `${BASE_URL}/assignable/users`, method: 'get', params: { deptId } })
+export function workOrderUsers(params) {
+  return request({ url: `${BASE_URL}/assignable/users`, method: 'get', params })
 }

+ 2 - 2
src/api/smxyjldczxt/yjxxfb/yjfb.js

@@ -1,8 +1,8 @@
 import request from '@/utils/request'
 
 // 预警发布专用人员选择,沿用预警新增/修改/发布权限。
-export function getWarningParticipantDepartments() {
-  return request({ url: '/warning/participants/deptTree', method: 'get' })
+export function getWarningParticipantDepartments(params) {
+  return request({ url: '/warning/participants/deptTree', method: 'get', params })
 }
 
 export function getWarningParticipants(params) {

+ 1 - 1
src/views/subSystem/alarm/AlarmDetail.vue

@@ -9,7 +9,7 @@
           <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>
+        <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>

+ 1 - 1
src/views/subSystem/alarm/common/list.vue

@@ -36,7 +36,7 @@
         <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>
         </el-table-column>
-        <el-table-column prop="receiveUserName" label="责任人" width="100"><template #default="{ row }">{{ row.receiveUserName || '未分配' }}</template></el-table-column>
+        <el-table-column prop="receiveUserName" label="处置人" width="100"><template #default="{ row }">{{ row.receiveUserName || '未分配' }}</template></el-table-column>
         <el-table-column label="操作" width="80" fixed="right" align="center"><template #default="{ row }"><el-button link type="primary" @click="detailRef.open(row.id)">详情</el-button></template></el-table-column>
         <template #empty><el-empty :description="error ? '数据加载失败' : '暂无匹配报警'" /></template>
       </el-table>

+ 2 - 0
src/views/subSystem/lifeCompany/WarningWorkflowActions.vue

@@ -1,5 +1,6 @@
 <template>
   <span class="workflow-actions" @click.stop>
+    <WarningPublishAction :row="row" @changed="emit('changed')" />
     <el-button v-if="allowed('RECEIVE')" v-hasPermi="['warning:warning:handle']" link type="primary" :loading="busy" @click="receive">接收</el-button>
     <el-button v-if="allowed('START')" v-hasPermi="['warning:warning:handle']" link type="primary" :loading="busy" @click="start">开始处置</el-button>
     <el-button v-if="allowed('HANDLE')" v-hasPermi="['warning:warning:handle']" link type="warning" @click="openProcess">提交处置</el-button>
@@ -25,6 +26,7 @@
 
 <script setup>
 import WarningItemSummary from './components/WarningItemSummary.vue'
+import WarningPublishAction from './components/WarningPublishAction.vue'
 import WarningReviewAction from './components/WarningReviewAction.vue'
 import { computed, ref } from 'vue'
 import { ElMessage, ElMessageBox } from 'element-plus'

+ 19 - 7
src/views/subSystem/lifeCompany/components/WarningDetailDrawer.vue

@@ -38,6 +38,7 @@
 
       <template v-if="hasDetail">
         <section class="detail-section detail-section-first">
+          <div class="section-heading"><span>预警信息</span></div>
           <el-descriptions :column="2" border>
             <el-descriptions-item label="预警内容" :span="2">
               <div class="warning-content">{{ basic.warningContent || '-' }}</div>
@@ -49,16 +50,28 @@
             </el-descriptions-item>
             <el-descriptions-item label="预警专项">{{ basic.warningSpecial || '-' }}</el-descriptions-item>
             <el-descriptions-item label="权属单位">{{ basic.ownershipUnit || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="发布时间">{{ formatDateTime(basic.publishTime) }}</el-descriptions-item>
+            <el-descriptions-item v-if="basic.remark" label="备注" :span="2">{{ basic.remark }}</el-descriptions-item>
+          </el-descriptions>
+        </section>
+
+        <section class="detail-section">
+          <div class="section-heading"><span>设备信息</span></div>
+          <el-descriptions :column="2" border>
             <el-descriptions-item label="关联设备" :span="2">
               <span>{{ basic.deviceName || '-' }}</span>
               <span v-if="basic.deviceCode" class="secondary-text">({{ basic.deviceCode }})</span>
             </el-descriptions-item>
             <el-descriptions-item v-if="basic.location" label="设备位置" :span="2">{{ basic.location }}</el-descriptions-item>
-            <el-descriptions-item label="发布人">{{ basic.publisher || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="处置人">{{ basic.handler || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="核验人">{{ basic.reviewer || basic.publisher || '-' }}</el-descriptions-item>
-            <el-descriptions-item label="发布时间">{{ formatDateTime(basic.publishTime) }}</el-descriptions-item>
-            <el-descriptions-item v-if="basic.remark" label="备注" :span="2">{{ basic.remark }}</el-descriptions-item>
+          </el-descriptions>
+        </section>
+
+        <section class="detail-section">
+          <div class="section-heading"><span>处置相关人员</span></div>
+          <el-descriptions :column="2" border>
+            <el-descriptions-item label="发布人">{{ basic.publisherName || basic.publisher || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="处置人">{{ basic.handlerName || basic.handler || '-' }}</el-descriptions-item>
+            <el-descriptions-item label="核验人">{{ basic.reviewerName || basic.reviewer || basic.publisherName || basic.publisher || '-' }}</el-descriptions-item>
           </el-descriptions>
         </section>
 
@@ -295,8 +308,7 @@ function displayValue(value) {
 
 .warning-content {
   min-height: 24px;
-  color: #303133;
-  font-size: 14px;
+  font-weight: 400;
   line-height: 1.75;
   overflow-wrap: anywhere;
   white-space: pre-wrap;

+ 91 - 0
src/views/subSystem/lifeCompany/components/WarningPublishAction.vue

@@ -0,0 +1,91 @@
+<template>
+  <span @click.stop>
+    <el-button v-if="row.availableActions?.includes('PUBLISH')" v-hasPermi="['warning:warning:publish']" link type="success" @click="open">发布</el-button>
+    <el-dialog v-model="visible" title="发布预警" width="560px" append-to-body :close-on-click-modal="!busy" :before-close="close">
+      <el-alert title="按关联设备所属公司和操作权限筛选人员;核验人可改选。" type="info" :closable="false" />
+      <el-form label-width="80px" style="margin-top: 20px">
+        <el-form-item label="处置人" required>
+          <el-select v-model="handler" filterable remote clearable :remote-method="keyword => search('handler', keyword)" :loading="loading.handler" placeholder="输入姓名或账号搜索" style="width:100%">
+            <el-option v-for="person in options.handler" :key="person.userName" :value="person.userName" :label="label(person)" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="核验人" required>
+          <el-select v-model="reviewer" filterable remote clearable :remote-method="keyword => search('reviewer', keyword)" :loading="loading.reviewer" placeholder="输入姓名或账号搜索" style="width:100%">
+            <el-option v-for="person in options.reviewer" :key="person.userName" :value="person.userName" :label="label(person)" />
+          </el-select>
+        </el-form-item>
+        <el-alert v-if="error" :title="error" type="error" :closable="false" />
+        <el-button v-if="error" link type="primary" @click="loadInitial">重新加载候选人</el-button>
+        <div v-else class="hint">没有候选人员时,请检查设备所属公司下人员是否启用并已授予对应处置/核验权限。可输入关键字搜索更多人员。</div>
+      </el-form>
+      <template #footer><el-button :disabled="busy" @click="visible = false">取消</el-button><el-button type="primary" :loading="busy" :disabled="loading.handler || loading.reviewer || !!error" @click="submit">确认发布</el-button></template>
+    </el-dialog>
+  </span>
+</template>
+
+<script setup>
+import { reactive, ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import useUserStore from '@/store/modules/user'
+import { getWarningParticipants, releaseYjxxData } from '@/api/smxyjldczxt/yjxxfb/yjfb'
+
+const props = defineProps({ row: { type: Object, required: true } })
+const emit = defineEmits(['changed'])
+const user = useUserStore()
+const visible = ref(false), busy = ref(false), handler = ref(''), reviewer = ref(''), error = ref('')
+const options = reactive({ handler: [], reviewer: [] })
+const loading = reactive({ handler: false, reviewer: false })
+const versions = { handler: 0, reviewer: 0 }
+let dialogVersion = 0
+const id = () => props.row.warningId || props.row.warning_id
+const label = person => `${person.nickName || person.userName}(${person.deptName || person.userName})`
+const close = done => { if (!busy.value) done() }
+async function search(role, keyword = '') {
+  const version = ++versions[role]
+  const warningId = id()
+  const session = dialogVersion
+  loading[role] = true
+  try {
+    const res = await getWarningParticipants({ warningId, role, keyword, pageNum: 1, pageSize: 100 })
+    if (version !== versions[role] || !visible.value || session !== dialogVersion || warningId !== id()) return
+    options[role] = res.data?.records || []
+  } catch {
+    if (version === versions[role]) error.value = '候选人加载失败,请重新加载'
+  } finally { if (version === versions[role]) loading[role] = false }
+}
+async function loadInitial() {
+  const session = dialogVersion
+  const warningId = id()
+  error.value = ''
+  await Promise.all([search('handler'), search('reviewer')])
+  // 当前账号可能不在第一页,单独查询,仍以服务端权限筛选结果为准。
+  if (!visible.value || session !== dialogVersion || warningId !== id() || reviewer.value || error.value || !user.name) return
+  try {
+    const res = await getWarningParticipants({ warningId, role: 'reviewer', keyword: user.name, pageSize: 100 })
+    const current = res.data?.records?.find(person => person.userName === user.name)
+    if (!visible.value || session !== dialogVersion || warningId !== id() || reviewer.value || !current) return
+    if (!options.reviewer.some(person => person.userName === current.userName)) options.reviewer.unshift(current)
+    reviewer.value = current.userName
+  } catch { if (session === dialogVersion) error.value = '默认核验人加载失败,请重新加载或手动选择' }
+}
+function open() {
+  dialogVersion++
+  handler.value = ''; reviewer.value = ''; options.handler = []; options.reviewer = []
+  visible.value = true
+  return loadInitial()
+}
+async function submit() {
+  if (busy.value) return
+  if (!handler.value || !reviewer.value) return ElMessage.warning('请选择处置人和核验人')
+  busy.value = true
+  try {
+    const res = await releaseYjxxData(id(), { handler: handler.value, reviewer: reviewer.value })
+    if (res.code !== 200) return ElMessage.error(res.msg || '发布失败')
+    visible.value = false
+    ElMessage.success('发布成功')
+    emit('changed')
+  } finally { busy.value = false }
+}
+</script>
+
+<style scoped>.hint { color: var(--el-text-color-secondary); font-size: 12px; line-height: 1.6; }</style>

+ 12 - 2
src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue

@@ -401,6 +401,8 @@ const { warningStatusOptions, databaseStatusLabels } = useWarningStatuses()
 import WarningItemSummary from '../components/WarningItemSummary.vue';
 import {computed, onMounted, reactive, ref, watch} from "vue";
 import {ElMessage, ElMessageBox} from "element-plus";
+import useUserStore from '@/store/modules/user';
+const currentUser = useUserStore();
 import { Search } from "@element-plus/icons-vue";
 import { parseTime } from "@/utils/ruoyi";
 import { getWarningFullDetailData } from "@/api/pipeNetwork/earlyWarning";
@@ -441,6 +443,7 @@ const publishVisible = ref(false);
 const publishLoading = ref(false);
 const publishRow = ref(null);
 const publishForm = reactive({ handler: '', reviewer: '' });
+let publishDialogVersion = 0;
 const dialogTitle = ref("新增预警");
 const isEdit = ref(false);
 const formRef = ref();
@@ -778,7 +781,7 @@ async function loadUserPickerData() {
   deptTree.value = [];
   const users = loadUserList();
   try {
-    const res = await getWarningParticipantDepartments();
+    const res = await getWarningParticipantDepartments({ warningId: publishRow.value.warningId, role: pickerTarget.value });
     if (version !== deptRequestVersion || !userPickerVisible.value) return;
     deptTree.value = [{ id: '', label: '全部部门', children: res.data || [] }];
   } catch {
@@ -795,7 +798,7 @@ async function loadUserList() {
   userList.value = [];
   userTotal.value = 0;
   try {
-    const res = await getWarningParticipants({ ...userPage, deptId: selectedDeptId.value || undefined, keyword: userFilterText.value.trim() || undefined });
+    const res = await getWarningParticipants({ ...userPage, warningId: publishRow.value.warningId, role: pickerTarget.value, deptId: selectedDeptId.value || undefined, keyword: userFilterText.value.trim() || undefined });
     if (version !== userRequestVersion || !userPickerVisible.value) return;
     userList.value = res.data?.records || [];
     userTotal.value = Number(res.data?.total || 0);
@@ -1000,10 +1003,17 @@ async function handleDelete(row) {
 }
 
 async function handlePublish(row) {
+  const version = ++publishDialogVersion;
   publishRow.value = row;
   publishForm.handler = '';
   publishForm.reviewer = '';
   publishVisible.value = true;
+  // 服务端候选包含权限和设备公司校验;只有当前账号确实可核验才默认选中。
+  try {
+    const res = await getWarningParticipants({ warningId: row.warningId, role: 'reviewer', keyword: currentUser.name, pageSize: 100 });
+    if (version !== publishDialogVersion || publishRow.value?.warningId !== row.warningId || !publishVisible.value || publishForm.reviewer) return;
+    if ((res.data?.records || []).some(user => user.userName === currentUser.name)) publishForm.reviewer = currentUser.name;
+  } catch { ElMessage.warning('默认核验人加载失败,请手动选择'); }
 }
 
 async function submitPublish() {

+ 1 - 0
src/views/subSystem/lifeCompany/lwarningInfoCont/LwarningDisp.vue

@@ -319,6 +319,7 @@ function normalizeWarningRow(item) {
     reviewer: item.reviewer || "",
     ownershipUnit: item.ownershipUnit || item.ownership_unit || "",
     publishTime: item.publishTime || item.publish_time || "",
+    availableActions: item.availableActions || [],
     status: item.status || ""
   };
 }

+ 3 - 0
src/views/subSystem/lifeCompany/warningDetail.js

@@ -33,8 +33,11 @@ export function normalizeWarningBasic(source = {}, references = {}) {
     deviceCode: warningValue(source, 'deviceCode', 'device_code'),
     location: warningValue(source, 'location', 'deviceLocation', 'device_location'),
     publisher: warningValue(source, 'publisher'),
+    publisherName: warningValue(source, 'publisherName', 'publisher_name'),
     handler: warningValue(source, 'handler'),
+    handlerName: warningValue(source, 'handlerName', 'handler_name'),
     reviewer: warningValue(source, 'reviewer'),
+    reviewerName: warningValue(source, 'reviewerName', 'reviewer_name'),
     publishTime: warningValue(source, 'publishTime', 'publish_time'),
     remark: warningValue(source, 'remark'),
     status: warningValue(source, 'status'),

+ 70 - 27
src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue

@@ -125,6 +125,19 @@
 
             <!-- 工单信息 -->
             <div v-show="activeTab === 'info'" class="pane">
+              <div class="sect">
+                <div class="sect-t">故障信息</div>
+                <div class="kv">
+                  <div class="full"><div class="k">故障描述</div><div class="v fault-description">{{ selectedOrder.orderDesc || '-' }}</div></div>
+                  <div>
+                    <div class="k">优先级</div>
+                    <div class="v"><span class="pri" :class="priClass(selectedOrder.orderLevel)"><i></i>{{ orderLevelText(selectedOrder.orderLevel) }}</span></div>
+                  </div>
+                  <div><div class="k">故障发生时间</div><div class="v">{{ timeText(selectedOrder.faultTime) }}</div></div>
+                  <div><div class="k">计划完成时间</div><div class="v">{{ timeText(selectedOrder.planFinishTime) }}</div></div>
+                </div>
+              </div>
+
               <div class="sect">
                 <div class="sect-t">设备信息</div>
                 <div class="kv">
@@ -136,16 +149,10 @@
               </div>
 
               <div class="sect">
-                <div class="sect-t">故障信息</div>
+                <div class="sect-t">处置相关人员</div>
                 <div class="kv">
-                  <div>
-                    <div class="k">优先级</div>
-                    <div class="v"><span class="pri" :class="priClass(selectedOrder.orderLevel)"><i></i>{{ orderLevelText(selectedOrder.orderLevel) }}</span></div>
-                  </div>
-                  <div><div class="k">故障发生时间</div><div class="v">{{ timeText(selectedOrder.faultTime) }}</div></div>
-                  <div><div class="k">计划完成时间</div><div class="v">{{ timeText(selectedOrder.planFinishTime) }}</div></div>
-                  <div><div class="k">责任人</div><div class="v">{{ selectedOrder.receiveUserName || '—' }}</div></div>
-                  <div class="full"><div class="k">故障描述</div><div class="v">{{ selectedOrder.orderDesc || '-' }}</div></div>
+                  <div><div class="k">处置人</div><div class="v">{{ selectedOrder.receiveUserName || '—' }}</div></div>
+                  <div><div class="k">核验人</div><div class="v">{{ selectedOrder.verifyUserName || '—' }}</div></div>
                 </div>
               </div>
 
@@ -291,9 +298,9 @@
     <!-- 派单弹窗 -->
     <el-dialog v-model="dispatchVisible" title="工单派单" width="720px" append-to-body destroy-on-close>
       <el-form label-position="top">
-        <el-form-item label="责任人" required>
+        <el-form-item label="处置人" required>
           <div class="handler-box">
-            <el-input :model-value="dispatchForm.handlerName" placeholder="请选择责任人" readonly style="width: 240px">
+            <el-input :model-value="dispatchForm.handlerName" placeholder="请选择处置人" readonly style="width: 240px">
               <template #append><el-button @click="openUserSelect('handler')">选择</el-button></template>
             </el-input>
             <el-button v-if="dispatchForm.handlerId" link type="danger" @click="clearHandler">清除</el-button>
@@ -326,7 +333,7 @@
       </template>
     </el-dialog>
 
-    <!-- 选择责任人 / 核验人 -->
+    <!-- 选择处置人 / 核验人 -->
     <el-dialog v-model="userSelectVisible" :title="userSelectTitle" width="760px" append-to-body>
       <el-row :gutter="12">
         <el-col :span="8">
@@ -801,20 +808,36 @@ const dispatchForm = reactive({
   planFinishTime: ''
 })
 
-function openDispatch(o) {
+let dispatchSession = 0
+let reviewerEdited = false
+async function openDispatch(o) {
+  const session = ++dispatchSession
+  reviewerEdited = false
+  userSelectVisible.value = false
   dispatchForm.orderId = o.orderId
   dispatchForm.handlerId = o.receiveUser || null
   dispatchForm.handlerName = o.receiveUserName || ''
-  dispatchForm.verifyUser = userStore.id || null
-  dispatchForm.verifyUserName = userStore.nickName || userStore.name || ''
+  dispatchForm.verifyUser = null
+  dispatchForm.verifyUserName = ''
   dispatchForm.remarks = ''
   dispatchForm.planFinishTime = o.planFinishTime || ''
   dispatchVisible.value = true
+  try {
+    const res = await workOrderUsers({ orderId: o.orderId, type: 'reviewer' })
+    if (session !== dispatchSession || !dispatchVisible.value || reviewerEdited) return
+    const current = (res.data || []).find(user => String(user.userId) === String(userStore.id))
+    if (current) {
+      dispatchForm.verifyUser = current.userId
+      dispatchForm.verifyUserName = current.nickName || current.userName
+    }
+  } catch {
+    // 请求层已展示错误;候选加载失败时保持未选择。
+  }
 }
 
 async function submitDispatch() {
   if (!dispatchForm.handlerId) {
-    proxy.$modal.msgWarning('请选择责任人')
+    proxy.$modal.msgWarning('请选择处置人')
     return
   }
   if (!dispatchForm.verifyUser) {
@@ -838,22 +861,34 @@ async function submitDispatch() {
   }
 }
 
-/* ============ 责任人 / 核验人选择 ============ */
+/* ============ 处置人 / 核验人选择 ============ */
 const userSelectVisible = ref(false)
 const userSelectTarget = ref('handler')
-const userSelectTitle = computed(() => userSelectTarget.value === 'verifier' ? '选择核验人' : '选择责任人')
+const userSelectTitle = computed(() => userSelectTarget.value === 'verifier' ? '选择核验人' : '选择处置人')
 const deptTree = ref([])
 const deptUserList = ref([])
 const selectedUserId = ref(null)
 const selectedUser = ref(null)
 
-function loadDeptTree() {
-  workOrderDeptTree().then((res) => { deptTree.value = res.data || [] })
+let pickerRequest = 0
+let pickerSession = 0
+function candidateParams(deptId) {
+  return { orderId: dispatchForm.orderId, type: userSelectTarget.value === 'verifier' ? 'reviewer' : 'handler', deptId }
 }
-function loadDeptUsers(deptId) {
-  workOrderUsers(deptId).then((res) => { deptUserList.value = res.data || [] })
-}
-function openUserSelect(target) {
+async function loadDeptUsers(deptId) {
+  const request = ++pickerRequest
+  const session = dispatchSession
+  deptUserList.value = []
+  try {
+    const res = await workOrderUsers(candidateParams(deptId))
+    if (request === pickerRequest && session === dispatchSession && userSelectVisible.value) deptUserList.value = res.data || []
+  } catch { /* 请求层统一展示错误 */ }
+}
+async function openUserSelect(target) {
+  ++pickerRequest
+  const picker = ++pickerSession
+  const session = dispatchSession
+  if (target === 'verifier') reviewerEdited = true
   userSelectTarget.value = target
   if (target === 'verifier') {
     selectedUserId.value = dispatchForm.verifyUser
@@ -867,6 +902,14 @@ function openUserSelect(target) {
       : null
   }
   userSelectVisible.value = true
+  deptTree.value = []
+  const params = candidateParams()
+  const users = loadDeptUsers()
+  try {
+    const res = await workOrderDeptTree(params)
+    if (session === dispatchSession && picker === pickerSession && userSelectVisible.value) deptTree.value = res.data || []
+  } catch { /* 请求层统一展示错误 */ }
+  await users
 }
 function handleDeptNodeClick(node) { loadDeptUsers(node.id) }
 function handleUserCurrentChange(row) {
@@ -874,7 +917,7 @@ function handleUserCurrentChange(row) {
 }
 function handleUserRadioChange(row) { selectedUser.value = row; selectedUserId.value = row.userId }
 function confirmUserSelect() {
-  const label = userSelectTarget.value === 'verifier' ? '核验人' : '责任人'
+  const label = userSelectTarget.value === 'verifier' ? '核验人' : '处置人'
   if (!selectedUser.value) { proxy.$modal.msgWarning(`请选择${label}`); return }
   if (userSelectTarget.value === 'verifier') {
     dispatchForm.verifyUser = selectedUser.value.userId
@@ -892,6 +935,7 @@ function clearHandler() {
   selectedUserId.value = null
 }
 function clearVerifier() {
+  reviewerEdited = true
   dispatchForm.verifyUser = null
   dispatchForm.verifyUserName = ''
   selectedUser.value = null
@@ -1053,8 +1097,6 @@ if (route.query?.keyword) {
 
 onMounted(() => {
   handleSearch()
-  loadDeptTree()
-  loadDeptUsers()
 })
 </script>
 
@@ -1451,6 +1493,7 @@ onMounted(() => {
 .k { font-size: 11.5px; color: var(--txt3); margin-bottom: 2px; }
 .v { font-size: 13px; word-break: break-all; }
 .v.mono { font-family: ui-monospace, Consolas, "Courier New", monospace; }
+.fault-description { font-weight: 400; line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; }
 
 .pri {
   display: inline-flex;

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

@@ -74,7 +74,7 @@
         <el-table-column label="设备编码" prop="deviceCode" min-width="150" show-overflow-tooltip>
           <template #default="{ row }">{{ row.deviceCode || '-' }}</template>
         </el-table-column>
-        <el-table-column label="责任人" prop="receiveUserName" min-width="100" show-overflow-tooltip>
+        <el-table-column label="处置人" prop="receiveUserName" min-width="100" show-overflow-tooltip>
           <template #default="{ row }">{{ row.receiveUserName || '-' }}</template>
         </el-table-column>
         <el-table-column label="故障时间" prop="faultTime" width="180" align="center">

+ 1 - 1
src/views/subSystem/pipeNetwork/ops/workOrder/list.vue

@@ -58,7 +58,7 @@
         <el-table-column label="设备编码" prop="deviceCode" min-width="150" show-overflow-tooltip>
           <template #default="{ row }">{{ row.deviceCode || '-' }}</template>
         </el-table-column>
-        <el-table-column label="责任人" prop="receiveUserName" min-width="100" show-overflow-tooltip>
+        <el-table-column label="处置人" prop="receiveUserName" min-width="100" show-overflow-tooltip>
           <template #default="{ row }">{{ row.receiveUserName || '-' }}</template>
         </el-table-column>
         <el-table-column label="故障时间" prop="faultTime" width="180" align="center">

+ 3 - 3
src/views/subSystem/waterSupply/monitorAlarm/workOrder.vue

@@ -531,11 +531,11 @@ async function handleStart() {
 async function submitDispatch() {
   const handlerIds = dispatchForm.handlerIds || []
   if (handlerIds.length === 0) {
-    ElMessage.warning('请选择接单责任人')
+    ElMessage.warning('请选择处置人')
     return
   }
   if (handlerIds.length > 1) {
-    ElMessage.warning('新工单流程仅支持单个责任人,请只选择一位接单人')
+    ElMessage.warning('新工单流程仅支持单个处置人,请只选择一位处置人')
     return
   }
   try {
@@ -544,7 +544,7 @@ async function submitDispatch() {
       handlerId: handlerIds[0],
       remarks: dispatchForm.remarks
     })
-    ElMessage.success('派单成功!已指派给所选责任人')
+    ElMessage.success('派单成功!已指派给所选处置人')
     dispatchDialogVisible.value = false
     handleSearch()
   } catch (e) {

+ 90 - 0
tests/warningPublishAction.test.mjs

@@ -0,0 +1,90 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { reactive, ref } from 'vue'
+import { parse } from '@vue/compiler-sfc'
+
+const read = path => readFileSync(new URL(path, import.meta.url), 'utf8')
+const script = parse(read('../src/views/subSystem/lifeCompany/components/WarningPublishAction.vue')).descriptor.scriptSetup.content
+  .replace(/^import .*\r?\n/gm, '')
+const person = name => ({ userName: name, nickName: name, deptName: '设备所属公司' })
+
+function setup({ eligible = true, candidates } = {}) {
+  const calls = [], published = [], events = []
+  const props = reactive({ row: { warningId: 'w1', availableActions: ['PUBLISH'] } })
+  const api = async query => {
+    calls.push(query)
+    if (candidates) return candidates(query)
+    // 当前用户不在第一页;默认值也必须经过服务端 reviewer 候选校验。
+    return { data: { records: query.keyword === 'current' ? (eligible ? [person('current')] : []) : [person('other')] } }
+  }
+  const factory = new Function('reactive', 'ref', 'ElMessage', 'useUserStore', 'getWarningParticipants',
+    'releaseYjxxData', 'defineProps', 'defineEmits',
+    `${script}; return { visible, handler, reviewer, options, open, submit }`)
+  const state = factory(reactive, ref, { warning() {}, error() {}, success() {} }, () => ({ name: 'current' }),
+    api, async (id, data) => { published.push({ id, data }); return { code: 200 } },
+    () => props, () => event => events.push(event))
+  return { ...state, props, calls, published, events }
+}
+
+test('有核验资格才默认当前账户,处置人始终明确选择', async () => {
+  for (const eligible of [true, false]) {
+    const s = setup({ eligible })
+    await s.open()
+    assert.equal(s.handler.value, '')
+    assert.equal(s.reviewer.value, eligible ? 'current' : '')
+    if (eligible) assert.ok(s.options.reviewer.some(p => p.userName === 'current'))
+    assert.ok(s.calls.every(q => q.warningId === 'w1'))
+  }
+})
+
+test('默认核验人可改选,发布携带实际选择并通知列表刷新', async () => {
+  const s = setup()
+  await s.open()
+  s.handler.value = 'handler'
+  s.reviewer.value = 'other'
+  await s.submit()
+  assert.deepEqual(s.published, [{ id: 'w1', data: { handler: 'handler', reviewer: 'other' } }])
+  assert.equal(s.visible.value, false)
+  assert.deepEqual(s.events, ['changed'])
+})
+
+test('异步默认值不会覆盖手工选择,也不会跨预警或跨弹窗回填', async () => {
+  let resolveDefault
+  const s = setup({ candidates: query => query.keyword === 'current'
+    ? new Promise(resolve => { resolveDefault = resolve }) : Promise.resolve({ data: { records: [person('other')] } }) })
+  const first = s.open()
+  await new Promise(resolve => setImmediate(resolve))
+  s.reviewer.value = 'other'
+  resolveDefault({ data: { records: [person('current')] } })
+  await first
+  assert.equal(s.reviewer.value, 'other')
+
+  const oldSession = s.open()
+  await new Promise(resolve => setImmediate(resolve))
+  const finishOld = resolveDefault
+  s.visible.value = false
+  s.props.row.warningId = 'w2'
+  const newSession = s.open()
+  await new Promise(resolve => setImmediate(resolve))
+  finishOld({ data: { records: [person('current')] } })
+  await oldSession
+  assert.equal(s.reviewer.value, '')
+  resolveDefault({ data: { records: [] } })
+  await newSession
+  assert.equal(s.reviewer.value, '')
+})
+
+test('预警发布列表同样只根据服务端核验候选默认当前账户', async () => {
+  const source = parse(read('../src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue')).descriptor.scriptSetup.content
+  const code = source.slice(source.indexOf('async function handlePublish('), source.indexOf('async function submitPublish('))
+  for (const eligible of [true, false]) {
+    const form = reactive({ handler: 'old', reviewer: 'old' })
+    const run = new Function('publishRow', 'publishForm', 'publishVisible', 'currentUser', 'getWarningParticipants', 'ElMessage',
+      `let publishDialogVersion = 0; ${code}; return handlePublish`)(ref(null), form, ref(false), { name: 'current' },
+      async query => { assert.equal(query.role, 'reviewer'); return { data: { records: eligible ? [person('current')] : [] } } }, { warning() {} })
+    await run({ warningId: 'w1' })
+    assert.equal(form.handler, '')
+    assert.equal(form.reviewer, eligible ? 'current' : '')
+  }
+})

+ 60 - 0
tests/workOrderAssignment.test.mjs

@@ -0,0 +1,60 @@
+import test from 'node:test'
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import { reactive, ref, computed } from 'vue'
+import { parse } from '@vue/compiler-sfc'
+
+const source = parse(readFileSync(new URL('../src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue', import.meta.url), 'utf8')).descriptor.scriptSetup.content
+const code = source.slice(source.indexOf('const dispatchVisible = ref(false)'), source.indexOf('/* ============ 通用操作'))
+function setup(users = async () => ({ data: [] }), tree = async () => ({ data: [] })) {
+  return new Function('ref', 'reactive', 'computed', 'userStore', 'workOrderUsers', 'workOrderDeptTree',
+    `${code}; return { openDispatch, dispatchVisible, dispatchForm, openUserSelect, clearVerifier, deptTree, deptUserList, loadDeptUsers }`)(
+    ref, reactive, computed, { id: 66 }, users, tree)
+}
+const user = id => ({ userId: id, nickName: `昵称${id}` })
+
+test('工单核验人只在服务端合格候选中默认当前用户', async () => {
+  for (const eligible of [true, false]) {
+    const s = setup(async params => {
+      assert.deepEqual(params, { orderId: 1, type: 'reviewer' })
+      return { data: eligible ? [user(66)] : [user(77)] }
+    })
+    await s.openDispatch({ orderId: 1 })
+    assert.equal(s.dispatchForm.verifyUser, eligible ? 66 : null)
+  }
+})
+
+test('迟到的默认核验结果不覆盖改选、清除及新工单', async () => {
+  const pending = []
+  const s = setup(() => new Promise(resolve => pending.push(resolve)))
+  const first = s.openDispatch({ orderId: 1 })
+  s.clearVerifier()
+  pending.shift()({ data: [user(66)] })
+  await first
+  assert.equal(s.dispatchForm.verifyUser, null)
+  const old = s.openDispatch({ orderId: 1 })
+  const next = s.openDispatch({ orderId: 2 })
+  pending.shift()({ data: [user(66)] })
+  await old
+  assert.equal(s.dispatchForm.verifyUser, null)
+  pending.shift()({ data: [] })
+  await next
+})
+
+test('选人携带工单及角色,部门切换迟到结果不能覆盖新列表', async () => {
+  const pending = []
+  const s = setup(params => params.type === 'reviewer'
+    ? Promise.resolve({ data: [] }) : new Promise(resolve => pending.push({ params, resolve })))
+  await s.openDispatch({ orderId: 2 })
+  const open = s.openUserSelect('handler')
+  const all = pending.shift()
+  assert.deepEqual(all.params, { orderId: 2, type: 'handler', deptId: undefined })
+  const dept = s.loadDeptUsers(101)
+  const filtered = pending.shift()
+  assert.equal(filtered.params.deptId, 101)
+  filtered.resolve({ data: [user(88)] })
+  await dept
+  all.resolve({ data: [user(99)] })
+  await open
+  assert.equal(s.deptUserList.value[0].userId, 88)
+})