Sfoglia il codice sorgente

预警,报警等级类型统一

zengy 1 giorno fa
parent
commit
dc9cbf645c

+ 2 - 0
memory/index.md

@@ -34,6 +34,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 当前已知风险
 
+- 工单、预警、应急事件选人已统一按合法候选展示部门层级;直属上级非 `100` 时返回 `上级部门.当前部门`。移动端使用共用分组选择器,PC 事件/简版预警使用共用部门树选择器,旧供水报警故障工单页也已改用工单合法候选并补选核验人。后端、PC、移动端测试和构建通过;需部署三端后使用真实部门数据与账号权限验收(2026-10-22)。
 - 两端预警/工单详情已按内容优先和“处置相关人员”分区调整,工单相关文案统一“处置人”;后端补充 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)。
@@ -71,6 +72,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 会话记录
 
+- [2026-10-22:处置人/核验人按部门层级选择](sessions/2026-10-22.md)
 - [2026-10-21:事件字段与状态调整、设备台账两级类型快捷筛选](sessions/2026-10-21.md)
 - [2026-10-19:移动端故障权限及PC应急指挥与预案调整](sessions/2026-10-19.md)
 - [2026-10-16:预警统一详情、工单与预警选人及待办闭环](sessions/2026-10-16.md)

+ 23 - 0
memory/sessions/2026-10-22.md

@@ -0,0 +1,23 @@
+# 2026-10-22:处置人/核验人按部门层级选择
+
+## 需求
+
+- 工单、报警故障、预警、应急事件涉及选择处置人/核验人时,PC 和移动端统一按部门层级筛选。
+- 用户直属上级部门不是根部门 `100` 时,候选人部门展示为 `上级部门.当前部门`,例如 `供水公司.运维部`。
+- 保持设备所属公司、用户启用状态和角色操作权限过滤,不能因界面改造放宽候选范围。
+
+## 实现
+
+- 后端工单、预警、事件候选查询统一关联直属上级部门,通过 `CASE` 生成部门展示路径;直属上级为 `100` 时仍只显示当前部门。
+- 移动端新增 `DepartmentUserPicker.vue`,支持姓名/账号/部门搜索、上级部门与子部门筛选、按部门分组和选中回显,接入工单派单、预警发布、事件核实。
+- PC 新增 `DepartmentUserPicker/index.vue`,支持部门树、搜索和人员列表,接入事件核实和简版预警发布。
+- PC 标准工单页、预警发布页继续沿用原有服务端部门树,部门列展示新的路径名称。
+- 旧供水报警故障工单页不再调用全量系统用户接口,改用工单 `assignable/users` 合法候选,并同时选择处置人和核验人。
+- 报警确认本身不新增选人流程,后续派单仍统一进入工单合法候选链路。
+
+## 验证
+
+- 后端:`AssignmentCandidateMapperTest`、`SysUserContactsMapperContractTest`、`DispatchUserMapperContractTest` 通过,Maven reactor build success。
+- PC:生产构建通过;`workOrderAssignment.test.mjs`、`warningPublishAction.test.mjs` 共 7 项通过。
+- 移动端:`vue-tsc --noEmit`、2 项选人回归、H5 构建通过。
+- 未进行真实账号、真实 Kingbase 数据及手机实机联调,需部署三端后验收部门树、权限候选和提交。

+ 104 - 0
src/components/DepartmentUserPicker/index.vue

