Просмотр исходного кода

处置人,核验人做成统一选择控件

zengy 8 часов назад
Родитель
Сommit
83b7da19aa

+ 1 - 1
memory/index.md

@@ -34,7 +34,7 @@ npm run dev       # 默认 http://localhost:80
 
 ## 当前已知风险
 
-- 工单、预警、应急事件选人已统一按合法候选展示部门层级;直属上级非 `100` 时返回 `上级部门.当前部门`。移动端使用共用分组选择器,PC 事件/简版预警使用共用部门树选择器,旧供水报警故障工单页也已改用工单合法候选并补选核验人。后端、PC、移动端测试和构建通过;需部署三端后使用真实部门数据与账号权限验收(2026-10-22)。
+- 工单、预警、应急事件选人已统一按合法候选展示部门层级;直属上级非 `100` 时返回 `上级部门.当前部门`。两端选择器只保留姓名/账号/部门搜索和人员列表,移除独立部门树/顶部部门筛选;移动端同时移除分组人数和姓名首字头像。旧供水报警故障工单页也已改用工单合法候选并补选核验人。后端、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)。

+ 3 - 3
memory/sessions/2026-10-22.md

@@ -9,9 +9,9 @@
 ## 实现
 
 - 后端工单、预警、事件候选查询统一关联直属上级部门,通过 `CASE` 生成部门展示路径;直属上级为 `100` 时仍只显示当前部门。
-- 移动端新增 `DepartmentUserPicker.vue`,支持姓名/账号/部门搜索、上级部门与子部门筛选、按部门分组和选中回显,接入工单派单、预警发布、事件核实。
-- PC 新增 `DepartmentUserPicker/index.vue`,支持部门树、搜索和人员列表,接入事件核实和简版预警发布。
-- PC 标准工单页、预警发布页继续沿用原有服务端部门树,部门列展示新的路径名称。
+- 移动端新增 `DepartmentUserPicker.vue`,保留姓名/账号/部门搜索、按部门分组和选中回显,接入工单派单、预警发布、事件核实;按最新界面要求移除顶部部门筛选按钮、分组人数和姓名首字圆形头像。
+- PC 新增 `DepartmentUserPicker/index.vue`,只保留搜索和人员列表,接入事件核实和简版预警发布。
+- PC 标准工单页、预警发布页同步移除独立部门树/部门筛选,只在人员列表中展示新的部门路径名称。
 - 旧供水报警故障工单页不再调用全量系统用户接口,改用工单 `assignable/users` 合法候选,并同时选择处置人和核验人。
 - 报警确认本身不新增选人流程,后续派单仍统一进入工单合法候选链路。
 

+ 2 - 46
src/components/DepartmentUserPicker/index.vue

@@ -1,22 +1,7 @@
 <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 :data="filteredOptions" height="380" 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>
@@ -25,9 +10,7 @@
           </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>
+    </el-table>
     <template #footer>
       <el-button @click="close">取消</el-button>
       <el-button type="primary" :disabled="!current" @click="confirm">确定</el-button>
@@ -47,38 +30,16 @@ const props = defineProps({
 })
 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))
   })
 })
@@ -86,7 +47,6 @@ const filteredOptions = computed(() => {
 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
 })
