Explorar o código

【功能合并】工作流:发起流程,使用新的界面

YunaiV hai 9 meses
pai
achega
5121d5694d

+ 0 - 1
src/assets/svgs/bpm/end.svg

@@ -1 +0,0 @@
-<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1729949323056" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12570" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M0 0h1024v1024H0z" fill="#333333" opacity=".01" p-id="12571"></path><path d="M512 0a32 32 0 0 0-32 32v448a32 32 0 0 0 64 0v-448A32 32 0 0 0 512 0zM282.688 109.696a32 32 0 0 0-44.544-7.936 480 480 0 1 0 549.12 0.96 32 32 0 1 0-36.8 52.352 416 416 0 1 1-475.776-0.832 32 32 0 0 0 8-44.544z" fill="#e6e6e6" p-id="12572" data-spm-anchor-id="a313x.search_index.0.i5.3c163a81TFKD3L" class="selected"></path></svg>

+ 21 - 17
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -2,16 +2,10 @@
   <ContentWrap :bodyStyle="{ padding: '10px 20px 0' }">
     <div class="processInstance-wrap-main">
       <el-scrollbar>
-        <div class="text-#878c93 h-15px">编号:{{ selectProcessDefinition.id }}</div>
+        <div class="text-#878c93 h-15px">流程:{{ selectProcessDefinition.name }}</div>
         <el-divider class="!my-8px" />
 
-        <div class="flex items-center justify-between gap-5 mb-10px h-40px">
-          <div class="text-26px font-bold mb-5px">{{ selectProcessDefinition.name }}</div>
-          <el-button style="float: right" type="primary" @click="handleCancel">
-            <Icon icon="ep:delete" /> 选择其它流程
-          </el-button>
-        </div>
-        <!-- 中间主要内容tab栏 -->
+        <!-- 中间主要内容 tab 栏 -->
         <el-tabs v-model="activeTab">
           <!-- 表单信息 -->
           <el-tab-pane label="表单填写" name="form">
@@ -65,7 +59,7 @@
                     <!-- 流程时间线 -->
                     <ProcessInstanceTimeline
                       ref="timelineRef"
-                      :approve-nodes="approveNodes"
+                      :activity-nodes="activityNodes"
                       :show-status-icon="false"
                       candidateField="candidateUserList"
                     />
@@ -103,7 +97,7 @@
   </ContentWrap>
 </template>
 <script lang="ts" setup>
-import { setConfAndFields2 } from '@/utils/formCreate'
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
 import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
 import type { ApiAttrs } from '@form-create/element-ui/types/config'
@@ -137,10 +131,10 @@ const bpmnXML: any = ref(null) // BPMN 数据
 const activeTab = ref('form')
 const emit = defineEmits(['cancel'])
 // 审批节点信息
-const approveNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
+const activityNodes = ref<ProcessInstanceApi.ApprovalNodeInfo[]>([])
 
 /** 设置表单信息、获取流程图数据 **/