@@ -0,0 +1,104 @@
+<template>
+  <el-dialog :model-value="modelValue" :title="title" width="780px" append-to-body @close="close">
+    <el-input v-model="keyword" clearable placeholder="搜索姓名、账号或部门" style="margin-bottom: 14px" />
+    <el-row :gutter="14">
+      <el-col :span="8">
+        <div class="tree-box">
+          <el-tree
+            :data="departmentTree"
+            node-key="id"
+            default-expand-all
+            highlight-current
+            :expand-on-click-node="false"
+            :props="{ label: 'label', children: 'children' }"
+            @node-click="node => activeDepartment = node.filter"
+          />
+        </div>
+      </el-col>
+      <el-col :span="16">
+        <el-table :data="filteredOptions" height="340" border highlight-current-row @current-change="selectRow">
+          <el-table-column width="48" align="center">
+            <template #default="{ row }"><el-radio v-model="currentId" :value="identity(row)" @change="selectRow(row)" /></template>
+          </el-table-column>
+          <el-table-column label="姓名" min-width="105" show-overflow-tooltip>
+            <template #default="{ row }">{{ row.nickName || row.userName }}</template>
+          </el-table-column>
+          <el-table-column v-if="showAccount" label="账号" prop="userName" min-width="105" show-overflow-tooltip />
+          <el-table-column label="部门" min-width="180" show-overflow-tooltip><template #default="{ row }">{{ departmentName(row) }}</template></el-table-column>
+        </el-table>
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button @click="close">取消</el-button>
+      <el-button type="primary" :disabled="!current" @click="confirm">确定</el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref, watch } from 'vue'
+
+const props = defineProps({
+  modelValue: { type: Boolean, default: false },
+  title: { type: String, default: '选择人员' },
+  options: { type: Array, default: () => [] },
+  selectedId: { type: [String, Number], default: null },
+  valueKey: { type: String, default: 'userId' }
+})
+const emit = defineEmits(['update:modelValue', 'select'])
+const keyword = ref('')
+const activeDepartment = ref('')
+const currentId = ref(null)
+const current = ref(null)
+const identity = row => row?.[props.valueKey]
+const departmentName = row => String(row?.deptName || row?.dept?.deptName || '未分配部门')
+const pathParts = name => name.split(/[.·]/).filter(Boolean)
+const showAccount = computed(() => props.options.some(row => row.userName))
+
+const departmentTree = computed(() => {
+  const parents = new Map()
+  props.options.forEach(row => {
+    const full = departmentName(row)
+    const parts = pathParts(full)
+    const parent = parts.length > 1 ? parts[0] : full
+    if (!parents.has(parent)) parents.set(parent, new Set())
+    parents.get(parent).add(full)
+  })
+  const nodes = [...parents].map(([parent, departments]) => {
+    const names = [...departments]
+    return names.length === 1 && names[0] === parent
+      ? { id: `dept:${parent}`, label: parent, filter: parent }
+      : { id: `parent:${parent}`, label: parent, filter: `parent:${parent}`, children: names.map(name => ({ id: `dept:${name}`, label: name, filter: name })) }
+  })
+  return [{ id: 'all', label: '全部部门', filter: '', children: nodes }]
+})
+
+const filteredOptions = computed(() => {
+  const text = keyword.value.trim().toLowerCase()
+  return props.options.filter(row => {
+    const dept = departmentName(row)
+    if (activeDepartment.value.startsWith('parent:') && pathParts(dept)[0] !== activeDepartment.value.slice(7)) return false
+    if (activeDepartment.value && !activeDepartment.value.startsWith('parent:') && dept !== activeDepartment.value) return false
+    return !text || [row.nickName, row.userName, dept].some(value => String(value || '').toLowerCase().includes(text))
+  })
+})
+
+watch(() => props.modelValue, open => {
+  if (!open) return
+  keyword.value = ''
+  activeDepartment.value = ''
+  currentId.value = props.selectedId
+  current.value = props.options.find(row => String(identity(row)) === String(props.selectedId)) || null
+})
+watch(() => props.options, options => {
+  if (!props.modelValue || current.value) return
+  current.value = options.find(row => String(identity(row)) === String(currentId.value)) || null
+})
+function selectRow(row) { if (row) { current.value = row; currentId.value = identity(row) } }
+function close() { emit('update:modelValue', false) }
+function confirm() { if (current.value) { emit('select', current.value); close() } }
+</script>
+
+<style scoped>
+.tree-box { height: 340px; overflow: auto; padding: 8px; border: 1px solid var(--el-border-color); border-radius: 4px; }
+</style>

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

@@ -3,7 +3,7 @@
     <el-card class="search-card" shadow="never">
       <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.alarmLevel" clearable placeholder="全部等级"><el-option v-for="item in alarmLevels" :key="item.value" :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>

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

@@ -14,7 +14,7 @@
     <el-card shadow="never" class="alarm-filter-card"><el-form :model="filters" inline class="alarm-filters" @submit.prevent="search">
       <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" @keyup.enter="search" /></el-form-item>
       <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="v in alarmLevels" :key="v.value" :label="v.label" :value="Number(v.value)" /></el-select></el-form-item>
+      <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="v in alarmLevels" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
       <el-form-item label="监测状态"><el-select v-model="filters.alarmStatus" clearable placeholder="全部状态"><el-option v-for="v in options.monitorStatuses" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item>
       <el-form-item label="报警时间"><el-date-picker v-model="dates" 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>

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

@@ -3,7 +3,7 @@
     <div class="alarm-stats"><div v-for="v in alarmLevels" :key="v.value" class="alarm-stat" :style="{ '--level-color': levelColor(v.value) }"><span>{{ v.label }}</span><strong>{{ count(v.value) }}</strong><span class="muted">当前未恢复报警事件</span></div></div>
     <el-card shadow="never">
       <div class="alarm-toolbar"><div><el-tag :type="refreshError ? 'danger' : 'success'">{{ refreshError ? '刷新失败' : '定时刷新已开启' }}</el-tag><span class="muted"> · 每 30 秒刷新 · {{ refreshedAt || '正在加载' }}</span></div><el-button :icon="manualPause ? VideoPlay : VideoPause" @click="manualPause = !manualPause">{{ manualPause ? '恢复滚动' : '暂停滚动' }}</el-button></div>
