فهرست منبع

Merge branch 'feature/bpm' of https://gitee.com/yudaocode/yudao-ui-admin-vue3

YunaiV 4 ماه پیش
والد
کامیت
16a98e5a21

+ 122 - 0
src/components/DeptSelectForm/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <Dialog v-model="dialogVisible" title="部门选择" width="600">
+    <el-row v-loading="formLoading">
+      <el-col :span="24">
+        <ContentWrap class="h-1/1">
+          <el-tree
+            ref="treeRef"
+            :data="deptTree"
+            :props="defaultProps"
+            show-checkbox
+            :check-strictly="checkStrictly"
+            check-on-click-node
+            default-expand-all
+            highlight-current
+            node-key="id"
+            @check="handleCheck"
+          />
+        </ContentWrap>
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button
+        :disabled="formLoading || !selectedDeptIds?.length"
+        type="primary"
+        @click="submitForm"
+      >
+        确 定
+      </el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+
+defineOptions({ name: 'DeptSelectForm' })
+
+const emit = defineEmits<{
+  confirm: [deptList: any[]]
+}>()
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps({
+  // 是否严格的遵循父子不互相关联
+  checkStrictly: {
+    type: Boolean,
+    default: false
+  },
+  // 是否支持多选
+  multiple: {
+    type: Boolean,
+    default: true
+  }
+})
+
+const treeRef = ref()
+const deptTree = ref<Tree[]>([]) // 部门树形结构
+const selectedDeptIds = ref<number[]>([]) // 选中的部门 ID 列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+
+/** 打开弹窗 */
+const open = async (selectedList?: DeptApi.DeptVO[]) => {
+  resetForm()
+  formLoading.value = true
+  try {
+    // 加载部门列表
+    const deptData = await DeptApi.getSimpleDeptList()
+    deptTree.value = handleTree(deptData)
+  } finally {
+    formLoading.value = false
+  }
+  dialogVisible.value = true
+  // 设置已选择的部门
+  if (selectedList?.length) {
+    await nextTick()
+    const selectedIds = selectedList
+      .map((dept) => dept.id)
+      .filter((id): id is number => id !== undefined)
+    selectedDeptIds.value = selectedIds
+    treeRef.value?.setCheckedKeys(selectedIds)
+  }
+}
+
+/** 处理选中状态变化 */
+const handleCheck = (data: any, checked: any) => {
+  selectedDeptIds.value = treeRef.value.getCheckedKeys()
+  if (!props.multiple && selectedDeptIds.value.length > 1) {
+    // 单选模式下,只保留最后选择的节点
+    const lastSelectedId = selectedDeptIds.value[selectedDeptIds.value.length - 1]
+    selectedDeptIds.value = [lastSelectedId]
+    treeRef.value.setCheckedKeys([lastSelectedId])
+  }
+}
+
+/** 提交选择 */
+const submitForm = async () => {
+  try {
+    // 获取选中的完整部门数据
+    const checkedNodes = treeRef.value.getCheckedNodes()
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    emit('confirm', checkedNodes)
+  } finally {
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  deptTree.value = []
+  selectedDeptIds.value = []
+  if (treeRef.value) {
+    treeRef.value.setCheckedKeys([])
+  }
+}
+
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 3 - 2
src/components/SimpleProcessDesignerV2/src/NodeHandler.vue

@@ -91,6 +91,7 @@ import {
   DEFAULT_CONDITION_GROUP_VALUE
 } from './consts'
 import { generateUUID } from '@/utils'
+import { cloneDeep } from 'lodash-es'
 
 defineOptions({
   name: 'NodeHandler'
@@ -184,7 +185,7 @@ const addNode = (type: number) => {
           conditionSetting: {
             defaultFlow: false,
             conditionType: ConditionType.RULE,
-            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+            conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
           }
         },
         {
@@ -242,7 +243,7 @@ const addNode = (type: number) => {
           conditionSetting: {
             defaultFlow: false,
             conditionType: ConditionType.RULE,
-            conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+            conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
           }
         },
         {

+ 6 - 0
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -59,6 +59,11 @@ const props = defineProps({
   startUserIds: {
     type: Array,
     required: false
+  },
+  // 可发起流程的部门编号
+  startDeptIds: {
+    type: Array,
+    required: false
   }
 })
 
@@ -82,6 +87,7 @@ provide('deptList', deptOptions)
 provide('userGroupList', userGroupOptions)
 provide('deptTree', deptTreeOptions)
 provide('startUserIds', props.startUserIds)
+provide('startDeptIds', props.startDeptIds)
 provide('tasks', [])
 provide('processInstance', {})
 const message = useMessage() // 国际化

+ 57 - 14
src/components/SimpleProcessDesignerV2/src/nodes-config/StartUserNodeConfig.vue

@@ -25,21 +25,46 @@
     </template>
     <el-tabs type="border-card" v-model="activeTabName">
       <el-tab-pane label="权限" name="user">
-        <el-text v-if="!startUserIds || startUserIds.length === 0"> 全部成员可以发起流程 </el-text>
-        <el-text v-else-if="startUserIds.length == 1">
-          {{ getUserNicknames(startUserIds) }} 可发起流程
-        </el-text>
-        <el-text v-else>
-          <el-tooltip
-            class="box-item"
-            effect="dark"
-            placement="top"
-            :content="getUserNicknames(startUserIds)"
-          >
-            {{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
-            {{ startUserIds.length }} 人可发起流程
-          </el-tooltip>
+        <el-text
+          v-if="
+            (!startUserIds || startUserIds.length === 0) &&
+            (!startDeptIds || startDeptIds.length === 0)
+          "
+        >
+          全部成员可以发起流程
         </el-text>
+        <div v-else-if="startUserIds && startUserIds.length > 0">
+          <el-text v-if="startUserIds.length == 1">
+            {{ getUserNicknames(startUserIds) }} 可发起流程
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="getUserNicknames(startUserIds)"
+            >
+              {{ getUserNicknames(startUserIds.slice(0, 2)) }} 等
+              {{ startUserIds.length }} 人可发起流程
+            </el-tooltip>
+          </el-text>
+        </div>
+        <div v-else-if="startDeptIds && startDeptIds.length > 0">
+          <el-text v-if="startDeptIds.length == 1">
+            {{ getDeptNames(startDeptIds) }} 可发起流程
+          </el-text>
+          <el-text v-else>
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              placement="top"
+              :content="getDeptNames(startDeptIds)"
+            >
+              {{ getDeptNames(startDeptIds.slice(0, 2)) }} 等
+              {{ startDeptIds.length }} 个部门可发起流程
+            </el-tooltip>
+          </el-text>
+        </div>
       </el-tab-pane>
       <el-tab-pane label="表单字段权限" name="fields" v-if="formType === 10">
         <div class="field-setting-pane">
@@ -107,6 +132,7 @@
 import { SimpleFlowNode, NodeType, FieldPermissionType, START_USER_BUTTON_SETTING } from '../consts'
 import { useWatchNode, useDrawer, useNodeName, useFormFieldsPermission } from '../node'
 import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
 defineOptions({
   name: 'StartUserNodeConfig'
 })
@@ -118,8 +144,12 @@ const props = defineProps({
 })
 // 可发起流程的用户编号
 const startUserIds = inject<Ref<any[]>>('startUserIds')
+// 可发起流程的部门编号
+const startDeptIds = inject<Ref<any[]>>('startDeptIds')
 // 用户列表
 const userOptions = inject<Ref<UserApi.UserVO[]>>('userList')
+// 部门列表
+const deptOptions = inject<Ref<DeptApi.DeptVO[]>>('deptList')
 // 抽屉配置
 const { settingVisible, closeDrawer, openDrawer } = useDrawer()
 // 当前节点
@@ -145,6 +175,19 @@ const getUserNicknames = (userIds: number[]): string => {
   })
   return nicknames.join(',')
 }
+const getDeptNames = (deptIds: number[]): string => {
+  if (!deptIds || deptIds.length === 0) {
+    return ''
+  }
+  const deptNames: string[] = []
+  deptIds.forEach((deptId) => {
+    const found = deptOptions?.value.find((item) => item.id === deptId)
+    if (found && found.name) {
+      deptNames.push(found.name)
+    }
+  })
+  return deptNames.join(',')
+}
 // 保存配置
 const saveConfig = async () => {
   activeTabName.value = 'user'

+ 6 - 5
src/components/SimpleProcessDesignerV2/src/nodes-config/TriggerNodeConfig.vue

@@ -254,6 +254,7 @@ import {
 import { useWatchNode, useDrawer, useNodeName, useFormFields, getConditionShowText } from '../node'
 import HttpRequestSetting from './components/HttpRequestSetting.vue'
 import ConditionDialog from './components/ConditionDialog.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 
 defineOptions({
@@ -290,7 +291,7 @@ const configForm = ref<TriggerSetting>({
   },
   formSettings: [
     {
-      conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+      conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
       updateFormFields: {},
       deleteFields: []
     }
@@ -346,7 +347,7 @@ const changeTriggerType = () => {
         ? originalSetting.formSettings
         : [
             {
-              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
               updateFormFields: {},
               deleteFields: []
             }
@@ -361,7 +362,7 @@ const changeTriggerType = () => {
         ? originalSetting.formSettings
         : [
             {
-              conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+              conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
               updateFormFields: undefined,
               deleteFields: []
             }
@@ -374,7 +375,7 @@ const changeTriggerType = () => {
 /** 添加新的修改表单设置 */
 const addFormSetting = () => {
   configForm.value.formSettings!.push({
-    conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+    conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
     updateFormFields: {},
     deleteFields: []
   })
@@ -509,7 +510,7 @@ const showTriggerNodeConfig = (node: SimpleFlowNode) => {
       },
       formSettings: node.triggerSetting.formSettings || [
         {
-          conditionGroups: DEFAULT_CONDITION_GROUP_VALUE,
+          conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE),
           updateFormFields: {},
           deleteFields: []
         }

+ 2 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/components/Condition.vue

@@ -154,6 +154,7 @@ import {
 } from '../../consts'
 import { BpmModelFormType } from '@/utils/constants'
 import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
 
 const props = defineProps({
   modelValue: {
@@ -196,7 +197,7 @@ const formRef = ref() // 表单 Ref
 const changeConditionType = () => {
   if (condition.value.conditionType === ConditionType.RULE) {
     if (!condition.value.conditionGroups) {
-      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+      condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
     }
   }
 }

+ 4 - 3
src/components/SimpleProcessDesignerV2/src/nodes-config/components/ConditionDialog.vue

@@ -1,5 +1,5 @@
 <!-- TODO @jason:有可能,它里面套 Condition 么?  -->
-<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion --> 
+<!-- TODO 怕影响其它节点功能,后面看看如何如何复用 Condtion -->
 <template>
   <Dialog v-model="dialogVisible" title="条件配置" width="600px" :fullscreen="false">
     <div class="h-410px">
@@ -165,6 +165,7 @@ import {
 } from '../../consts'
 import { BpmModelFormType } from '@/utils/constants'
 import { useFormFieldsAndStartUser } from '../../node'
+import { cloneDeep } from 'lodash-es'
 defineOptions({
   name: 'ConditionDialog'
 })
@@ -175,7 +176,7 @@ const condition = ref<{
   conditionGroups?: ConditionGroup
 }>({
   conditionType: ConditionType.RULE,
-  conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+  conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
 })
 
 const emit = defineEmits<{
@@ -210,7 +211,7 @@ const formRef = ref() // 表单 Ref
 const changeConditionType = () => {
   if (condition.value.conditionType === ConditionType.RULE) {
     if (!condition.value.conditionGroups) {
-      condition.value.conditionGroups = DEFAULT_CONDITION_GROUP_VALUE
+      condition.value.conditionGroups = cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
     }
   }
 }

+ 11 - 3
src/components/SimpleProcessDesignerV2/src/nodes/ExclusiveNode.vue

@@ -108,11 +108,18 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
+import {
+  SimpleFlowNode,
+  NodeType,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  NODE_DEFAULT_TEXT
+} from '../consts'
 import { getDefaultConditionNodeName } from '../utils'
 import { useTaskStatusClass } from '../node'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'ExclusiveNode'
@@ -149,7 +156,8 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+    conditionNode.name ||
+    getDefaultConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -181,7 +189,7 @@ const addCondition = () => {
       conditionSetting: {
         defaultFlow: false,
         conditionType: ConditionType.RULE,
-        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+        conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
       }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)

+ 11 - 3
src/components/SimpleProcessDesignerV2/src/nodes/InclusiveNode.vue

@@ -110,11 +110,18 @@
 <script setup lang="ts">
 import NodeHandler from '../NodeHandler.vue'
 import ProcessNodeTree from '../ProcessNodeTree.vue'
-import { SimpleFlowNode, NodeType, ConditionType, DEFAULT_CONDITION_GROUP_VALUE, NODE_DEFAULT_TEXT } from '../consts'
+import {
+  SimpleFlowNode,
+  NodeType,
+  ConditionType,
+  DEFAULT_CONDITION_GROUP_VALUE,
+  NODE_DEFAULT_TEXT
+} from '../consts'
 import { useTaskStatusClass } from '../node'
 import { getDefaultInclusiveConditionNodeName } from '../utils'
 import { generateUUID } from '@/utils'
 import ConditionNodeConfig from '../nodes-config/ConditionNodeConfig.vue'
+import { cloneDeep } from 'lodash-es'
 const { proxy } = getCurrentInstance() as any
 defineOptions({
   name: 'InclusiveNode'
@@ -153,7 +160,8 @@ const blurEvent = (index: number) => {
   showInputs.value[index] = false
   const conditionNode = currentNode.value.conditionNodes?.at(index) as SimpleFlowNode
   conditionNode.name =
-    conditionNode.name || getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
+    conditionNode.name ||
+    getDefaultInclusiveConditionNodeName(index, conditionNode.conditionSetting?.defaultFlow)
 }
 
 // 点击条件名称
@@ -185,7 +193,7 @@ const addCondition = () => {
       conditionSetting: {
         defaultFlow: false,
         conditionType: ConditionType.RULE,
-        conditionGroups: DEFAULT_CONDITION_GROUP_VALUE
+        conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE)
       }
     }
     conditionNodes.splice(lastIndex, 0, conditionData)

+ 14 - 1
src/views/bpm/model/CategoryDraggableModel.vue

@@ -97,10 +97,23 @@
         </el-table-column>
         <el-table-column label="可见范围" prop="startUserIds" min-width="150">
           <template #default="{ row }">
-            <el-text v-if="!row.startUsers?.length"> 全部可见 </el-text>
+            <el-text v-if="!row.startUsers?.length && !row.startDepts?.length"> 全部可见 </el-text>
             <el-text v-else-if="row.startUsers.length === 1">
               {{ row.startUsers[0].nickname }}
             </el-text>
+            <el-text v-else-if="row.startDepts?.length === 1">
+              {{ row.startDepts[0].name }}
+            </el-text>
+            <el-text v-else-if="row.startDepts?.length > 1">
+              <el-tooltip
+                class="box-item"
+                effect="dark"
+                placement="top"
+                :content="row.startDepts.map((dept: any) => dept.name).join('、')"
+              >
+                {{ row.startDepts[0].name }}等 {{ row.startDepts.length }} 个部门可见
+              </el-tooltip>
+            </el-text>
             <el-text v-else>
               <el-tooltip
                 class="box-item"

+ 73 - 1
src/views/bpm/model/form/BasicInfo.vue

@@ -77,6 +77,7 @@
       >
         <el-option label="全员" :value="0" />
         <el-option label="指定人员" :value="1" />
+        <el-option label="指定部门" :value="2" />
       </el-select>
       <div v-if="modelData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
         <div
@@ -99,6 +100,24 @@
           <Icon icon="ep:plus" /> 选择人员
         </el-button>
       </div>
+      <div v-if="modelData.startUserType === 2" class="mt-2 flex flex-wrap gap-2">
+        <div
+          v-for="dept in selectedStartDepts" 
+          :key="dept.id"
+          class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+        >
+          <Icon icon="ep:office-building" class="!m-5px text-20px" />
+          {{ dept.name }}
+          <Icon
+            icon="ep:close"
+            class="ml-2 cursor-pointer hover:text-red-500"
+            @click="handleRemoveStartDept(dept)"
+          />
+        </div>
+        <el-button type="primary" link @click="openStartDeptSelect">
+          <Icon icon="ep:plus" /> 选择部门
+        </el-button>
+      </div>
     </el-form-item>
     <el-form-item label="流程管理员" prop="managerUserIds" class="mb-20px">
       <div class="flex flex-wrap gap-2">
@@ -127,11 +146,19 @@
 
   <!-- 用户选择弹窗 -->
   <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+  <!-- 部门选择弹窗 -->
+  <DeptSelectForm
+    ref="deptSelectFormRef"
+    :multiple="true"
+    :check-strictly="true"
+    @confirm="handleDeptSelectConfirm"
+  />
 </template>
 
 <script lang="ts" setup>
 import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { UserVO } from '@/api/system/user'
+import { DeptVO } from '@/api/system/dept'
 import { CategoryVO } from '@/api/bpm/category'
 
 const props = defineProps({
@@ -142,13 +169,19 @@ const props = defineProps({
   userList: {
     type: Array,
     required: true
+  },
+  deptList: {
+    type: Array,
+    required: true
   }
 })
 
 const formRef = ref()
 const selectedStartUsers = ref<UserVO[]>([])
+const selectedStartDepts = ref<DeptVO[]>([])
 const selectedManagerUsers = ref<UserVO[]>([])
 const userSelectFormRef = ref()
+const deptSelectFormRef = ref()
 const currentSelectType = ref<'start' | 'manager'>('start')
 
 const rules = {
@@ -174,6 +207,13 @@ watch(
     } else {
       selectedStartUsers.value = []
     }
+    if (newVal.startDeptIds?.length) {
+      selectedStartDepts.value = props.deptList.filter((dept: DeptVO) =>
+        newVal.startDeptIds.includes(dept.id)
+      ) as DeptVO[]
+    } else {
+      selectedStartDepts.value = []
+    }
     if (newVal.managerUserIds?.length) {
       selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
         newVal.managerUserIds.includes(user.id)
@@ -193,6 +233,11 @@ const openStartUserSelect = () => {
   userSelectFormRef.value.open(0, selectedStartUsers.value)
 }
 
+/** 打开部门选择 */
+const openStartDeptSelect = () => {
+  deptSelectFormRef.value.open(selectedStartDepts.value)
+}
+
 /** 打开管理员选择 */
 const openManagerUserSelect = () => {
   currentSelectType.value = 'manager'
@@ -214,9 +259,28 @@ const handleUserSelectConfirm = (_, users: UserVO[]) => {
   }
 }
 
+/** 处理部门选择确认 */
+const handleDeptSelectConfirm = (depts: DeptVO[]) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: depts.map((d) => d.id)
+  }
+}
+
 /** 处理发起人类型变化 */
 const handleStartUserTypeChange = (value: number) => {
-  if (value !== 1) {
+  if (value === 0) {
+    modelData.value = {
+      ...modelData.value,
+      startUserIds: [],
+      startDeptIds: []
+    }
+  } else if (value === 1) {
+    modelData.value = {
+      ...modelData.value,
+      startDeptIds: []
+    }
+  } else if (value === 2) {
     modelData.value = {
       ...modelData.value,
       startUserIds: []
@@ -232,6 +296,14 @@ const handleRemoveStartUser = (user: UserVO) => {
   }
 }
 
+/** 移除部门 */
+const handleRemoveStartDept = (dept: DeptVO) => {
+  modelData.value = {
+    ...modelData.value,
+    startDeptIds: modelData.value.startDeptIds.filter((id: number) => id !== dept.id)
+  }
+}
+
 /** 移除管理员 */
 const handleRemoveManagerUser = (user: UserVO) => {
   modelData.value = {

+ 81 - 5
src/views/bpm/model/form/ExtraSettings.vue

@@ -148,7 +148,7 @@
         <div class="flex">
           <el-switch
             v-model="processBeforeTriggerEnable"
-            @change="handlePreProcessNotifyEnableChange"
+            @change="handleProcessBeforeTriggerEnableChange"
           />
           <div class="ml-80px">流程启动后通知</div>
         </div>
@@ -168,9 +168,9 @@
         <div class="flex">
           <el-switch
             v-model="processAfterTriggerEnable"
-            @change="handlePostProcessNotifyEnableChange"
+            @change="handleProcessAfterTriggerEnableChange"
           />
-          <div class="ml-80px">流程启动后通知</div>
+          <div class="ml-80px">流程结束后通知</div>
         </div>
         <HttpRequestSetting
           v-if="processAfterTriggerEnable"
@@ -180,6 +180,46 @@
         />
       </div>
     </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务前置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskBeforeTriggerEnable"
+            @change="handleTaskBeforeTriggerEnableChange"
+          />
+          <div class="ml-80px">任务执行时通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskBeforeTriggerEnable"
+          v-model:setting="modelData.taskBeforeTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskBeforeTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
+    <el-form-item class="mb-20px">
+      <template #label>
+        <el-text size="large" tag="b">任务后置通知</el-text>
+      </template>
+      <div class="flex flex-col w-100%">
+        <div class="flex">
+          <el-switch
+            v-model="taskAfterTriggerEnable"
+            @change="handleTaskAfterTriggerEnableChange"
+          />
+          <div class="ml-80px">任务结束后通知</div>
+        </div>
+        <HttpRequestSetting
+          v-if="taskAfterTriggerEnable"
+          v-model:setting="modelData.taskAfterTriggerSetting"
+          :responseEnable="true"
+          :formItemPrefix="'taskAfterTriggerSetting'"
+        />
+      </div>
+    </el-form-item>
   </el-form>
 </template>
 
@@ -248,7 +288,7 @@ const numberExample = computed(() => {
 
 /** 是否开启流程前置通知 */
 const processBeforeTriggerEnable = ref(false)
-const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
+const handleProcessBeforeTriggerEnableChange = (val: boolean | string | number) => {
   if (val) {
     modelData.value.processBeforeTriggerSetting = {
       url: '',
@@ -263,7 +303,7 @@ const handlePreProcessNotifyEnableChange = (val: boolean | string | number) => {
 
 /** 是否开启流程后置通知 */
 const processAfterTriggerEnable = ref(false)
-const handlePostProcessNotifyEnableChange = (val: boolean | string | number) => {
+const handleProcessAfterTriggerEnableChange = (val: boolean | string | number) => {
   if (val) {
     modelData.value.processAfterTriggerSetting = {
       url: '',
@@ -276,6 +316,36 @@ const handlePostProcessNotifyEnableChange = (val: boolean | string | number) =>
   }
 }
 
+/** 是否开启任务前置通知 */
+const taskBeforeTriggerEnable = ref(false)
+const handleTaskBeforeTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskBeforeTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskBeforeTriggerSetting = null
+  }
+}
+
+/** 是否开启任务后置通知 */
+const taskAfterTriggerEnable = ref(false)
+const handleTaskAfterTriggerEnableChange = (val: boolean | string | number) => {
+  if (val) {
+    modelData.value.taskAfterTriggerSetting = {
+      url: '',
+      header: [],
+      body: [],
+      response: []
+    }
+  } else {
+    modelData.value.taskAfterTriggerSetting = null
+  }
+}
+
 /** 表单选项 */
 const formField = ref<Array<{ field: string; title: string }>>([])
 const formFieldOptions4Title = computed(() => {
@@ -341,6 +411,12 @@ const initData = () => {
   if (modelData.value.processAfterTriggerSetting) {
     processAfterTriggerEnable.value = true
   }
+  if (modelData.value.taskBeforeTriggerSetting) {
+    taskBeforeTriggerEnable.value = true
+  }
+  if (modelData.value.taskAfterTriggerSetting) {
+    taskAfterTriggerEnable.value = true
+  }
 }
 defineExpose({ initData })
 

+ 1 - 0
src/views/bpm/model/form/ProcessDesign.vue

@@ -18,6 +18,7 @@
       :model-key="modelData.key"
       :model-name="modelData.name"
       :start-user-ids="modelData.startUserIds"
+      :start-dept-ids="modelData.startDeptIds"
       @success="handleDesignSuccess"
     />
   </template>

+ 12 - 3
src/views/bpm/model/form/index.vue

@@ -62,6 +62,7 @@
             v-model="formData"
             :categoryList="categoryList"
             :userList="userList"
+            :deptList="deptList"
             ref="basicInfoRef"
           />
         </div>
@@ -92,6 +93,7 @@ import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import * as UserApi from '@/api/system/user'
+import * as DeptApi from '@/api/system/dept'
 import * as DefinitionApi from '@/api/bpm/definition'
 import { BpmModelFormType, BpmModelType, BpmAutoApproveType } from '@/utils/constants'
 import BasicInfo from './BasicInfo.vue'
@@ -153,6 +155,7 @@ const formData: any = ref({
   visible: true,
   startUserType: undefined,
   startUserIds: [],
+  startDeptIds: [],
   managerUserIds: [],
   allowCancelRunningProcess: true,
   processIdRule: {
@@ -183,6 +186,7 @@ provide('modelData', formData)
 const formList = ref([])
 const categoryList = ref<CategoryVO[]>([])
 const userList = ref<UserApi.UserVO[]>([])
+const deptList = ref<DeptApi.DeptVO[]>([])
 
 /** 初始化数据 */
 const actionType = route.params.type as string
@@ -200,14 +204,17 @@ const initData = async () => {
       data.simpleModel = JSON.parse(data.simpleModel)
     }
     formData.value = data
-    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
   } else if (['update', 'copy'].includes(actionType)) {
     // 情况二:修改场景/复制场景
     const modelId = route.params.id as string
     formData.value = await ModelApi.getModel(modelId)
-    formData.value.startUserType = formData.value.startUserIds?.length > 0 ? 1 : 0
+    formData.value.startUserType =
+      formData.value.startUserIds?.length > 0 ? 1 : formData.value?.startDeptIds?.length > 0 ? 2 : 0
+
     // 特殊:复制场景
-    if (actionType === 'copy') {
+    if (route.params.type === 'copy') {
       delete formData.value.id
       formData.value.name += '副本'
       formData.value.key += '_copy'
@@ -225,6 +232,8 @@ const initData = async () => {
   categoryList.value = await CategoryApi.getCategorySimpleList()
   // 获取用户列表
   userList.value = await UserApi.getSimpleUserList()
+  // 获取部门列表
+  deptList.value = await DeptApi.getSimpleDeptList()
 
   // 最终,设置 currentStep 切换到第一步
   currentStep.value = 0

+ 152 - 85
src/views/bpm/oa/leave/create.vue

@@ -1,83 +1,78 @@
 <template>
-  <el-form
-    ref="formRef"
-    v-loading="formLoading"
-    :model="formData"
-    :rules="formRules"
-    label-width="80px"
-  >
-    <el-form-item label="请假类型" prop="type">
-      <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
-        <el-option
-          v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
-          :key="dict.value"
-          :label="dict.label"
-          :value="dict.value"
-        />
-      </el-select>
-    </el-form-item>
-    <el-form-item label="开始时间" prop="startTime">
-      <el-date-picker
-        v-model="formData.startTime"
-        clearable
-        placeholder="请选择开始时间"
-        type="datetime"
-        value-format="x"
-      />
-    </el-form-item>
-    <el-form-item label="结束时间" prop="endTime">
-      <el-date-picker
-        v-model="formData.endTime"
-        clearable
-        placeholder="请选择结束时间"
-        type="datetime"
-        value-format="x"
-      />
-    </el-form-item>
-    <el-form-item label="原因" prop="reason">
-      <el-input v-model="formData.reason" placeholder="请输请假原因" type="textarea" />
-    </el-form-item>
-    <el-col v-if="startUserSelectTasks.length > 0">
-      <el-card class="mb-10px">
-        <template #header>指定审批人</template>
+  <el-row :gutter="20">
+    <el-col :span="16">
+      <ContentWrap title="申请信息">
         <el-form
-          :model="startUserSelectAssignees"
-          :rules="startUserSelectAssigneesFormRules"
-          ref="startUserSelectAssigneesFormRef"
+          ref="formRef"
+          v-loading="formLoading"
+          :model="formData"
+          :rules="formRules"
+          label-width="80px"
         >
-          <el-form-item
-            v-for="userTask in startUserSelectTasks"
-            :key="userTask.id"
-            :label="`任务【${userTask.name}】`"
-            :prop="userTask.id"
-          >
-            <el-select
-              v-model="startUserSelectAssignees[userTask.id]"
-              multiple
-              placeholder="请选择审批人"
-            >
+          <el-form-item label="请假类型" prop="type">
+            <el-select v-model="formData.type" clearable placeholder="请选择请假类型">
               <el-option
-                v-for="user in userList"
-                :key="user.id"
-                :label="user.nickname"
-                :value="user.id"
+                v-for="dict in getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
               />
             </el-select>
           </el-form-item>
+          <el-form-item label="开始时间" prop="startTime">
+            <el-date-picker
+              v-model="formData.startTime"
+              clearable
+              placeholder="请选择开始时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="结束时间" prop="endTime">
+            <el-date-picker
+              v-model="formData.endTime"
+              clearable
+              placeholder="请选择结束时间"
+              type="datetime"
+              value-format="x"
+            />
+          </el-form-item>
+          <el-form-item label="原因" prop="reason">
+            <el-input v-model="formData.reason" placeholder="请输入请假原因" type="textarea" />
+          </el-form-item>
+          <el-form-item>
+            <el-button :disabled="formLoading" type="primary" @click="submitForm">
+              确 定
+            </el-button>
+          </el-form-item>
         </el-form>
-      </el-card>
+      </ContentWrap>
     </el-col>
-    <el-form-item>
-      <el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
-    </el-form-item>
-  </el-form>
+
+    <!-- 审批相关:流程信息 -->
+    <el-col :span="8">
+      <ContentWrap title="审批流程" :bodyStyle="{ padding: '0 20px 0' }">
+        <ProcessInstanceTimeline
+          ref="timelineRef"
+          :activity-nodes="activityNodes"
+          :show-status-icon="false"
+          @select-user-confirm="selectUserConfirm"
+        />
+      </ContentWrap>
+    </el-col>
+  </el-row>
 </template>
 <script lang="ts" setup>
 import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
 import * as LeaveApi from '@/api/bpm/leave'
 import { useTagsViewStore } from '@/store/modules/tagsView'
+
+// 审批相关:import
 import * as DefinitionApi from '@/api/bpm/definition'
-import * as UserApi from '@/api/system/user'
+import ProcessInstanceTimeline from '@/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { CandidateStrategy, NodeId } from '@/components/SimpleProcessDesignerV2/src/consts'
+import { ApprovalNodeInfo } from '@/api/bpm/processInstance'
 
 defineOptions({ name: 'BpmOALeaveCreate' })
 
@@ -100,30 +95,37 @@ const formRules = reactive({
 })
 const formRef = ref() // 表单 Ref
 
-// 指定审批人
+// 审批相关:变量
 const processDefineKey = 'oa_leave' // 流程定义 Key
 const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
 const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
+const tempStartUserSelectAssignees = ref({}) // 历史发起人选择审批人的数据,用于每次表单变更时,临时保存
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([]) // 审批节点信息
+const processDefinitionId = ref('')
 
 /** 提交表单 */
 const submitForm = async () => {
-  // 校验表单
+  // 1.1 校验表单
   if (!formRef) return
   const valid = await formRef.value.validate()
   if (!valid) return
-  // 校验指定审批人
+  // 1.2 审批相关:校验指定审批人
   if (startUserSelectTasks.value?.length > 0) {
-    await startUserSelectAssigneesFormRef.value.validate()
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      ) {
+        return message.warning(`请选择${userTask.name}的审批人`)
+      }
+    }
   }
 
-  // 提交请求
+  // 2. 提交请求
   formLoading.value = true
   try {
     const data = { ...formData.value } as unknown as LeaveApi.LeaveVO
-    // 设置指定审批人
+    // 审批相关:设置指定审批人
     if (startUserSelectTasks.value?.length > 0) {
       data.startUserSelectAssignees = startUserSelectAssignees.value
     }
@@ -137,28 +139,93 @@ const submitForm = async () => {
   }
 }
 
+/** 审批相关:获取审批详情 */
+const getApprovalDetail = async () => {
+  try {
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: processDefinitionId.value,
+      // TODO 小北:可以支持 processDefinitionKey 查询
+      activityId: NodeId.START_USER_NODE_ID,
+      processVariablesStr: JSON.stringify({ day: daysDifference() }) // 解决 GET 无法传递对象的问题,后端 String 再转 JSON
+    })
+
+    if (!data) {
+      message.error('查询不到审批详情信息!')
+      return
+    }
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) => CandidateStrategy.START_USER_SELECT === node.candidateStrategy
+    )
+    // 恢复之前的选择审批人
+    if (startUserSelectTasks.value?.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        if (
+          tempStartUserSelectAssignees.value[node.id] &&
+          tempStartUserSelectAssignees.value[node.id].length > 0
+        ) {
+          startUserSelectAssignees.value[node.id] = tempStartUserSelectAssignees.value[node.id]
+        } else {
+          startUserSelectAssignees.value[node.id] = []
+        }
+      }
+    }
+  } finally {
+  }
+}
+
+/** 审批相关:选择发起人 */
+const selectUserConfirm = (id: string, userList: any[]) => {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id)
+}
+
+// 计算天数差
+// TODO @小北:可以搞到 formatTime 里面去,然后看看 dayjs 里面有没有现成的方法,或者辅助计算的方法。
+const daysDifference = () => {
+  const oneDay = 24 * 60 * 60 * 1000 // 一天的毫秒数
+  const diffTime = Math.abs(Number(formData.value.endTime) - Number(formData.value.startTime))
+  return Math.floor(diffTime / oneDay)
+}
+
 /** 初始化 */
 onMounted(async () => {
+  // TODO @小北:这里可以简化,统一通过 getApprovalDetail 处理么?
   const processDefinitionDetail = await DefinitionApi.getProcessDefinition(
     undefined,
     processDefineKey
   )
+
   if (!processDefinitionDetail) {
     message.error('OA 请假的流程模型未配置,请检查!')
     return
   }
+  processDefinitionId.value = processDefinitionDetail.id
   startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
-  // 设置指定审批人
-  if (startUserSelectTasks.value?.length > 0) {
-    // 设置校验规则
-    for (const userTask of startUserSelectTasks.value) {
-      startUserSelectAssignees.value[userTask.id] = []
-      startUserSelectAssigneesFormRules.value[userTask.id] = [
-        { required: true, message: '请选择审批人', trigger: 'blur' }
-      ]
+
+  // 审批相关:加载最新的审批详情,主要用于节点预测
+  await getApprovalDetail()
+})
+
+/** 审批相关:预测流程节点会因为输入的参数值而产生新的预测结果值,所以需重新预测一次, formData.value可改成实际业务中的特定字段 */
+watch(
+  formData.value,
+  (newValue, oldValue) => {
+    if (!oldValue) {
+      return
     }
-    // 加载用户列表
-    userList.value = await UserApi.getSimpleUserList()
+    if (newValue && Object.keys(newValue).length > 0) {
+      // 记录之前的节点审批人
+      tempStartUserSelectAssignees.value = startUserSelectAssignees.value
+      startUserSelectAssignees.value = {}
+      // 加载最新的审批详情,主要用于节点预测
+      getApprovalDetail()
+    }
+  },
+  {
+    immediate: true
   }
-})
+)
 </script>

+ 17 - 0
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -37,6 +37,11 @@
             {{ getApprovalNodeTime(activity) }}
           </div>
         </div>
+        <div v-if="activity.nodeType === NodeType.CHILD_PROCESS_NODE">
+          <el-button type="primary" plain size="small" @click="handleChildProcess(activity)">
+            查看子流程
+          </el-button>
+        </div>
         <!-- 需要自定义选择审批人 -->
         <div
           class="flex flex-wrap gap2 items-center"
@@ -194,6 +199,7 @@ withDefaults(
     showStatusIcon: true // 默认值为 true
   }
 )
+const { push } = useRouter() // 路由
 
 // 审批节点
 const statusIconMap2 = {
@@ -310,4 +316,15 @@ const handleUserSelectConfirm = (activityId: string, userList: any[]) => {
   customApproveUsers.value[activityId] = userList || []
   emit('selectUserConfirm', activityId, userList)
 }
+
+/** 跳转子流程 */
+const handleChildProcess = (activity: any) => {
+  // TODO @lesan:貌似跳不过去?!
+  push({
+    name: 'BpmProcessInstanceDetail',
+    query: {
+      id: activity.processInstanceId
+    }
+  })
+}
 </script>

+ 2 - 0
src/views/bpm/simple/SimpleModelDesign.vue

@@ -6,6 +6,7 @@
       :model-name="modelName"
       @success="handleSuccess"
       :start-user-ids="startUserIds"
+      :start-dept-ids="startDeptIds"
       ref="designerRef"
     />
   </ContentWrap>
@@ -22,6 +23,7 @@ defineProps<{
   modelKey?: string
   modelName?: string
   startUserIds?: number[]
+  startDeptIds?: number[]
 }>()
 
 const emit = defineEmits(['success'])