-const initProcessInfo = async (row, formVariables?) => {
+const initProcessInfo = async (row: any, formVariables?: any) => {
   // 重置指定审批人
   startUserSelectTasks.value = []
   startUserSelectAssignees.value = {}
@@ -149,11 +143,20 @@ const initProcessInfo = async (row, formVariables?) => {
   // 情况一:流程表单
   if (row.formType == 10) {
     // 设置表单
+    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
+    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
+    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
+    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
+    for (const key in formVariables) {
+      if (!allowedFields.includes(key)) {
+        delete formVariables[key]
+      }
+    }
     setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
     await nextTick()
     fApi.value?.btn.show(false) // 隐藏提交按钮
     // 获取流程审批信息
-    getApprovalDetail(row)
+    await getApprovalDetail(row)
 
     // 加载流程图
     const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
@@ -190,7 +193,7 @@ const initProcessInfo = async (row, formVariables?) => {
 }
 
 /** 获取审批详情 */
-const getApprovalDetail = async (row) => {
+const getApprovalDetail = async (row: any) => {
   try {
     const param = {
       processDefinitionId: row.id
@@ -201,13 +204,14 @@ const getApprovalDetail = async (row) => {
       return
     }
     // 获取审批节点,显示 Timeline 的数据
-    approveNodes.value = data.approveNodes
+    activityNodes.value = data.activityNodes
   } finally {
   }
 }
 /** 提交按钮 */
-const submitForm = async (formData) => {
-  if (!fApi.value || props.selectProcessDefinition) {
+const submitForm = async (formData: any) => {
+  debugger
+  if (!fApi.value || !props.selectProcessDefinition) {
     return
   }
   // 如果有指定审批人,需要校验

+ 79 - 196
src/views/bpm/processInstance/create/index.vue

@@ -1,120 +1,72 @@
 <template>
-  <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
-
   <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
-    <el-tabs tab-position="left" v-model="categoryActive">
-      <el-tab-pane
-        :label="category.name"
-        :name="category.code"
-        :key="category.code"
-        v-for="category in categoryList"
-      >
-        <el-row :gutter="20">
-          <el-col
-            :lg="6"
-            :sm="12"
-            :xs="24"
+  <ContentWrap
+    class="process-definition-container position-relative pb-20px"
+    v-if="!selectProcessDefinition"
+    v-loading="loading"
+  >
+    <el-row :gutter="20" class="!flex-nowrap">
+      <el-col :span="5">
+        <div class="flex flex-col">
+          <div
+            v-for="category in categoryList"
+            :key="category.code"
+            class="flex items-center p-10px cursor-pointer text-14px rounded-md"
+            :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
+            @click="handleCategoryClick(category)"
+          >
+            {{ category.name }}
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="19">
+        <h3 class="text-16px font-bold mb-10px mt-5px">{{ categoryActive.name }}</h3>
+        <div class="grid grid-cols-3 gap3" v-if="categoryProcessDefinitionList.length">
+          <el-card
             v-for="definition in categoryProcessDefinitionList"
             :key="definition.id"
+            shadow="hover"
+            class="cursor-pointer definition-item-card"
+            @click="handleSelect(definition)"
           >
-            <el-card
-              shadow="hover"
-              class="mb-20px cursor-pointer"
-              @click="handleSelect(definition)"
-            >
-              <template #default>
-                <div class="flex">
-                  <el-image :src="definition.icon" class="w-32px h-32px" />
-                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
-                </div>
-              </template>
-            </el-card>
-          </el-col>
-        </el-row>
-      </el-tab-pane>
-    </el-tabs>
+            <template #default>
+              <div class="flex">
+                <el-image :src="definition.icon" class="w-32px h-32px" />
+                <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+              </div>
+            </template>
+          </el-card>
+        </div>
+        <el-empty v-else />
+      </el-col>
+    </el-row>
   </ContentWrap>
 
   <!-- 第二步,填写表单,进行流程的提交 -->
-  <ContentWrap v-else>
-    <el-card class="box-card">
-      <div class="clearfix">
-        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
-        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
-          <Icon icon="ep:delete" /> 选择其它流程
-        </el-button>
-      </div>
-      <el-col :span="16" :offset="6" style="margin-top: 20px">
-        <form-create
-          :rule="detailForm.rule"
-          v-model:api="fApi"
-          v-model="detailForm.value"
-          :option="detailForm.option"
-          @submit="submitForm"
-        >
-          <template #type-startUserSelect>
-            <el-col :span="24">
-              <el-card class="mb-10px">
-                <template #header>指定审批人</template>
-                <el-form
-                  :model="startUserSelectAssignees"
-                  :rules="startUserSelectAssigneesFormRules"
-                  ref="startUserSelectAssigneesFormRef"
-                >
-                  <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-option
-                        v-for="user in userList"
-                        :key="user.id"
-                        :label="user.nickname"
-                        :value="user.id"
-                      />
-                    </el-select>
-                  </el-form-item>
-                </el-form>
-              </el-card>
-            </el-col>
-          </template>
-        </form-create>
-      </el-col>
-    </el-card>
-    <!-- 流程图预览 -->
-    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
-  </ContentWrap>
+  <ProcessDefinitionDetail
+    v-else
+    ref="processDefinitionDetailRef"
+    :selectProcessDefinition="selectProcessDefinition"
+    @cancel="selectProcessDefinition = undefined"
+  />
 </template>
+
 <script lang="ts" setup>
 import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
-import type { ApiAttrs } from '@form-create/element-ui/types/config'
-import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
 import { CategoryApi } from '@/api/bpm/category'
-import { useTagsViewStore } from '@/store/modules/tagsView'
-import * as UserApi from '@/api/system/user'
+import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
 const route = useRoute() // 路由
-const { push, currentRoute } = useRouter() // 路由
 const message = useMessage() // 消息
-const { delView } = useTagsViewStore() // 视图操作
 
-const processInstanceId = route.query.processInstanceId
+const processInstanceId: any = route.query.processInstanceId
 const loading = ref(true) // 加载中
-const categoryList = ref([]) // 分类的列表
-const categoryActive = ref('') // 选中的分类
+const categoryList: any = ref([]) // 分类的列表
+const categoryActive: any = ref({}) // 选中的分类
 const processDefinitionList = ref([]) // 流程定义的列表
-
 /** 查询列表 */
 const getList = async () => {
   loading.value = true
@@ -122,7 +74,7 @@ const getList = async () => {
     // 流程分类
     categoryList.value = await CategoryApi.getCategorySimpleList()
     if (categoryList.value.length > 0) {
-      categoryActive.value = categoryList.value[0].code
+      categoryActive.value = categoryList.value[0]
     }
     // 流程定义
     processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
@@ -137,7 +89,7 @@ const getList = async () => {
         return
       }
       const processDefinition = processDefinitionList.value.find(
-        (item) => item.key == processInstance.processDefinition?.key
+        (item: any) => item.key == processInstance.processDefinition?.key
       )
       if (!processDefinition) {
         message.error('重新发起流程失败,原因:流程定义不存在')
@@ -151,113 +103,27 @@ const getList = async () => {
 }
 
 /** 选中分类对应的流程定义列表 */
-const categoryProcessDefinitionList = computed(() => {
-  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+const categoryProcessDefinitionList: any = computed(() => {
+  return processDefinitionList.value.filter(
+    (item: any) => item.category == categoryActive.value.code
+  )
 })
 
 // ========== 表单相关 ==========
-const fApi = ref<ApiAttrs>()
-const detailForm = ref({
-  rule: [],
-  option: {},
-  value: {}
-}) // 流程表单详情
 const selectProcessDefinition = ref() // 选择的流程定义
-
-// 指定审批人
-const bpmnXML = ref(null) // BPMN 数据
-const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
-const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
+const processDefinitionDetailRef = ref()
 
 /** 处理选择流程的按钮操作 **/
-const handleSelect = async (row, formVariables) => {
+const handleSelect = async (row, formVariables?) => {
   // 设置选择的流程
   selectProcessDefinition.value = row
-
-  // 重置指定审批人
-  startUserSelectTasks.value = []
-  startUserSelectAssignees.value = {}
-  startUserSelectAssigneesFormRules.value = {}
-
-  // 情况一:流程表单
-  if (row.formType == 10) {
-    // 设置表单
-    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
-    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
-    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
-    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
-    for (const key in formVariables) {
-      if (!allowedFields.includes(key)) {
-        delete formVariables[key]
-      }
-    }
-    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
-
-    // 加载流程图
-    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
-    if (processDefinitionDetail) {
-      bpmnXML.value = processDefinitionDetail.bpmnXml
-      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
-
-      // 设置指定审批人
-      if (startUserSelectTasks.value?.length > 0) {
-        detailForm.value.rule.push({
-          type: 'startUserSelect',
-          props: {
-            title: '指定审批人'
-          }
-        })
-        // 设置校验规则
-        for (const userTask of startUserSelectTasks.value) {
-          startUserSelectAssignees.value[userTask.id] = []
-          startUserSelectAssigneesFormRules.value[userTask.id] = [
-            { required: true, message: '请选择审批人', trigger: 'blur' }
-          ]
-        }
-        // 加载用户列表
-        userList.value = await UserApi.getSimpleUserList()
-      }
-    }
-    // 情况二:业务表单
-  } else if (row.formCustomCreatePath) {
-    await push({
-      path: row.formCustomCreatePath
-    })
-    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
-  }
+  // 初始化流程定义详情
+  await nextTick()
+  processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
 }
-
-/** 提交按钮 */
-const submitForm = async (formData) => {
-  if (!fApi.value || !selectProcessDefinition.value) {
-    return
-  }
-  // 如果有指定审批人,需要校验
-  if (startUserSelectTasks.value?.length > 0) {
-    await startUserSelectAssigneesFormRef.value.validate()
-  }
-
-  // 提交请求
-  fApi.value.btn.loading(true)
-  try {
-    await ProcessInstanceApi.createProcessInstance({
-      processDefinitionId: selectProcessDefinition.value.id,
-      variables: formData,
-      startUserSelectAssignees: startUserSelectAssignees.value
-    })
-    // 提示
-    message.success('发起流程成功')
-    // 跳转回去
-    delView(unref(currentRoute))
-    await push({
-      name: 'BpmProcessInstanceMy'
-    })
-  } finally {
-    fApi.value.btn.loading(false)
-  }
+// 左侧分类切换
+const handleCategoryClick = (val: number) => {
+  categoryActive.value = val
 }
 
 /** 初始化 */
@@ -265,3 +131,20 @@ onMounted(() => {
   getList()
 })
 </script>
+
+<style lang="scss" scoped>
+.process-definition-container::before {
+  content: '';
+  border-left: 1px solid #e6e6e6;
+  position: absolute;
+  left: 20.8%;
+  height: 100%;
+}
+:deep() {
+  .definition-item-card {
+    .el-card__body {
+      padding: 14px;
+    }
+  }
+}
+</style>

+ 0 - 150
src/views/bpm/processInstance/create/index_new.vue

@@ -1,150 +0,0 @@
-<template>
-  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-  <ContentWrap
-    class="process-definition-container position-relative pb-20px"
-    v-if="!selectProcessDefinition"
-    v-loading="loading"
-  >
-    <el-row :gutter="20" class="!flex-nowrap">
-      <el-col :span="5">
-        <div class="flex flex-col">
-          <div
-            v-for="category in categoryList"
-            :key="category.code"
-            class="flex items-center p-10px cursor-pointer text-14px rounded-md"
-            :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
-            @click="handleCategoryClick(category)"
-          >
-            {{ category.name }}
-          </div>
-        </div>
-      </el-col>
-      <el-col :span="19">
-        <h3 class="text-16px font-bold mb-10px mt-5px">{{ categoryActive.name }}</h3>
-        <div class="grid grid-cols-3 gap3" v-if="categoryProcessDefinitionList.length">
-          <el-card
-            v-for="definition in categoryProcessDefinitionList"
-            :key="definition.id"
-            shadow="hover"
-            class="cursor-pointer definition-item-card"
-            @click="handleSelect(definition)"
-          >
-            <template #default>
-              <div class="flex">
-                <el-image :src="definition.icon" class="w-32px h-32px" />
-                <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
-              </div>
-            </template>
-          </el-card>
-        </div>
-        <el-empty v-else />
-      </el-col>
-    </el-row>
-  </ContentWrap>
-
-  <!-- 第二步,填写表单,进行流程的提交 -->
-  <ProcessDefinitionDetail
-    v-else
-    ref="processDefinitionDetailRef"
-    :selectProcessDefinition="selectProcessDefinition"
-    @cancel="selectProcessDefinition = undefined"
-  />
-</template>
-
-<script lang="ts" setup>
-import * as DefinitionApi from '@/api/bpm/definition'
-import * as ProcessInstanceApi from '@/api/bpm/processInstance'
-import { CategoryApi } from '@/api/bpm/category'
-import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
-
-defineOptions({ name: 'BpmProcessInstanceCreate' })
-
-const route = useRoute() // 路由
-const message = useMessage() // 消息
-
-const processInstanceId: any = route.query.processInstanceId
-const loading = ref(true) // 加载中
-const categoryList: any = ref([]) // 分类的列表
-const categoryActive: any = ref({}) // 选中的分类
-const processDefinitionList = ref([]) // 流程定义的列表
-/** 查询列表 */
-const getList = async () => {
-  loading.value = true
-  try {
-    // 流程分类
-    categoryList.value = await CategoryApi.getCategorySimpleList()
-    if (categoryList.value.length > 0) {
-      categoryActive.value = categoryList.value[0]
-    }
-    // 流程定义
-    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
-      suspensionState: 1
-    })
-
-    // 如果 processInstanceId 非空,说明是重新发起
-    if (processInstanceId?.length > 0) {
-      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
-      if (!processInstance) {
-        message.error('重新发起流程失败,原因:流程实例不存在')
-        return
-      }
-      const processDefinition = processDefinitionList.value.find(
-        (item: any) => item.key == processInstance.processDefinition?.key
-      )
-      if (!processDefinition) {
-        message.error('重新发起流程失败,原因:流程定义不存在')
-        return
-      }
-      await handleSelect(processDefinition, processInstance.formVariables)
-    }
-  } finally {
-    loading.value = false
-  }
-}
-
-/** 选中分类对应的流程定义列表 */
-const categoryProcessDefinitionList: any = computed(() => {
-  return processDefinitionList.value.filter(
-    (item: any) => item.category == categoryActive.value.code
-  )
-})
-
-// ========== 表单相关 ==========
-const selectProcessDefinition = ref() // 选择的流程定义
-const processDefinitionDetailRef = ref()
-
-/** 处理选择流程的按钮操作 **/
-const handleSelect = async (row, formVariables?) => {
-  // 设置选择的流程
-  selectProcessDefinition.value = row
-  // 初始化流程定义详情
-  await nextTick()
-  processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
-}
-// 左侧分类切换
-const handleCategoryClick = (val) => {
-  categoryActive.value = val
-}
-
-/** 初始化 */
-onMounted(() => {
-  getList()
-})
-</script>
-
-<style lang="scss" scoped>
-.process-definition-container::before {
-  content: '';
-  border-left: 1px solid #e6e6e6;
-  position: absolute;
-  left: 20.8%;
-  height: 100%;
-}
-:deep() {
-  .definition-item-card {
-    .el-card__body {
-      padding: 14px;
-    }
-  }
-}
-</style>

+ 267 - 0
src/views/bpm/processInstance/create/index_old.vue

@@ -0,0 +1,267 @@
+<template>
+  <doc-alert title="流程发起、取消、重新发起" url="https://doc.iocoder.cn/bpm/process-instance/" />
+
+  <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
+  <ContentWrap v-if="!selectProcessDefinition" v-loading="loading">
+    <el-tabs tab-position="left" v-model="categoryActive">
+      <el-tab-pane
+        :label="category.name"
+        :name="category.code"
+        :key="category.code"
+        v-for="category in categoryList"
+      >
+        <el-row :gutter="20">
+          <el-col
+            :lg="6"
+            :sm="12"
+            :xs="24"
+            v-for="definition in categoryProcessDefinitionList"
+            :key="definition.id"
+          >
+            <el-card
+              shadow="hover"
+              class="mb-20px cursor-pointer"
+              @click="handleSelect(definition)"
+            >
+              <template #default>
+                <div class="flex">
+                  <el-image :src="definition.icon" class="w-32px h-32px" />
+                  <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                </div>
+              </template>
+            </el-card>
+          </el-col>
+        </el-row>
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+
+  <!-- 第二步,填写表单,进行流程的提交 -->
+  <ContentWrap v-else>
+    <el-card class="box-card">
+      <div class="clearfix">
+        <span class="el-icon-document">申请信息【{{ selectProcessDefinition.name }}】</span>
+        <el-button style="float: right" type="primary" @click="selectProcessDefinition = undefined">
+          <Icon icon="ep:delete" /> 选择其它流程
+        </el-button>
+      </div>
+      <el-col :span="16" :offset="6" style="margin-top: 20px">
+        <form-create
+          :rule="detailForm.rule"
+          v-model:api="fApi"
+          v-model="detailForm.value"
+          :option="detailForm.option"
+          @submit="submitForm"
+        >
+          <template #type-startUserSelect>
+            <el-col :span="24">
+              <el-card class="mb-10px">
+                <template #header>指定审批人</template>
+                <el-form
+                  :model="startUserSelectAssignees"
+                  :rules="startUserSelectAssigneesFormRules"
+                  ref="startUserSelectAssigneesFormRef"
+                >
+                  <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-option
+                        v-for="user in userList"
+                        :key="user.id"
+                        :label="user.nickname"
+                        :value="user.id"
+                      />
+                    </el-select>
+                  </el-form-item>
+                </el-form>
+              </el-card>
+            </el-col>
+          </template>
+        </form-create>
+      </el-col>
+    </el-card>
+    <!-- 流程图预览 -->
+    <ProcessInstanceBpmnViewer :bpmn-xml="bpmnXML as any" />
+  </ContentWrap>
+</template>
+<script lang="ts" setup>
+import * as DefinitionApi from '@/api/bpm/definition'
+import * as ProcessInstanceApi from '@/api/bpm/processInstance'
+import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
+import type { ApiAttrs } from '@form-create/element-ui/types/config'
+import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
+import { CategoryApi } from '@/api/bpm/category'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'BpmProcessInstanceCreate' })
+
+const route = useRoute() // 路由
+const { push, currentRoute } = useRouter() // 路由
+const message = useMessage() // 消息
+const { delView } = useTagsViewStore() // 视图操作
+
+const processInstanceId = route.query.processInstanceId
+const loading = ref(true) // 加载中
+const categoryList = ref([]) // 分类的列表
+const categoryActive = ref('') // 选中的分类
+const processDefinitionList = ref([]) // 流程定义的列表
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+    if (categoryList.value.length > 0) {
+      categoryActive.value = categoryList.value[0].code
+    }
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
+
+    // 如果 processInstanceId 非空,说明是重新发起
+    if (processInstanceId?.length > 0) {
+      const processInstance = await ProcessInstanceApi.getProcessInstance(processInstanceId)
+      if (!processInstance) {
+        message.error('重新发起流程失败,原因:流程实例不存在')
+        return
+      }
+      const processDefinition = processDefinitionList.value.find(
+        (item) => item.key == processInstance.processDefinition?.key
+      )
+      if (!processDefinition) {
+        message.error('重新发起流程失败,原因:流程定义不存在')
+        return
+      }
+      await handleSelect(processDefinition, processInstance.formVariables)
+    }
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 选中分类对应的流程定义列表 */
+const categoryProcessDefinitionList = computed(() => {
+  return processDefinitionList.value.filter((item) => item.category == categoryActive.value)
+})
+
+// ========== 表单相关 ==========
+const fApi = ref<ApiAttrs>()
+const detailForm = ref({
+  rule: [],
+  option: {},
+  value: {}
+}) // 流程表单详情
+const selectProcessDefinition = ref() // 选择的流程定义
+
+// 指定审批人
+const bpmnXML = ref(null) // BPMN 数据
+const startUserSelectTasks = ref([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
+const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
+const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
+const userList = ref<any[]>([]) // 用户列表
+
+/** 处理选择流程的按钮操作 **/
+const handleSelect = async (row, formVariables) => {
+  // 设置选择的流程
+  selectProcessDefinition.value = row
+
+  // 重置指定审批人
+  startUserSelectTasks.value = []
+  startUserSelectAssignees.value = {}
+  startUserSelectAssigneesFormRules.value = {}
+
+  // 情况一:流程表单
+  if (row.formType == 10) {
+    // 设置表单
+    // 注意:需要从 formVariables 中,移除不在 row.formFields 的值。
+    // 原因是:后端返回的 formVariables 里面,会有一些非表单的信息。例如说,某个流程节点的审批人。
+    //        这样,就可能导致一个流程被审批不通过后,重新发起时,会直接后端报错!!!
+    const allowedFields = decodeFields(row.formFields).map((fieldObj: any) => fieldObj.field)
+    for (const key in formVariables) {
+      if (!allowedFields.includes(key)) {
+        delete formVariables[key]
+      }
+    }
+    setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+
+    // 加载流程图
+    const processDefinitionDetail = await DefinitionApi.getProcessDefinition(row.id)
+    if (processDefinitionDetail) {
+      bpmnXML.value = processDefinitionDetail.bpmnXml
+      startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
+
+      // 设置指定审批人
+      if (startUserSelectTasks.value?.length > 0) {
+        detailForm.value.rule.push({
+          type: 'startUserSelect',
+          props: {
+            title: '指定审批人'
+          }
+        })
+        // 设置校验规则
+        for (const userTask of startUserSelectTasks.value) {
+          startUserSelectAssignees.value[userTask.id] = []
+          startUserSelectAssigneesFormRules.value[userTask.id] = [
+            { required: true, message: '请选择审批人', trigger: 'blur' }
+          ]
+        }
+        // 加载用户列表
+        userList.value = await UserApi.getSimpleUserList()
+      }
+    }
+    // 情况二:业务表单
+  } else if (row.formCustomCreatePath) {
+    await push({
+      path: row.formCustomCreatePath
+    })
+    // 这里暂时无需加载流程图,因为跳出到另外个 Tab;
+  }
+}
+
+/** 提交按钮 */
+const submitForm = async (formData) => {
+  if (!fApi.value || !selectProcessDefinition.value) {
+    return
+  }
+  // 如果有指定审批人,需要校验
+  if (startUserSelectTasks.value?.length > 0) {
+    await startUserSelectAssigneesFormRef.value.validate()
+  }
+
+  // 提交请求
+  fApi.value.btn.loading(true)
+  try {
+    await ProcessInstanceApi.createProcessInstance({
+      processDefinitionId: selectProcessDefinition.value.id,
+      variables: formData,
+      startUserSelectAssignees: startUserSelectAssignees.value
+    })
+    // 提示
+    message.success('发起流程成功')
+    // 跳转回去
+    delView(unref(currentRoute))
+    await push({
+      name: 'BpmProcessInstanceMy'
+    })
+  } finally {
+    fApi.value.btn.loading(false)
+  }
+}
+
+/** 初始化 */
+onMounted(() => {
+  getList()
+})
+</script>

+ 2 - 8
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -134,16 +134,12 @@ import auditorSvg from '@/assets/svgs/bpm/auditor.svg'
 import copySvg from '@/assets/svgs/bpm/copy.svg'
 import conditionSvg from '@/assets/svgs/bpm/condition.svg'
 import parallelSvg from '@/assets/svgs/bpm/parallel.svg'
-import endSvg from '@/assets/svgs/bpm/end.svg'
 import finishSvg from '@/assets/svgs/bpm/finish.svg'
 
 defineOptions({ name: 'BpmProcessInstanceTimeline' })
-defineProps<{
-  activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
-}>()
 withDefaults(
   defineProps<{
-    approveNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
+    activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
     showStatusIcon?: boolean // 是否显示头像右下角状态图标
   }>(),
   {
@@ -205,9 +201,7 @@ const nodeTypeSvgMap = {
   // 条件分支节点
   [NodeType.CONDITION_NODE]: { color: '#14bb83', svg: conditionSvg },
   // 并行分支节点
-  [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg },
-  // 结束节点
-  [NodeType.END_EVENT_NODE]: { color: '#ffffff', svg: endSvg }
+  [NodeType.PARALLEL_BRANCH_NODE]: { color: '#14bb83', svg: parallelSvg }
 }
 
 // 只有只有状态是 -1、0、1 才展示头像右小角状态小icon