-      <div class="alarm-toolbar"><el-form inline class="alarm-filters"><el-form-item label="报警等级"><el-select v-model="level" clearable placeholder="全部等级" @change="restart"><el-option v-for="v in alarmLevels" :key="v.value" :label="v.label" :value="Number(v.value)" /></el-select></el-form-item></el-form><span class="muted">{{ pending ? `有 ${pending} 条更新,恢复后显示` : `最近更新的 ${visibleRows.length} 条事件` }}</span></div>
+      <div class="alarm-toolbar"><el-form inline class="alarm-filters"><el-form-item label="报警等级"><el-select v-model="level" clearable placeholder="全部等级" @change="restart"><el-option v-for="v in alarmLevels" :key="v.value" :label="v.label" :value="v.value" /></el-select></el-form-item></el-form><span class="muted">{{ pending ? `有 ${pending} 条更新,恢复后显示` : `最近更新的 ${visibleRows.length} 条事件` }}</span></div>
       <el-alert v-if="refreshError" :title="refreshError" show-icon type="error" :closable="false"><el-button link @click="retry">重新加载</el-button></el-alert>
       <div ref="feed" v-loading="loading && !hasLoaded" class="reminder-feed" tabindex="0" @mouseenter="hoverPause = true" @mouseleave="hoverPause = false" @focusin="focusPause = true" @focusout="focusPause = false">
         <article v-for="row in visibleRows" :key="row.id" class="reminder-message" :style="{ borderLeftColor: levelColor(row.alarmLevel) }">

+ 38 - 12
src/views/subSystem/dispatch/disOnDuty/cphs.vue

@@ -134,20 +134,10 @@
           </el-select>
         </el-form-item>
         <el-form-item label="处置人" prop="handlerId">
-          <el-select
-            v-model="verifyForm.handlerId"
-            placeholder="请选择处置人"
-            filterable
-            style="width:100%"
-            @change="handleHandlerChange"
-          >
-            <el-option v-for="u in handlerOptions" :key="u.userId" :label="u.deptName ? `${u.nickName}(${u.deptName})` : u.nickName" :value="u.userId" />
-          </el-select>
+          <el-input :model-value="selectedUserLabel('handler')" placeholder="请选择处置人" readonly @click="openUserPicker('handler')" />
         </el-form-item>
         <el-form-item label="核验人" prop="verifierId">
-          <el-select v-model="verifyForm.verifierId" placeholder="请选择核验人" filterable clearable style="width:100%" @change="handleVerifierChange">
-            <el-option v-for="u in verifierOptions" :key="u.userId" :label="u.deptName ? `${u.nickName}(${u.deptName})` : u.nickName" :value="u.userId" />
-          </el-select>
+          <el-input :model-value="selectedUserLabel('verifier')" placeholder="请选择核验人" readonly @click="openUserPicker('verifier')" />
         </el-form-item>
         </template>
         <el-form-item label="核实意见" prop="verifyOpinion">
@@ -160,6 +150,14 @@
       </template>
     </el-dialog>
 
+    <DepartmentUserPicker
+      v-model="userPickerVisible"
+      :title="userPickerTarget === 'handler' ? '选择处置人' : '选择核验人'"
+      :options="userPickerTarget === 'handler' ? handlerOptions : verifierOptions"
+      :selected-id="userPickerTarget === 'handler' ? verifyForm.handlerId : verifyForm.verifierId"
+      @select="selectVerifyUser"
+    />
+
     <EventDetailDrawer ref="eventDetailRef" />
   </div>
 </template>
@@ -173,6 +171,7 @@ import { bd09ToWgs84 } from '@/utils/coordTransform'
 import { gisPointStatusIcons } from '@/utils/gisMapColors'
 import GisMapCanvas from '@/components/GisMapCanvas.vue'
 import EventDetailDrawer from './EventDetailDrawer.vue'
+import DepartmentUserPicker from '@/components/DepartmentUserPicker/index.vue'
 import { getEventPage, deleteEventInfo, verifyEventInfo, getEventStatusOptions, getHandlerOptions, getVerifierOptions } from '@/api/dispatch'
 import { Search, Refresh } from '@element-plus/icons-vue'
 
