Browse Source

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

# Conflicts:
#	src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue
#	src/views/bpm/model/editor/index.vue
YunaiV 8 months ago
parent
commit
c083a0c986

+ 72 - 23
src/components/SimpleProcessDesignerV2/src/SimpleProcessDesigner.vue

@@ -38,12 +38,21 @@ import * as UserGroupApi from '@/api/bpm/userGroup'
 defineOptions({
   name: 'SimpleProcessDesigner'
 })
+
 const emits = defineEmits(['success']) // 保存成功事件
 
 const props = defineProps({
   modelId: {
     type: String,
-    required: true
+    required: false
+  },
+  modelKey: {
+    type: String,
+    required: false
+  },
+  modelName: {
+    type: String,
+    required: false
   }
 })
 
@@ -69,6 +78,33 @@ const message = useMessage() // 国际化
 const processNodeTree = ref<SimpleFlowNode | undefined>()
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
+
+// 添加更新模型的方法
+const updateModel = (key?: string, name?: string) => {
+  if (!processNodeTree.value) {
+    processNodeTree.value = {
+      name: name || '发起人',
+      type: NodeType.START_USER_NODE,
+      id: NodeId.START_USER_NODE_ID,
+      childNode: {
+        id: NodeId.END_EVENT_NODE_ID,
+        name: '结束',
+        type: NodeType.END_EVENT_NODE
+      }
+    }
+  } else if (name) {
+    // 更新现有模型的名称
+    processNodeTree.value.name = name
+  }
+}
+
+// 监听属性变化
+watch([() => props.modelKey, () => props.modelName], ([newKey, newName]) => {
+  if (!props.modelId && newKey && newName) {
+    updateModel(newKey, newName)
+  }
+}, { immediate: true, deep: true })
+
 const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
   if (!simpleModelNode) {
     message.error('模型数据为空')
@@ -76,21 +112,28 @@ const saveSimpleFlowModel = async (simpleModelNode: SimpleFlowNode) => {
   }
   try {
     loading.value = true
-    const data = {
-      id: props.modelId,
-      simpleModel: simpleModelNode
-    }
-    const result = await updateBpmSimpleModel(data)
-    if (result) {
-      message.success('修改成功')
-      emits('success')
+    if (props.modelId) {
+      // 编辑模式
+      const data = {
+        id: props.modelId,
+        simpleModel: simpleModelNode
+      }
+      const result = await updateBpmSimpleModel(data)
+      if (result) {
+        message.success('修改成功')
+        emits('success')
+      } else {
+        message.alert('修改失败')
+      }
     } else {
-      message.alert('修改失败')
+      // 新建模式,直接返回数据
+      emits('success', simpleModelNode)
     }
   } finally {
     loading.value = false
   }
 }
+
 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
@@ -134,12 +177,14 @@ onMounted(async () => {
   try {
     loading.value = true
     // 获取表单字段
-    const bpmnModel = await getModel(props.modelId)
-    if (bpmnModel) {
-      formType.value = bpmnModel.formType
-      if (formType.value === 10) {
-        const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
-        formFields.value = bpmnForm?.fields
+    if (props.modelId) {
+      const bpmnModel = await getModel(props.modelId)
+      if (bpmnModel) {
+        formType.value = bpmnModel.formType
+        if (formType.value === 10) {
+          const bpmnForm = (await getForm(bpmnModel.formId)) as unknown as FormVO
+          formFields.value = bpmnForm?.fields
+        }
       }
     }
     // 获得角色列表
@@ -155,14 +200,18 @@ onMounted(async () => {
     // 获取用户组列表
     userGroupOptions.value = await UserGroupApi.getUserGroupSimpleList()
 
-    //获取 SIMPLE 设计器模型
-    const result = await getBpmSimpleModel(props.modelId)
-    if (result) {
-      processNodeTree.value = result
-    } else {
-      // 初始值
+    if (props.modelId) {
+      //获取 SIMPLE 设计器模型
+      const result = await getBpmSimpleModel(props.modelId)
+      if (result) {
+        processNodeTree.value = result
+      }
+    }
+    
+    // 如果没有现有模型,创建初始模型
+    if (!processNodeTree.value) {
       processNodeTree.value = {
-        name: '发起人',
+        name: props.modelName || '发起人',
         type: NodeType.START_USER_NODE,
         id: NodeId.START_USER_NODE_ID,
         childNode: {

+ 29 - 10
src/components/UserSelectForm/index.vue

@@ -39,7 +39,7 @@
   </Dialog>
 </template>
 <script lang="ts" setup>
-import { defaultProps, findTreeNode, handleTree } from '@/utils/tree'
+import { defaultProps, handleTree } from '@/utils/tree'
 import * as DeptApi from '@/api/system/dept'
 import * as UserApi from '@/api/system/user'
 
@@ -50,6 +50,7 @@ const emit = defineEmits<{
 const { t } = useI18n() // 国际
 const message = useMessage() // 消息弹窗
 const deptTree = ref<Tree[]>([]) // 部门树形结构化
+const deptList = ref<any[]>([]) // 保存扁平化的部门列表数据
 const userList = ref<UserApi.UserVO[]>([]) // 所有用户列表
 const filteredUserList = ref<UserApi.UserVO[]>([]) // 当前部门过滤后的用户列表
 const selectedUserIdList: any = ref([]) // 选中的用户列表
@@ -79,7 +80,9 @@ const open = async (id: number, selectedList?: any[]) => {
   resetForm()
 
   // 加载部门、用户列表
-  deptTree.value = handleTree(await DeptApi.getSimpleDeptList())
+  const deptData = await DeptApi.getSimpleDeptList()
+  deptList.value = deptData // 保存扁平结构的部门数据
+  deptTree.value = handleTree(deptData) // 转换成树形结构
   userList.value = await UserApi.getSimpleUserList()
 
   // 初始状态下,过滤列表等于所有用户列表
@@ -88,16 +91,31 @@ const open = async (id: number, selectedList?: any[]) => {
   dialogVisible.value = true
 }
 
+/** 获取指定部门及其所有子部门的ID列表 */
+const getChildDeptIds = (deptId: number, deptList: any[]): number[] => {
+  const ids = [deptId]
+  const children = deptList.filter((dept) => dept.parentId === deptId)
+  children.forEach((child) => {
+    ids.push(...getChildDeptIds(child.id, deptList))
+  })
+  return ids
+}
+
 /** 获取部门过滤后的用户列表 */
-const getUserList = async (deptId?: number) => {
+const filterUserList = async (deptId?: number) => {
   formLoading.value = true
   try {
-    // @ts-ignore
-    // TODO @芋艿:替换到 simple List 暂不支持 deptId 过滤
-    // TODO @Zqqq:这个,可以使用前端过滤么?通过 deptList 获取到 deptId 子节点,然后去 userList
-    const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
-    // 更新过滤后的用户列表
-    filteredUserList.value = data.list
+    if (!deptId) {
+      // 如果没有选择部门,显示所有用户
+      filteredUserList.value = [...userList.value]
+      return
+    }
+
+    // 直接使用已保存的部门列表数据进行过滤
+    const deptIds = getChildDeptIds(deptId, deptList.value)
+
+    // 过滤出这些部门下的用户
+    filteredUserList.value = userList.value.filter((user) => deptIds.includes(user.deptId))
   } finally {
     formLoading.value = false
   }
@@ -121,6 +139,7 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = () => {
   deptTree.value = []
+  deptList.value = []
   userList.value = []
   filteredUserList.value = []
   selectedUserIdList.value = []
@@ -128,7 +147,7 @@ const resetForm = () => {
 
 /** 处理部门被点击 */
 const handleNodeClick = (row: { [key: string]: any }) => {
-  getUserList(row.id)
+  filterUserList(row.id)
 }
 
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗

+ 77 - 40
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -1,6 +1,6 @@
 <template>
-  <div class="process-panel__container" :style="{ width: `${width}px`, maxHeight: '700px' }">
-    <el-collapse v-model="activeTab">
+  <div class="process-panel__container" :style="{ width: `${width}px` }">
+    <el-collapse v-model="activeTab" v-if="isReady">
       <el-collapse-item name="base">
         <!-- class="panel-tab__title" -->
         <template #title>
@@ -119,24 +119,16 @@ const elementBusinessObject = ref<any>({}) // 元素 businessObject 镜像,提
 const conditionFormVisible = ref(false) // 流转条件设置
 const formVisible = ref(false) // 表单配置
 const bpmnElement = ref()
+const isReady = ref(false)
 
 provide('prefix', props.prefix)
 provide('width', props.width)
-const bpmnInstances = () => (window as any)?.bpmnInstances
-
-// 监听 props.bpmnModeler 然后 initModels
-const unwatchBpmn = watch(
-  () => props.bpmnModeler,
-  () => {
-    // 避免加载时 流程图 并未加载完成
-    if (!props.bpmnModeler) {
-      console.log('缺少props.bpmnModeler')
-      return
-    }
 
-    console.log('props.bpmnModeler 有值了!!!')
-    const w = window as any
-    w.bpmnInstances = {
+// 初始化 bpmnInstances
+const initBpmnInstances = () => {
+  if (!props.bpmnModeler) return false
+  try {
+    const instances = {
       modeler: props.bpmnModeler,
       modeling: props.bpmnModeler.get('modeling'),
       moddle: props.bpmnModeler.get('moddle'),
@@ -148,9 +140,45 @@ const unwatchBpmn = watch(
       selection: props.bpmnModeler.get('selection')
     }
 
-    console.log(bpmnInstances(), 'window.bpmnInstances')
-    getActiveElement()
-    unwatchBpmn()
+    // 检查所有实例是否都存在
+    const allInstancesExist = Object.values(instances).every(instance => instance)
+    if (allInstancesExist) {
+      const w = window as any
+      w.bpmnInstances = instances
+      return true
+    }
+    return false
+  } catch (error) {
+    console.error('初始化 bpmnInstances 失败:', error)
+    return false
+  }
+}
+
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+// 监听 props.bpmnModeler 然后 initModels
+const unwatchBpmn = watch(
+  () => props.bpmnModeler,
+  async () => {
+    // 避免加载时 流程图 并未加载完成
+    if (!props.bpmnModeler) {
+      console.log('缺少props.bpmnModeler')
+      return
+    }
+
+    try {
+      // 等待 modeler 初始化完成
+      await nextTick()
+      if (initBpmnInstances()) {
+        isReady.value = true
+        await nextTick()
+        getActiveElement()
+      } else {
+        console.error('modeler 实例未完全初始化')
+      }
+    } catch (error) {
+      console.error('初始化失败:', error)
+    }
   },
   {
     immediate: true
@@ -158,6 +186,8 @@ const unwatchBpmn = watch(
 )
 
 const getActiveElement = () => {
+  if (!isReady.value || !props.bpmnModeler) return
+
   // 初始第一个选中元素 bpmn:Process
   initFormOnChanged(null)
   props.bpmnModeler.on('import.done', (e) => {
@@ -175,8 +205,11 @@ const getActiveElement = () => {
     }
   })
 }
+
 // 初始化数据
 const initFormOnChanged = (element) => {
+  if (!isReady.value || !bpmnInstances()) return
+
   let activatedElement = element
   if (!activatedElement) {
     activatedElement =
@@ -184,32 +217,36 @@ const initFormOnChanged = (element) => {
       bpmnInstances().elementRegistry.find((el) => el.type === 'bpmn:Collaboration')
   }
   if (!activatedElement) return
-  console.log(`
-              ----------
-      select element changed:
-                id:  ${activatedElement.id}
-              type:  ${activatedElement.businessObject.$type}
-              ----------
-              `)
-  console.log('businessObject: ', activatedElement.businessObject)
-  bpmnInstances().bpmnElement = activatedElement
-  bpmnElement.value = activatedElement
-  elementId.value = activatedElement.id
-  elementType.value = activatedElement.type.split(':')[1] || ''
-  elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject))
-  conditionFormVisible.value = !!(
-    elementType.value === 'SequenceFlow' &&
-    activatedElement.source &&
-    activatedElement.source.type.indexOf('StartEvent') === -1
-  )
-  formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
+
+  try {
+    console.log(`
+                ----------
+        select element changed:
+                  id:  ${activatedElement.id}
+                type:  ${activatedElement.businessObject.$type}
+                ----------
+                `)
+    console.log('businessObject: ', activatedElement.businessObject)
+    bpmnInstances().bpmnElement = activatedElement
+    bpmnElement.value = activatedElement
+    elementId.value = activatedElement.id
+    elementType.value = activatedElement.type.split(':')[1] || ''
+    elementBusinessObject.value = JSON.parse(JSON.stringify(activatedElement.businessObject))
+    conditionFormVisible.value = !!(
+      elementType.value === 'SequenceFlow' &&
+      activatedElement.source &&
+      activatedElement.source.type.indexOf('StartEvent') === -1
+    )
+    formVisible.value = elementType.value === 'UserTask' || elementType.value === 'StartEvent'
+  } catch (error) {
+    console.error('初始化表单数据失败:', error)
+  }
 }
 
 onBeforeUnmount(() => {
   const w = window as any
   w.bpmnInstances = null
-  console.log(props, 'props1')
-  console.log(props.bpmnModeler, 'props.bpmnModeler1')
+  isReady.value = false
 })
 
 watch(

+ 12 - 0
src/router/modules/remaining.ts

@@ -330,6 +330,18 @@ const remainingRouter: AppRouteRecordRaw[] = [
           title: '查看 OA 请假',
           activeMenu: '/bpm/oa/leave'
         }
+      },
+      {
+        path: 'manager/model/create-update',
+        component: () => import('@/views/bpm/model/CreateUpdate.vue'),
+        name: 'BpmModelCreateUpdate',
+        meta: {
+          noCache: true,
+          hidden: true,
+          canTo: true,
+          title: '创建/修改流程',
+          activeMenu: '/bpm/manager/model'
+        }
       }
     ]
   },

+ 22 - 17
src/views/bpm/model/CategoryDraggableModel.vue

@@ -249,7 +249,7 @@ import { formatDate } from '@/utils/formatTime'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
 import { setConfAndFields2 } from '@/utils/formCreate'
-import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import { BpmModelFormType } from '@/utils/constants'
 import { checkPermi } from '@/utils/permission'
 import { useUserStoreWithOut } from '@/store/modules/user'
 import { useAppStore } from '@/store/modules/app'
@@ -339,21 +339,22 @@ const handleChangeState = async (row: any) => {
 
 /** 设计流程 */
 const handleDesign = (row: any) => {
-  if (row.type == BpmModelType.BPMN) {
-    push({
-      name: 'BpmModelEditor',
-      query: {
-        modelId: row.id
-      }
-    })
-  } else {
-    push({
-      name: 'SimpleModelDesign',
-      query: {
-        modelId: row.id
-      }
-    })
-  }
+  // if (row.type == BpmModelType.BPMN) {
+  //   push({
+  //     name: 'BpmModelEditor',
+  //     query: {
+  //       modelId: row.id
+  //     }
+  //   })
+  // } else {
+  //   push({
+  //     name: 'SimpleModelDesign',
+  //     query: {
+  //       modelId: row.id
+  //     }
+  //   })
+  // }
+  push(`/bpm/manager/model/create-update?id=${row.id}`)
 }
 
 /** 发布流程 */
@@ -496,7 +497,11 @@ const handleDeleteCategory = async () => {
 /** 添加流程模型弹窗 */
 const modelFormRef = ref()
 const openModelForm = (type: string, id?: number) => {
-  modelFormRef.value.open(type, id)
+  if (type === 'create') {
+    push('/bpm/manager/model/create-update')
+  } else {
+    push(`/bpm/manager/model/create-update?id=${id}`)
+  }
 }
 
 watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })

+ 644 - 0
src/views/bpm/model/CreateUpdate.vue

@@ -0,0 +1,644 @@
+<template>
+  <!-- 头部导航栏 -->
+  <div
+    class="absolute top-0 left-0 right-0 h-50px bg-white border-bottom z-10 flex items-center px-20px"
+  >
+    <!-- 左侧标题,固定宽度 -->
+    <div class="w-200px flex items-center overflow-hidden">
+      <Icon icon="ep:arrow-left" class="cursor-pointer flex-shrink-0" @click="router.back()" />
+      <span class="ml-10px text-16px truncate" :title="formData.name || '创建流程'">
+        {{ formData.name || '创建流程' }}
+      </span>
+    </div>
+
+    <!-- 步骤条,固定在中间 -->
+    <div class="flex-1 flex items-center justify-center h-full">
+      <div class="w-400px flex items-center justify-between h-full">
+        <div
+          v-for="(step, index) in steps"
+          :key="index"
+          class="flex items-center cursor-pointer mx-15px relative h-full"
+          :class="[
+            currentStep === index
+              ? 'text-[#3473ff] border-[#3473ff] border-b-2 border-b-solid'
+              : 'text-gray-500'
+          ]"
+          @click="handleStepClick(index)"
+        >
+          <div
+            class="w-28px h-28px rounded-full flex items-center justify-center mr-8px border-2 border-solid text-15px"
+            :class="[
+              currentStep === index
+                ? 'bg-[#3473ff] text-white border-[#3473ff]'
+                : 'border-gray-300 bg-white text-gray-500'
+            ]"
+          >
+            {{ index + 1 }}
+          </div>
+          <span class="text-16px font-bold whitespace-nowrap">{{ step.title }}</span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 右侧按钮,固定宽度 -->
+    <div class="w-200px flex items-center justify-end gap-2">
+      <el-button @click="handleSave">保 存</el-button>
+      <el-button type="primary" @click="handleDeploy">发 布</el-button>
+    </div>
+  </div>
+
+  <!-- 主体内容 -->
+  <ContentWrap class="mt-50px">
+    <!-- 第一步:基本信息 -->
+    <div v-show="currentStep === 0">
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+        class="mt-20px"
+        :class="{ 'w-600px': currentStep === 0 }"
+      >
+        <el-form-item label="流程标识" prop="key" class="mb-20px">
+          <el-input v-model="formData.key" :disabled="!!formData.id" placeholder="请输入流标标识" />
+          <el-tooltip
+            v-if="!formData.id"
+            class="item"
+            content="新建后,流程标识不可修改!"
+            effect="light"
+            placement="top"
+          >
+            <Icon icon="ep:question" class="ml-5px" />
+          </el-tooltip>
+          <el-tooltip
+            v-else
+            class="item"
+            content="流程标识不可修改!"
+            effect="light"
+            placement="top"
+          >
+            <Icon icon="ep:question" class="ml-5px" />
+          </el-tooltip>
+        </el-form-item>
+        <el-form-item label="流程名称" prop="name" class="mb-20px">
+          <el-input
+            v-model="formData.name"
+            :disabled="!!formData.id"
+            clearable
+            placeholder="请输入流程名称"
+          />
+        </el-form-item>
+        <el-form-item label="流程分类" prop="category" class="mb-20px">
+          <el-select
+            v-model="formData.category"
+            clearable
+            placeholder="请选择流程分类"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="category in categoryList"
+              :key="category.code"
+              :label="category.name"
+              :value="category.code"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="流程图标" prop="icon" class="mb-20px">
+          <UploadImg v-model="formData.icon" :limit="1" height="64px" width="64px" />
+        </el-form-item>
+        <el-form-item label="流程描述" prop="description" class="mb-20px">
+          <el-input v-model="formData.description" clearable type="textarea" />
+        </el-form-item>
+        <el-form-item label="流程类型" prop="type" class="mb-20px">
+          <el-radio-group v-model="formData.type">
+            <el-radio
+              v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)"
+              :key="dict.value"
+              :value="dict.value"
+            >
+              {{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="是否可见" prop="visible" class="mb-20px">
+          <el-radio-group v-model="formData.visible">
+            <el-radio
+              v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+              :key="dict.value"
+              :value="dict.value"
+            >
+              {{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="谁可以发起" prop="startUserType" class="mb-20px">
+          <el-select
+            v-model="formData.startUserType"
+            placeholder="请选择谁可以发起"
+            @change="handleStartUserTypeChange"
+          >
+            <el-option label="全员" :value="0" />
+            <el-option label="指定人员" :value="1" />
+            <el-option label="均不可提交" :value="2" />
+          </el-select>
+          <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
+            <div
+              v-for="user in selectedStartUsers"
+              :key="user.id"
+              class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+            >
+              <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
+                icon="ep:close"
+                class="ml-2 cursor-pointer hover:text-red-500"
+                @click="handleRemoveStartUser(user)"
+              />
+            </div>
+            <el-button type="primary" link @click="openStartUserSelect">
+              <Icon icon="ep:plus" />选择人员
+            </el-button>
+          </div>
+        </el-form-item>
+        <el-form-item label="流程管理员" prop="managerUserType" class="mb-20px">
+          <el-select
+            v-model="formData.managerUserType"
+            placeholder="请选择流程管理员"
+            @change="handleManagerUserTypeChange"
+          >
+            <el-option label="全员" :value="0" />
+            <el-option label="指定人员" :value="1" />
+            <el-option label="均不可提交" :value="2" />
+          </el-select>
+          <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
+            <div
+              v-for="user in selectedManagerUsers"
+              :key="user.id"
+              class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+            >
+              <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
+                icon="ep:close"
+                class="ml-2 cursor-pointer hover:text-red-500"
+                @click="handleRemoveManagerUser(user)"
+              />
+            </div>
+            <el-button type="primary" link @click="openManagerUserSelect">
+              <Icon icon="ep:plus" />选择人员
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <div v-if="currentStep === 1">
+      <!-- 第二步:表单设计 -->
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="120px"
+        class="mt-20px"
+      >
+        <el-form-item label="表单类型" prop="formType" class="mb-20px">
+          <el-radio-group v-model="formData.formType">
+            <el-radio
+              v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_FORM_TYPE)"
+              :key="dict.value"
+              :value="dict.value"
+            >
+              {{ dict.label }}
+            </el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item v-if="formData.formType === 10" label="流程表单" prop="formId">
+          <el-select v-model="formData.formId" clearable style="width: 100%">
+            <el-option
+              v-for="form in formList"
+              :key="form.id"
+              :label="form.name"
+              :value="form.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+          v-if="formData.formType === 20"
+          label="表单提交路由"
+          prop="formCustomCreatePath"
+        >
+          <el-input
+            v-model="formData.formCustomCreatePath"
+            placeholder="请输入表单提交路由"
+            style="width: 330px"
+          />
+          <el-tooltip
+            class="item"
+            content="自定义表单的提交路径,使用 Vue 的路由地址,例如说:bpm/oa/leave/create.vue"
+            effect="light"
+            placement="top"
+          >
+            <Icon icon="ep:question" class="ml-5px" />
+          </el-tooltip>
+        </el-form-item>
+        <el-form-item
+          v-if="formData.formType === 20"
+          label="表单查看地址"
+          prop="formCustomViewPath"
+        >
+          <el-input
+            v-model="formData.formCustomViewPath"
+            placeholder="请输入表单查看的组件地址"
+            style="width: 330px"
+          />
+          <el-tooltip
+            class="item"
+            content="自定义表单的查看组件地址,使用 Vue 的组件地址,例如说:bpm/oa/leave/detail.vue"
+            effect="light"
+            placement="top"
+          >
+            <Icon icon="ep:question" class="ml-5px" />
+          </el-tooltip>
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <!-- 第三步:流程设计 -->
+    <div v-show="currentStep === 2">
+      <!-- BPMN设计器 -->
+      <template v-if="formData.type === BpmModelType.BPMN">
+        <BpmModelEditor
+          v-if="showDesigner"
+          :model-id="formData.id"
+          :model-key="formData.key"
+          :model-name="formData.name"
+          @success="handleDesignSuccess"
+        />
+      </template>
+
+      <!-- Simple设计器 -->
+      <template v-else>
+        <SimpleModelDesign
+          v-if="showDesigner"
+          :model-id="formData.id"
+          :model-key="formData.key"
+          :model-name="formData.name"
+          @success="handleDesignSuccess"
+        />
+      </template>
+    </div>
+  </ContentWrap>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+</template>
+
+<script lang="ts" setup>
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { CategoryApi } from '@/api/bpm/category'
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import * as UserApi from '@/api/system/user'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { UserVO } from '@/api/system/user'
+import BpmModelEditor from './editor/index.vue'
+import SimpleModelDesign from '../simple/SimpleModelDesign.vue'
+
+const message = useMessage() // 消息弹窗
+const router = useRouter()
+const route = useRoute()
+const userStore = useUserStoreWithOut()
+
+// 步骤控制
+const currentStep = ref(0)
+// 表单数据
+const formRef = ref()
+const formData: any = ref({
+  id: undefined,
+  name: '',
+  key: '',
+  category: undefined,
+  icon: undefined,
+  description: '',
+  type: BpmModelType.BPMN,
+  formType: BpmModelFormType.NORMAL,
+  formId: '',
+  formCustomCreatePath: '',
+  formCustomViewPath: '',
+  visible: true,
+  startUserType: undefined,
+  managerUserType: undefined,
+  startUserIds: [],
+  managerUserIds: []
+})
+
+// 表单校验规则
+const formRules = {
+  name: [{ required: true, message: '流程名称不能为空', trigger: 'blur' }],
+  key: [{ required: true, message: '流程标识不能为空', trigger: 'blur' }],
+  category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }],
+  icon: [{ required: true, message: '流程图标不能为空', trigger: 'blur' }],
+  type: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  formType: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
+  formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
+  formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }],
+  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
+}
+
+// 流程设计器相关
+const xmlString = ref(undefined)
+
+// 数据列表
+const formList: any = ref([])
+const categoryList: any = ref([])
+const userList = ref<UserVO[]>([])
+const selectedStartUsers = ref<UserVO[]>([])
+const selectedManagerUsers = ref<UserVO[]>([])
+
+// 用户选择相关
+const userSelectFormRef = ref()
+const currentSelectType = ref<'start' | 'manager'>('start')
+
+// 打开发起人选择
+const openStartUserSelect = () => {
+  currentSelectType.value = 'start'
+  userSelectFormRef.value.open(0, selectedStartUsers.value)
+}
+
+// 打开管理员选择
+const openManagerUserSelect = () => {
+  currentSelectType.value = 'manager'
+  userSelectFormRef.value.open(0, selectedManagerUsers.value)
+}
+
+// 处理用户选择确认
+const handleUserSelectConfirm = (_, users: UserVO[]) => {
+  if (currentSelectType.value === 'start') {
+    selectedStartUsers.value = users
+    formData.value.startUserIds = users.map((u) => u.id)
+  } else {
+    selectedManagerUsers.value = users
+    formData.value.managerUserIds = users.map((u) => u.id)
+  }
+}
+
+// 处理发起人类型变化
+const handleStartUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedStartUsers.value = []
+    formData.value.startUserIds = []
+  }
+}
+
+// 处理管理员类型变化
+const handleManagerUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedManagerUsers.value = []
+    formData.value.managerUserIds = []
+  }
+}
+
+// 移除发起人
+const handleRemoveStartUser = (user: UserVO) => {
+  selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
+  formData.value.startUserIds = formData.value.startUserIds.filter((id) => id !== user.id)
+}
+
+// 移除管理员
+const handleRemoveManagerUser = (user: UserVO) => {
+  selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
+  formData.value.managerUserIds = formData.value.managerUserIds.filter((id) => id !== user.id)
+}
+
+// 保存操作
+const handleSave = async () => {
+  try {
+    if (formData.value.id) {
+      await ModelApi.updateModel(formData.value)
+      message.success('修改成功')
+    } else {
+      const result = await ModelApi.createModel(formData.value)
+      formData.value.id = result.id // 保存id用于设计器
+      message.success('新增成功')
+    }
+    // 不再跳转,继续停留在当前页面
+  } catch (error) {
+    console.error('保存失败:', error)
+  }
+}
+
+// 发布操作
+const handleDeploy = async () => {
+  try {
+    await message.confirm('是否确认发布该流程?')
+    // 发布时才进行全部校验
+    await validateAllSteps()
+    await handleSave()
+    await ModelApi.deployModel(formData.value.id)
+    message.success('发布成功')
+    router.push({ path: '/bpm/manager/model' })
+  } catch (error) {
+    if (error instanceof Error) {
+      // 校验失败时,跳转到对应步骤
+      const failedStep = steps.findIndex((step) => {
+        try {
+          step.validator && step.validator()
+          return false
+        } catch {
+          return true
+        }
+      })
+      if (failedStep !== -1) {
+        currentStep.value = failedStep
+        message.warning('请完善必填信息')
+      }
+    }
+  }
+}
+
+// 初始化数据
+const initData = async () => {
+  const modelId = route.query.id as unknown as string
+  if (modelId) {
+    // 修改场景
+    formData.value = await ModelApi.getModel(modelId)
+    // 加载数据时,根据已有的用户ID列表初始化已选用户
+    if (formData.value.startUserIds?.length) {
+      formData.value.startUserType = 1
+      selectedStartUsers.value = userList.value.filter((user) =>
+        formData.value.startUserIds.includes(user.id)
+      )
+    }
+    if (formData.value.managerUserIds?.length) {
+      formData.value.managerUserType = 1
+      selectedManagerUsers.value = userList.value.filter((user) =>
+        formData.value.managerUserIds.includes(user.id)
+      )
+    }
+  } else {
+    // 新增场景
+    formData.value.managerUserIds.push(userStore.getUser.id)
+  }
+
+  // 获取表单列表
+  formList.value = await FormApi.getFormSimpleList()
+  // 获取分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取用户列表
+  userList.value = await UserApi.getSimpleUserList()
+}
+
+onMounted(async () => {
+  await initData()
+})
+
+// 第一步校验
+const validateStep1 = async () => {
+  await formRef.value?.validate(['name', 'key', 'category', 'icon', 'type', 'visible'])
+}
+
+// 第二步校验
+const validateStep2 = async () => {
+  await formRef.value?.validate([
+    'formType',
+    'formId',
+    'formCustomCreatePath',
+    'formCustomViewPath'
+  ])
+}
+
+// 第三步校验
+const validateStep3 = async () => {
+  if (!xmlString.value) {
+    throw new Error('请设计流程')
+  }
+}
+
+const validateAllSteps = async () => {
+  for (const step of steps) {
+    if (step.validator) {
+      await step.validator()
+    }
+  }
+}
+
+const steps = [
+  { title: '基本信息', validator: validateStep1 },
+  { title: '表单设计', validator: validateStep2 },
+  { title: '流程设计', validator: validateStep3 }
+]
+
+// 处理设计器保存成功
+const handleDesignSuccess = (bpmnXml?: string) => {
+  if (bpmnXml) {
+    // 新建时,保存设计器生成的XML
+    formData.value.bpmnXml = bpmnXml
+  }
+  message.success('保存成功')
+}
+
+// 步骤切换处理
+const handleStepClick = async (index: number) => {
+  // 如果是切换到第三步(流程设计),需要校验key和name
+  if (index === 2) {
+    if (!formData.value.key || !formData.value.name) {
+      message.warning('请先填写流程标识和流程名称')
+      return
+    }
+  }
+
+  currentStep.value = index
+}
+
+// 添加一个计算属性来判断是否显示设计器
+const showDesigner = computed(() => {
+  return (
+    currentStep.value === 2 &&
+    Boolean(formData.value.id || (formData.value.key && formData.value.name))
+  )
+})
+
+// 监听步骤变化,确保数据准备完成
+watch(
+  () => currentStep.value,
+  async (newStep) => {
+    if (newStep === 2) {
+      await nextTick()
+    }
+  },
+  { immediate: false }
+)
+
+// 在组件卸载时清理
+onBeforeUnmount(() => {
+  const w = window as any
+  if (w.bpmnInstances) {
+    w.bpmnInstances = null
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.process-panel {
+  position: absolute;
+  top: 0;
+  right: 0;
+}
+
+.bg-gray-100 {
+  background-color: #f5f7fa;
+  transition: all 0.3s;
+
+  &:hover {
+    background-color: #e6e8eb;
+  }
+
+  .ep-close {
+    font-size: 14px;
+    color: #909399;
+    transition: color 0.3s;
+
+    &:hover {
+      color: #f56c6c;
+    }
+  }
+}
+
+.border-bottom {
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+  color: #3473ff;
+}
+
+.bg-primary {
+  background-color: #3473ff;
+}
+
+.border-primary {
+  border-color: #3473ff;
+}
+
+.text-16px {
+  font-size: 16px;
+}
+
+.h-2px {
+  height: 2px;
+}
+
+.mx-15px {
+  margin-left: 15px;
+  margin-right: 15px;
+}
+</style>

+ 155 - 19
src/views/bpm/model/ModelForm.vue

@@ -123,29 +123,69 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="谁可以发起" prop="startUserIds">
+      <el-form-item label="谁可以发起" prop="startUserType">
         <el-select
-          v-model="formData.startUserIds"
-          multiple
-          placeholder="请选择可发起人,默认(不选择)则所有人都可以发起"
+          v-model="formData.startUserType"
+          placeholder="请选择谁可以发起"
+          @change="handleStartUserTypeChange"
         >
-          <el-option
-            v-for="user in userList"
-            :key="user.id"
-            :label="user.nickname"
-            :value="user.id"
-          />
+          <el-option label="全员" :value="0" />
+          <el-option label="指定人员" :value="1" />
+          <el-option label="均不可提交" :value="2" />
         </el-select>
-      </el-form-item>
-      <el-form-item label="流程管理员" prop="managerUserIds">
-        <el-select v-model="formData.managerUserIds" multiple placeholder="请选择流程管理员">
-          <el-option
-            v-for="user in userList"
+        <div v-if="formData.startUserType === 1" class="mt-2 flex flex-wrap gap-2">
+          <div
+            v-for="user in selectedStartUsers"
             :key="user.id"
-            :label="user.nickname"
-            :value="user.id"
-          />
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+          >
+            <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
+              icon="ep:close"
+              class="ml-2 cursor-pointer hover:text-red-500"
+              @click="handleRemoveStartUser(user)"
+            />
+          </div>
+          <el-button type="primary" link @click="openStartUserSelect">
+            <Icon icon="ep:plus" />选择人员
+          </el-button>
+        </div>
+      </el-form-item>
+      <el-form-item label="流程管理员" prop="managerUserType">
+        <el-select
+          v-model="formData.managerUserType"
+          placeholder="请选择流程管理员"
+          @change="handleManagerUserTypeChange"
+        >
+          <el-option label="全员" :value="0" />
+          <el-option label="指定人员" :value="1" />
+          <el-option label="均不可提交" :value="2" />
         </el-select>
+        <div v-if="formData.managerUserType === 1" class="mt-2 flex flex-wrap gap-2">
+          <div
+            v-for="user in selectedManagerUsers"
+            :key="user.id"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center pr-8px dark:color-gray-600 position-relative"
+          >
+            <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
+              icon="ep:close"
+              class="ml-2 cursor-pointer hover:text-red-500"
+              @click="handleRemoveManagerUser(user)"
+            />
+          </div>
+          <el-button type="primary" link @click="openManagerUserSelect">
+            <Icon icon="ep:plus" />选择人员
+          </el-button>
+        </div>
       </el-form-item>
     </el-form>
     <template #footer>
@@ -153,6 +193,7 @@
       <el-button @click="dialogVisible = false">取 消</el-button>
     </template>
   </Dialog>
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
 </template>
 <script lang="ts" setup>
 import { propTypes } from '@/utils/propTypes'
@@ -178,7 +219,7 @@ const dialogVisible = ref(false) // 弹窗的是否展示
 const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
-const formData = ref({
+const formData: any = ref({
   id: undefined,
   name: '',
   key: '',
@@ -191,6 +232,8 @@ const formData = ref({
   formCustomCreatePath: '',
   formCustomViewPath: '',
   visible: true,
+  startUserType: undefined,
+  managerUserType: undefined,
   startUserIds: [],
   managerUserIds: []
 })
@@ -211,6 +254,10 @@ const formRef = ref() // 表单 Ref
 const formList = ref([]) // 流程表单的下拉框的数据
 const categoryList = ref([]) // 流程分类列表
 const userList = ref<UserVO[]>([]) // 用户列表
+const selectedStartUsers = ref<UserVO[]>([]) // 已选择的发起人列表
+const selectedManagerUsers = ref<UserVO[]>([]) // 已选择的管理员列表
+const userSelectFormRef = ref() // 用户选择弹窗ref
+const currentSelectType = ref<'start' | 'manager'>('start') // 当前选择的是发起人还是管理员
 
 /** 打开弹窗 */
 const open = async (type: string, id?: string) => {
@@ -226,6 +273,19 @@ const open = async (type: string, id?: string) => {
     } finally {
       formLoading.value = false
     }
+    // 加载数据时,根据已有的用户ID列表初始化已选用户
+    if (formData.value.startUserIds?.length) {
+      formData.value.startUserType = 1
+      selectedStartUsers.value = userList.value.filter((user) =>
+        formData.value.startUserIds.includes(user.id)
+      )
+    }
+    if (formData.value.managerUserIds?.length) {
+      formData.value.managerUserType = 1
+      selectedManagerUsers.value = userList.value.filter((user) =>
+        formData.value.managerUserIds.includes(user.id)
+      )
+    }
   } else {
     formData.value.managerUserIds.push(userStore.getUser.id)
   }
@@ -293,9 +353,85 @@ const resetForm = () => {
     formCustomCreatePath: '',
     formCustomViewPath: '',
     visible: true,
+    startUserType: undefined,
+    managerUserType: undefined,
     startUserIds: [],
     managerUserIds: []
   }
   formRef.value?.resetFields()
+  selectedStartUsers.value = []
+  selectedManagerUsers.value = []
+}
+
+// 处理发起人类型变化
+const handleStartUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedStartUsers.value = []
+    formData.value.startUserIds = []
+  }
+}
+
+// 处理管理员类型变化
+const handleManagerUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedManagerUsers.value = []
+    formData.value.managerUserIds = []
+  }
+}
+
+// 打开发起人选择
+const openStartUserSelect = () => {
+  currentSelectType.value = 'start'
+  userSelectFormRef.value.open(0, selectedStartUsers.value)
+}
+
+// 打开管理员选择
+const openManagerUserSelect = () => {
+  currentSelectType.value = 'manager'
+  userSelectFormRef.value.open(0, selectedManagerUsers.value)
+}
+
+// 处理用户选择确认
+const handleUserSelectConfirm = (_, users: UserVO[]) => {
+  if (currentSelectType.value === 'start') {
+    selectedStartUsers.value = users
+    formData.value.startUserIds = users.map((u) => u.id)
+  } else {
+    selectedManagerUsers.value = users
+    formData.value.managerUserIds = users.map((u) => u.id)
+  }
+}
+
+// 移除发起人
+const handleRemoveStartUser = (user: UserVO) => {
+  selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
+  formData.value.startUserIds = formData.value.startUserIds.filter((id) => id !== user.id)
+}
+
+// 移除管理员
+const handleRemoveManagerUser = (user: UserVO) => {
+  selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
+  formData.value.managerUserIds = formData.value.managerUserIds.filter((id) => id !== user.id)
 }
 </script>
+
+<style lang="scss" scoped>
+.bg-gray-100 {
+  background-color: #f5f7fa;
+  transition: all 0.3s;
+
+  &:hover {
+    background-color: #e6e8eb;
+  }
+
+  .ep-close {
+    font-size: 14px;
+    color: #909399;
+    transition: color 0.3s;
+
+    &:hover {
+      color: #f56c6c;
+    }
+  }
+}
+</style>

+ 133 - 50
src/views/bpm/model/editor/index.vue

@@ -3,7 +3,6 @@
     <!-- 流程设计器,负责绘制流程等 -->
     <MyProcessDesigner
       key="designer"
-      v-if="xmlString !== undefined"
       v-model="xmlString"
       :value="xmlString"
       v-bind="controlForm"
@@ -11,12 +10,14 @@
       ref="processDesigner"
       @init-finished="initModeler"
       :additionalModel="controlForm.additionalModel"
+      :model="model"
       @save="save"
     />
     <!-- 流程属性器,负责编辑每个流程节点的属性 -->
     <MyProcessPenal
+      v-if="isModelerReady && modeler"
       key="penal"
-      :bpmnModeler="modeler as any"
+      :bpmnModeler="modeler"
       :prefix="controlForm.prefix"
       class="process-panel"
       :model="model"
@@ -35,8 +36,13 @@ import { getForm, FormVO } from '@/api/bpm/form'
 
 defineOptions({ name: 'BpmModelEditor' })
 
-const router = useRouter() // 路由
-const { query } = useRoute() // 路由的查询
+const props = defineProps<{
+  modelId?: string
+  modelKey?: string
+  modelName?: string
+}>()
+
+const emit = defineEmits(['success'])
 const message = useMessage() // 国际化
 
 // 表单信息
@@ -45,8 +51,10 @@ const formType = ref(20)
 provide('formFields', formFields)
 provide('formType', formType)
 
-const xmlString = ref(undefined) // BPMN XML
-const modeler = ref(null) // BPMN Modeler
+const xmlString = ref<string>('') // BPMN XML
+const modeler = shallowRef() // BPMN Modeler
+const processDesigner = ref()
+const isModelerReady = ref(false)
 const controlForm = ref({
   simulation: true,
   labelEditing: false,
@@ -57,67 +65,142 @@ const controlForm = ref({
 })
 const model = ref<ModelApi.ModelVO>() // 流程模型的信息
 
+// 初始化 bpmnInstances
+const initBpmnInstances = () => {
+  if (!modeler.value) return false
+  try {
+    const instances = {
+      modeler: modeler.value,
+      modeling: modeler.value.get('modeling'),
+      moddle: modeler.value.get('moddle'),
+      eventBus: modeler.value.get('eventBus'),
+      bpmnFactory: modeler.value.get('bpmnFactory'),
+      elementFactory: modeler.value.get('elementFactory'),
+      elementRegistry: modeler.value.get('elementRegistry'),
+      replace: modeler.value.get('replace'),
+      selection: modeler.value.get('selection')
+    }
+
+    // 检查所有实例是否都存在
+    return Object.values(instances).every(instance => instance)
+  } catch (error) {
+    console.error('初始化 bpmnInstances 失败:', error)
+    return false
+  }
+}
+
 /** 初始化 modeler */
-const initModeler = (item) => {
-  setTimeout(() => {
+const initModeler = async (item) => {
+  try {
     modeler.value = item
-  }, 10)
+    // 等待 modeler 初始化完成
+    await nextTick()
+
+    // 确保 modeler 的所有实例都已经准备好
+    if (initBpmnInstances()) {
+      isModelerReady.value = true
+      if (!props.modelId && props.modelKey && props.modelName) {
+        await updateModelData(props.modelKey, props.modelName)
+      }
+    } else {
+      console.error('modeler 实例未完全初始化')
+    }
+  } catch (error) {
+    console.error('初始化 modeler 失败:', error)
+  }
+}
+
+/** 获取默认的BPMN XML */
+const getDefaultBpmnXml = (key: string, name: string) => {
+  return `<?xml version="1.0" encoding="UTF-8"?>
+<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
+  <process id="${key}" name="${name}" isExecutable="true" />
+  <bpmndi:BPMNDiagram id="BPMNDiagram">
+    <bpmndi:BPMNPlane id="${key}_di" bpmnElement="${key}" />
+  </bpmndi:BPMNDiagram>
+</definitions>`
 }
 
 /** 添加/修改模型 */
 const save = async (bpmnXml: string) => {
-  const data = {
-    ...model.value,
-    bpmnXml: bpmnXml // bpmnXml 只是初始化流程图,后续修改无法通过它获得
-  } as unknown as ModelApi.ModelVO
-  // 提交
-  if (data.id) {
-    await ModelApi.updateModelBpmn(data)
-    message.success('修改成功')
-  } else {
-    await ModelApi.updateModelBpmn(data)
-    message.success('新增成功')
+  try {
+    if (props.modelId) {
+      // 编辑模式
+      const data = {
+        ...model.value,
+        bpmnXml: bpmnXml
+      } as unknown as ModelApi.ModelVO
+      await ModelApi.updateModelBpmn(data)
+      emit('success')
+    } else {
+      // 新建模式,直接返回XML
+      emit('success', bpmnXml)
+    }
+  } catch (error) {
+    console.error('保存失败:', error)
+    message.error('保存失败')
   }
-  // 跳转回去
-  close()
-}
-
-/** 关闭按钮 */
-const close = () => {
-  router.push({ path: '/bpm/manager/model' })
 }
 
 /** 初始化 */
 onMounted(async () => {
-  const modelId = query.modelId as unknown as number
-  if (!modelId) {
-    message.error('缺少模型 modelId 编号')
-    return
+  try {
+    if (props.modelId) {
+      // 编辑模式
+      // 查询模型
+      const data = await ModelApi.getModel(props.modelId)
+      model.value = {
+        ...data,
+        bpmnXml: undefined // 清空 bpmnXml 属性
+      }
+      xmlString.value = data.bpmnXml || getDefaultBpmnXml(data.key, data.name)
+    } else if (props.modelKey && props.modelName) {
+      // 新建模式
+      xmlString.value = getDefaultBpmnXml(props.modelKey, props.modelName)
+      model.value = {
+        key: props.modelKey,
+        name: props.modelName
+      } as ModelApi.ModelVO
+    }
+  } catch (error) {
+    console.error('初始化失败:', error)
+    message.error('初始化失败')
   }
-  // 查询模型
-  const data = await ModelApi.getModel(modelId)
-  if (!data.bpmnXml) {
-    // 首次创建的 Model 模型,它是没有 bpmnXml,此时需要给它一个默认的
-    data.bpmnXml = ` <?xml version="1.0" encoding="UTF-8"?>
-<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:xsd="http://www.w3.org/2001/XMLSchema" targetNamespace="http://www.activiti.org/processdef">
-  <process id="${data.key}" name="${data.name}" isExecutable="true" />
-  <bpmndi:BPMNDiagram id="BPMNDiagram">
-    <bpmndi:BPMNPlane id="${data.key}_di" bpmnElement="${data.key}" />
-  </bpmndi:BPMNDiagram>
-</definitions>`
+})
+
+// 更新模型数据
+const updateModelData = async (key?: string, name?: string) => {
+  if (key && name) {
+    xmlString.value = getDefaultBpmnXml(key, name)
+    model.value = {
+      ...model.value,
+      key: key,
+      name: name
+    } as ModelApi.ModelVO
+    // 确保更新后重新渲染
+    await nextTick()
+    if (processDesigner.value?.refresh) {
+      processDesigner.value.refresh()
+    }
   }
+}
 
-  formType.value = data.formType
-  if (data.formType === 10) {
-    const bpmnForm = (await getForm(data.formId)) as unknown as FormVO
-    formFields.value = bpmnForm?.fields
+// 监听key和name的变化
+watch([() => props.modelKey, () => props.modelName], async ([newKey, newName]) => {
+  if (!props.modelId && newKey && newName && modeler.value) {
+    await updateModelData(newKey, newName)
   }
+}, { immediate: true, deep: true })
 
-  model.value = {
-    ...data,
-    bpmnXml: undefined // 清空 bpmnXml 属性
+// 在组件卸载时清理
+onBeforeUnmount(() => {
+  isModelerReady.value = false
+  modeler.value = null
+  // 清理全局实例
+  const w = window as any
+  if (w.bpmnInstances) {
+    w.bpmnInstances = null
   }
-  xmlString.value = data.bpmnXml
 })
 </script>
 <style lang="scss">

+ 6 - 1
src/views/bpm/model/index.vue

@@ -106,6 +106,7 @@ import CategoryDraggableModel from './CategoryDraggableModel.vue'
 
 defineOptions({ name: 'BpmModel' })
 
+const { push } = useRouter()
 const message = useMessage() // 消息弹窗
 const loading = ref(true) // 列表的加载中
 const isCategorySorting = ref(false) // 是否 category 正处于排序状态
@@ -124,7 +125,11 @@ const handleQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+  if (type === 'create') {
+    push('/bpm/manager/model/create-update')
+  } else {
+    push(`/bpm/manager/model/create-update?id=${id}`)
+  }
 }
 
 /** 流程表单的详情按钮操作 */

+ 27 - 6
src/views/bpm/simple/SimpleModelDesign.vue

@@ -1,6 +1,12 @@
 <template>
   <ContentWrap :bodyStyle="{ padding: '20px 16px' }">
-    <SimpleProcessDesigner :model-id="modelId" @success="close" />
+    <SimpleProcessDesigner 
+      :model-id="modelId" 
+      :model-key="modelKey"
+      :model-name="modelName"
+      @success="handleSuccess" 
+      ref="designerRef"
+    />
   </ContentWrap>
 </template>
 <script setup lang="ts">
@@ -9,11 +15,26 @@ import { SimpleProcessDesigner } from '@/components/SimpleProcessDesignerV2/src/
 defineOptions({
   name: 'SimpleModelDesign'
 })
-const router = useRouter() // 路由
-const { query } = useRoute() // 路由的查询
-const modelId = query.modelId as string
-const close = () => {
-  router.push({ path: '/bpm/manager/model' })
+
+const props = defineProps<{
+  modelId?: string
+  modelKey?: string
+  modelName?: string
+}>()
+
+const emit = defineEmits(['success'])
+const designerRef = ref()
+
+// 监听属性变化
+watch([() => props.modelKey, () => props.modelName], ([newKey, newName]) => {
+  if (designerRef.value && newKey && newName) {
+    designerRef.value.updateModel(newKey, newName)
+  }
+}, { immediate: true, deep: true })
+
+// 修改成功回调
+const handleSuccess = (data?: any) => {
+  emit('success', data)
 }
 </script>
 <style lang="scss" scoped></style>