瀏覽代碼

Merge pull request #102 from GoldenZqqq/feature/bpm

工作流发起与审核页面具体细节优化
芋道源码 9 月之前
父節點
當前提交
3783582b33

+ 1 - 0
src/assets/svgs/bpm/add-user.svg

@@ -0,0 +1 @@
+<svg t="1731390087280" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4297" width="200" height="200"><path d="M639.9 541.7c76.4-44.2 127.9-126.8 127.9-221.5C767.7 179 653.2 64.5 512 64.5S256.3 179 256.3 320.2c0 89.6 46.1 168.4 115.8 214.1C193.5 593 64.5 761.2 64.5 959.5h63.9c0-211.5 172.1-383.6 383.6-383.6 44.9 0 87.8 8.1 127.9 22.4v-56.6zM320.2 320.2c0-105.8 86-191.8 191.8-191.8s191.8 86 191.8 191.8S617.7 512 512 512s-191.8-86-191.8-191.8zM831.6 767.7V639.9h-63.9v127.8H639.9v63.9h127.8v127.9h63.9V831.6h127.9v-63.9z" fill="#5f6266" p-id="4298"></path></svg>

+ 0 - 0
src/assets/svgs/bpm/audit2.svg → src/assets/svgs/bpm/approve.svg


+ 0 - 0
src/assets/svgs/bpm/audit4.svg → src/assets/svgs/bpm/cancel.svg


+ 0 - 0
src/assets/svgs/bpm/audit3.svg → src/assets/svgs/bpm/reject.svg


+ 0 - 0
src/assets/svgs/bpm/audit1.svg → src/assets/svgs/bpm/running.svg


+ 76 - 32
src/components/UserSelectForm/index.vue

@@ -1,25 +1,27 @@
 <template>
-  <Dialog v-model="dialogVisible" title="人员选择" width="900">
-    <el-row>
+  <Dialog v-model="dialogVisible" title="人员选择" width="800">
+    <el-row class="gap2" v-loading="formLoading">
       <el-col :span="6">
-        <el-tree
-          ref="treeRef"
-          :data="deptList"
-          :expand-on-click-node="false"
-          :props="defaultProps"
-          default-expand-all
-          highlight-current
-          node-key="id"
-          @node-click="handleNodeClick"
-        />
+        <ContentWrap class="h-1/1">
+          <el-tree
+            ref="treeRef"
+            :data="deptList"
+            :expand-on-click-node="false"
+            :props="defaultProps"
+            default-expand-all
+            highlight-current
+            node-key="id"
+            @node-click="handleNodeClick"
+          />
+        </ContentWrap>
       </el-col>
-      <el-col :span="17" :offset="1">
+      <el-col :span="17">
         <el-transfer
           v-model="selectedUserIdList"
           :titles="['未选', '已选']"
           filterable
           filter-placeholder="搜索成员"
-          :data="userList"
+          :data="transferUserList"
           :props="{ label: 'nickname', key: 'id' }"
         />
       </el-col>
@@ -47,62 +49,87 @@ const emit = defineEmits<{
 }>()
 const { t } = useI18n() // 国际
 const message = useMessage() // 消息弹窗
-
 const deptList = ref<Tree[]>([]) // 部门树形结构化
-const userList: any = ref([]) // 用户列表
+const allUserList = ref<UserApi.UserVO[]>([]) // 所有用户列表
+const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
 const selectedUserIdList: any = ref([]) // 选中的用户列表
 const dialogVisible = ref(false) // 弹窗的是否展示
 const formLoading = ref(false) // 表单的加载中
-const activityId = ref() // 关联的主键编号 TODO @goldenzqqq:这个 activityId 有没可能不传递。在使用 @submitForm="xxx()" 时,传递的参数。目的是,更加解耦一些。
+const activityId = ref()
+
+// 计算属性:合并已选择的用户和当前部门过滤后的用户
+const transferUserList = computed(() => {
+  // 获取所有已选择的用户
+  const selectedUsers = allUserList.value.filter((user: any) =>
+    selectedUserIdList.value.includes(user.id)
+  )
+
+  // 获取当前部门过滤后的未选择用户
+  const filteredUnselectedUsers = filteredUserList.value.filter(
+    (user: any) => !selectedUserIdList.value.includes(user.id)
+  )
+
+  // 合并并去重
+  return [...selectedUsers, ...filteredUnselectedUsers]
+})
 
 /** 打开弹窗 */
 const open = async (id: number, selectedList?: any[]) => {
   activityId.value = id
-  // 重置表单
   resetForm()
 
-  // 加载相关数据
   deptList.value = handleTree(await DeptApi.getSimpleDeptList())
-  await getUserList()
-  // 设置选中的用户列表
-  selectedUserIdList.value = selectedList?.map((item: any) => item.id)
-
-  // 设置可见
+  // 初始加载所有用户
+  await getAllUserList()
+  // 初始状态下,过滤列表等于所有用户列表
+  filteredUserList.value = [...allUserList.value]
+  selectedUserIdList.value = selectedList?.map((item: any) => item.id) || []
   dialogVisible.value = true
 }