@@ -210,6 +209,8 @@ function formatGeo(lng, lat) {
 // ==================== 处置人 / 核验人 ====================
 const handlerOptions = ref([])
 const verifierOptions = ref([])
+const userPickerVisible = ref(false)
+const userPickerTarget = ref('handler')
 
 async function loadUserOptions() {
   try {
@@ -230,6 +231,31 @@ function handleVerifierChange(userId) {
   verifyForm.value.verifier = verifierOptions.value.find(item => String(item.userId) === String(userId))?.nickName || ''
 }
 
+function openUserPicker(target) {
+  const options = target === 'handler' ? handlerOptions.value : verifierOptions.value
+  if (!options.length) return ElMessage.warning(`暂无可选${target === 'handler' ? '处置人' : '核验人'}`)
+  userPickerTarget.value = target
+  userPickerVisible.value = true
+}
+
+function selectVerifyUser(user) {
+  if (userPickerTarget.value === 'handler') {
+    verifyForm.value.handlerId = user.userId
+    handleHandlerChange(user.userId)
+  } else {
+    verifyForm.value.verifierId = user.userId
+    handleVerifierChange(user.userId)
+  }
+  nextTick(() => verifyFormRef.value?.clearValidate(userPickerTarget.value === 'handler' ? 'handlerId' : 'verifierId'))
+}
+
+function selectedUserLabel(target) {
+  const options = target === 'handler' ? handlerOptions.value : verifierOptions.value
+  const id = target === 'handler' ? verifyForm.value.handlerId : verifyForm.value.verifierId
+  const user = options.find(item => String(item.userId) === String(id))
+  return user ? (user.deptName ? `${user.nickName}(${user.deptName})` : user.nickName) : ''
+}
+
 function handleVerifyResultChange(value) {
   if (value !== 1) return
   verifyForm.value.eventCategory = ''

+ 2 - 2
src/views/subSystem/drainage/alarm/alarmGIS.vue

@@ -120,10 +120,10 @@ const baseUrl = import.meta.env.VITE_APP_BASE_API
 const keyword = ref(''), level = ref(), options = ref({}), rows = ref([]), selected = ref(null), tab = ref('curve'), curveId = ref('')
 const loading = ref(false), error = ref(''), mapError = ref(''), mapReady = ref(false), mapKey = ref(0), mapRef = ref(), detailRef = ref(), cluster = ref([])
 const detailVisible = ref(false)
-const devices = computed(() => groupAlarmDevices(rows.value).sort((a, b) => a.alarmLevel - b.alarmLevel || String(a.deviceCode).localeCompare(String(b.deviceCode))))
+const devices = computed(() => groupAlarmDevices(rows.value).sort((a, b) => Number(a.alarmLevel) - Number(b.alarmLevel) || String(a.deviceCode).localeCompare(String(b.deviceCode))))
 const points = computed(() => devices.value.filter(d => d.hasCoordinates))
 const missingCoordinates = computed(() => devices.value.length - points.value.length)
-const levelMetrics = computed(() => (options.value.alarmLevels || []).map(item => ({ ...item, count: devices.value.filter(device => device.alarmLevel === Number(item.value)).length })))
+const levelMetrics = computed(() => (options.value.alarmLevels || []).map(item => ({ ...item, count: devices.value.filter(device => Number(device.alarmLevel) === Number(item.value)).length })))
 const latestAlarm = computed(() => selected.value?.alarms[0])
 const levelStyle = value => ({ '--grade-color': levelColor(value), '--grade-tint': levelColor(value) + '14' })
 const levelLabel = value => options.value.alarmLevels?.find(item => Number(item.value) === Number(value))?.label || '未知等级'

+ 13 - 7
src/views/subSystem/drainage/home.vue

@@ -657,11 +657,12 @@ async function loadEquipmentData() {
       const alarmDataList = alarmPageData?.records || alarmPageData?.rows || []
       console.log('[大屏调试] alarm_data返回:', alarmDataList.length, alarmDataList.length > 0 ? JSON.stringify(alarmDataList[0]) : '')
       deviceWarnData.value = alarmDataList.slice(0, 10).map(a => {
-        const lvl = a.alarmLevel != null ? a.alarmLevel : (a.alarm_level != null ? a.alarm_level : null)
-        // alarmLevel: 1-最高 4-最低,为null时根据warningCode推断
+        const raw = a.alarmLevel != null ? a.alarmLevel : (a.alarm_level != null ? a.alarm_level : null)
+        // alarmLevel: 1-最高 4-最低(字典编码字符串),统一转数值后比较
+        const lvl = raw == null || raw === '' ? null : Number(raw)
         let levelText = '--'
         let levelClass = 'yellow'
-        if (lvl != null && lvl > 0) {
+        if (lvl != null && Number.isFinite(lvl) && lvl > 0) {
           levelText = lvl === 1 ? '一级' : lvl === 2 ? '二级' : lvl === 3 ? '三级' : '四级'
           levelClass = lvl <= 2 ? 'red' : lvl === 3 ? 'orange' : 'yellow'
         } else {
@@ -669,7 +670,7 @@ async function loadEquipmentData() {
           levelClass = 'orange'
         }
         return {
-          level: lvl || 3,
+          level: lvl != null && Number.isFinite(lvl) ? lvl : 3,
           levelText,
           levelClass,
           code: a.deviceCode || a.device_code || '',
@@ -677,9 +678,14 @@ async function loadEquipmentData() {
           content: a.warningType || a.warning_type || a.remark || ''
         }
       })
-      // 报警统计
-      const general = alarmDataList.filter(a => (a.alarmLevel || a.alarm_level || 0) >= 3).length
-      const severe = alarmDataList.filter(a => (a.alarmLevel || a.alarm_level || 0) >= 1 && (a.alarmLevel || a.alarm_level || 0) <= 2).length
+      // 报警统计(等级为字典编码字符串,需转数值后比较)
+      const levelOf = a => {
+        const raw = a.alarmLevel != null ? a.alarmLevel : a.alarm_level
+        const n = raw == null || raw === '' ? NaN : Number(raw)
+        return Number.isFinite(n) ? n : 0
+      }
+      const general = alarmDataList.filter(a => levelOf(a) >= 3).length
+      const severe = alarmDataList.filter(a => levelOf(a) >= 1 && levelOf(a) <= 2).length
       alarmStat.value = { general, severe }
     } catch (e) {
       deviceWarnData.value = []

+ 3 - 3
src/views/subSystem/drainage/jcyj/dqyjgl.vue

@@ -265,9 +265,9 @@ const statusLegends = computed(() => [
   { name: '已接收', value: tableData.value.filter(o => o.status === 'RECEIVED').length, color: '#67C23A' }
 ])
 const levelLegends = computed(() => [
-  { name: '严重', value: tableData.value.filter(o => o.warningLevel === 1).length, color: '#F56C6C' },
-  { name: '重要', value: tableData.value.filter(o => o.warningLevel === 2).length, color: '#E6A23C' },
-  { name: '一般', value: tableData.value.filter(o => o.warningLevel === 3).length, color: '#409EFF' }
+  { name: '严重', value: tableData.value.filter(o => Number(o.warningLevel) === 1).length, color: '#F56C6C' },
+  { name: '重要', value: tableData.value.filter(o => Number(o.warningLevel) === 2).length, color: '#E6A23C' },
+  { name: '一般', value: tableData.value.filter(o => Number(o.warningLevel) === 3).length, color: '#409EFF' }
 ])
 const typeLegends = computed(() => {
   const types = [...new Set(tableData.value.map(o => o.deviceCode || o.warningType).filter(Boolean))]

+ 35 - 7
src/views/subSystem/lifeCompany/components/WarningPublishAction.vue

@@ -5,14 +5,10 @@
       <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-input :model-value="selectedLabel('handler')" readonly placeholder="请选择处置人" @click="openPicker('handler')" />
         </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-input :model-value="selectedLabel('reviewer')" readonly placeholder="请选择核验人" @click="openPicker('reviewer')" />
         </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>
@@ -20,6 +16,14 @@
       </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>
+    <DepartmentUserPicker
+      v-model="pickerVisible"
+      :title="pickerTarget === 'handler' ? '选择处置人' : '选择核验人'"
+      :options="options[pickerTarget]"
+      :selected-id="pickerTarget === 'handler' ? handler : reviewer"
+      value-key="userName"
+      @select="selectPerson"
+    />
   </span>
 </template>
 
@@ -28,17 +32,33 @@ import { reactive, ref } from 'vue'
 import { ElMessage } from 'element-plus'
 import useUserStore from '@/store/modules/user'
 import { getWarningParticipants, releaseYjxxData } from '@/api/smxyjldczxt/yjxxfb/yjfb'
+import DepartmentUserPicker from '@/components/DepartmentUserPicker/index.vue'
 
 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 pickerVisible = ref(false), pickerTarget = ref('handler')
 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 selectedLabel = role => {
+  const value = role === 'handler' ? handler.value : reviewer.value
+  const person = options[role].find(item => item.userName === value)
+  return person ? label(person) : ''
+}
+function openPicker(role) {
+  if (!options[role].length) return ElMessage.warning(`暂无可选${role === 'handler' ? '处置人' : '核验人'}`)
+  pickerTarget.value = role
+  pickerVisible.value = true
+}
+function selectPerson(person) {
+  if (pickerTarget.value === 'handler') handler.value = person.userName
+  else reviewer.value = person.userName
+}
 const close = done => { if (!busy.value) done() }
 async function search(role, keyword = '') {
   const version = ++versions[role]
@@ -48,7 +68,15 @@ async function search(role, keyword = '') {
   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 || []
+    const records = [...(res.data?.records || [])]
+    const total = Number(res.data?.total || records.length)
+    const pages = Math.ceil(total / 100)
+    for (let pageNum = 2; pageNum <= pages; pageNum++) {
+      const page = await getWarningParticipants({ warningId, role, keyword, pageNum, pageSize: 100 })
+      if (version !== versions[role] || !visible.value || session !== dialogVersion || warningId !== id()) return
+      records.push(...(page.data?.records || []))
+    }
+    options[role] = records
   } catch {
     if (version === versions[role]) error.value = '候选人加载失败,请重新加载'
   } finally { if (version === versions[role]) loading[role] = false }

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

@@ -114,7 +114,7 @@
           <template #default="{ row }">
             <div v-if="row.deviceCode || row.deviceName" class="device-cell">
               <span v-if="row.deviceName" class="device-name">{{ row.deviceName }}</span>
-              <el-tag v-if="row.deviceCode" :type="row.warningLevel === '4' ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
+              <el-tag v-if="row.deviceCode" :type="Number(row.warningLevel) === 4 ? 'danger' : 'info'" size="small" effect="plain">{{ row.deviceCode }}</el-tag>
             </div>
             <span v-else class="no-device">未关联</span>
           </template>

+ 2 - 2
src/views/subSystem/waterSupply/alarm/alarmGIS.vue

@@ -121,10 +121,10 @@ const baseUrl = import.meta.env.VITE_APP_BASE_API
 const keyword = ref(''), level = ref(), options = ref({}), rows = ref([]), selected = ref(null), tab = ref('curve'), curveId = ref('')
 const loading = ref(false), error = ref(''), mapError = ref(''), mapReady = ref(false), mapKey = ref(0), mapRef = ref(), detailRef = ref(), cluster = ref([])
 const detailVisible = ref(false)
-const devices = computed(() => groupAlarmDevices(rows.value).sort((a, b) => a.alarmLevel - b.alarmLevel || String(a.deviceCode).localeCompare(String(b.deviceCode))))
+const devices = computed(() => groupAlarmDevices(rows.value).sort((a, b) => Number(a.alarmLevel) - Number(b.alarmLevel) || String(a.deviceCode).localeCompare(String(b.deviceCode))))
 const points = computed(() => devices.value.filter(d => d.hasCoordinates))
 const missingCoordinates = computed(() => devices.value.length - points.value.length)
-const levelMetrics = computed(() => (options.value.alarmLevels || []).map(item => ({ ...item, count: devices.value.filter(device => device.alarmLevel === Number(item.value)).length })))
+const levelMetrics = computed(() => (options.value.alarmLevels || []).map(item => ({ ...item, count: devices.value.filter(device => Number(device.alarmLevel) === Number(item.value)).length })))
 const latestAlarm = computed(() => selected.value?.alarms[0])
 const levelStyle = value => ({ '--grade-color': levelColor(value), '--grade-tint': levelColor(value) + '14' })
 const levelLabel = value => options.value.alarmLevels?.find(item => Number(item.value) === Number(value))?.label || '未知等级'

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

@@ -29,7 +29,7 @@
       <el-form :inline="true" :model="filters" @submit.prevent="search">
         <el-form-item label="报警单号"><el-input v-model="filters.alarmNo" clearable placeholder="请输入报警单号" /></el-form-item>
         <el-form-item label="设备搜索"><el-input v-model="filters.keyword" clearable placeholder="请输入设备编码/名称" /></el-form-item>
-        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="level in levels" :key="level.value" :label="level.label" :value="Number(level.value)" /></el-select></el-form-item>
+        <el-form-item label="报警等级"><el-select v-model="filters.alarmLevel" clearable placeholder="全部等级"><el-option v-for="level in levels" :key="level.value" :label="level.label" :value="level.value" /></el-select></el-form-item>
         <el-form-item label="报警展示状态"><el-select v-model="filters.displayStatus" clearable placeholder="全部状态"><el-option v-for="status in displayStatuses" :key="status.value" v-bind="status" /></el-select></el-form-item>
         <el-form-item label="设备报警状态"><el-select v-model="filters.deviceAlarmStatus" clearable placeholder="全部状态"><el-option label="报警中" :value="1" /><el-option label="正常" :value="0" /></el-select></el-form-item>
         <el-form-item label="报警时间"><el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" /></el-form-item>

+ 46 - 116
src/views/subSystem/waterSupply/monitorAlarm/workOrder.vue

@@ -247,61 +247,24 @@
     </el-row>
 
     <!-- 派单弹窗 -->
-    <el-dialog v-model="dispatchDialogVisible" title="下派任务 - 选择接单人" width="750px">
-      <div style="display: flex; gap: 16px; min-height: 350px;">
-        <!-- 左侧:部门树 -->
-        <div style="width: 280px; border-right: 1px solid #e4e7ed; padding-right: 12px;">
-          <el-input
-            v-model="deptFilterText"
-            placeholder="筛选部门"
-            clearable
-            size="small"
-            style="margin-bottom: 8px"
-          />
-          <el-tree
-            ref="deptTreeRef"
-            :data="deptTreeData"
-            :props="{ label: 'deptName', children: 'children' }"
-            :filter-node-method="filterDeptNode"
-            node-key="deptId"
-            highlight-current
-            @node-click="handleDeptClick"
-            style="font-size: 14px"
-          />
-        </div>
-        <!-- 右侧:用户列表 -->
-        <div style="flex: 1; min-width: 0;">
-          <el-table
-            :data="userList"
-            border
-            size="small"
-            max-height="300"
-            ref="userTableRef"
-            row-key="userId"
-            @selection-change="handleSelectionChange"
-            @row-click="handleRowClick"
-          >
-            <el-table-column type="selection" width="40" align="center" :reserve-selection="true" />
-            <el-table-column prop="userName" label="用户名" width="100" />
-            <el-table-column prop="nickName" label="昵称" width="100" />
-            <el-table-column prop="dept.deptName" label="部门" show-overflow-tooltip />
-          </el-table>
-          <div v-if="userList.length === 0" style="text-align: center; color: #909399; padding: 40px 0;">请先在左侧选择部门</div>
-        </div>
-      </div>
+    <el-dialog v-model="dispatchDialogVisible" title="下派任务" width="560px">
       <el-form :model="dispatchForm" label-width="80px" style="margin-top: 16px">
+        <el-form-item label="处置人" required><el-input :model-value="assignmentLabel(selectedHandler)" readonly placeholder="请选择处置人" @click="openAssignmentPicker('handler')" /></el-form-item>
+        <el-form-item label="核验人" required><el-input :model-value="assignmentLabel(selectedReviewer)" readonly placeholder="请选择核验人" @click="openAssignmentPicker('reviewer')" /></el-form-item>
         <el-form-item label="备注"><el-input v-model="dispatchForm.remarks" placeholder="请输入派单备注" /></el-form-item>
       </el-form>
       <template #footer>
-      <span style="float: left; line-height: 32px; color: #606266;">
-        已选人员:<span v-if="selectedUserNames.length > 0" style="color: #409eff; font-weight: bold;">{{ selectedUserNames.join(', ') }}</span>
-        <span v-else style="color: #909399">未选择</span>
-        (<span v-if="dispatchForm.handlerIds && dispatchForm.handlerIds.length > 0">{{ dispatchForm.handlerIds.length }}</span><span v-else>0</span>)
-      </span>
       <el-button @click="dispatchDialogVisible = false">取消</el-button>
       <el-button type="primary" @click="submitDispatch">确认</el-button>
     </template>
     </el-dialog>
+    <DepartmentUserPicker
+      v-model="assignmentPickerVisible"
+      :title="assignmentTarget === 'handler' ? '选择处置人' : '选择核验人'"
+      :options="assignmentTarget === 'handler' ? handlerOptions : reviewerOptions"
+      :selected-id="assignmentTarget === 'handler' ? selectedHandler?.userId : selectedReviewer?.userId"
+      @select="selectAssignmentUser"
+    />
 
     <!-- 核查/处理弹窗 -->
     <el-dialog v-model="actionDialogVisible" :title="actionTitle" width="600px">
@@ -343,10 +306,8 @@
 <script setup name="BusinessWorkOrder">
 import { ElMessage } from 'element-plus'
 import ImagePreview from '@/components/ImagePreview'
-import { listWorkOrder, getWorkOrder, dispatchWorkOrder, acceptWorkOrder, processWorkOrder, submitVerifyWorkOrder, verifyWorkOrder, cancelWorkOrder } from '@/api/pipeNetwork/ops/workOrder.js'
-import { listDept } from '@/api/system/dept'
-import { listUser } from '@/api/system/user'
-import { handleTree } from '@/utils/ruoyi'
+import { listWorkOrder, getWorkOrder, dispatchWorkOrder, acceptWorkOrder, processWorkOrder, submitVerifyWorkOrder, verifyWorkOrder, cancelWorkOrder, workOrderUsers } from '@/api/pipeNetwork/ops/workOrder.js'
+import DepartmentUserPicker from '@/components/DepartmentUserPicker/index.vue'
 import {
   canOperate,
   orderLogText,
@@ -383,59 +344,25 @@ const workImageList = computed(() => {
 
 // 派单相关
 const dispatchDialogVisible = ref(false)
-const dispatchForm = reactive({ handlerIds: [], remarks: '' })
-const deptTreeRef = ref(null)
-const userTableRef = ref(null)
-const deptTreeData = ref([])
-const deptFilterText = ref('')
-const userList = ref([])
-const selectedUserList = ref([]) // 存储选中的用户行数据
-
-// 部门筛选
-watch(deptFilterText, (val) => { deptTreeRef.value && deptTreeRef.value.filter(val) })
-function filterDeptNode(value, data) {
-  if (!value) return true
-  return data.deptName.includes(value)
+const dispatchForm = reactive({ remarks: '' })
+const handlerOptions = ref([])
+const reviewerOptions = ref([])
+const selectedHandler = ref(null)
+const selectedReviewer = ref(null)
+const assignmentPickerVisible = ref(false)
+const assignmentTarget = ref('handler')
+const assignmentLabel = user => user ? (user.dept?.deptName ? `${user.nickName || user.userName}(${user.dept.deptName})` : user.deptName ? `${user.nickName || user.userName}(${user.deptName})` : user.nickName || user.userName) : ''
+function openAssignmentPicker(target) {
+  const options = target === 'handler' ? handlerOptions.value : reviewerOptions.value
+  if (!options.length) return ElMessage.warning(`暂无可选${target === 'handler' ? '处置人' : '核验人'}`)
+  assignmentTarget.value = target
+  assignmentPickerVisible.value = true
 }
-
-// 加载部门树
-async function loadDeptTree() {
-  if (deptTreeData.value.length > 0) return
-  try {
-    const res = await listDept()
-    deptTreeData.value = handleTree(res.data || [], 'deptId')
-  } catch (e) { console.error('加载部门失败', e) }
+function selectAssignmentUser(user) {
+  if (assignmentTarget.value === 'handler') selectedHandler.value = user
+  else selectedReviewer.value = user
 }
 
-// 点击部门加载用户
-async function handleDeptClick(node) {
-  try {
-    // 清除当前选择
-    userTableRef.value && userTableRef.value.clearSelection()
-    selectedUserList.value = []
-    dispatchForm.handlerIds = []
-
-    const res = await listUser({ deptId: node.deptId, pageNum: 1, pageSize: 100 })
-    userList.value = res.rows || []
-  } catch (e) { console.error('加载用户失败', e) }
-}
-
-// 选择变化时触发
-function handleSelectionChange(selection) {
-  selectedUserList.value = selection
-  dispatchForm.handlerIds = selection.map(item => item.userId)
-}
-
-// 点击行切换选中状态
-function handleRowClick(row) {
-  userTableRef.value && userTableRef.value.toggleRowSelection(row)
-}
-
-// 获取已选人员昵称(用于显示)
-const selectedUserNames = computed(() => {
-  return selectedUserList.value.map(item => item.nickName || item.userName)
-})
-
 /** 查询工单列表 */
 async function handleSearch() {
   listLoading.value = true
@@ -498,16 +425,25 @@ function canComplete() { return canOperate.submitVerify(selectedOrder.value) }
 function canClose() { return canOperate.verify(selectedOrder.value) }
 function canReject() { return canOperate.cancel(selectedOrder.value) }
 
-function handleDispatch() {
+async function handleDispatch() {
   if (!canDispatch()) {
     ElMessage.warning('仅可对待派单工单进行派单操作')
     return
   }
-  Object.assign(dispatchForm, { handlerIds: [], remarks: '' })
-  selectedUserList.value = []
-  userList.value = []
-  loadDeptTree()
+  dispatchForm.remarks = ''
+  selectedHandler.value = null
+  selectedReviewer.value = null
+  handlerOptions.value = []
+  reviewerOptions.value = []
   dispatchDialogVisible.value = true
+  try {
+    const [handlers, reviewers] = await Promise.all([
+      workOrderUsers({ orderId: selectedOrder.value.orderId, type: 'handler' }),
+      workOrderUsers({ orderId: selectedOrder.value.orderId, type: 'reviewer' })
+    ])
+    handlerOptions.value = handlers.data || []
+    reviewerOptions.value = reviewers.data || []
+  } catch { /* 请求层统一提示 */ }
 }
 
 async function handleAccept() {
@@ -529,19 +465,13 @@ async function handleStart() {
 }
 
 async function submitDispatch() {
-  const handlerIds = dispatchForm.handlerIds || []
-  if (handlerIds.length === 0) {
-    ElMessage.warning('请选择处置人')
-    return
-  }
-  if (handlerIds.length > 1) {
-    ElMessage.warning('新工单流程仅支持单个处置人,请只选择一位处置人')
-    return
-  }
+  if (!selectedHandler.value) return ElMessage.warning('请选择处置人')
+  if (!selectedReviewer.value) return ElMessage.warning('请选择核验人')
   try {
     await dispatchWorkOrder({
       orderId: selectedOrder.value.orderId,
-      handlerId: handlerIds[0],
+      handlerId: selectedHandler.value.userId,
+      verifyUser: selectedReviewer.value.userId,
       remarks: dispatchForm.remarks
     })
     ElMessage.success('派单成功!已指派给所选处置人')