Browse Source

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

YunaiV 7 tháng trước cách đây
mục cha
commit
472da9274e

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

@@ -1,6 +1,7 @@
 <template>
   <div v-loading="loading" class="overflow-auto">
     <SimpleProcessModel
+      ref="simpleProcessModelRef"
       v-if="processNodeTree"
       :flow-node="processNodeTree"
       :readonly="false"
@@ -38,12 +39,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 +79,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 +113,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 +178,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 +201,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: {
@@ -176,4 +226,19 @@ onMounted(async () => {
     loading.value = false
   }
 })
+
+const simpleProcessModelRef = ref()
+
+/** 获取当前流程数据 */
+const getCurrentFlowData = async () => {
+  if (simpleProcessModelRef.value) {
+    return await simpleProcessModelRef.value.getCurrentFlowData()
+  }
+  return undefined
+}
+
+defineExpose({
+  getCurrentFlowData,
+  updateModel
+})
 </script>

+ 27 - 19
src/components/SimpleProcessDesignerV2/src/SimpleProcessModel.vue

@@ -8,15 +8,6 @@
           <el-button size="default" class="w-80px"> {{ scaleValue }}% </el-button>
           <el-button size="default" :plain="true" :icon="ZoomIn" @click="zoomIn()" />
         </el-button-group>
-        <el-button
-          v-if="!readonly"
-          size="default"
-          class="ml-4px"
-          type="primary"
-          :icon="Select"
-          @click="saveSimpleFlowModel"
-          >保存模型</el-button
-        >
       </el-row>
     </div>
     <div class="simple-process-model" :style="`transform: scale(${scaleValue / 100});`">
@@ -42,7 +33,8 @@
 import ProcessNodeTree from './ProcessNodeTree.vue'
 import { SimpleFlowNode, NodeType, NODE_DEFAULT_TEXT } from './consts'
 import { useWatchNode } from './node'
-import { Select, ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+import { ZoomOut, ZoomIn, ScaleToOriginal } from '@element-plus/icons-vue'
+
 defineOptions({
   name: 'SimpleProcessModel'
 })
@@ -58,6 +50,7 @@ const props = defineProps({
     default: true
   }
 })
+
 const emits = defineEmits<{
   'save': [node: SimpleFlowNode | undefined]
 }>()
@@ -68,6 +61,7 @@ provide('readonly', props.readonly)
 let scaleValue = ref(100)
 const MAX_SCALE_VALUE = 200
 const MIN_SCALE_VALUE = 50