@@ -98,7 +58,3 @@ function selectRow(row) { if (row) { current.value = row; currentId.value = iden
 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>

+ 4 - 65
src/views/subSystem/lifeCompany/lAlertInformation/LRelease.vue

@@ -342,28 +342,7 @@
       <el-alert v-if="userPickerError" :title="userPickerError" type="error" :closable="false" show-icon style="margin-bottom: 12px">
         <el-button link type="primary" @click="loadUserPickerData">重新加载</el-button>
       </el-alert>
-      <div class="user-picker-container">
-        <!-- 左侧部门树 -->
-        <div class="user-picker-left">
-          <div class="picker-title">部门列表</div>
-          <el-input v-model="deptFilterText" placeholder="搜索部门" clearable style="margin-bottom: 12px;" />
-          <el-tree
-            ref="deptTreeRef"
-            :data="deptTree"
-            node-key="id"
-            :props="{ label: 'label', children: 'children' }"
-            :filter-node-method="filterDeptNode"
-            :highlight-current="true"
-            default-expand-all
-            @node-click="handleDeptClick"
-          />
-        </div>
-        <!-- 右侧用户列表 -->
         <div class="user-picker-right">
-          <div class="picker-title">
-            用户列表
-            <span v-if="currentDeptName" class="current-dept">({{ currentDeptName }})</span>
-          </div>
           <el-input v-model="userFilterText" placeholder="搜索账号或姓名,回车查询" maxlength="100" clearable style="margin-bottom: 12px;" @keyup.enter="searchPickerUsers" @clear="searchPickerUsers">
             <template #append><el-button :icon="Search" @click="searchPickerUsers" /></template>
           </el-input>
@@ -379,9 +358,8 @@
             <el-table-column label="用户昵称" prop="nickName" min-width="120" show-overflow-tooltip />
             <el-table-column label="部门" prop="deptName" min-width="140" show-overflow-tooltip />
           </el-table>
-          <pagination v-show="userTotal > 0" :total="userTotal" v-model:page="userPage.pageNum" v-model:limit="userPage.pageSize" :auto-scroll="false" layout="total, prev, pager, next" @pagination="loadUserList" />
+          <pagination v-show="userTotal > userPage.pageSize" :total="userTotal" v-model:page="userPage.pageNum" v-model:limit="userPage.pageSize" :auto-scroll="false" layout="prev, pager, next" @pagination="loadUserList" />
         </div>
-      </div>
       <template #footer>
         <div class="dialog-footer">
           <el-button @click="userPickerVisible = false">取 消</el-button>
@@ -413,7 +391,6 @@ import {
   getdbyjlistData,
   getWarningReasonListData,
   getWarningItemListData,
-  getWarningParticipantDepartments,
   getWarningParticipants,
   getWarningEquipmentTypeIdsData,
   releaseYjxxData,
@@ -467,22 +444,15 @@ const deviceQuery = reactive({
 });
 
 // 用户选择器相关
-const deptTree = ref([]);
 const userList = ref([]);
 const userTotal = ref(0);
 const userPage = reactive({ pageNum: 1, pageSize: 10 });
-const deptPickerError = ref("");
 const userListError = ref("");
-const userPickerError = computed(() => deptPickerError.value || userListError.value);
+const userPickerError = computed(() => userListError.value);
 let userRequestVersion = 0;
-let deptRequestVersion = 0;
-const selectedDeptId = ref("");
-const currentDeptName = ref("");
 const selectedUser = ref(null);
 const userPickerLoading = ref(false);
-const deptFilterText = ref("");
 const userFilterText = ref("");
-const deptTreeRef = ref();
 
 const queryParams = reactive({
   pageNum: 1,
@@ -758,36 +728,13 @@ function resetQuery() {
   loadList();
 }
 
-// 部门树过滤
-watch(deptFilterText, (val) => {
-  deptTreeRef.value?.filter(val);
-});
-
-function filterDeptNode(value, data) {
-  if (!value) return true;
-  return data.label.indexOf(value) !== -1;
-}
-
 // 用户选择器相关函数
 async function loadUserPickerData() {
-  const version = ++deptRequestVersion;
-  deptPickerError.value = "";
   userList.value = [];
   userTotal.value = 0;
   selectedUser.value = null;
   userPage.pageNum = 1;
-  selectedDeptId.value = "";
-  currentDeptName.value = "全部部门";
-  deptTree.value = [];
-  const users = loadUserList();
-  try {
-    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 {
-    if (version === deptRequestVersion && userPickerVisible.value) deptPickerError.value = "部门加载失败,请重新加载";
-  }
-  await users;
+  await loadUserList();
 }
 
 async function loadUserList() {
@@ -798,7 +745,7 @@ async function loadUserList() {
   userList.value = [];
   userTotal.value = 0;
   try {
-    const res = await getWarningParticipants({ ...userPage, warningId: publishRow.value.warningId, role: pickerTarget.value, deptId: selectedDeptId.value || undefined, keyword: userFilterText.value.trim() || undefined });
+    const res = await getWarningParticipants({ ...userPage, warningId: publishRow.value.warningId, role: pickerTarget.value, keyword: userFilterText.value.trim() || undefined });
     if (version !== userRequestVersion || !userPickerVisible.value) return;
     userList.value = res.data?.records || [];
     userTotal.value = Number(res.data?.total || 0);
@@ -809,12 +756,6 @@ async function loadUserList() {
   }
 }
 
-function handleDeptClick(data) {
-  selectedDeptId.value = data.id;
-  currentDeptName.value = data.label;
-  searchPickerUsers();
-}
-
 function searchPickerUsers() {
   userPage.pageNum = 1;
   loadUserList();
@@ -822,7 +763,6 @@ function searchPickerUsers() {
 
 function closeUserPicker() {
   ++userRequestVersion;
-  ++deptRequestVersion;
   selectedUser.value = null;
   userPickerLoading.value = false;
 }
@@ -835,7 +775,6 @@ function openUserPicker(target) {
   pickerTarget.value = target;
   selectedUser.value = null;
   userFilterText.value = '';
-  deptFilterText.value = '';
   userPickerVisible.value = true;
 }
 

+ 16 - 29
src/views/subSystem/pipeNetwork/ops/workOrder/dispose.vue

@@ -334,18 +334,9 @@
     </el-dialog>
 
     <!-- 选择处置人 / 核验人 -->
-    <el-dialog v-model="userSelectVisible" :title="userSelectTitle" width="760px" append-to-body>
-      <el-row :gutter="12">
-        <el-col :span="8">
-          <div class="dept-tree-box">
-            <el-tree
-              :data="deptTree" :props="{ label: 'label', children: 'children' }" node-key="id"
-              highlight-current default-expand-all :expand-on-click-node="false" @node-click="handleDeptNodeClick"
-            />
-          </div>
-        </el-col>
-        <el-col :span="16">
-          <el-table :data="deptUserList" height="320" border highlight-current-row @current-change="handleUserCurrentChange">
+    <el-dialog v-model="userSelectVisible" :title="userSelectTitle" width="680px" append-to-body>
+      <el-input v-model="userSelectKeyword" clearable placeholder="搜索姓名、账号或部门" style="margin-bottom: 12px" />
+          <el-table :data="filteredDeptUserList" height="360" border highlight-current-row @current-change="handleUserCurrentChange">
             <el-table-column width="48" align="center">
               <template #default="{ row }">
                 <el-radio v-model="selectedUserId" :value="row.userId" @change="handleUserRadioChange(row)" />
@@ -355,8 +346,6 @@
             <el-table-column label="登录账号" prop="userName" show-overflow-tooltip />
             <el-table-column label="部门" prop="dept.deptName" show-overflow-tooltip />
           </el-table>
-        </el-col>
-      </el-row>
       <template #footer>
         <el-button @click="userSelectVisible = false">取消</el-button>
         <el-button type="primary" @click="confirmUserSelect">确定</el-button>
@@ -459,7 +448,6 @@ import {
   applyDelayWorkOrder,
   auditDelayWorkOrder,
   delWorkOrder,
-  workOrderDeptTree,
   workOrderUsers
 } from '@/api/pipeNetwork/ops/workOrder.js'
 import {
@@ -865,22 +853,28 @@ async function submitDispatch() {
 const userSelectVisible = ref(false)
 const userSelectTarget = ref('handler')
 const userSelectTitle = computed(() => userSelectTarget.value === 'verifier' ? '选择核验人' : '选择处置人')
-const deptTree = ref([])
 const deptUserList = ref([])
+const userSelectKeyword = ref('')
+const filteredDeptUserList = computed(() => {
+  const keyword = userSelectKeyword.value.trim().toLowerCase()
+  if (!keyword) return deptUserList.value
+  return deptUserList.value.filter(user => [user.nickName, user.userName, user.dept?.deptName]
+    .some(value => String(value || '').toLowerCase().includes(keyword)))
+})
 const selectedUserId = ref(null)
 const selectedUser = ref(null)
 
 let pickerRequest = 0
 let pickerSession = 0
-function candidateParams(deptId) {
-  return { orderId: dispatchForm.orderId, type: userSelectTarget.value === 'verifier' ? 'reviewer' : 'handler', deptId }
+function candidateParams() {
+  return { orderId: dispatchForm.orderId, type: userSelectTarget.value === 'verifier' ? 'reviewer' : 'handler' }
 }
-async function loadDeptUsers(deptId) {
+async function loadDeptUsers() {
   const request = ++pickerRequest
   const session = dispatchSession
   deptUserList.value = []
   try {
-    const res = await workOrderUsers(candidateParams(deptId))
+    const res = await workOrderUsers(candidateParams())
     if (request === pickerRequest && session === dispatchSession && userSelectVisible.value) deptUserList.value = res.data || []
   } catch { /* 请求层统一展示错误 */ }
 }
@@ -902,16 +896,9 @@ async 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
+  userSelectKeyword.value = ''
+  await loadDeptUsers()
 }
-function handleDeptNodeClick(node) { loadDeptUsers(node.id) }
 function handleUserCurrentChange(row) {
   if (row) { selectedUser.value = row; selectedUserId.value = row.userId }
 }

+ 11 - 12
tests/workOrderAssignment.test.mjs

@@ -8,7 +8,7 @@ const source = parse(readFileSync(new URL('../src/views/subSystem/pipeNetwork/op
 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 }`)(
+    `${code}; return { openDispatch, dispatchVisible, dispatchForm, openUserSelect, clearVerifier, deptUserList, loadDeptUsers }`)(
     ref, reactive, computed, { id: 66 }, users, tree)
 }
 const user = id => ({ userId: id, nickName: `昵称${id}` })
@@ -41,20 +41,19 @@ test('迟到的默认核验结果不覆盖改选、清除及新工单', async ()
   await next
 })
 
-test('选人携带工单及角色,部门切换迟到结果不能覆盖新列表', async () => {
+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
+  const first = s.openUserSelect('handler')
+  const old = pending.shift()
+  assert.deepEqual(old.params, { orderId: 2, type: 'handler' })
+  const second = s.loadDeptUsers()
+  const latest = pending.shift()
+  latest.resolve({ data: [user(88)] })
+  await second
+  old.resolve({ data: [user(99)] })
+  await first
   assert.equal(s.deptUserList.value[0].userId, 88)
 })