+/** 获取所有用户列表 */
+const getAllUserList = async () => {
+  try {
+    // @ts-ignore
+    const data = await UserApi.getSimpleUserList()
+    allUserList.value = data
+  } finally {
+  }
+}
 
-/** 获取用户列表 */
+/** 获取部门过滤后的用户列表 */
 const getUserList = async (deptId?: number) => {
+  formLoading.value = true
   try {
     // @ts-ignore
-    // TODO @芋艿:替换到 simple List
+    // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
     const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
-    userList.value = data.list
+    // 更新过滤后的用户列表
+    filteredUserList.value = data.list
   } finally {
+    formLoading.value = false
   }
 }
 
 /** 提交选择 */
 const submitForm = async () => {
-  // 提交请求
-  formLoading.value = true
   try {
     message.success(t('common.updateSuccess'))
     dialogVisible.value = false
-    const emitUserList = userList.value.filter((user: any) =>
+    // 从所有用户列表中筛选出已选择的用户
+    const emitUserList = allUserList.value.filter((user: any) =>
       selectedUserIdList.value.includes(user.id)
     )
     // 发送操作成功的事件
     emit('confirm', activityId.value, emitUserList)
   } finally {
-    formLoading.value = false
   }
 }
 
 /** 重置表单 */
 const resetForm = () => {
   deptList.value = []
-  userList.value = []
+  allUserList.value = []
+  filteredUserList.value = []
   selectedUserIdList.value = []
 }
 
@@ -113,3 +140,20 @@ const handleNodeClick = (row: { [key: string]: any }) => {
 
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 </script>
+
+<style lang="scss" scoped>
+:deep() {
+  .el-transfer {
+    display: flex;
+  }
+  .el-transfer__buttons {
+    display: flex !important;
+    flex-direction: column-reverse;
+    justify-content: center;
+    gap: 20px;
+    .el-transfer__button:nth-child(2) {
+      margin: 0;
+    }
+  }
+}
+</style>

+ 12 - 2
src/views/bpm/processInstance/create/index.vue

@@ -41,7 +41,7 @@
               :ref="`category-${categoryCode}`"
             >
               <h3 class="text-18px font-bold mb-10px mt-5px">
-                {{ getCategoryName(categoryCode) }}
+                {{ getCategoryName(categoryCode as any) }}
               </h3>
               <div class="grid grid-cols-3 gap3">
                 <el-tooltip
@@ -175,7 +175,17 @@ const handleQuery = () => {
 /** 流程定义的分组 */
 const processDefinitionGroup: any = computed(() => {
   if (!processDefinitionList.value?.length) return {}
-  return groupBy(filteredProcessDefinitionList.value, 'category')
+  const grouped = groupBy(filteredProcessDefinitionList.value, 'category')
+
+  const orderedGroup = {}
+  // 按照 categoryList 的顺序重新组织数据
+  categoryList.value.forEach((category: any) => {
+    if (grouped[category.code]) {
+      orderedGroup[category.code] = grouped[category.code]
+    }
+  })
+
+  return orderedGroup
 })
 
 /** 左侧分类切换 */

+ 6 - 1
src/views/bpm/processInstance/detail/ProcessInstanceBpmnViewer.vue

@@ -1,6 +1,6 @@
 <template>
   <el-card v-loading="loading" class="box-card">
-    <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="h-700px" />
+    <MyProcessViewer key="designer" :xml="view.bpmnXml" :view="view" class="process-viewer" />
   </el-card>
 </template>
 <script lang="ts" setup>
@@ -45,4 +45,9 @@ watch(
   width: 100%;
   margin-bottom: 20px;
 }
+
+:deep(.process-viewer) {
+  height: 100% !important;
+  min-height: 500px;
+}
 </style>

+ 8 - 0
src/views/bpm/processInstance/detail/ProcessInstanceSimpleViewer.vue

@@ -4,6 +4,7 @@
       :flow-node="simpleModel"
       :tasks="tasks"
       :process-instance="processInstance"
+      class="process-viewer"
     />
   </div>
 </template>
@@ -151,3 +152,10 @@ const setSimpleModelNodeTaskStatus = (
   )
 }
 </script>
+
+<style lang="scss" scoped>
+:deep(.process-viewer) {
+  height: 100% !important;
+  min-height: 500px;
+}
+</style>

+ 35 - 34
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -16,9 +16,10 @@
           <img class="w-full h-full" :src="getApprovalNodeImg(activity.nodeType)" alt="" />
           <div
             v-if="showStatusIcon"
-            class="position-absolute top-17px left-17px bg-#fff rounded-full flex items-center p-2px"
+            class="position-absolute top-17px left-17px rounded-full flex items-center p-2px"
+            :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
           >
-            <el-icon :size="12" :color="getApprovalNodeColor(activity.status)">
+            <el-icon :size="12" color="#fff">
               <component :is="getApprovalNodeIcon(activity.status, activity.nodeType)" />
             </el-icon>
           </div>
@@ -46,19 +47,22 @@
           "
         >
           <!--  && activity.nodeType === NodeType.USER_TASK_NODE -->
-          <el-button
-            class="!px-8px"
-            @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
-          >
-            <Icon icon="fa:user-plus" />
-          </el-button>
+
+          <el-tooltip content="添加用户" placement="left">
+            <el-button
+              class="!px-6px"
+              @click="handleSelectUser(activity.id, customApproveUsers[activity.id])"
+            >
+              <img class="w-18px text-#ccc" src="@/assets/svgs/bpm/add-user.svg" alt="" />
+            </el-button>
+          </el-tooltip>
           <div
             v-for="(user, idx1) in customApproveUsers[activity.id]"
             :key="idx1"
-            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
           >
-            <el-avatar :size="28" v-if="user.avatar" :src="user.avatar" />
-            <el-avatar :size="28" v-else>
+            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar class="!m-5px" :size="28" v-else>
               {{ user.nickname.substring(0, 1) }}
             </el-avatar>
             {{ user.nickname }}
@@ -73,40 +77,39 @@
             >
               <!-- 信息:头像昵称 -->
               <div
-                class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
+                class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
               >
                 <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
                   <el-avatar
+                    class="!m-5px"
                     :size="28"
                     v-if="task.assigneeUser?.avatar"
                     :src="task.assigneeUser?.avatar"
                   />
-                  <el-avatar :size="28" v-else>
+                  <el-avatar class="!m-5px" :size="28" v-else>
                     {{ task.assigneeUser?.nickname.substring(0, 1) }}
                   </el-avatar>
                   {{ task.assigneeUser?.nickname }}
                 </template>
                 <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
                   <el-avatar
+                    class="!m-5px"
                     :size="28"
                     v-if="task.ownerUser?.avatar"
                     :src="task.ownerUser?.avatar"
                   />
-                  <el-avatar :size="28" v-else>
+                  <el-avatar class="!m-5px" :size="28" v-else>
                     {{ task.ownerUser?.nickname.substring(0, 1) }}
                   </el-avatar>
                   {{ task.ownerUser?.nickname }}
                 </template>
                 <!-- 信息:任务 ICON -->
                 <div
-                  v-if="onlyStatusIconShow.includes(task.status)"
-                  class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px"
+                  v-if="showStatusIcon && onlyStatusIconShow.includes(task.status)"
+                  class="position-absolute top-19px left-23px rounded-full flex items-center p-2px"
+                  :style="{ backgroundColor: statusIconMap2[task.status]?.color }"
                 >
-                  <Icon
-                    :size="12"
-                    :icon="statusIconMap2[task.status]?.icon"
-                    :color="statusIconMap2[task.status]?.color"
-                  />
+                  <Icon :size="12" :icon="statusIconMap2[task.status]?.icon" color="#FFFFFF" />
                 </div>
               </div>
             </div>
@@ -126,23 +129,21 @@
           <div
             v-for="(user, idx1) in activity.candidateUsers"
             :key="idx1"
-            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
           >
-            <el-avatar :size="28" v-if="user.avatar" :src="user.avatar" />
-            <el-avatar :size="28" v-else>
+            <el-avatar class="!m-5px" :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar class="!m-5px" :size="28" v-else>
               {{ user.nickname.substring(0, 1) }}
             </el-avatar>
             {{ user.nickname }}
 
             <!-- 信息:任务 ICON -->
             <div
-              class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px"
+              v-if="showStatusIcon"
+              class="position-absolute top-19px left-23px rounded-full flex items-center p-2px"
+              :style="{ backgroundColor: statusIconMap2['-1']?.color }"
             >
-              <Icon
-                :size="12"
-                :icon="statusIconMap2['-1']?.icon"
-                :color="statusIconMap2['-1']?.color"
-              />
+              <Icon :size="12" :icon="statusIconMap2['-1']?.icon" color="#FFFFFF" />
             </div>
           </div>
         </div>
@@ -184,7 +185,7 @@ const statusIconMap2 = {
   // 未开始
   '-1': { color: '#909398', icon: 'ep-clock' },
   // 待审批
-  '0': { color: '#e5e7ec', icon: 'ep:loading' },
+  '0': { color: '#00b32a', icon: 'ep:loading' },
   // 审批中
   '1': { color: '#448ef7', icon: 'ep:loading' },
   // 审批通过
@@ -204,7 +205,7 @@ const statusIconMap2 = {
 const statusIconMap = {
   // 审批未开始
   '-1': { color: '#909398', icon: Clock },
-  '0': { color: '#e5e7ec', icon: Clock },
+  '0': { color: '#00b32a', icon: Clock },
   // 审批中
   '1': { color: '#448ef7', icon: Loading },
   // 审批通过
@@ -223,9 +224,9 @@ const statusIconMap = {
 
 const nodeTypeSvgMap = {
   // 结束节点
-  [NodeType.END_EVENT_NODE]: { color: '#ffffff', svg: finishSvg },
+  [NodeType.END_EVENT_NODE]: { color: '#909398', svg: finishSvg },
   // 发起人节点
-  [NodeType.START_USER_NODE]: { color: '#ffffff', svg: starterSvg },
+  [NodeType.START_USER_NODE]: { color: '#909398', svg: starterSvg },
   // 审批人节点
   [NodeType.USER_TASK_NODE]: { color: '#ff943e', svg: auditorSvg },
   // 抄送人节点

+ 20 - 12
src/views/bpm/processInstance/detail/index.vue

@@ -5,7 +5,7 @@
         <img
           class="position-absolute right-20px"
           width="150"
-          :src="auditIcons[processInstance.status]"
+          :src="auditIconsMap[processInstance.status]"
           alt=""
         />
         <div class="text-#878c93 h-15px">编号:{{ id }}</div>
@@ -137,11 +137,11 @@ import ProcessInstanceTaskList from './ProcessInstanceTaskList.vue'
 import ProcessInstanceOperationButton from './ProcessInstanceOperationButton.vue'
 import ProcessInstanceTimeline from './ProcessInstanceTimeline.vue'
 import { FieldPermissionType } from '@/components/SimpleProcessDesignerV2/src/consts'
-// TODO 代码优化,换个明确的 icon 名字
-import audit1 from '@/assets/svgs/bpm/audit1.svg'
-import audit2 from '@/assets/svgs/bpm/audit2.svg'
-import audit3 from '@/assets/svgs/bpm/audit3.svg'
-import audit4 from '@/assets/svgs/bpm/audit4.svg'
+import { TaskStatusEnum } from '@/api/bpm/task'
+import runningSvg from '@/assets/svgs/bpm/running.svg'
+import approveSvg from '@/assets/svgs/bpm/approve.svg'
+import rejectSvg from '@/assets/svgs/bpm/reject.svg'
+import cancelSvg from '@/assets/svgs/bpm/cancel.svg'
 
 defineOptions({ name: 'BpmProcessInstanceDetail' })
 const props = defineProps<{
@@ -155,11 +155,11 @@ const processInstance = ref<any>({}) // 流程实例
 const processDefinition = ref<any>({}) // 流程定义
 const processModelView = ref<any>({}) // 流程模型视图
 const operationButtonRef = ref() // 操作按钮组件 ref
-const auditIcons = {
-  1: audit1,
-  2: audit2,
-  3: audit3,
-  4: audit4
+const auditIconsMap = {
+  [TaskStatusEnum.RUNNING]: runningSvg,
+  [TaskStatusEnum.APPROVE]: approveSvg,
+  [TaskStatusEnum.REJECT]: rejectSvg,
+  [TaskStatusEnum.CANCEL]: cancelSvg
 }
 
 // ========== 申请信息 ==========
@@ -242,7 +242,6 @@ const getApprovalDetail = async () => {
 
 /** 获取流程模型视图*/
 const getProcessModelView = async () => {
-
   if (BpmModelType.BPMN === processDefinition.value?.modelType) {
     // 重置,解决 BPMN 流程图刷新不会重新渲染问题
     processModelView.value = {
@@ -320,6 +319,15 @@ $process-header-height: 194px;
         $process-header-height - 40px
     );
     overflow: auto;
+
+    :deep(.box-card) {
+      height: 100%;
+
+      .el-card__body {
+        height: 100%;
+        padding: 0;
+      }
+    }
   }
 }