+
 // 放大
 const zoomIn = () => {
   if (scaleValue.value == MAX_SCALE_VALUE) {
@@ -75,6 +69,7 @@ const zoomIn = () => {
   }
   scaleValue.value += 10
 }
+
 // 缩小
 const zoomOut = () => {
   if (scaleValue.value == MIN_SCALE_VALUE) {
@@ -82,21 +77,14 @@ const zoomOut = () => {
   }
   scaleValue.value -= 10
 }
+
 const processReZoom = () => {
   scaleValue.value = 100
 }
 
 const errorDialogVisible = ref(false)
 let errorNodes: SimpleFlowNode[] = []
-const saveSimpleFlowModel = async () => {
-  errorNodes = []
-  validateNode(processNodeTree.value, errorNodes)
-  if (errorNodes.length > 0) {
-    errorDialogVisible.value = true
-    return
-  }
-  emits('save', processNodeTree.value)
-}
+
 // 校验节点设置。 暂时以 showText 为空 未节点错误配置
 const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNode[]) => {
   if (node) {
@@ -135,6 +123,26 @@ const validateNode = (node: SimpleFlowNode | undefined, errorNodes: SimpleFlowNo
     }
   }
 }
+
+/** 获取当前流程数据 */
+const getCurrentFlowData = async () => {
+  try {
+    errorNodes = []
+    validateNode(processNodeTree.value, errorNodes)
+    if (errorNodes.length > 0) {
+      errorDialogVisible.value = true
+      return undefined
+    }
+    return processNodeTree.value
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return undefined
+  }
+}
+
+defineExpose({
+  getCurrentFlowData
+})
 </script>
 
 <style lang="scss" scoped></style>

+ 1 - 1
src/components/SimpleProcessDesignerV2/src/nodes-config/ConditionNodeConfig.vue

@@ -381,7 +381,7 @@ const fieldOptions = computed(() => {
 
 /** 获取字段名称 */
 const getFieldTitle = (field: string) => {
-  const item = fieldsInfo.find((item) => item.field === field)
+  const item = fieldOptions.value.find((item) => item.field === field)
   return item?.title
 }
 

+ 6 - 1
src/components/SimpleProcessDesignerV2/theme/simple-process-designer.scss

@@ -173,13 +173,16 @@
   height: 100%;
   padding-top: 32px;
   background-color: #fafafa;
+  overflow-x: auto;
+  width: 100%;
+
   .simple-process-model {
     display: flex;
     flex-direction: column;
     justify-content: center;
     align-items: center;
     transform-origin: 50% 0 0;
-    overflow: auto;
+    min-width: fit-content;
     transform: scale(1);
     transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
     background: url(@/assets/svgs/bpm/simple-process-bg.svg) 0 0 repeat;
@@ -473,6 +476,7 @@
       .branch-node-container {
         position: relative;
         display: flex;
+        min-width: fit-content;
 
         &::before {
           position: absolute;
@@ -548,6 +552,7 @@
           background: transparent;
           border-top: 2px solid #dedede;
           border-bottom: 2px solid #dedede;
+          flex-shrink: 0;
 
           &::before {
             position: absolute;

+ 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 方法,用于打开弹窗

+ 23 - 62
src/components/bpmnProcessDesigner/package/designer/ProcessDesigner.vue

@@ -160,13 +160,6 @@
             <XButton preIcon="ep:refresh" @click="processRestart()" />
           </el-tooltip>
         </ElButtonGroup>
-        <XButton
-          preIcon="ep:plus"
-          title="保存模型"
-          @click="processSave"
-          :type="props.headerButtonType"
-          :disabled="simulationStatus"
-        />
       </template>
       <!-- 用于打开本地文件-->
       <input
@@ -315,6 +308,28 @@ const props = defineProps({
   }
 })
 
+// 监听value变化,重新加载流程图
+watch(
+  () => props.value,
+  (newValue) => {
+    if (newValue && bpmnModeler) {
+      createNewDiagram(newValue)
+    }
+  },
+  { immediate: true }
+)
+
+// 监听processId和processName变化
+watch(
+  [() => props.processId, () => props.processName],
+  ([newId, newName]) => {
+    if (newId && newName && !props.value) {
+      createNewDiagram(null)
+    }
+  },
+  { immediate: true }
+)
+
 provide('configGlobal', props)
 let bpmnModeler: any = null
 const defaultZoom = ref(1)
@@ -592,16 +607,6 @@ const processZoomOut = (zoomStep = 0.1) => {
   defaultZoom.value = newZoom
   bpmnModeler.get('canvas').zoom(defaultZoom.value)
 }
-// const processZoomTo = (newZoom = 1) => {
-//   if (newZoom < 0.2) {
-//     throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
-//   }
-//   if (newZoom > 4) {
-//     throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
-//   }
-//   defaultZoom = newZoom
-//   bpmnModeler.get('canvas').zoom(newZoom)
-// }
 const processReZoom = () => {
   defaultZoom.value = 1
   bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
@@ -640,63 +645,19 @@ const previewProcessXML = () => {
 }
 const previewProcessJson = () => {
   bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
-    // console.log(xml, 'xml')
-
-    // const rootNode = parseXmlString(xml)
-    // console.log(rootNode, 'rootNoderootNode')
     const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml))
-    // console.log(rootNodes, 'rootNodesrootNodesrootNodes')
-    // console.log(rootNodes.parent.toJsObject(), 'rootNodes.toJSON()')
-    // console.log(JSON.stringify(rootNodes.parent.toJsObject()), 'rootNodes.toJSON()')
-    // console.log(JSON.stringify(rootNodes.parent.toJSON()), 'rootNodes.toJSON()')
-
-    // const parser = new xml2js.XMLParser()
-    // let jObj = parser.parse(xml)
-    // console.log(jObj, 'jObjjObjjObjjObjjObj')
-    // const builder = new xml2js.XMLBuilder(xml)
-    // const xmlContent = builder
-    // console.log(xmlContent, 'xmlContent')
-    // console.log(xml2js, 'convertconvertconvert')
     previewResult.value = rootNodes.parent?.toJSON() as unknown as string
-    // previewResult.value = jObj
-    // previewResult.value = convert.xml2json(xml,  {explicitArray : false},{ spaces: 2 })
     previewType.value = 'json'
     previewModelVisible.value = true
   })
 }
+
 /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
-const processSave = async () => {
-  // console.log(bpmnModeler, 'bpmnModelerbpmnModelerbpmnModelerbpmnModeler')
-  const { err, xml } = await bpmnModeler.saveXML()
-  // console.log(err, 'errerrerrerrerr')
-  // console.log(xml, 'xmlxmlxmlxmlxml')
-  // 读取异常时抛出异常
-  if (err) {
-    // this.$modal.msgError('保存模型失败,请重试!')
-    alert('保存模型失败,请重试!')
-    return
-  }
-  // 触发 save 事件
-  emit('save', xml)
-}
-/** 高亮显示 */
-// const highlightedCode = (previewType, previewResult) => {
-//   console.log(previewType, 'previewType, previewResult')
-//   console.log(previewResult, 'previewType, previewResult')
-//   console.log(hljs.highlight, 'hljs.highlight')
-//   const result = hljs.highlight(previewType, previewResult.value || '', true)
-//   return result.value || '&nbsp;'
-// }
-onBeforeMount(() => {
-  console.log(props, 'propspropspropsprops')
-})
 onMounted(() => {
   initBpmnModeler()
   createNewDiagram(props.value)
 })
 onBeforeUnmount(() => {
-  // this.$once('hook:beforeDestroy', () => {
-  // })
   if (bpmnModeler) bpmnModeler.destroy()
   emit('destroy', bpmnModeler)
   bpmnModeler = null

+ 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(

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

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

+ 13 - 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,10 @@ 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
-      }
-    })
-  }
+  push({
+    name: 'BpmModelUpdate',
+    params: { id: row.id }
+  })
 }
 
 /** 发布流程 */
@@ -496,7 +485,14 @@ const handleDeleteCategory = async () => {
 /** 添加流程模型弹窗 */
 const modelFormRef = ref()
 const openModelForm = (type: string, id?: number) => {
-  modelFormRef.value.open(type, id)
+  if (type === 'create') {
+    push({ name: 'BpmModelCreate' })
+  } else {
+    push({
+      name: 'BpmModelUpdate',
+      params: { id }
+    })
+  }
 }
 
 watch(() => props.categoryInfo.modelList, updateModeList, { immediate: true })

+ 161 - 22
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'
@@ -160,11 +201,12 @@ import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
 import { ElMessageBox } from 'element-plus'
 import * as ModelApi from '@/api/bpm/model'
 import * as FormApi from '@/api/bpm/form'
-import { CategoryApi } from '@/api/bpm/category'
+import { CategoryApi, CategoryVO } from '@/api/bpm/category'
 import { BpmModelFormType, BpmModelType } from '@/utils/constants'
 import { UserVO } from '@/api/system/user'
 import * as UserApi from '@/api/system/user'
 import { useUserStoreWithOut } from '@/store/modules/user'
+import { FormVO } from '@/api/bpm/form'
 
 defineOptions({ name: 'ModelForm' })
 
@@ -178,7 +220,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 +233,8 @@ const formData = ref({
   formCustomCreatePath: '',
   formCustomViewPath: '',
   visible: true,
+  startUserType: undefined,
+  managerUserType: undefined,
   startUserIds: [],
   managerUserIds: []
 })
@@ -208,9 +252,13 @@ const formRules = reactive({
   managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
-const formList = ref([]) // 流程表单的下拉框的数据
-const categoryList = ref([]) // 流程分类列表
+const formList = ref<FormVO[]>([]) // 流程表单的下拉框的数据
+const categoryList = ref<CategoryVO[]>([]) // 流程分类列表
 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 +274,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 +354,87 @@ 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: number) => 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: number) => 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>

+ 181 - 53
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"
@@ -31,12 +32,16 @@ import CustomContentPadProvider from '@/components/bpmnProcessDesigner/package/d
 // 自定义左侧菜单(修改 默认任务 为 用户任务)
 import CustomPaletteProvider from '@/components/bpmnProcessDesigner/package/designer/plugins/palette'
 import * as ModelApi from '@/api/bpm/model'
-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 +50,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,73 +64,194 @@ 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 }
+)
+
+// 在组件卸载时清理
+onBeforeUnmount(() => {
+  isModelerReady.value = false
+  modeler.value = null
+  // 清理全局实例
+  const w = window as any
+  if (w.bpmnInstances) {
+    w.bpmnInstances = null
   }
+})
 
-  model.value = {
-    ...data,
-    bpmnXml: undefined // 清空 bpmnXml 属性
+/** 获取 XML 字符串 */
+const saveXML = async () => {
+  if (!modeler.value) {
+    return { xml: undefined }
   }
-  xmlString.value = data.bpmnXml
+  try {
+    return await modeler.value.saveXML({ format: true })
+  } catch (error) {
+    console.error('获取XML失败:', error)
+    return { xml: undefined }
+  }
+}
+
+/** 获取SVG字符串 */
+const saveSVG = async () => {
+  if (!modeler.value) {
+    return { svg: undefined }
+  }
+  try {
+    return await modeler.value.saveSVG()
+  } catch (error) {
+    console.error('获取SVG失败:', error)
+    return { svg: undefined }
+  }
+}
+
+/** 刷新视图 */
+const refresh = () => {
+  if (processDesigner.value?.refresh) {
+    processDesigner.value.refresh()
+  }
+}
+
+// 暴露必要的属性和方法给父组件
+defineExpose({
+  modeler,
+  isModelerReady,
+  saveXML,
+  saveSVG,
+  refresh
 })
 </script>
 <style lang="scss">
 .process-panel__container {
   position: absolute;
-  top: 90px;
-  right: 60px;
+  top: 172px;
+  right: 70px;
 }
 </style>

+ 301 - 0
src/views/bpm/model/form/BasicInfo.vue

@@ -0,0 +1,301 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+    <el-form-item label="流程标识" prop="key" class="mb-20px">
+      <div class="flex items-center">
+        <el-input
+          class="!w-440px"
+          v-model="modelData.key"
+          :disabled="!!modelData.id"
+          placeholder="请输入流标标识"
+        />
+        <el-tooltip
+          class="item"
+          :content="modelData.id ? '流程标识不可修改!' : '新建后,流程标识不可修改!'"
+          effect="light"
+          placement="top"
+        >
+          <Icon icon="ep:question-filled" class="ml-5px" />
+        </el-tooltip>
+      </div>
+    </el-form-item>
+    <el-form-item label="流程名称" prop="name" class="mb-20px">
+      <el-input
+        v-model="modelData.name"
+        :disabled="!!modelData.id"
+        clearable
+        placeholder="请输入流程名称"
+      />
+    </el-form-item>
+    <el-form-item label="流程分类" prop="category" class="mb-20px">
+      <el-select
+        class="!w-full"
+        v-model="modelData.category"
+        clearable
+        placeholder="请选择流程分类"
+      >
+        <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="modelData.icon" :limit="1" height="64px" width="64px" />
+    </el-form-item>
+    <el-form-item label="流程描述" prop="description" class="mb-20px">
+      <el-input v-model="modelData.description" clearable type="textarea" />
+    </el-form-item>
+    <el-form-item label="流程类型" prop="type" class="mb-20px">
+      <el-radio-group v-model="modelData.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="modelData.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="modelData.startUserType"
+        placeholder="请选择谁可以发起"
+        @change="handleStartUserTypeChange"
+      >
+        <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
+          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="modelData.managerUserType"
+        placeholder="请选择流程管理员"
+        @change="handleManagerUserTypeChange"
+      >
+        <el-option label="全员" :value="0" />
+        <el-option label="指定人员" :value="1" />
+        <el-option label="均不可提交" :value="2" />
+      </el-select>
+      <div v-if="modelData.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>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
+import { UserVO } from '@/api/system/user'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  },
+  categoryList: {
+    type: Array,
+    required: true
+  },
+  userList: {
+    type: Array,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const formRef = ref()
+const selectedStartUsers = ref<UserVO[]>([])
+const selectedManagerUsers = ref<UserVO[]>([])
+const userSelectFormRef = ref()
+const currentSelectType = ref<'start' | 'manager'>('start')
+
+const rules = {
+  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' }],
+  visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }],
+  managerUserIds: [{ required: true, message: '流程管理员不能为空', trigger: 'blur' }]
+}
+
+// 创建本地数据副本
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 初始化选中的用户
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal.startUserIds?.length) {
+      selectedStartUsers.value = props.userList.filter((user: UserVO) =>
+        newVal.startUserIds.includes(user.id)
+      ) as UserVO[]
+    }
+    if (newVal.managerUserIds?.length) {
+      selectedManagerUsers.value = props.userList.filter((user: UserVO) =>
+        newVal.managerUserIds.includes(user.id)
+      ) as UserVO[]
+    }
+  },
+  { immediate: true }
+)
+
+/** 打开发起人选择 */
+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
+    emit('update:modelValue', {
+      ...modelData.value,
+      startUserIds: users.map((u) => u.id)
+    })
+  } else {
+    selectedManagerUsers.value = users
+    emit('update:modelValue', {
+      ...modelData.value,
+      managerUserIds: users.map((u) => u.id)
+    })
+  }
+}
+
+/** 处理发起人类型变化 */
+const handleStartUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedStartUsers.value = []
+    emit('update:modelValue', {
+      ...modelData.value,
+      startUserIds: []
+    })
+  }
+}
+
+/** 处理管理员类型变化 */
+const handleManagerUserTypeChange = (value: number) => {
+  if (value !== 1) {
+    selectedManagerUsers.value = []
+    emit('update:modelValue', {
+      ...modelData.value,
+      managerUserIds: []
+    })
+  }
+}
+
+/** 移除发起人 */
+const handleRemoveStartUser = (user: UserVO) => {
+  selectedStartUsers.value = selectedStartUsers.value.filter((u) => u.id !== user.id)
+  emit('update:modelValue', {
+    ...modelData.value,
+    startUserIds: modelData.value.startUserIds.filter((id: number) => id !== user.id)
+  })
+}
+
+/** 移除管理员 */
+const handleRemoveManagerUser = (user: UserVO) => {
+  selectedManagerUsers.value = selectedManagerUsers.value.filter((u) => u.id !== user.id)
+  emit('update:modelValue', {
+    ...modelData.value,
+    managerUserIds: modelData.value.managerUserIds.filter((id: number) => id !== user.id)
+  })
+}
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+
+defineExpose({
+  validate
+})
+</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>

+ 137 - 0
src/views/bpm/model/form/FormDesign.vue

@@ -0,0 +1,137 @@
+<template>
+  <el-form ref="formRef" :model="modelData" :rules="rules" label-width="120px" class="mt-20px">
+    <el-form-item label="表单类型" prop="formType" class="mb-20px">
+      <el-radio-group v-model="modelData.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="modelData.formType === 10" label="流程表单" prop="formId">
+      <el-select v-model="modelData.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="modelData.formType === 20" label="表单提交路由" prop="formCustomCreatePath">
+      <el-input
+        v-model="modelData.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="modelData.formType === 20" label="表单查看地址" prop="formCustomViewPath">
+      <el-input
+        v-model="modelData.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>
+    <!-- 表单预览 -->
+    <div
+      v-if="modelData.formType === 10 && modelData.formId && formPreview.rule.length > 0"
+      class="mt-20px"
+    >
+      <div class="flex items-center mb-15px">
+        <div class="h-15px w-4px bg-[#1890ff] mr-10px"></div>
+        <span class="text-15px font-bold">表单预览</span>
+      </div>
+      <form-create
+        v-model="formPreview.formData"
+        :rule="formPreview.rule"
+        :option="formPreview.option"
+      />
+    </div>
+  </el-form>
+</template>
+
+<script lang="ts" setup>
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+import * as FormApi from '@/api/bpm/form'
+import { setConfAndFields2 } from '@/utils/formCreate'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  },
+  formList: {
+    type: Array,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const formRef = ref()
+
+// 创建本地数据副本
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+// 表单预览数据
+const formPreview = ref({
+  formData: {},
+  rule: [],
+  option: {
+    submitBtn: false,
+    resetBtn: false,
+    formData: {}
+  }
+})
+
+// 监听表单ID变化,加载表单数据
+watch(
+  () => modelData.value.formId,
+  async (newFormId) => {
+    if (newFormId && modelData.value.formType === 10) {
+      const data = await FormApi.getForm(newFormId)
+      setConfAndFields2(formPreview.value, data.conf, data.fields)
+      // 设置只读
+      formPreview.value.rule.forEach((item: any) => {
+        item.props = { ...item.props, disabled: true }
+      })
+    } else {
+      formPreview.value.rule = []
+    }
+  },
+  { immediate: true }
+)
+
+const rules = {
+  formType: [{ required: true, message: '表单类型不能为空', trigger: 'blur' }],
+  formId: [{ required: true, message: '流程表单不能为空', trigger: 'blur' }],
+  formCustomCreatePath: [{ required: true, message: '表单提交路由不能为空', trigger: 'blur' }],
+  formCustomViewPath: [{ required: true, message: '表单查看地址不能为空', trigger: 'blur' }]
+}
+
+/** 表单校验 */
+const validate = async () => {
+  await formRef.value?.validate()
+}
+
+defineExpose({
+  validate
+})
+</script>

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

@@ -0,0 +1,130 @@
+<template>
+  <!-- BPMN设计器 -->
+  <template v-if="modelData.type === BpmModelType.BPMN">
+    <BpmModelEditor
+      v-if="showDesigner"
+      :model-id="modelData.id"
+      :model-key="modelData.key"
+      :model-name="modelData.name"
+      :value="modelData.bpmnXml"
+      ref="bpmnEditorRef"
+      @success="handleDesignSuccess"
+    />
+  </template>
+
+  <!-- Simple设计器 -->
+  <template v-else>
+    <SimpleModelDesign
+      v-if="showDesigner"
+      :model-id="modelData.id"
+      :model-key="modelData.key"
+      :model-name="modelData.name"
+      :value="modelData.bpmnXml"
+      ref="simpleEditorRef"
+      @success="handleDesignSuccess"
+    />
+  </template>
+</template>
+
+<script lang="ts" setup>
+import { BpmModelType } from '@/utils/constants'
+import BpmModelEditor from '../editor/index.vue'
+import SimpleModelDesign from '../../simple/SimpleModelDesign.vue'
+
+const props = defineProps({
+  modelValue: {
+    type: Object,
+    required: true
+  }
+})
+
+const emit = defineEmits(['update:modelValue', 'success'])
+
+const bpmnEditorRef = ref()
+const simpleEditorRef = ref()
+
+// 创建本地数据副本
+const modelData = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+/** 获取当前流程数据 */
+const getProcessData = async () => {
+  try {
+    if (modelData.value.type === BpmModelType.BPMN) {
+      // BPMN设计器
+      if (bpmnEditorRef.value) {
+        const { xml } = await bpmnEditorRef.value.saveXML()
+        if (xml) {
+          return xml
+        }
+      }
+    } else {
+      // Simple设计器
+      if (simpleEditorRef.value) {
+        const flowData = await simpleEditorRef.value.getCurrentFlowData()
+        if (flowData) {
+          return flowData // 直接返回流程数据对象,不需要转换为字符串
+        }
+      }
+    }
+    return undefined
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return undefined
+  }
+}
+
+/** 表单校验 */
+const validate = async () => {
+  try {
+    // 根据流程类型获取对应的编辑器引用
+    const editorRef =
+      modelData.value.type === BpmModelType.BPMN ? bpmnEditorRef.value : simpleEditorRef.value
+    if (!editorRef) {
+      throw new Error('流程设计器未初始化')
+    }
+
+    // 获取最新的流程数据
+    const processData = await getProcessData()
+    if (!processData) {
+      throw new Error('请设计流程')
+    }
+
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 处理设计器保存成功 */
+const handleDesignSuccess = (data?: any) => {
+  if (data) {
+    if (modelData.value.type === BpmModelType.BPMN) {
+      modelData.value = {
+        ...modelData.value,
+        bpmnXml: data,
+        simpleModel: null
+      }
+    } else {
+      modelData.value = {
+        ...modelData.value,
+        bpmnXml: null,
+        simpleModel: data
+      }
+    }
+    emit('success', data)
+  }
+}
+
+/** 是否显示设计器 */
+const showDesigner = computed(() => {
+  return Boolean(modelData.value?.key && modelData.value?.name)
+})
+
+defineExpose({
+  validate,
+  getProcessData
+})
+</script>

+ 392 - 0
src/views/bpm/model/form/index.vue

@@ -0,0 +1,392 @@
+<template>
+  <ContentWrap>
+    <div class="mx-auto">
+      <!-- 头部导航栏 -->
+      <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="handleBack" />
+          <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 v-if="route.params.id" type="success" @click="handleDeploy">发 布</el-button>
+          <el-button type="primary" @click="handleSave">保 存</el-button>
+        </div>
+      </div>
+
+      <!-- 主体内容 -->
+      <div class="mt-50px">
+        <!-- 第一步:基本信息 -->
+        <div v-if="currentStep === 0" class="mx-auto w-560px">
+          <BasicInfo
+            v-model="formData"
+            :categoryList="categoryList"
+            :userList="userList"
+            ref="basicInfoRef"
+          />
+        </div>
+
+        <!-- 第二步:表单设计 -->
+        <div v-if="currentStep === 1" class="mx-auto w-560px">
+          <FormDesign v-model="formData" :formList="formList" ref="formDesignRef" />
+        </div>
+
+        <!-- 第三步:流程设计 -->
+        <ProcessDesign
+          v-if="currentStep === 2"
+          v-model="formData"
+          ref="processDesignRef"
+          @success="handleDesignSuccess"
+        />
+      </div>
+    </div>
+  </ContentWrap>
+</template>
+
+<script lang="ts" setup>
+import { useRoute, useRouter } from 'vue-router'
+import { useMessage } from '@/hooks/web/useMessage'
+import * as ModelApi from '@/api/bpm/model'
+import * as FormApi from '@/api/bpm/form'
+import { CategoryApi } from '@/api/bpm/category'
+import * as UserApi from '@/api/system/user'
+import { useUserStoreWithOut } from '@/store/modules/user'
+import { BpmModelFormType, BpmModelType } from '@/utils/constants'
+import BasicInfo from './BasicInfo.vue'
+import FormDesign from './FormDesign.vue'
+import ProcessDesign from './ProcessDesign.vue'
+import { useTagsViewStore } from '@/store/modules/tagsView'
+
+const router = useRouter()
+const { delView } = useTagsViewStore() // 视图操作
+const route = useRoute()
+const message = useMessage()
+const userStore = useUserStoreWithOut()
+
+// 组件引用
+const basicInfoRef = ref()
+const formDesignRef = ref()
+const processDesignRef = ref()
+
+/** 步骤校验函数 */
+const validateBasic = async () => {
+  await basicInfoRef.value?.validate()
+}
+
+/** 表单设计校验 */
+const validateForm = async () => {
+  await formDesignRef.value?.validate()
+}
+
+/** 流程设计校验 */
+const validateProcess = async () => {
+  await processDesignRef.value?.validate()
+}
+
+const currentStep = ref(0) // 步骤控制
+const steps = [
+  { title: '基本信息', validator: validateBasic },
+  { title: '表单设计', validator: validateForm },
+  { title: '流程设计', validator: validateProcess }
+]
+
+// 表单数据
+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 formList = ref([])
+const categoryList = ref([])
+const userList = ref<UserApi.UserVO[]>([])
+
+/** 初始化数据 */
+const initData = async () => {
+  const modelId = route.params.id as string
+  if (modelId) {
+    // 修改场景
+    formData.value = await ModelApi.getModel(modelId)
+  } else {
+    // 新增场景
+    formData.value.managerUserIds.push(userStore.getUser.id)
+  }
+
+  // 获取表单列表
+  formList.value = await FormApi.getFormSimpleList()
+  // 获取分类列表
+  categoryList.value = await CategoryApi.getCategorySimpleList()
+  // 获取用户列表
+  userList.value = await UserApi.getSimpleUserList()
+}
+
+/** 校验所有步骤数据是否完整 */
+const validateAllSteps = async () => {
+  try {
+    // 基本信息校验
+    await basicInfoRef.value?.validate()
+    if (!formData.value.key || !formData.value.name || !formData.value.category) {
+      currentStep.value = 0
+      throw new Error('请完善基本信息')
+    }
+
+    // 表单设计校验
+    await formDesignRef.value?.validate()
+    if (formData.value.formType === 10 && !formData.value.formId) {
+      currentStep.value = 1
+      throw new Error('请选择流程表单')
+    }
+    if (
+      formData.value.formType === 20 &&
+      (!formData.value.formCustomCreatePath || !formData.value.formCustomViewPath)
+    ) {
+      currentStep.value = 1
+      throw new Error('请完善自定义表单信息')
+    }
+
+    // 流程设计校验
+    await processDesignRef.value?.validate()
+    const processData = await processDesignRef.value?.getProcessData()
+    if (!processData) {
+      currentStep.value = 2
+      throw new Error('请设计流程')
+    }
+
+    return true
+  } catch (error) {
+    throw error
+  }
+}
+
+/** 保存操作 */
+const handleSave = async () => {
+  try {
+    // 保存前校验所有步骤的数据
+    await validateAllSteps()
+
+    // 获取最新的流程设计数据
+    const processData = await processDesignRef.value?.getProcessData()
+    if (!processData) {
+      throw new Error('获取流程数据失败')
+    }
+
+    // 更新表单数据
+    const modelData = {
+      ...formData.value
+    }
+    if (formData.value.type === BpmModelType.BPMN) {
+      modelData.bpmnXml = processData
+      modelData.simpleModel = null
+    } else {
+      modelData.bpmnXml = null
+      modelData.simpleModel = processData // 直接使用流程数据对象
+    }
+
+    if (formData.value.id) {
+      // 修改场景
+      await ModelApi.updateModel(modelData)
+      // 询问是否发布流程
+      try {
+        await message.confirm('修改流程成功,是否发布流程?')
+        // 用户点击确认,执行发布
+        await handleDeploy()
+      } catch {
+        // 用户点击取消,停留在当前页面
+      }
+    } else {
+      // 新增场景
+      formData.value.id = await ModelApi.createModel(modelData)
+      message.success('新增成功')
+      try {
+        await message.confirm('创建流程成功,是否继续编辑?')
+        // 用户点击继续编辑,跳转到编辑页面
+        await nextTick()
+        // 先删除当前页签
+        delView(unref(router.currentRoute))
+        // 跳转到编辑页面
+        await router.push({
+          name: 'BpmModelUpdate',
+          params: { id: formData.value.id }
+        })
+      } catch {
+        // 先删除当前页签
+        delView(unref(router.currentRoute))
+        // 用户点击返回列表
+        await router.push({ name: 'BpmModel' })
+      }
+    }
+  } catch (error: any) {
+    console.error('保存失败:', error)
+    message.warning(error.message || '请完善所有步骤的必填信息')
+  }
+}
+
+/** 发布操作 */
+const handleDeploy = async () => {
+  try {
+    // 修改场景下直接发布,新增场景下需要先确认
+    if (!formData.value.id) {
+      await message.confirm('是否确认发布该流程?')
+    }
+
+    // 校验所有步骤
+    await validateAllSteps()
+
+    // 获取最新的流程设计数据
+    const processData = await processDesignRef.value?.getProcessData()
+    if (!processData) {
+      throw new Error('获取流程数据失败')
+    }
+
+    // 更新表单数据
+    const modelData = {
+      ...formData.value
+    }
+    if (formData.value.type === BpmModelType.BPMN) {
+      modelData.bpmnXml = processData
+      modelData.simpleModel = null
+    } else {
+      modelData.bpmnXml = null
+      modelData.simpleModel = processData // 直接使用流程数据对象
+    }
+
+    // 先保存所有数据
+    if (formData.value.id) {
+      await ModelApi.updateModel(modelData)
+    } else {
+      const result = await ModelApi.createModel(modelData)
+      formData.value.id = result.id
+    }
+
+    // 发布
+    await ModelApi.deployModel(formData.value.id)
+    message.success('发布成功')
+    // 返回列表页
+    await router.push({ name: 'BpmModel' })
+  } catch (error: any) {
+    console.error('发布失败:', error)
+    message.warning(error.message || '发布失败')
+  }
+}
+
+/** 步骤切换处理 */
+const handleStepClick = async (index: number) => {
+  // 如果是切换到第三步(流程设计),需要校验key和name
+  if (index === 2) {
+    if (!formData.value.key || !formData.value.name) {
+      message.warning('请先填写流程标识和流程名称')
+      return
+    }
+  }
+
+  // 只有在向后切换时才进行校验
+  if (index > currentStep.value) {
+    try {
+      if (typeof steps[currentStep.value].validator === 'function') {
+        await steps[currentStep.value].validator()
+      }
+      currentStep.value = index
+    } catch (error) {
+      message.warning('请先完善当前步骤必填信息')
+    }
+  } else {
+    // 向前切换时直接切换
+    currentStep.value = index
+  }
+}
+
+/** 处理设计器保存成功 */
+const handleDesignSuccess = (bpmnXml?: string) => {
+  if (bpmnXml) {
+    formData.value.bpmnXml = bpmnXml
+  }
+}
+
+/** 返回列表页 */
+const handleBack = () => {
+  // 先删除当前页签
+  delView(unref(router.currentRoute))
+  // 跳转到列表页
+  router.push({ name: 'BpmModel' })
+}
+
+/** 初始化 */
+onMounted(async () => {
+  await initData()
+})
+
+// 添加组件卸载前的清理代码
+onBeforeUnmount(() => {
+  // 清理所有的引用
+  basicInfoRef.value = null
+  formDesignRef.value = null
+  processDesignRef.value = null
+})
+</script>
+
+<style lang="scss" scoped>
+.border-bottom {
+  border-bottom: 1px solid #dcdfe6;
+}
+
+.text-primary {
+  color: #3473ff;
+}
+
+.bg-primary {
+  background-color: #3473ff;
+}
+
+.border-primary {
+  border-color: #3473ff;
+}
+</style>

+ 9 - 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,14 @@ const handleQuery = () => {
 /** 添加/修改操作 */
 const formRef = ref()
 const openForm = (type: string, id?: number) => {
-  formRef.value.open(type, id)
+  if (type === 'create') {
+    push({ name: 'BpmModelCreate' })
+  } else {
+    push({
+      name: 'BpmModelUpdate',
+      params: { id }
+    })
+  }
 }
 
 /** 流程表单的详情按钮操作 */

+ 40 - 3
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -8,7 +8,7 @@
         <!-- 中间主要内容 tab 栏 -->
         <el-tabs v-model="activeTab">
           <!-- 表单信息 -->
-          <el-tab-pane label="表单填写" name="form" >
+          <el-tab-pane label="表单填写" name="form">
             <div class="form-scroll-area" v-loading="processInstanceStartLoading">
               <el-scrollbar>
                 <el-row>
@@ -75,7 +75,11 @@
 <script lang="ts" setup>
 import { decodeFields, setConfAndFields2 } from '@/utils/formCreate'
 import { BpmModelType } from '@/utils/constants'
-import { CandidateStrategy } from '@/components/SimpleProcessDesignerV2/src/consts'
+import {
+  CandidateStrategy,
+  NodeId,
+  FieldPermissionType
+} from '@/components/SimpleProcessDesignerV2/src/consts'
 import ProcessInstanceBpmnViewer from '../detail/ProcessInstanceBpmnViewer.vue'
 import ProcessInstanceSimpleViewer from '../detail/ProcessInstanceSimpleViewer.vue'
 import ProcessInstanceTimeline from '../detail/ProcessInstanceTimeline.vue'
@@ -129,8 +133,10 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
       }
     }
     setConfAndFields2(detailForm, row.formConf, row.formFields, formVariables)
+
     await nextTick()
     fApi.value?.btn.show(false) // 隐藏提交按钮
+
     // 获取流程审批信息
     await getApprovalDetail(row)
 
@@ -152,7 +158,12 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
 /** 获取审批详情 */
 const getApprovalDetail = async (row: any) => {
   try {
-    const data = await ProcessInstanceApi.getApprovalDetail({ processDefinitionId: row.id })
+    // TODO 获取审批详情,设置 activityId 为发起人节点(为了获取字段权限。暂时只对 Simple 设计器有效)
+    const data = await ProcessInstanceApi.getApprovalDetail({
+      processDefinitionId: row.id,
+      activityId: NodeId.START_USER_NODE_ID
+    })
+
     if (!data) {
       message.error('查询不到审批详情信息!')
       return
@@ -170,10 +181,36 @@ const getApprovalDetail = async (row: any) => {
 
     // 获取审批节点,显示 Timeline 的数据
     activityNodes.value = data.activityNodes
+    // 获取表单字段权限
+    const formFieldsPermission = data.formFieldsPermission
+    // 设置表单字段权限
+    if (formFieldsPermission) {
+      Object.keys(formFieldsPermission).forEach((item) => {
+        setFieldPermission(item, formFieldsPermission[item])
+      })
+    }
   } finally {
   }
 }
 
+/**
+ * 设置表单权限
+ */
+const setFieldPermission = (field: string, permission: string) => {
+  if (permission === FieldPermissionType.READ) {
+    //@ts-ignore
+    fApi.value?.disabled(true, field)
+  }
+  if (permission === FieldPermissionType.WRITE) {
+    //@ts-ignore
+    fApi.value?.disabled(false, field)
+  }
+  if (permission === FieldPermissionType.NONE) {
+    //@ts-ignore
+    fApi.value?.hidden(true, field)
+  }
+}
+
 /** 提交按钮 */
 const submitForm = async () => {
   if (!fApi.value || !props.selectProcessDefinition) {

+ 44 - 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,43 @@ 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)
 }
+
+/** 获取当前流程数据 */
+const getCurrentFlowData = async () => {
+  try {
+    if (designerRef.value) {
+      return await designerRef.value.getCurrentFlowData()
+    }
+    return undefined
+  } catch (error) {
+    console.error('获取流程数据失败:', error)
+    return undefined
+  }
+}
+
+defineExpose({
+  getCurrentFlowData
+})
 </script>
 <style lang="scss" scoped></style>