feat: 流程设计- Simple 模型设计 20%
							parent
							
								
									4469e9dfc7
								
							
						
					
					
						commit
						0f576e006b
					
				|  | @ -3,7 +3,7 @@ import type { DataNode } from 'ant-design-vue/es/tree'; | |||
| 
 | ||||
| import type { SystemDeptApi } from '#/api/system/dept'; | ||||
| 
 | ||||
| import { defineProps, ref } from 'vue'; | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| import { useVbenModal } from '@vben/common-ui'; | ||||
| import { handleTree } from '@vben/utils'; | ||||
|  |  | |||
|  | @ -0,0 +1,47 @@ | |||
| <script setup lang="ts"> | ||||
| import type { Ref } from 'vue'; | ||||
| 
 | ||||
| import type { SimpleFlowNode } from '../../consts'; | ||||
| 
 | ||||
| import { inject, ref } from 'vue'; | ||||
| 
 | ||||
| import { useTaskStatusClass, useWatchNode } from '../../helpers'; | ||||
| 
 | ||||
| defineOptions({ name: 'EndEventNode' }); | ||||
| const props = defineProps({ | ||||
|   flowNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     default: () => null, | ||||
|   }, | ||||
| }); | ||||
| // 监控节点变化 | ||||
| const currentNode = useWatchNode(props); | ||||
| // 是否只读 | ||||
| const readonly = inject<Boolean>('readonly'); | ||||
| const processInstance = inject<Ref<any>>('processInstance', ref({})); | ||||
| 
 | ||||
| const processInstanceInfos = ref<any[]>([]); // 流程的审批信息 | ||||
| 
 | ||||
| const nodeClick = () => { | ||||
|   if (readonly && processInstance && processInstance.value) { | ||||
|     console.warn( | ||||
|       'TODO 只读模式,弹窗显示审批信息', | ||||
|       processInstance.value, | ||||
|       processInstanceInfos.value, | ||||
|     ); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <div class="end-node-wrapper"> | ||||
|     <div | ||||
|       class="end-node-box cursor-pointer" | ||||
|       :class="`${useTaskStatusClass(currentNode?.activityStatus)}`" | ||||
|       @click="nodeClick" | ||||
|     > | ||||
|       <span class="node-fixed-name" title="结束">结束</span> | ||||
|     </div> | ||||
|   </div> | ||||
|   <!-- TODO 审批信息 --> | ||||
| </template> | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,338 @@ | |||
| <script setup lang="ts"> | ||||
| import type { SimpleFlowNode } from '../../consts'; | ||||
| 
 | ||||
| import { inject, ref } from 'vue'; | ||||
| 
 | ||||
| import { IconifyIcon } from '@vben/icons'; | ||||
| import { cloneDeep, buildShortUUID as generateUUID } from '@vben/utils'; | ||||
| 
 | ||||
| import { message, Popover } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { | ||||
|   ApproveMethodType, | ||||
|   AssignEmptyHandlerType, | ||||
|   AssignStartUserHandlerType, | ||||
|   ConditionType, | ||||
|   DEFAULT_CONDITION_GROUP_VALUE, | ||||
|   NODE_DEFAULT_NAME, | ||||
|   NodeType, | ||||
|   RejectHandlerType, | ||||
| } from '../../consts'; | ||||
| 
 | ||||
| defineOptions({ | ||||
|   name: 'NodeHandler', | ||||
| }); | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   childNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     default: null, | ||||
|   }, | ||||
|   currentNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     required: true, | ||||
|   }, | ||||
| }); | ||||
| const emits = defineEmits(['update:childNode']); | ||||
| const popoverShow = ref(false); | ||||
| const readonly = inject<Boolean>('readonly'); // 是否只读 | ||||
| 
 | ||||
| const addNode = (type: number) => { | ||||
|   // 校验:条件分支、包容分支后面,不允许直接添加并行分支 | ||||
|   if ( | ||||
|     type === NodeType.PARALLEL_BRANCH_NODE && | ||||
|     [NodeType.CONDITION_BRANCH_NODE, NodeType.INCLUSIVE_BRANCH_NODE].includes( | ||||
|       props.currentNode?.type, | ||||
|     ) | ||||
|   ) { | ||||
|     message.error('条件分支、包容分支后面,不允许直接添加并行分支'); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   popoverShow.value = false; | ||||
|   if (type === NodeType.USER_TASK_NODE || type === NodeType.TRANSACTOR_NODE) { | ||||
|     const id = `Activity_${generateUUID()}`; | ||||
|     const data: SimpleFlowNode = { | ||||
|       id, | ||||
|       name: NODE_DEFAULT_NAME.get(type) as string, | ||||
|       showText: '', | ||||
|       type, | ||||
|       approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, | ||||
|       // 超时处理 | ||||
|       rejectHandler: { | ||||
|         type: RejectHandlerType.FINISH_PROCESS, | ||||
|       }, | ||||
|       timeoutHandler: { | ||||
|         enable: false, | ||||
|       }, | ||||
|       assignEmptyHandler: { | ||||
|         type: AssignEmptyHandlerType.APPROVE, | ||||
|       }, | ||||
|       assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, | ||||
|       childNode: props.childNode, | ||||
|       taskCreateListener: { | ||||
|         enable: false, | ||||
|       }, | ||||
|       taskAssignListener: { | ||||
|         enable: false, | ||||
|       }, | ||||
|       taskCompleteListener: { | ||||
|         enable: false, | ||||
|       }, | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.COPY_TASK_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       id: `Activity_${generateUUID()}`, | ||||
|       name: NODE_DEFAULT_NAME.get(NodeType.COPY_TASK_NODE) as string, | ||||
|       showText: '', | ||||
|       type: NodeType.COPY_TASK_NODE, | ||||
|       childNode: props.childNode, | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.CONDITION_BRANCH_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       name: '条件分支', | ||||
|       type: NodeType.CONDITION_BRANCH_NODE, | ||||
|       id: `GateWay_${generateUUID()}`, | ||||
|       childNode: props.childNode, | ||||
|       conditionNodes: [ | ||||
|         { | ||||
|           id: `Flow_${generateUUID()}`, | ||||
|           name: '条件1', | ||||
|           showText: '', | ||||
|           type: NodeType.CONDITION_NODE, | ||||
|           childNode: undefined, | ||||
|           conditionSetting: { | ||||
|             defaultFlow: false, | ||||
|             conditionType: ConditionType.RULE, | ||||
|             conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE), | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           id: `Flow_${generateUUID()}`, | ||||
|           name: '其它情况', | ||||
|           showText: '未满足其它条件时,将进入此分支', | ||||
|           type: NodeType.CONDITION_NODE, | ||||
|           childNode: undefined, | ||||
|           conditionSetting: { | ||||
|             defaultFlow: true, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.PARALLEL_BRANCH_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       name: '并行分支', | ||||
|       type: NodeType.PARALLEL_BRANCH_NODE, | ||||
|       id: `GateWay_${generateUUID()}`, | ||||
|       childNode: props.childNode, | ||||
|       conditionNodes: [ | ||||
|         { | ||||
|           id: `Flow_${generateUUID()}`, | ||||
|           name: '并行1', | ||||
|           showText: '无需配置条件同时执行', | ||||
|           type: NodeType.CONDITION_NODE, | ||||
|           childNode: undefined, | ||||
|         }, | ||||
|         { | ||||
|           id: `Flow_${generateUUID()}`, | ||||
|           name: '并行2', | ||||
|           showText: '无需配置条件同时执行', | ||||
|           type: NodeType.CONDITION_NODE, | ||||
|           childNode: undefined, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.INCLUSIVE_BRANCH_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       name: '包容分支', | ||||
|       type: NodeType.INCLUSIVE_BRANCH_NODE, | ||||
|       id: `GateWay_${generateUUID()}`, | ||||
|       childNode: props.childNode, | ||||
|       conditionNodes: [ | ||||
|         { | ||||
|           id: `Flow_${generateUUID()}`, | ||||
|           name: '包容条件1', | ||||
|           showText: '', | ||||
|           type: NodeType.CONDITION_NODE, | ||||
|           childNode: undefined, | ||||
|           conditionSetting: { | ||||
|             defaultFlow: false, | ||||
|             conditionType: ConditionType.RULE, | ||||
|             conditionGroups: cloneDeep(DEFAULT_CONDITION_GROUP_VALUE), | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           id: `Flow_${generateUUID()}`, | ||||
|           name: '其它情况', | ||||
|           showText: '未满足其它条件时,将进入此分支', | ||||
|           type: NodeType.CONDITION_NODE, | ||||
|           childNode: undefined, | ||||
|           conditionSetting: { | ||||
|             defaultFlow: true, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.DELAY_TIMER_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       id: `Activity_${generateUUID()}`, | ||||
|       name: NODE_DEFAULT_NAME.get(NodeType.DELAY_TIMER_NODE) as string, | ||||
|       showText: '', | ||||
|       type: NodeType.DELAY_TIMER_NODE, | ||||
|       childNode: props.childNode, | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.ROUTER_BRANCH_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       id: `GateWay_${generateUUID()}`, | ||||
|       name: NODE_DEFAULT_NAME.get(NodeType.ROUTER_BRANCH_NODE) as string, | ||||
|       showText: '', | ||||
|       type: NodeType.ROUTER_BRANCH_NODE, | ||||
|       childNode: props.childNode, | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.TRIGGER_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       id: `Activity_${generateUUID()}`, | ||||
|       name: NODE_DEFAULT_NAME.get(NodeType.TRIGGER_NODE) as string, | ||||
|       showText: '', | ||||
|       type: NodeType.TRIGGER_NODE, | ||||
|       childNode: props.childNode, | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
|   if (type === NodeType.CHILD_PROCESS_NODE) { | ||||
|     const data: SimpleFlowNode = { | ||||
|       id: `Activity_${generateUUID()}`, | ||||
|       name: NODE_DEFAULT_NAME.get(NodeType.CHILD_PROCESS_NODE) as string, | ||||
|       showText: '', | ||||
|       type: NodeType.CHILD_PROCESS_NODE, | ||||
|       childNode: props.childNode, | ||||
|       childProcessSetting: { | ||||
|         calledProcessDefinitionKey: '', | ||||
|         calledProcessDefinitionName: '', | ||||
|         async: false, | ||||
|         skipStartUserNode: false, | ||||
|         startUserSetting: { | ||||
|           type: 1, | ||||
|         }, | ||||
|         timeoutSetting: { | ||||
|           enable: false, | ||||
|         }, | ||||
|         multiInstanceSetting: { | ||||
|           enable: false, | ||||
|         }, | ||||
|       }, | ||||
|     }; | ||||
|     emits('update:childNode', data); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <div class="node-handler-wrapper"> | ||||
|     <div class="node-handler"> | ||||
|       <Popover trigger="hover" placement="right" width="auto" v-if="!readonly"> | ||||
|         <template #content> | ||||
|           <div class="handler-item-wrapper"> | ||||
|             <div class="handler-item" @click="addNode(NodeType.USER_TASK_NODE)"> | ||||
|               <div class="approve handler-item-icon"> | ||||
|                 <span class="iconfont icon-approve icon-size"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">审批人</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.TRANSACTOR_NODE)" | ||||
|             > | ||||
|               <div class="transactor handler-item-icon"> | ||||
|                 <span class="iconfont icon-transactor icon-size"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">办理人</div> | ||||
|             </div> | ||||
|             <div class="handler-item" @click="addNode(NodeType.COPY_TASK_NODE)"> | ||||
|               <div class="handler-item-icon copy"> | ||||
|                 <span class="iconfont icon-size icon-copy"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">抄送</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.CONDITION_BRANCH_NODE)" | ||||
|             > | ||||
|               <div class="handler-item-icon condition"> | ||||
|                 <span class="iconfont icon-size icon-exclusive"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">条件分支</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.PARALLEL_BRANCH_NODE)" | ||||
|             > | ||||
|               <div class="handler-item-icon parallel"> | ||||
|                 <span class="iconfont icon-size icon-parallel"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">并行分支</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.INCLUSIVE_BRANCH_NODE)" | ||||
|             > | ||||
|               <div class="handler-item-icon inclusive"> | ||||
|                 <span class="iconfont icon-size icon-inclusive"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">包容分支</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.DELAY_TIMER_NODE)" | ||||
|             > | ||||
|               <div class="handler-item-icon delay"> | ||||
|                 <span class="iconfont icon-size icon-delay"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">延迟器</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.ROUTER_BRANCH_NODE)" | ||||
|             > | ||||
|               <div class="handler-item-icon router"> | ||||
|                 <span class="iconfont icon-size icon-router"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">路由分支</div> | ||||
|             </div> | ||||
|             <div class="handler-item" @click="addNode(NodeType.TRIGGER_NODE)"> | ||||
|               <div class="handler-item-icon trigger"> | ||||
|                 <span class="iconfont icon-size icon-trigger"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">触发器</div> | ||||
|             </div> | ||||
|             <div | ||||
|               class="handler-item" | ||||
|               @click="addNode(NodeType.CHILD_PROCESS_NODE)" | ||||
|             > | ||||
|               <div class="handler-item-icon child-process"> | ||||
|                 <span class="iconfont icon-size icon-child-process"></span> | ||||
|               </div> | ||||
|               <div class="handler-item-text">子流程</div> | ||||
|             </div> | ||||
|           </div> | ||||
|         </template> | ||||
|         <div class="add-icon"><IconifyIcon icon="ep:plus" /></div> | ||||
|       </Popover> | ||||
|     </div> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,119 @@ | |||
| <script setup lang="ts"> | ||||
| import type { Ref } from 'vue'; | ||||
| 
 | ||||
| import type { SimpleFlowNode } from '../../consts'; | ||||
| 
 | ||||
| import { inject, ref } from 'vue'; | ||||
| 
 | ||||
| import { IconifyIcon } from '@vben/icons'; | ||||
| 
 | ||||
| import { Input } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { NODE_DEFAULT_TEXT, NodeType } from '../../consts'; | ||||
| import { useNodeName2, useTaskStatusClass, useWatchNode } from '../../helpers'; | ||||
| import NodeHandler from './node-handler.vue'; | ||||
| 
 | ||||
| defineOptions({ name: 'StartUserNode' }); | ||||
| const props = defineProps({ | ||||
|   flowNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     default: () => null, | ||||
|   }, | ||||
| }); | ||||
| // 定义事件,更新父组件。 | ||||
| // eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars | ||||
| const emits = defineEmits<{ | ||||
|   'update:modelValue': [node: SimpleFlowNode | undefined]; | ||||
| }>(); | ||||
| const readonly = inject<Boolean>('readonly'); // 是否只读 | ||||
| const tasks = inject<Ref<any[]>>('tasks', ref([])); | ||||
| // 监控节点变化 | ||||
| const currentNode = useWatchNode(props); | ||||
| // 节点名称编辑 | ||||
| const { showInput, blurEvent, clickTitle } = useNodeName2( | ||||
|   currentNode, | ||||
|   NodeType.START_USER_NODE, | ||||
| ); | ||||
| 
 | ||||
| const nodeSetting = ref(); | ||||
| 
 | ||||
| // 任务的弹窗显示,用于只读模式 | ||||
| const selectTasks = ref<any[] | undefined>([]); // 选中的任务数组 | ||||
| 
 | ||||
| const nodeClick = () => { | ||||
|   if (readonly) { | ||||
|     // 只读模式,弹窗显示任务信息 | ||||
|     if (tasks && tasks.value) { | ||||
|       console.warn( | ||||
|         'TODO 只读模式,弹窗显示任务信息', | ||||
|         tasks.value, | ||||
|         selectTasks.value, | ||||
|       ); | ||||
|     } | ||||
|   } else { | ||||
|     console.warn( | ||||
|       'TODO 编辑模式,打开节点配置、把当前节点传递给配置组件', | ||||
|       nodeSetting.value, | ||||
|     ); | ||||
|     // nodeSetting.value.showStartUserNodeConfig(currentNode.value); | ||||
|     // nodeSetting.value.openDrawer(); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <div class="node-wrapper"> | ||||
|     <div class="node-container"> | ||||
|       <div | ||||
|         class="node-box" | ||||
|         :class="[ | ||||
|           { 'node-config-error': !currentNode.showText }, | ||||
|           `${useTaskStatusClass(currentNode?.activityStatus)}`, | ||||
|         ]" | ||||
|       > | ||||
|         <div class="node-title-container"> | ||||
|           <div class="node-title-icon start-user"> | ||||
|             <span class="iconfont icon-start-user"></span> | ||||
|           </div> | ||||
|           <Input | ||||
|             v-if="!readonly && showInput" | ||||
|             type="text" | ||||
|             class="editable-title-input" | ||||
|             @blur="blurEvent()" | ||||
|             v-model:value="currentNode.name" | ||||
|             :placeholder="currentNode.name" | ||||
|           /> | ||||
|           <div v-else class="node-title" @click="clickTitle"> | ||||
|             {{ currentNode.name }} | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="node-content" @click="nodeClick"> | ||||
|           <div | ||||
|             class="node-text" | ||||
|             :title="currentNode.showText" | ||||
|             v-if="currentNode.showText" | ||||
|           > | ||||
|             {{ currentNode.showText }} | ||||
|           </div> | ||||
|           <div class="node-text" v-else> | ||||
|             {{ NODE_DEFAULT_TEXT.get(NodeType.START_USER_NODE) }} | ||||
|           </div> | ||||
|           <IconifyIcon icon="ep:arrow-right-bold" v-if="!readonly" /> | ||||
|         </div> | ||||
|       </div> | ||||
|       <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> | ||||
|       <NodeHandler | ||||
|         v-if="currentNode" | ||||
|         v-model:child-node="currentNode.childNode" | ||||
|         :current-node="currentNode" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
|   <!-- TODO 发起人配置节点 | ||||
|    <StartUserNodeConfig | ||||
|     v-if="!readonly && currentNode" | ||||
|     ref="nodeSetting" | ||||
|     :flow-node="currentNode" | ||||
|   /> --> | ||||
|   <!-- 审批记录  TODO --> | ||||
| </template> | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,147 @@ | |||
| <script setup lang="ts"> | ||||
| import type { SimpleFlowNode } from '../consts'; | ||||
| 
 | ||||
| import { NodeType } from '../consts'; | ||||
| import { useWatchNode } from '../helpers'; | ||||
| import EndEventNode from './nodes/end-event-node.vue'; | ||||
| import StartUserNode from './nodes/start-user-node.vue'; | ||||
| 
 | ||||
| defineOptions({ name: 'ProcessNodeTree' }); | ||||
| const props = defineProps({ | ||||
|   parentNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     default: () => null, | ||||
|   }, | ||||
|   flowNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     default: () => null, | ||||
|   }, | ||||
| }); | ||||
| const emits = defineEmits<{ | ||||
|   recursiveFindParentNode: [ | ||||
|     nodeList: SimpleFlowNode[], | ||||
|     curentNode: SimpleFlowNode, | ||||
|     nodeType: number, | ||||
|   ]; | ||||
|   'update:flowNode': [node: SimpleFlowNode | undefined]; | ||||
| }>(); | ||||
| 
 | ||||
| const currentNode = useWatchNode(props); | ||||
| 
 | ||||
| // 用于删除节点 | ||||
| // eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars | ||||
| const handleModelValueUpdate = (updateValue: any) => { | ||||
|   emits('update:flowNode', updateValue); | ||||
| }; | ||||
| 
 | ||||
| // eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars | ||||
| const triggerFromParentNode = ( | ||||
|   nodeList: SimpleFlowNode[], | ||||
|   nodeType: number, | ||||
| ) => { | ||||
|   emits('recursiveFindParentNode', nodeList, props.parentNode, nodeType); | ||||
| }; | ||||
| 
 | ||||
| // 递归从父节点中查询匹配的节点 | ||||
| const recursiveFindParentNode = ( | ||||
|   nodeList: SimpleFlowNode[], | ||||
|   findNode: SimpleFlowNode, | ||||
|   nodeType: number, | ||||
| ) => { | ||||
|   if (!findNode) { | ||||
|     return; | ||||
|   } | ||||
|   if (findNode.type === NodeType.START_USER_NODE) { | ||||
|     nodeList.push(findNode); | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   if (findNode.type === nodeType) { | ||||
|     nodeList.push(findNode); | ||||
|   } | ||||
|   emits('recursiveFindParentNode', nodeList, props.parentNode, nodeType); | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <!-- 发起人节点 --> | ||||
|   <StartUserNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.START_USER_NODE" | ||||
|     :flow-node="currentNode" | ||||
|   /> | ||||
|   <!-- 审批节点 --> | ||||
|   <!-- <UserTaskNode | ||||
|     v-if=" | ||||
|       currentNode && | ||||
|       (currentNode.type === NodeType.USER_TASK_NODE || | ||||
|         currentNode.type === NodeType.TRANSACTOR_NODE) | ||||
|     " | ||||
|     :flow-node="currentNode" | ||||
|     @update:flow-node="handleModelValueUpdate" | ||||
|     @find:parent-node="findFromParentNode" | ||||
|   /> --> | ||||
|   <!-- 抄送节点 --> | ||||
|   <!-- <CopyTaskNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.COPY_TASK_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:flow-node="handleModelValueUpdate" | ||||
|   /> --> | ||||
|   <!-- 条件节点 --> | ||||
|   <!-- <ExclusiveNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.CONDITION_BRANCH_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:model-value="handleModelValueUpdate" | ||||
|     @find:parent-node="findFromParentNode" | ||||
|   /> --> | ||||
|   <!-- 并行节点 --> | ||||
|   <!-- <ParallelNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.PARALLEL_BRANCH_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:model-value="handleModelValueUpdate" | ||||
|     @find:parent-node="findFromParentNode" | ||||
|   /> --> | ||||
|   <!-- 包容分支节点 --> | ||||
|   <!-- <InclusiveNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.INCLUSIVE_BRANCH_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:model-value="handleModelValueUpdate" | ||||
|     @find:parent-node="findFromParentNode" | ||||
|   /> --> | ||||
|   <!-- 延迟器节点 --> | ||||
|   <!-- <DelayTimerNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.DELAY_TIMER_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:flow-node="handleModelValueUpdate" | ||||
|   /> --> | ||||
|   <!-- 路由分支节点 --> | ||||
|   <!-- <RouterNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:flow-node="handleModelValueUpdate" | ||||
|   /> --> | ||||
|   <!-- 触发器节点 --> | ||||
|   <!-- <TriggerNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:flow-node="handleModelValueUpdate" | ||||
|   /> --> | ||||
|   <!-- 子流程节点 --> | ||||
|   <!-- <ChildProcessNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.CHILD_PROCESS_NODE" | ||||
|     :flow-node="currentNode" | ||||
|     @update:flow-node="handleModelValueUpdate" | ||||
|   /> --> | ||||
|   <!-- 递归显示孩子节点  --> | ||||
|   <ProcessNodeTree | ||||
|     v-if="currentNode && currentNode.childNode" | ||||
|     v-model:flow-node="currentNode.childNode" | ||||
|     :parent-node="currentNode" | ||||
|     @recursive-find-parent-node="recursiveFindParentNode" | ||||
|   /> | ||||
| 
 | ||||
|   <!-- 结束节点 --> | ||||
|   <EndEventNode | ||||
|     v-if="currentNode && currentNode.type === NodeType.END_EVENT_NODE" | ||||
|     :flow-node="currentNode" | ||||
|   /> | ||||
| </template> | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -0,0 +1,246 @@ | |||
| <script setup lang="ts"> | ||||
| import type { Ref } from 'vue'; | ||||
| 
 | ||||
| import type { SimpleFlowNode } from '../consts'; | ||||
| 
 | ||||
| import type { BpmFormApi } from '#/api/bpm/form'; | ||||
| import type { BpmUserGroupApi } from '#/api/bpm/userGroup'; | ||||
| import type { SystemDeptApi } from '#/api/system/dept'; | ||||
| import type { SystemPostApi } from '#/api/system/post'; | ||||
| import type { SystemRoleApi } from '#/api/system/role'; | ||||
| import type { SystemUserApi } from '#/api/system/user'; | ||||
| 
 | ||||
| import { inject, onMounted, provide, ref } from 'vue'; | ||||
| 
 | ||||
| import { handleTree } from '@vben/utils'; | ||||
| 
 | ||||
| import { Button, Modal } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { getFormDetail } from '#/api/bpm/form'; | ||||
| import { getModel } from '#/api/bpm/model'; | ||||
| import { getUserGroupSimpleList } from '#/api/bpm/userGroup'; | ||||
| import { getSimpleDeptList } from '#/api/system/dept'; | ||||
| import { getSimplePostList } from '#/api/system/post'; | ||||
| import { getSimpleRoleList } from '#/api/system/role'; | ||||
| import { getSimpleUserList } from '#/api/system/user'; | ||||
| import { BpmModelFormType } from '#/utils/constants'; | ||||
| 
 | ||||
| import { NODE_DEFAULT_TEXT, NodeId, NodeType } from '../consts'; | ||||
| import SimpleProcessModel from './simple-process-model.vue'; | ||||
| 
 | ||||
| defineOptions({ | ||||
|   name: 'SimpleProcessDesigner', | ||||
| }); | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   modelId: { | ||||
|     type: String, | ||||
|     required: false, | ||||
|     default: undefined, | ||||
|   }, | ||||
|   modelKey: { | ||||
|     type: String, | ||||
|     required: false, | ||||
|     default: undefined, | ||||
|   }, | ||||
|   modelName: { | ||||
|     type: String, | ||||
|     required: false, | ||||
|     default: undefined, | ||||
|   }, | ||||
|   // 可发起流程的人员编号 | ||||
|   startUserIds: { | ||||
|     type: Array, | ||||
|     required: false, | ||||
|     default: undefined, | ||||
|   }, | ||||
|   // 可发起流程的部门编号 | ||||
|   startDeptIds: { | ||||
|     type: Array, | ||||
|     required: false, | ||||
|     default: undefined, | ||||
|   }, | ||||
| }); | ||||
| // 保存成功事件 | ||||
| const emits = defineEmits(['success']); | ||||
| const processData = inject('processData') as Ref; | ||||
| const loading = ref(false); | ||||
| const formFields = ref<string[]>([]); | ||||
| const formType = ref(20); | ||||
| const roleOptions = ref<SystemRoleApi.Role[]>([]); // 角色列表 | ||||
| const postOptions = ref<SystemPostApi.Post[]>([]); // 岗位列表 | ||||
| const userOptions = ref<SystemUserApi.User[]>([]); // 用户列表 | ||||
| const deptOptions = ref<SystemDeptApi.Dept[]>([]); // 部门列表 | ||||
| const deptTreeOptions = ref(); | ||||
| const userGroupOptions = ref<BpmUserGroupApi.UserGroupVO[]>([]); // 用户组列表 | ||||
| 
 | ||||
| provide('formFields', formFields); | ||||
| provide('formType', formType); | ||||
| provide('roleList', roleOptions); | ||||
| provide('postList', postOptions); | ||||
| provide('userList', userOptions); | ||||
| provide('deptList', deptOptions); | ||||
| provide('userGroupList', userGroupOptions); | ||||
| provide('deptTree', deptTreeOptions); | ||||
| provide('startUserIds', props.startUserIds); | ||||
| provide('startDeptIds', props.startDeptIds); | ||||
| provide('tasks', []); | ||||
| provide('processInstance', {}); | ||||
| const processNodeTree = ref<SimpleFlowNode | undefined>(); | ||||
| provide('processNodeTree', processNodeTree); | ||||
| const errorDialogVisible = ref(false); | ||||
| const errorNodes: SimpleFlowNode[] = []; | ||||
| 
 | ||||
| // 添加更新模型的方法 | ||||
| const updateModel = () => { | ||||
|   if (!processNodeTree.value) { | ||||
|     processNodeTree.value = { | ||||
|       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, | ||||
|       }, | ||||
|     }; | ||||
|     // 初始化时也触发一次保存 | ||||
|     saveSimpleFlowModel(processNodeTree.value); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| const saveSimpleFlowModel = async ( | ||||
|   simpleModelNode: SimpleFlowNode | undefined, | ||||
| ) => { | ||||
|   if (!simpleModelNode) { | ||||
|     return; | ||||
|   } | ||||
| 
 | ||||
|   try { | ||||
|     processData.value = simpleModelNode; | ||||
|     emits('success', simpleModelNode); | ||||
|   } catch (error) { | ||||
|     console.error('保存失败:', error); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * 校验节点设置。 暂时以 showText 为空 未节点错误配置 | ||||
|  */ | ||||
| // eslint-disable-next-line unused-imports/no-unused-vars, no-unused-vars | ||||
| const validateNode = ( | ||||
|   node: SimpleFlowNode | undefined, | ||||
|   errorNodes: SimpleFlowNode[], | ||||
| ) => { | ||||
|   if (node) { | ||||
|     const { type, showText, conditionNodes } = node; | ||||
|     if (type === NodeType.END_EVENT_NODE) { | ||||
|       return; | ||||
|     } | ||||
|     if (type === NodeType.START_USER_NODE) { | ||||
|       // 发起人节点暂时不用校验,直接校验孩子节点 | ||||
|       validateNode(node.childNode, errorNodes); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       type === NodeType.USER_TASK_NODE || | ||||
|       type === NodeType.COPY_TASK_NODE || | ||||
|       type === NodeType.CONDITION_NODE | ||||
|     ) { | ||||
|       if (!showText) { | ||||
|         errorNodes.push(node); | ||||
|       } | ||||
|       validateNode(node.childNode, errorNodes); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       type === NodeType.CONDITION_BRANCH_NODE || | ||||
|       type === NodeType.PARALLEL_BRANCH_NODE || | ||||
|       type === NodeType.INCLUSIVE_BRANCH_NODE | ||||
|     ) { | ||||
|       // 分支节点 | ||||
|       // 1. 先校验各个分支 | ||||
|       conditionNodes?.forEach((item) => { | ||||
|         validateNode(item, errorNodes); | ||||
|       }); | ||||
|       // 2. 校验孩子节点 | ||||
|       validateNode(node.childNode, errorNodes); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| onMounted(async () => { | ||||
|   try { | ||||
|     loading.value = true; | ||||
|     // 获取表单字段 | ||||
|     if (props.modelId) { | ||||
|       const bpmnModel = await getModel(props.modelId); | ||||
|       if (bpmnModel) { | ||||
|         formType.value = bpmnModel.formType; | ||||
|         if (formType.value === BpmModelFormType.NORMAL && bpmnModel.formId) { | ||||
|           const bpmnForm = (await getFormDetail( | ||||
|             bpmnModel.formId, | ||||
|           )) as unknown as BpmFormApi.FormVO; | ||||
|           formFields.value = bpmnForm?.fields; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     // 获得角色列表 | ||||
|     roleOptions.value = await getSimpleRoleList(); | ||||
|     // 获得岗位列表 | ||||
|     postOptions.value = await getSimplePostList(); | ||||
|     // 获得用户列表 | ||||
|     userOptions.value = await getSimpleUserList(); | ||||
|     // 获得部门列表 | ||||
|     const deptList = await getSimpleDeptList(); | ||||
|     deptOptions.value = deptList; | ||||
|     // 转换成树形结构 | ||||
|     deptTreeOptions.value = handleTree(deptList); | ||||
|     // 获取用户组列表 | ||||
|     userGroupOptions.value = await getUserGroupSimpleList(); | ||||
|     // 加载流程数据 | ||||
|     if (processData.value) { | ||||
|       processNodeTree.value = processData?.value; | ||||
|     } else { | ||||
|       updateModel(); | ||||
|     } | ||||
|   } finally { | ||||
|     loading.value = false; | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| const simpleProcessModelRef = ref(); | ||||
| 
 | ||||
| defineExpose({}); | ||||
| </script> | ||||
| <template> | ||||
|   <div v-loading="loading"> | ||||
|     <SimpleProcessModel | ||||
|       ref="simpleProcessModelRef" | ||||
|       v-if="processNodeTree" | ||||
|       :flow-node="processNodeTree" | ||||
|       :readonly="false" | ||||
|       @save="saveSimpleFlowModel" | ||||
|     /> | ||||
|     <Modal | ||||
|       v-model="errorDialogVisible" | ||||
|       title="保存失败" | ||||
|       width="400" | ||||
|       :fullscreen="false" | ||||
|     > | ||||
|       <div class="mb-2">以下节点内容不完善,请修改后保存</div> | ||||
|       <div | ||||
|         class="b-rounded-1 line-height-normal mb-3 bg-gray-100 p-2" | ||||
|         v-for="(item, index) in errorNodes" | ||||
|         :key="index" | ||||
|       > | ||||
|         {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }} | ||||
|       </div> | ||||
|       <template #footer> | ||||
|         <Button type="primary" @click="errorDialogVisible = false"> | ||||
|           知道了 | ||||
|         </Button> | ||||
|       </template> | ||||
|     </Modal> | ||||
|   </div> | ||||
| </template> | ||||
|  | @ -0,0 +1,286 @@ | |||
| <script setup lang="ts"> | ||||
| import type { SimpleFlowNode } from '../consts'; | ||||
| 
 | ||||
| import { onMounted, provide, ref } from 'vue'; | ||||
| 
 | ||||
| import { IconifyIcon } from '@vben/icons'; | ||||
| import { downloadFileFromBlob, isString } from '@vben/utils'; | ||||
| 
 | ||||
| import { Button, ButtonGroup, Modal, Row } from 'ant-design-vue'; | ||||
| 
 | ||||
| import { NODE_DEFAULT_TEXT, NodeType } from '../consts'; | ||||
| import { useWatchNode } from '../helpers'; | ||||
| import ProcessNodeTree from './process-node-tree.vue'; | ||||
| 
 | ||||
| defineOptions({ | ||||
|   name: 'SimpleProcessModel', | ||||
| }); | ||||
| 
 | ||||
| const props = defineProps({ | ||||
|   flowNode: { | ||||
|     type: Object as () => SimpleFlowNode, | ||||
|     required: true, | ||||
|   }, | ||||
|   readonly: { | ||||
|     type: Boolean, | ||||
|     required: false, | ||||
|     default: true, | ||||
|   }, | ||||
| }); | ||||
| 
 | ||||
| const emits = defineEmits<{ | ||||
|   save: [node: SimpleFlowNode | undefined]; | ||||
| }>(); | ||||
| 
 | ||||
| const processNodeTree = useWatchNode(props); | ||||
| 
 | ||||
| provide('readonly', props.readonly); | ||||
| 
 | ||||
| // TODO 可优化:拖拽有点卡顿 | ||||
| /** 拖拽、放大缩小等操作 */ | ||||
| const scaleValue = ref(100); | ||||
| const MAX_SCALE_VALUE = 200; | ||||
| const MIN_SCALE_VALUE = 50; | ||||
| const isDragging = ref(false); | ||||
| const startX = ref(0); | ||||
| const startY = ref(0); | ||||
| const currentX = ref(0); | ||||
| const currentY = ref(0); | ||||
| const initialX = ref(0); | ||||
| const initialY = ref(0); | ||||
| 
 | ||||
| const setGrabCursor = () => { | ||||
|   document.body.style.cursor = 'grab'; | ||||
| }; | ||||
| 
 | ||||
| const resetCursor = () => { | ||||
|   document.body.style.cursor = 'default'; | ||||
| }; | ||||
| 
 | ||||
| const startDrag = (e: MouseEvent) => { | ||||
|   isDragging.value = true; | ||||
|   startX.value = e.clientX - currentX.value; | ||||
|   startY.value = e.clientY - currentY.value; | ||||
|   setGrabCursor(); // 设置小手光标 | ||||
| }; | ||||
| 
 | ||||
| const onDrag = (e: MouseEvent) => { | ||||
|   if (!isDragging.value) return; | ||||
|   e.preventDefault(); // 禁用文本选择 | ||||
| 
 | ||||
|   // 使用 requestAnimationFrame 优化性能 | ||||
|   requestAnimationFrame(() => { | ||||
|     currentX.value = e.clientX - startX.value; | ||||
|     currentY.value = e.clientY - startY.value; | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| const stopDrag = () => { | ||||
|   isDragging.value = false; | ||||
|   resetCursor(); // 重置光标 | ||||
| }; | ||||
| 
 | ||||
| const zoomIn = () => { | ||||
|   if (scaleValue.value === MAX_SCALE_VALUE) { | ||||
|     return; | ||||
|   } | ||||
|   scaleValue.value += 10; | ||||
| }; | ||||
| 
 | ||||
| const zoomOut = () => { | ||||
|   if (scaleValue.value === MIN_SCALE_VALUE) { | ||||
|     return; | ||||
|   } | ||||
|   scaleValue.value -= 10; | ||||
| }; | ||||
| 
 | ||||
| const processReZoom = () => { | ||||
|   scaleValue.value = 100; | ||||
| }; | ||||
| 
 | ||||
| const resetPosition = () => { | ||||
|   currentX.value = initialX.value; | ||||
|   currentY.value = initialY.value; | ||||
| }; | ||||
| 
 | ||||
| /** 校验节点设置 */ | ||||
| const errorDialogVisible = ref(false); | ||||
| let errorNodes: SimpleFlowNode[] = []; | ||||
| 
 | ||||
| const validateNode = ( | ||||
|   node: SimpleFlowNode | undefined, | ||||
|   errorNodes: SimpleFlowNode[], | ||||
| ) => { | ||||
|   if (node) { | ||||
|     const { type, showText, conditionNodes } = node; | ||||
|     if (type === NodeType.END_EVENT_NODE) { | ||||
|       return; | ||||
|     } | ||||
|     if (type === NodeType.START_USER_NODE) { | ||||
|       // 发起人节点暂时不用校验,直接校验孩子节点 | ||||
|       validateNode(node.childNode, errorNodes); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       type === NodeType.USER_TASK_NODE || | ||||
|       type === NodeType.COPY_TASK_NODE || | ||||
|       type === NodeType.CONDITION_NODE | ||||
|     ) { | ||||
|       if (!showText) { | ||||
|         errorNodes.push(node); | ||||
|       } | ||||
|       validateNode(node.childNode, errorNodes); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|       type === NodeType.CONDITION_BRANCH_NODE || | ||||
|       type === NodeType.PARALLEL_BRANCH_NODE || | ||||
|       type === NodeType.INCLUSIVE_BRANCH_NODE | ||||
|     ) { | ||||
|       // 分支节点 | ||||
|       // 1. 先校验各个分支 | ||||
|       conditionNodes?.forEach((item) => { | ||||
|         validateNode(item, errorNodes); | ||||
|       }); | ||||
|       // 2. 校验孩子节点 | ||||
|       validateNode(node.childNode, errorNodes); | ||||
|     } | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** 获取当前流程数据 */ | ||||
| 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, | ||||
| }); | ||||
| 
 | ||||
| /** 导出 JSON */ | ||||
| const exportJson = () => { | ||||
|   downloadFileFromBlob({ | ||||
|     fileName: 'model.json', | ||||
|     source: new Blob([JSON.stringify(processNodeTree.value)]), | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| /** 导入 JSON */ | ||||
| const refFile = ref(); | ||||
| const importJson = () => { | ||||
|   refFile.value.click(); | ||||
| }; | ||||
| const importLocalFile = () => { | ||||
|   const file = refFile.value.files[0]; | ||||
|   file.text().then((result: any) => { | ||||
|     if (isString(result)) { | ||||
|       processNodeTree.value = JSON.parse(result); | ||||
|       emits('save', processNodeTree.value); | ||||
|     } | ||||
|   }); | ||||
| }; | ||||
| 
 | ||||
| // 在组件初始化时记录初始位置 | ||||
| onMounted(() => { | ||||
|   initialX.value = currentX.value; | ||||
|   initialY.value = currentY.value; | ||||
| }); | ||||
| </script> | ||||
| <template> | ||||
|   <div class="simple-process-model-container position-relative h-full"> | ||||
|     <div class="z-index-button-group absolute right-[0px] top-[0px] bg-[#fff]"> | ||||
|       <Row type="flex" justify="end"> | ||||
|         <ButtonGroup key="scale-control"> | ||||
|           <Button v-if="!readonly" @click="exportJson"> | ||||
|             <IconifyIcon icon="ep:download" /> 导出 | ||||
|           </Button> | ||||
|           <Button v-if="!readonly" @click="importJson"> | ||||
|             <IconifyIcon icon="ep:upload" />导入 | ||||
|           </Button> | ||||
|           <!-- 用于打开本地文件--> | ||||
|           <input | ||||
|             v-if="!readonly" | ||||
|             type="file" | ||||
|             id="files" | ||||
|             ref="refFile" | ||||
|             style="display: none" | ||||
|             accept=".json" | ||||
|             @change="importLocalFile" | ||||
|           /> | ||||
|           <Button @click="processReZoom()"> | ||||
|             <IconifyIcon icon="tabler:relation-one-to-one" /> | ||||
|           </Button> | ||||
|           <Button :plain="true" @click="zoomOut()"> | ||||
|             <IconifyIcon icon="tabler:zoom-out" /> | ||||
|           </Button> | ||||
|           <Button class="w-80px"> {{ scaleValue }}% </Button> | ||||
|           <Button :plain="true" @click="zoomIn()"> | ||||
|             <IconifyIcon icon="tabler:zoom-in" /> | ||||
|           </Button> | ||||
|           <Button @click="resetPosition">重置</Button> | ||||
|         </ButtonGroup> | ||||
|       </Row> | ||||
|     </div> | ||||
|     <div | ||||
|       class="simple-process-model" | ||||
|       :style="`transform: translate(${currentX}px, ${currentY}px) scale(${scaleValue / 100});`" | ||||
|       @mousedown="startDrag" | ||||
|       @mousemove="onDrag" | ||||
|       @mouseup="stopDrag" | ||||
|       @mouseleave="stopDrag" | ||||
|       @mouseenter="setGrabCursor" | ||||
|     > | ||||
|       <ProcessNodeTree | ||||
|         v-if="processNodeTree" | ||||
|         v-model:flow-node="processNodeTree" | ||||
|       /> | ||||
|     </div> | ||||
|   </div> | ||||
|   <Modal | ||||
|     v-model:open="errorDialogVisible" | ||||
|     title="保存失败" | ||||
|     width="400" | ||||
|     :fullscreen="false" | ||||
|   > | ||||
|     <div class="mb-2">以下节点内容不完善,请修改后保存</div> | ||||
|     <div | ||||
|       class="b-rounded-1 line-height-normal mb-3 bg-gray-100 p-2" | ||||
|       v-for="(item, index) in errorNodes" | ||||
|       :key="index" | ||||
|     > | ||||
|       {{ item.name }} : {{ NODE_DEFAULT_TEXT.get(item.type) }} | ||||
|     </div> | ||||
|     <template #footer> | ||||
|       <Button type="primary" @click="errorDialogVisible = false">知道了</Button> | ||||
|     </template> | ||||
|   </Modal> | ||||
| </template> | ||||
| <style lang="scss" scoped> | ||||
| .simple-process-model-container { | ||||
|   width: 100%; | ||||
|   height: 100%; | ||||
|   overflow: auto; | ||||
|   user-select: none; // 禁用文本选择 | ||||
| } | ||||
| 
 | ||||
| .simple-process-model { | ||||
|   position: relative; // 确保相对定位 | ||||
|   min-width: 100%; // 确保宽度为100% | ||||
|   min-height: 100%; // 确保高度为100% | ||||
| } | ||||
| 
 | ||||
| .z-index-ButtonGroup { | ||||
|   z-index: 10; | ||||
| } | ||||
| </style> | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1,739 @@ | |||
| import type { Ref } from 'vue'; | ||||
| 
 | ||||
| import type { | ||||
|   ConditionGroup, | ||||
|   HttpRequestParam, | ||||
|   SimpleFlowNode, | ||||
| } from './consts'; | ||||
| 
 | ||||
| import type { BpmUserGroupApi } from '#/api/bpm/userGroup'; | ||||
| import type { SystemDeptApi } from '#/api/system/dept'; | ||||
| import type { SystemPostApi } from '#/api/system/post'; | ||||
| import type { SystemRoleApi } from '#/api/system/role'; | ||||
| import type { SystemUserApi } from '#/api/system/user'; | ||||
| 
 | ||||
| import { inject, ref, toRaw, unref, watch } from 'vue'; | ||||
| 
 | ||||
| import { | ||||
|   ApproveMethodType, | ||||
|   AssignEmptyHandlerType, | ||||
|   AssignStartUserHandlerType, | ||||
|   CandidateStrategy, | ||||
|   COMPARISON_OPERATORS, | ||||
|   ConditionType, | ||||
|   FieldPermissionType, | ||||
|   NODE_DEFAULT_NAME, | ||||
|   NodeType, | ||||
|   ProcessVariableEnum, | ||||
|   RejectHandlerType, | ||||
|   TaskStatusEnum, | ||||
| } from './consts'; | ||||
| 
 | ||||
| export function useWatchNode(props: { | ||||
|   flowNode: SimpleFlowNode; | ||||
| }): Ref<SimpleFlowNode> { | ||||
|   const node = ref<SimpleFlowNode>(props.flowNode); | ||||
|   watch( | ||||
|     () => props.flowNode, | ||||
|     (newValue) => { | ||||
|       node.value = newValue; | ||||
|     }, | ||||
|   ); | ||||
|   return node; | ||||
| } | ||||
| 
 | ||||
| // 解析 formCreate 所有表单字段, 并返回
 | ||||
| const parseFormCreateFields = (formFields?: string[]) => { | ||||
|   const result: Array<Record<string, any>> = []; | ||||
|   if (formFields) { | ||||
|     formFields.forEach((fieldStr: string) => { | ||||
|       parseFormFields(JSON.parse(fieldStr), result); | ||||
|     }); | ||||
|   } | ||||
|   return result; | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * 解析表单组件的  field, title 等字段(递归,如果组件包含子组件) | ||||
|  * | ||||
|  * @param rule  组件的生成规则 https://www.form-create.com/v3/guide/rule
 | ||||
|  * @param fields 解析后表单组件字段 | ||||
|  * @param parentTitle  如果是子表单,子表单的标题,默认为空 | ||||
|  */ | ||||
| export const parseFormFields = ( | ||||
|   rule: Record<string, any>, | ||||
|   fields: Array<Record<string, any>> = [], | ||||
|   parentTitle: string = '', | ||||
| ) => { | ||||
|   const { type, field, $required, title: tempTitle, children } = rule; | ||||
|   if (field && tempTitle) { | ||||
|     let title = tempTitle; | ||||
|     if (parentTitle) { | ||||
|       title = `${parentTitle}.${tempTitle}`; | ||||
|     } | ||||
|     let required = false; | ||||
|     if ($required) { | ||||
|       required = true; | ||||
|     } | ||||
|     fields.push({ | ||||
|       field, | ||||
|       title, | ||||
|       type, | ||||
|       required, | ||||
|     }); | ||||
|     // TODO 子表单 需要处理子表单字段
 | ||||
|     // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
 | ||||
|     //   // 解析子表单的字段
 | ||||
|     //   rule.props.rule.forEach((item) => {
 | ||||
|     //     parseFields(item, fieldsPermission, title)
 | ||||
|     //   })
 | ||||
|     // }
 | ||||
|   } | ||||
|   if (children && Array.isArray(children)) { | ||||
|     children.forEach((rule) => { | ||||
|       parseFormFields(rule, fields); | ||||
|     }); | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @description 表单数据权限配置,用于发起人节点 、审批节点、抄送节点 | ||||
|  */ | ||||
| export function useFormFieldsPermission( | ||||
|   defaultPermission: FieldPermissionType, | ||||
| ) { | ||||
|   // 字段权限配置. 需要有 field, title,  permissioin 属性
 | ||||
|   const fieldsPermissionConfig = ref<Array<Record<string, any>>>([]); | ||||
| 
 | ||||
|   const formType = inject<Ref<number | undefined>>('formType', ref()); // 表单类型
 | ||||
| 
 | ||||
|   const formFields = inject<Ref<string[]>>('formFields', ref([])); // 流程表单字段
 | ||||
| 
 | ||||
|   const getNodeConfigFormFields = ( | ||||
|     nodeFormFields?: Array<Record<string, string>>, | ||||
|   ) => { | ||||
|     nodeFormFields = toRaw(nodeFormFields); | ||||
|     fieldsPermissionConfig.value = | ||||
|       !nodeFormFields || nodeFormFields.length === 0 | ||||
|         ? getDefaultFieldsPermission(unref(formFields)) | ||||
|         : mergeFieldsPermission(nodeFormFields, unref(formFields)); | ||||
|   }; | ||||
|   // 合并已经设置的表单字段权限,当前流程表单字段 (可能新增,或删除了字段)
 | ||||
|   const mergeFieldsPermission = ( | ||||
|     formFieldsPermisson: Array<Record<string, string>>, | ||||
|     formFields?: string[], | ||||
|   ) => { | ||||
|     let mergedFieldsPermission: Array<Record<string, any>> = []; | ||||
|     if (formFields) { | ||||
|       mergedFieldsPermission = parseFormCreateFields(formFields).map((item) => { | ||||
|         const found = formFieldsPermisson.find( | ||||
|           (fieldPermission) => fieldPermission.field === item.field, | ||||
|         ); | ||||
|         return { | ||||
|           field: item.field, | ||||
|           title: item.title, | ||||
|           permission: found ? found.permission : defaultPermission, | ||||
|         }; | ||||
|       }); | ||||
|     } | ||||
|     return mergedFieldsPermission; | ||||
|   }; | ||||
| 
 | ||||
|   // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
 | ||||
|   const getDefaultFieldsPermission = (formFields?: string[]) => { | ||||
|     let defaultFieldsPermission: Array<Record<string, any>> = []; | ||||
|     if (formFields) { | ||||
|       defaultFieldsPermission = parseFormCreateFields(formFields).map( | ||||
|         (item) => { | ||||
|           return { | ||||
|             field: item.field, | ||||
|             title: item.title, | ||||
|             permission: defaultPermission, | ||||
|           }; | ||||
|         }, | ||||
|       ); | ||||
|     } | ||||
|     return defaultFieldsPermission; | ||||
|   }; | ||||
| 
 | ||||
|   // 获取表单的所有字段,作为下拉框选项
 | ||||
|   const formFieldOptions = parseFormCreateFields(unref(formFields)); | ||||
| 
 | ||||
|   return { | ||||
|     formType, | ||||
|     fieldsPermissionConfig, | ||||
|     formFieldOptions, | ||||
|     getNodeConfigFormFields, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @description 获取流程表单的字段 | ||||
|  */ | ||||
| export function useFormFields() { | ||||
|   const formFields = inject<Ref<string[]>>('formFields', ref([])); // 流程表单字段
 | ||||
|   return parseFormCreateFields(unref(formFields)); | ||||
| } | ||||
| 
 | ||||
| // TODO @芋艿:后续需要把各种类似 useFormFieldsPermission 的逻辑,抽成一个通用方法。
 | ||||
| /** | ||||
|  * @description 获取流程表单的字段和发起人字段 | ||||
|  */ | ||||
| export function useFormFieldsAndStartUser() { | ||||
|   const injectFormFields = inject<Ref<string[]>>('formFields', ref([])); // 流程表单字段
 | ||||
|   const formFields = parseFormCreateFields(unref(injectFormFields)); | ||||
|   // 添加发起人
 | ||||
|   formFields.unshift({ | ||||
|     field: ProcessVariableEnum.START_USER_ID, | ||||
|     title: '发起人', | ||||
|     required: true, | ||||
|   }); | ||||
|   return formFields; | ||||
| } | ||||
| 
 | ||||
| export type UserTaskFormType = { | ||||
|   approveMethod: ApproveMethodType; | ||||
|   approveRatio?: number; | ||||
|   assignEmptyHandlerType?: AssignEmptyHandlerType; | ||||
|   assignEmptyHandlerUserIds?: number[]; | ||||
|   assignStartUserHandlerType?: AssignStartUserHandlerType; | ||||
|   buttonsSetting: any[]; | ||||
|   candidateStrategy: CandidateStrategy; | ||||
|   deptIds?: number[]; // 部门
 | ||||
|   deptLevel?: number; // 部门层级
 | ||||
|   expression?: string; // 流程表达式
 | ||||
|   formDept?: string; // 表单内部门字段
 | ||||
|   formUser?: string; // 表单内用户字段
 | ||||
|   maxRemindCount?: number; | ||||
|   postIds?: number[]; // 岗位
 | ||||
|   reasonRequire: boolean; | ||||
|   rejectHandlerType?: RejectHandlerType; | ||||
|   returnNodeId?: string; | ||||
|   roleIds?: number[]; // 角色
 | ||||
|   signEnable: boolean; | ||||
|   taskAssignListener?: { | ||||
|     body: HttpRequestParam[]; | ||||
|     header: HttpRequestParam[]; | ||||
|   }; | ||||
|   taskAssignListenerEnable?: boolean; | ||||
|   taskAssignListenerPath?: string; | ||||
|   taskCompleteListener?: { | ||||
|     body: HttpRequestParam[]; | ||||
|     header: HttpRequestParam[]; | ||||
|   }; | ||||
|   taskCompleteListenerEnable?: boolean; | ||||
|   taskCompleteListenerPath?: string; | ||||
|   taskCreateListener?: { | ||||
|     body: HttpRequestParam[]; | ||||
|     header: HttpRequestParam[]; | ||||
|   }; | ||||
|   taskCreateListenerEnable?: boolean; | ||||
|   taskCreateListenerPath?: string; | ||||
|   timeDuration?: number; | ||||
|   timeoutHandlerEnable?: boolean; | ||||
|   timeoutHandlerType?: number; | ||||
|   userGroups?: number[]; // 用户组
 | ||||
|   userIds?: number[]; // 用户
 | ||||
| }; | ||||
| 
 | ||||
| export type CopyTaskFormType = { | ||||
|   candidateStrategy: CandidateStrategy; | ||||
|   deptIds?: number[]; // 部门
 | ||||
|   deptLevel?: number; // 部门层级
 | ||||
|   expression?: string; // 流程表达式
 | ||||
|   formDept?: string; // 表单内部门字段
 | ||||
|   formUser?: string; // 表单内用户字段
 | ||||
|   postIds?: number[]; // 岗位
 | ||||
|   roleIds?: number[]; // 角色
 | ||||
|   userGroups?: number[]; // 用户组
 | ||||
|   userIds?: number[]; // 用户
 | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * @description 节点表单数据。 用于审批节点、抄送节点 | ||||
|  */ | ||||
| export function useNodeForm(nodeType: NodeType) { | ||||
|   const roleOptions = inject<Ref<SystemRoleApi.Role[]>>('roleList', ref([])); // 角色列表
 | ||||
|   const postOptions = inject<Ref<SystemPostApi.Post[]>>('postList', ref([])); // 岗位列表
 | ||||
|   const userOptions = inject<Ref<SystemUserApi.User[]>>('userList', ref([])); // 用户列表
 | ||||
|   const deptOptions = inject<Ref<SystemDeptApi.Dept[]>>('deptList', ref([])); // 部门列表
 | ||||
|   const userGroupOptions = inject<Ref<BpmUserGroupApi.UserGroupVO[]>>( | ||||
|     'userGroupList', | ||||
|     ref([]), | ||||
|   ); // 用户组列表
 | ||||
|   const deptTreeOptions = inject<Ref<SystemDeptApi.Dept[]>>( | ||||
|     'deptTree', | ||||
|     ref([]), | ||||
|   ); // 部门树
 | ||||
|   const formFields = inject<Ref<string[]>>('formFields', ref([])); // 流程表单字段
 | ||||
|   const configForm = ref<any | CopyTaskFormType | UserTaskFormType>(); | ||||
| 
 | ||||
|   // eslint-disable-next-line unicorn/prefer-ternary
 | ||||
|   if ( | ||||
|     nodeType === NodeType.USER_TASK_NODE || | ||||
|     nodeType === NodeType.TRANSACTOR_NODE | ||||
|   ) { | ||||
|     configForm.value = { | ||||
|       candidateStrategy: CandidateStrategy.USER, | ||||
|       approveMethod: ApproveMethodType.SEQUENTIAL_APPROVE, | ||||
|       approveRatio: 100, | ||||
|       rejectHandlerType: RejectHandlerType.FINISH_PROCESS, | ||||
|       assignStartUserHandlerType: AssignStartUserHandlerType.START_USER_AUDIT, | ||||
|       returnNodeId: '', | ||||
|       timeoutHandlerEnable: false, | ||||
|       timeoutHandlerType: 1, | ||||
|       timeDuration: 6, // 默认 6小时
 | ||||
|       maxRemindCount: 1, // 默认 提醒 1次
 | ||||
|       buttonsSetting: [], | ||||
|     }; | ||||
|   } else { | ||||
|     configForm.value = { | ||||
|       candidateStrategy: CandidateStrategy.USER, | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   const getShowText = (): string => { | ||||
|     let showText = ''; | ||||
|     // 指定成员
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === CandidateStrategy.USER && | ||||
|       configForm.value?.userIds?.length > 0 | ||||
|     ) { | ||||
|       const candidateNames: string[] = []; | ||||
|       userOptions?.value.forEach((item: any) => { | ||||
|         if (configForm.value?.userIds?.includes(item.id)) { | ||||
|           candidateNames.push(item.nickname); | ||||
|         } | ||||
|       }); | ||||
|       showText = `指定成员:${candidateNames.join(',')}`; | ||||
|     } | ||||
|     // 指定角色
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === CandidateStrategy.ROLE && | ||||
|       configForm.value.roleIds?.length > 0 | ||||
|     ) { | ||||
|       const candidateNames: string[] = []; | ||||
|       roleOptions?.value.forEach((item: any) => { | ||||
|         if (configForm.value?.roleIds?.includes(item.id)) { | ||||
|           candidateNames.push(item.name); | ||||
|         } | ||||
|       }); | ||||
|       showText = `指定角色:${candidateNames.join(',')}`; | ||||
|     } | ||||
|     // 指定部门
 | ||||
|     if ( | ||||
|       (configForm.value?.candidateStrategy === CandidateStrategy.DEPT_MEMBER || | ||||
|         configForm.value?.candidateStrategy === CandidateStrategy.DEPT_LEADER || | ||||
|         configForm.value?.candidateStrategy === | ||||
|           CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) && | ||||
|       configForm.value?.deptIds?.length > 0 | ||||
|     ) { | ||||
|       const candidateNames: string[] = []; | ||||
|       deptOptions?.value.forEach((item) => { | ||||
|         if (configForm.value?.deptIds?.includes(item.id)) { | ||||
|           candidateNames.push(item.name); | ||||
|         } | ||||
|       }); | ||||
|       if ( | ||||
|         configForm.value.candidateStrategy === CandidateStrategy.DEPT_MEMBER | ||||
|       ) { | ||||
|         showText = `部门成员:${candidateNames.join(',')}`; | ||||
|       } else if ( | ||||
|         configForm.value.candidateStrategy === CandidateStrategy.DEPT_LEADER | ||||
|       ) { | ||||
|         showText = `部门的负责人:${candidateNames.join(',')}`; | ||||
|       } else { | ||||
|         showText = `多级部门的负责人:${candidateNames.join(',')}`; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // 指定岗位
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === CandidateStrategy.POST && | ||||
|       configForm.value.postIds?.length > 0 | ||||
|     ) { | ||||
|       const candidateNames: string[] = []; | ||||
|       postOptions?.value.forEach((item) => { | ||||
|         if (configForm.value?.postIds?.includes(item.id)) { | ||||
|           candidateNames.push(item.name); | ||||
|         } | ||||
|       }); | ||||
|       showText = `指定岗位: ${candidateNames.join(',')}`; | ||||
|     } | ||||
|     // 指定用户组
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === CandidateStrategy.USER_GROUP && | ||||
|       configForm.value?.userGroups?.length > 0 | ||||
|     ) { | ||||
|       const candidateNames: string[] = []; | ||||
|       userGroupOptions?.value.forEach((item) => { | ||||
|         if (configForm.value?.userGroups?.includes(item.id)) { | ||||
|           candidateNames.push(item.name); | ||||
|         } | ||||
|       }); | ||||
|       showText = `指定用户组: ${candidateNames.join(',')}`; | ||||
|     } | ||||
| 
 | ||||
|     // 表单内用户字段
 | ||||
|     if (configForm.value?.candidateStrategy === CandidateStrategy.FORM_USER) { | ||||
|       const formFieldOptions = parseFormCreateFields(unref(formFields)); | ||||
|       const item = formFieldOptions.find( | ||||
|         (item) => item.field === configForm.value?.formUser, | ||||
|       ); | ||||
|       showText = `表单用户:${item?.title}`; | ||||
|     } | ||||
| 
 | ||||
|     // 表单内部门负责人
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER | ||||
|     ) { | ||||
|       showText = `表单内部门负责人`; | ||||
|     } | ||||
| 
 | ||||
|     // 审批人自选
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === | ||||
|       CandidateStrategy.APPROVE_USER_SELECT | ||||
|     ) { | ||||
|       showText = `审批人自选`; | ||||
|     } | ||||
| 
 | ||||
|     // 发起人自选
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === | ||||
|       CandidateStrategy.START_USER_SELECT | ||||
|     ) { | ||||
|       showText = `发起人自选`; | ||||
|     } | ||||
|     // 发起人自己
 | ||||
|     if (configForm.value?.candidateStrategy === CandidateStrategy.START_USER) { | ||||
|       showText = `发起人自己`; | ||||
|     } | ||||
|     // 发起人的部门负责人
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === | ||||
|       CandidateStrategy.START_USER_DEPT_LEADER | ||||
|     ) { | ||||
|       showText = `发起人的部门负责人`; | ||||
|     } | ||||
|     // 发起人的部门负责人
 | ||||
|     if ( | ||||
|       configForm.value?.candidateStrategy === | ||||
|       CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER | ||||
|     ) { | ||||
|       showText = `发起人连续部门负责人`; | ||||
|     } | ||||
|     // 流程表达式
 | ||||
|     if (configForm.value?.candidateStrategy === CandidateStrategy.EXPRESSION) { | ||||
|       showText = `流程表达式:${configForm.value.expression}`; | ||||
|     } | ||||
|     return showText; | ||||
|   }; | ||||
| 
 | ||||
|   /** | ||||
|    *  处理候选人参数的赋值 | ||||
|    */ | ||||
|   const handleCandidateParam = () => { | ||||
|     let candidateParam: string | undefined; | ||||
|     if (!configForm.value) { | ||||
|       return candidateParam; | ||||
|     } | ||||
|     switch (configForm.value.candidateStrategy) { | ||||
|       case CandidateStrategy.DEPT_LEADER: | ||||
|       case CandidateStrategy.DEPT_MEMBER: { | ||||
|         candidateParam = configForm.value.deptIds?.join(','); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.EXPRESSION: { | ||||
|         candidateParam = configForm.value.expression; | ||||
|         break; | ||||
|       } | ||||
|       // 表单内部门的负责人
 | ||||
|       case CandidateStrategy.FORM_DEPT_LEADER: { | ||||
|         // 候选人参数格式: | 分隔 。左边为表单内部门字段。 右边为部门层级
 | ||||
|         const deptFieldOnForm = configForm.value.formDept; | ||||
|         candidateParam = deptFieldOnForm?.concat( | ||||
|           `|${configForm.value.deptLevel}`, | ||||
|         ); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.FORM_USER: { | ||||
|         candidateParam = configForm.value?.formUser; | ||||
|         break; | ||||
|       } | ||||
|       // 指定连续多级部门的负责人
 | ||||
|       case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { | ||||
|         // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
 | ||||
|         const deptIds = configForm.value.deptIds?.join(','); | ||||
|         candidateParam = deptIds?.concat(`|${configForm.value.deptLevel}`); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.POST: { | ||||
|         candidateParam = configForm.value.postIds?.join(','); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.ROLE: { | ||||
|         candidateParam = configForm.value.roleIds?.join(','); | ||||
|         break; | ||||
|       } | ||||
|       // 发起人部门负责人
 | ||||
|       case CandidateStrategy.START_USER_DEPT_LEADER: | ||||
|       case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: { | ||||
|         candidateParam = `${configForm.value.deptLevel}`; | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.USER: { | ||||
|         candidateParam = configForm.value.userIds?.join(','); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.USER_GROUP: { | ||||
|         candidateParam = configForm.value.userGroups?.join(','); | ||||
|         break; | ||||
|       } | ||||
|       default: { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|     return candidateParam; | ||||
|   }; | ||||
|   /** | ||||
|    *  解析候选人参数 | ||||
|    */ | ||||
|   const parseCandidateParam = ( | ||||
|     candidateStrategy: CandidateStrategy, | ||||
|     candidateParam: string | undefined, | ||||
|   ) => { | ||||
|     if (!configForm.value || !candidateParam) { | ||||
|       return; | ||||
|     } | ||||
|     switch (candidateStrategy) { | ||||
|       case CandidateStrategy.DEPT_LEADER: | ||||
|       case CandidateStrategy.DEPT_MEMBER: { | ||||
|         configForm.value.deptIds = candidateParam | ||||
|           .split(',') | ||||
|           .map((item) => +item); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.EXPRESSION: { | ||||
|         configForm.value.expression = candidateParam; | ||||
|         break; | ||||
|       } | ||||
|       // 表单内的部门负责人
 | ||||
|       case CandidateStrategy.FORM_DEPT_LEADER: { | ||||
|         // 候选人参数格式: | 分隔 。左边为表单内的部门字段。 右边为部门层级
 | ||||
|         const paramArray = candidateParam.split('|'); | ||||
|         if (paramArray.length > 1) { | ||||
|           configForm.value.formDept = paramArray[0]; | ||||
|           if (paramArray[1]) configForm.value.deptLevel = +paramArray[1]; | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.FORM_USER: { | ||||
|         configForm.value.formUser = candidateParam; | ||||
|         break; | ||||
|       } | ||||
|       // 指定连续多级部门的负责人
 | ||||
|       case CandidateStrategy.MULTI_LEVEL_DEPT_LEADER: { | ||||
|         // 候选人参数格式: | 分隔 。左边为部门(多个部门用 , 分隔)。 右边为部门层级
 | ||||
|         const paramArray = candidateParam.split('|') as string[]; | ||||
|         if (paramArray.length > 1) { | ||||
|           configForm.value.deptIds = paramArray[0] | ||||
|             ?.split(',') | ||||
|             .map((item) => +item); | ||||
|           if (paramArray[1]) configForm.value.deptLevel = +paramArray[1]; | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.POST: { | ||||
|         configForm.value.postIds = candidateParam | ||||
|           .split(',') | ||||
|           .map((item) => +item); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.ROLE: { | ||||
|         configForm.value.roleIds = candidateParam | ||||
|           .split(',') | ||||
|           .map((item) => +item); | ||||
|         break; | ||||
|       } | ||||
|       // 发起人部门负责人
 | ||||
|       case CandidateStrategy.START_USER_DEPT_LEADER: | ||||
|       case CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER: { | ||||
|         configForm.value.deptLevel = +candidateParam; | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.USER: { | ||||
|         configForm.value.userIds = candidateParam | ||||
|           .split(',') | ||||
|           .map((item) => +item); | ||||
|         break; | ||||
|       } | ||||
|       case CandidateStrategy.USER_GROUP: { | ||||
|         configForm.value.userGroups = candidateParam | ||||
|           .split(',') | ||||
|           .map((item) => +item); | ||||
|         break; | ||||
|       } | ||||
|       default: { | ||||
|         break; | ||||
|       } | ||||
|     } | ||||
|   }; | ||||
|   return { | ||||
|     configForm, | ||||
|     roleOptions, | ||||
|     postOptions, | ||||
|     userOptions, | ||||
|     userGroupOptions, | ||||
|     deptTreeOptions, | ||||
|     handleCandidateParam, | ||||
|     parseCandidateParam, | ||||
|     getShowText, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @description 抽屉配置 | ||||
|  */ | ||||
| export function useDrawer() { | ||||
|   // 抽屉配置是否可见
 | ||||
|   const settingVisible = ref(false); | ||||
|   // 关闭配置抽屉
 | ||||
|   const closeDrawer = () => { | ||||
|     settingVisible.value = false; | ||||
|   }; | ||||
|   // 打开配置抽屉
 | ||||
|   const openDrawer = () => { | ||||
|     settingVisible.value = true; | ||||
|   }; | ||||
|   return { | ||||
|     settingVisible, | ||||
|     closeDrawer, | ||||
|     openDrawer, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @description 节点名称配置 | ||||
|  */ | ||||
| export function useNodeName(nodeType: NodeType) { | ||||
|   // 节点名称
 | ||||
|   const nodeName = ref<string>(); | ||||
|   // 节点名称输入框
 | ||||
|   const showInput = ref(false); | ||||
|   // 点击节点名称编辑图标
 | ||||
|   const clickIcon = () => { | ||||
|     showInput.value = true; | ||||
|   }; | ||||
|   // 节点名称输入框失去焦点
 | ||||
|   const blurEvent = () => { | ||||
|     showInput.value = false; | ||||
|     nodeName.value = | ||||
|       nodeName.value || (NODE_DEFAULT_NAME.get(nodeType) as string); | ||||
|   }; | ||||
|   return { | ||||
|     nodeName, | ||||
|     showInput, | ||||
|     clickIcon, | ||||
|     blurEvent, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| export function useNodeName2(node: Ref<SimpleFlowNode>, nodeType: NodeType) { | ||||
|   // 显示节点名称输入框
 | ||||
|   const showInput = ref(false); | ||||
|   // 节点名称输入框失去焦点
 | ||||
|   const blurEvent = () => { | ||||
|     showInput.value = false; | ||||
|     node.value.name = | ||||
|       node.value.name || (NODE_DEFAULT_NAME.get(nodeType) as string); | ||||
|   }; | ||||
|   // 点击节点标题进行输入
 | ||||
|   const clickTitle = () => { | ||||
|     showInput.value = true; | ||||
|   }; | ||||
|   return { | ||||
|     showInput, | ||||
|     clickTitle, | ||||
|     blurEvent, | ||||
|   }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * @description 根据节点任务状态,获取节点任务状态样式 | ||||
|  */ | ||||
| export function useTaskStatusClass( | ||||
|   taskStatus: TaskStatusEnum | undefined, | ||||
| ): string { | ||||
|   if (!taskStatus) { | ||||
|     return ''; | ||||
|   } | ||||
|   if (taskStatus === TaskStatusEnum.APPROVE) { | ||||
|     return 'status-pass'; | ||||
|   } | ||||
|   if (taskStatus === TaskStatusEnum.RUNNING) { | ||||
|     return 'status-running'; | ||||
|   } | ||||
|   if (taskStatus === TaskStatusEnum.REJECT) { | ||||
|     return 'status-reject'; | ||||
|   } | ||||
|   if (taskStatus === TaskStatusEnum.CANCEL) { | ||||
|     return 'status-cancel'; | ||||
|   } | ||||
|   return ''; | ||||
| } | ||||
| 
 | ||||
| /** 条件组件文字展示 */ | ||||
| export function getConditionShowText( | ||||
|   conditionType: ConditionType | undefined, | ||||
|   conditionExpression: string | undefined, | ||||
|   conditionGroups: ConditionGroup | undefined, | ||||
|   fieldOptions: Array<Record<string, any>>, | ||||
| ) { | ||||
|   let showText: string | undefined; | ||||
|   if (conditionType === ConditionType.EXPRESSION && conditionExpression) { | ||||
|     showText = `表达式:${conditionExpression}`; | ||||
|   } | ||||
|   if (conditionType === ConditionType.RULE) { | ||||
|     // 条件组是否为与关系
 | ||||
|     const groupAnd = conditionGroups?.and; | ||||
|     let warningMessage: string | undefined; | ||||
|     const conditionGroup = conditionGroups?.conditions.map((item) => { | ||||
|       return `(${item.rules | ||||
|         .map((rule) => { | ||||
|           if (rule.leftSide && rule.rightSide) { | ||||
|             return `${getFormFieldTitle( | ||||
|               fieldOptions, | ||||
|               rule.leftSide, | ||||
|             )} ${getOpName(rule.opCode)} ${rule.rightSide}`;
 | ||||
|           } else { | ||||
|             // 有一条规则不完善。提示错误
 | ||||
|             warningMessage = '请完善条件规则'; | ||||
|             return ''; | ||||
|           } | ||||
|         }) | ||||
|         .join(item.and ? ' 且 ' : ' 或 ')} ) `;
 | ||||
|     }); | ||||
|     showText = warningMessage | ||||
|       ? '' | ||||
|       : conditionGroup?.join(groupAnd ? ' 且 ' : ' 或 '); | ||||
|   } | ||||
|   return showText; | ||||
| } | ||||
| 
 | ||||
| /** 获取表单字段名称*/ | ||||
| const getFormFieldTitle = ( | ||||
|   fieldOptions: Array<Record<string, any>>, | ||||
|   field: string, | ||||
| ) => { | ||||
|   const item = fieldOptions.find((item) => item.field === field); | ||||
|   return item?.title; | ||||
| }; | ||||
| 
 | ||||
| /** 获取操作符名称 */ | ||||
| const getOpName = (opCode: string): string | undefined => { | ||||
|   const opName = COMPARISON_OPERATORS.find( | ||||
|     (item: any) => item.value === opCode, | ||||
|   ); | ||||
|   return opName?.label; | ||||
| }; | ||||
|  | @ -0,0 +1,3 @@ | |||
| import './styles/simple-process-designer.scss'; | ||||
| 
 | ||||
| export { default as SimpleProcessDesigner } from './components/simple-process-designer.vue'; | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -0,0 +1,826 @@ | |||
| // TODO 这个样式要不要重新优化一下 | ||||
| // 配置节点头部 | ||||
| .config-header { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
| 
 | ||||
|   .node-name { | ||||
|     display: flex; | ||||
|     height: 24px; | ||||
|     line-height: 24px; | ||||
|     font-size: 16px; | ||||
|     cursor: pointer; | ||||
|     align-items: center; | ||||
|   } | ||||
| 
 | ||||
|   .divide-line { | ||||
|     width: 100%; | ||||
|     height: 1px; | ||||
|     margin-top: 16px; | ||||
|     background: #eee; | ||||
|   } | ||||
| 
 | ||||
|   .config-editable-input { | ||||
|     height: 24px; | ||||
|     max-width: 510px; | ||||
|     font-size: 16px; | ||||
|     line-height: 24px; | ||||
|     border: 1px solid #d9d9d9; | ||||
|     border-radius: 4px; | ||||
|     transition: all 0.3s; | ||||
| 
 | ||||
|     &:focus { | ||||
|       border-color: #40a9ff; | ||||
|       outline: 0; | ||||
|       box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 表单字段权限 | ||||
| .field-setting-pane { | ||||
|   display: flex; | ||||
|   flex-direction: column; | ||||
|   font-size: 14px; | ||||
| 
 | ||||
|   .field-setting-desc { | ||||
|     padding-right: 8px; | ||||
|     margin-bottom: 16px; | ||||
|     font-size: 16px; | ||||
|     font-weight: 700; | ||||
|   } | ||||
| 
 | ||||
|   .field-permit-title { | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     align-items: center; | ||||
|     height: 45px; | ||||
|     padding-left: 12px; | ||||
|     line-height: 45px; | ||||
|     background-color: #f8fafc0a; | ||||
|     border: 1px solid #1f38581a; | ||||
| 
 | ||||
|     .first-title { | ||||
|       text-align: left !important; | ||||
|     } | ||||
| 
 | ||||
|     .other-titles { | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|     } | ||||
| 
 | ||||
|     .setting-title-label { | ||||
|       display: inline-block; | ||||
|       width: 110px; | ||||
|       padding: 5px 0; | ||||
|       font-size: 13px; | ||||
|       font-weight: 700; | ||||
|       color: #000; | ||||
|       text-align: center; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .field-setting-item { | ||||
|     align-items: center; | ||||
|     display: flex; | ||||
|     justify-content: space-between; | ||||
|     height: 38px; | ||||
|     padding-left: 12px; | ||||
|     border: 1px solid #1f38581a; | ||||
|     border-top: 0; | ||||
| 
 | ||||
|     .field-setting-item-label { | ||||
|       display: inline-block; | ||||
|       width: 110px; | ||||
|       min-height: 16px; | ||||
|       overflow: hidden; | ||||
|       text-overflow: ellipsis; | ||||
|       white-space: nowrap; | ||||
|       cursor: text; | ||||
|     } | ||||
| 
 | ||||
|     .field-setting-item-group { | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
| 
 | ||||
|       .item-radio-wrap { | ||||
|         display: inline-block; | ||||
|         width: 110px; | ||||
|         text-align: center; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // 节点连线气泡卡片样式 | ||||
| .handler-item-wrapper { | ||||
|   width: 320px; | ||||
|   display: flex; | ||||
|   flex-wrap: wrap; | ||||
|   cursor: pointer; | ||||
| 
 | ||||
|   .handler-item { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     align-items: center; | ||||
|     margin-top: 12px; | ||||
|   } | ||||
| 
 | ||||
|   .handler-item-icon { | ||||
|     width: 50px; | ||||
|     height: 50px; | ||||
|     background: #fff; | ||||
|     border: 1px solid #e2e2e2; | ||||
|     border-radius: 50%; | ||||
|     user-select: none; | ||||
|     text-align: center; | ||||
| 
 | ||||
|     &:hover { | ||||
|       background: #e2e2e2; | ||||
|       box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.1); | ||||
|     } | ||||
| 
 | ||||
|     .icon-size { | ||||
|       font-size: 25px; | ||||
|       line-height: 50px; | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   .approve { | ||||
|     color: #ff943e; | ||||
|   } | ||||
| 
 | ||||
|   .copy { | ||||
|     color: #3296fa; | ||||
|   } | ||||
| 
 | ||||
|   .condition { | ||||
|     color: #67c23a; | ||||
|   } | ||||
| 
 | ||||
|   .parallel { | ||||
|     color: #626aef; | ||||
|   } | ||||
| 
 | ||||
|   .inclusive { | ||||
|     color: #345da2; | ||||
|   } | ||||
| 
 | ||||
|   .delay { | ||||
|     color: #e47470; | ||||
|   } | ||||
| 
 | ||||
|   .trigger { | ||||
|     color: #3373d2; | ||||
|   } | ||||
| 
 | ||||
|   .router { | ||||
|     color: #ca3a31 | ||||
|   } | ||||
| 
 | ||||
|   .transactor { | ||||
|     color: #330099; | ||||
|   } | ||||
| 
 | ||||
|   .child-process { | ||||
|     color: #996633; | ||||
|   } | ||||
| 
 | ||||
|   .async-child-process { | ||||
|     color: #006666; | ||||
|   } | ||||
| 
 | ||||
|   .handler-item-text { | ||||
|     margin-top: 4px; | ||||
|     width: 80px; | ||||
|     text-align: center; | ||||
|     font-size: 13px; | ||||
|   } | ||||
| } | ||||
| // Simple 流程模型样式 | ||||
| .simple-process-model-container { | ||||
|   height: 100%; | ||||
|   background-color: #fafafa; | ||||
|   overflow-x: auto; | ||||
|   width: 100%; | ||||
|   background: url('./svg/simple-process-bg.svg') 0 0 repeat; | ||||
| 
 | ||||
|   .simple-process-model { | ||||
|     display: flex; | ||||
|     flex-direction: column; | ||||
|     justify-content: center; | ||||
|     align-items: center; | ||||
|     transform-origin: 50% 0 0; | ||||
|     min-width: fit-content; | ||||
|     transform: scale(1); | ||||
|     transition: transform 0.3s cubic-bezier(0.645, 0.045, 0.355, 1); | ||||
|     // 节点容器 定义节点宽度 | ||||
|     .node-container { | ||||
|       width: 200px; | ||||
|     } | ||||
|     // 节点 | ||||
|     .node-box { | ||||
|       position: relative; | ||||
|       display: flex; | ||||
|       min-height: 70px; | ||||
|       padding: 5px 10px 8px; | ||||
|       cursor: pointer; | ||||
|       background-color: #fff; | ||||
|       flex-direction: column; | ||||
|       border: 2px solid transparent; | ||||
|       border-radius: 8px; | ||||
|       box-shadow: 0 1px 4px 0 rgb(10 30 65 / 16%); | ||||
|       transition: all 0.1s cubic-bezier(0.645, 0.045, 0.355, 1); | ||||
| 
 | ||||
|       &.status-pass { | ||||
|         background-color: #a9da90; | ||||
|         border-color: #67c23a; | ||||
|       } | ||||
| 
 | ||||
|       &.status-pass:hover { | ||||
|         border-color: #67c23a; | ||||
|       } | ||||
| 
 | ||||
|       &.status-running { | ||||
|         background-color: #e7f0fe; | ||||
|         border-color: #5a9cf8; | ||||
|       } | ||||
| 
 | ||||
|       &.status-running:hover { | ||||
|         border-color: #5a9cf8; | ||||
|       } | ||||
| 
 | ||||
|       &.status-reject { | ||||
|         background-color: #f6e5e5; | ||||
|         border-color: #e47470; | ||||
|       } | ||||
| 
 | ||||
|       &.status-reject:hover { | ||||
|         border-color: #e47470; | ||||
|       } | ||||
| 
 | ||||
|       &:hover { | ||||
|         border-color: #0089ff; | ||||
| 
 | ||||
|         .node-toolbar { | ||||
|           opacity: 1; | ||||
|         } | ||||
| 
 | ||||
|         .branch-node-move { | ||||
|           display: flex; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // 普通节点标题 | ||||
|       .node-title-container { | ||||
|         display: flex; | ||||
|         padding: 4px; | ||||
|         cursor: pointer; | ||||
|         border-radius: 4px 4px 0 0; | ||||
|         align-items: center; | ||||
| 
 | ||||
|         .node-title-icon { | ||||
|           display: flex; | ||||
|           align-items: center; | ||||
| 
 | ||||
|           &.user-task { | ||||
|             color: #ff943e; | ||||
|           } | ||||
| 
 | ||||
|           &.copy-task { | ||||
|             color: #3296fa; | ||||
|           } | ||||
| 
 | ||||
|           &.start-user { | ||||
|             color: #676565; | ||||
|           } | ||||
| 
 | ||||
|           &.delay-node { | ||||
|             color: #e47470; | ||||
|           } | ||||
| 
 | ||||
|           &.trigger-node { | ||||
|             color: #3373d2; | ||||
|           } | ||||
| 
 | ||||
|           &.router-node { | ||||
|             color: #ca3a31 | ||||
|           } | ||||
| 
 | ||||
|           &.transactor-task { | ||||
|             color: #330099; | ||||
|           } | ||||
| 
 | ||||
|           &.child-process { | ||||
|             color: #996633; | ||||
|           } | ||||
| 
 | ||||
|           &.async-child-process { | ||||
|             color: #006666; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         .node-title { | ||||
|           margin-left: 4px; | ||||
|           overflow: hidden; | ||||
|           font-size: 14px; | ||||
|           font-weight: 600; | ||||
|           line-height: 18px; | ||||
|           color: #1f1f1f; | ||||
|           text-overflow: ellipsis; | ||||
|           white-space: nowrap; | ||||
| 
 | ||||
|           &:hover { | ||||
|             border-bottom: 1px dashed #f60; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // 条件节点标题 | ||||
|       .branch-node-title-container { | ||||
|         display: flex; | ||||
|         padding: 4px 0; | ||||
|         cursor: pointer; | ||||
|         border-radius: 4px 4px 0 0; | ||||
|         align-items: center; | ||||
|         justify-content: space-between; | ||||
| 
 | ||||
|         .input-max-width { | ||||
|           max-width: 115px !important; | ||||
|         } | ||||
| 
 | ||||
|         .branch-title { | ||||
|           overflow: hidden; | ||||
|           font-size: 13px; | ||||
|           font-weight: 600; | ||||
|           color: #f60; | ||||
|           text-overflow: ellipsis; | ||||
|           white-space: nowrap; | ||||
| 
 | ||||
|           &:hover { | ||||
|             border-bottom: 1px dashed #000; | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         .branch-priority { | ||||
|           min-width: 50px; | ||||
|           font-size: 12px; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .node-content { | ||||
|         display: flex; | ||||
|         min-height: 32px; | ||||
|         padding: 4px 8px; | ||||
|         margin-top: 4px; | ||||
|         line-height: 32px; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         color: #111f2c; | ||||
|         background: rgb(0 0 0 / 3%); | ||||
|         border-radius: 4px; | ||||
| 
 | ||||
|         .node-text { | ||||
|           display: -webkit-box; | ||||
|           overflow: hidden; | ||||
|           font-size: 14px; | ||||
|           line-height: 24px; | ||||
|           text-overflow: ellipsis; | ||||
|           word-break: break-all; | ||||
|           -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ | ||||
|           -webkit-box-orient: vertical; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       //条件节点内容 | ||||
|       .branch-node-content { | ||||
|         display: flex; | ||||
|         min-height: 32px; | ||||
|         padding: 4px 0; | ||||
|         margin-top: 4px; | ||||
|         line-height: 32px; | ||||
|         align-items: center; | ||||
|         color: #111f2c; | ||||
|         border-radius: 4px; | ||||
| 
 | ||||
|         .branch-node-text { | ||||
|           overflow: hidden; | ||||
|           font-size: 12px; | ||||
|           line-height: 24px; | ||||
|           text-overflow: ellipsis; | ||||
|           word-break: break-all; | ||||
|           -webkit-line-clamp: 2; /* 这将限制文本显示为两行 */ | ||||
|           -webkit-box-orient: vertical; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // 节点操作 :删除 | ||||
|       .node-toolbar { | ||||
|         position: absolute; | ||||
|         top: -20px; | ||||
|         right: 0; | ||||
|         display: flex; | ||||
|         opacity: 0; | ||||
| 
 | ||||
|         .toolbar-icon { | ||||
|           text-align: center; | ||||
|           vertical-align: middle; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       // 条件节点左右移动 | ||||
|       .branch-node-move { | ||||
|         position: absolute; | ||||
|         display: none; | ||||
|         width: 10px; | ||||
|         height: 100%; | ||||
|         cursor: pointer; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|       } | ||||
| 
 | ||||
|       .move-node-left { | ||||
|         top: 0; | ||||
|         left: -2px; | ||||
|         background: rgb(126 134 142 / 8%); | ||||
|         border-bottom-left-radius: 8px; | ||||
|         border-top-left-radius: 8px; | ||||
|       } | ||||
| 
 | ||||
|       .move-node-right { | ||||
|         top: 0; | ||||
|         right: -2px; | ||||
|         background: rgb(126 134 142 / 8%); | ||||
|         border-top-right-radius: 6px; | ||||
|         border-bottom-right-radius: 6px; | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .node-config-error { | ||||
|       border-color: #ff5219 !important; | ||||
|     } | ||||
|     // 普通节点包装 | ||||
|     .node-wrapper { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
|     } | ||||
|     // 节点连线处理 | ||||
|     .node-handler-wrapper { | ||||
|       position: relative; | ||||
|       display: flex; | ||||
|       height: 70px; | ||||
|       align-items: center; | ||||
|       user-select: none; | ||||
|       justify-content: center; | ||||
|       flex-direction: column; | ||||
| 
 | ||||
|       &::before { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         z-index: 0; | ||||
|         width: 2px; | ||||
|         height: 100%; | ||||
|         margin: auto; | ||||
|         background-color: #dedede; | ||||
|         content: ''; | ||||
|       } | ||||
| 
 | ||||
|       .node-handler { | ||||
|         .add-icon { | ||||
|           position: relative; | ||||
|           top: -5px; | ||||
|           display: flex; | ||||
|           width: 25px; | ||||
|           height: 25px; | ||||
|           color: #fff; | ||||
|           cursor: pointer; | ||||
|           background-color: #0089ff; | ||||
|           border-radius: 50%; | ||||
|           align-items: center; | ||||
|           justify-content: center; | ||||
| 
 | ||||
|           &:hover { | ||||
|             transform: scale(1.1); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       .node-handler-arrow { | ||||
|         position: absolute; | ||||
|         bottom: 0; | ||||
|         left: 50%; | ||||
|         display: flex; | ||||
|         transform: translateX(-50%); | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // 条件节点包装 | ||||
|     .branch-node-wrapper { | ||||
|       position: relative; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
|       margin-top: 16px; | ||||
| 
 | ||||
|       .branch-node-container { | ||||
|         position: relative; | ||||
|         display: flex; | ||||
|         min-width: fit-content; | ||||
| 
 | ||||
|         &::before { | ||||
|           position: absolute; | ||||
|           left: 50%; | ||||
|           width: 4px; | ||||
|           height: 100%; | ||||
|           background-color: #fafafa; | ||||
|           content: ''; | ||||
|           transform: translate(-50%); | ||||
|         } | ||||
| 
 | ||||
|         .branch-node-add { | ||||
|           position: absolute; | ||||
|           top: -18px; | ||||
|           left: 50%; | ||||
|           z-index: 1; | ||||
|           height: 36px; | ||||
|           padding: 0 10px; | ||||
|           font-size: 12px; | ||||
|           line-height: 36px; | ||||
|           border: 2px solid #dedede; | ||||
|           border-radius: 18px; | ||||
|           transform: translateX(-50%); | ||||
|           transform-origin: center center; | ||||
|         } | ||||
| 
 | ||||
|         .branch-node-readonly { | ||||
|           position: absolute; | ||||
|           top: -18px; | ||||
|           left: 50%; | ||||
|           z-index: 1; | ||||
|           display: flex; | ||||
|           width: 36px; | ||||
|           height: 36px; | ||||
|           background-color: #fff; | ||||
|           border: 2px solid #dedede; | ||||
|           border-radius: 50%; | ||||
|           transform: translateX(-50%); | ||||
|           align-items: center; | ||||
|           justify-content: center; | ||||
|           transform-origin: center center; | ||||
| 
 | ||||
|           &.status-pass { | ||||
|             background-color: #e9f4e2; | ||||
|             border-color: #6bb63c; | ||||
|           } | ||||
| 
 | ||||
|           &.status-pass:hover { | ||||
|             border-color: #6bb63c; | ||||
|           } | ||||
| 
 | ||||
|           .icon-size { | ||||
|             font-size: 22px; | ||||
|             &.condition { | ||||
|               color: #67c23a; | ||||
|             } | ||||
|             &.parallel { | ||||
|               color: #626aef; | ||||
|             } | ||||
|             &.inclusive { | ||||
|               color: #345da2; | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         .branch-node-item { | ||||
|           position: relative; | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|           align-items: center; | ||||
|           min-width: 280px; | ||||
|           padding: 40px 40px 0; | ||||
|           background: transparent; | ||||
|           border-top: 2px solid #dedede; | ||||
|           border-bottom: 2px solid #dedede; | ||||
|           flex-shrink: 0; | ||||
| 
 | ||||
|           &::before { | ||||
|             position: absolute; | ||||
|             width: 2px; | ||||
|             height: 100%; | ||||
|             margin: auto; | ||||
|             inset: 0; | ||||
|             background-color: #dedede; | ||||
|             content: ''; | ||||
|           } | ||||
|         } | ||||
|         // 覆盖条件节点第一个节点左上角的线 | ||||
|         .branch-line-first-top { | ||||
|           position: absolute; | ||||
|           top: -5px; | ||||
|           left: -1px; | ||||
|           width: 50%; | ||||
|           height: 7px; | ||||
|           background-color: #fafafa; | ||||
|           content: ''; | ||||
|         } | ||||
|         // 覆盖条件节点第一个节点左下角的线 | ||||
|         .branch-line-first-bottom { | ||||
|           position: absolute; | ||||
|           bottom: -5px; | ||||
|           left: -1px; | ||||
|           width: 50%; | ||||
|           height: 7px; | ||||
|           background-color: #fafafa; | ||||
|           content: ''; | ||||
|         } | ||||
|         // 覆盖条件节点最后一个节点右上角的线 | ||||
|         .branch-line-last-top { | ||||
|           position: absolute; | ||||
|           top: -5px; | ||||
|           right: -1px; | ||||
|           width: 50%; | ||||
|           height: 7px; | ||||
|           background-color: #fafafa; | ||||
|           content: ''; | ||||
|         } | ||||
|         // 覆盖条件节点最后一个节点右下角的线 | ||||
|         .branch-line-last-bottom { | ||||
|           position: absolute; | ||||
|           right: -1px; | ||||
|           bottom: -5px; | ||||
|           width: 50%; | ||||
|           height: 7px; | ||||
|           background-color: #fafafa; | ||||
|           content: ''; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     .node-fixed-name { | ||||
|       display: inline-block; | ||||
|       width: auto; | ||||
|       padding: 0 4px; | ||||
|       overflow: hidden; | ||||
|       text-align: center; | ||||
|       text-overflow: ellipsis; | ||||
|       white-space: nowrap; | ||||
|     } | ||||
|     // 开始节点包装 | ||||
|     .start-node-wrapper { | ||||
|       position: relative; | ||||
|       margin-top: 16px; | ||||
| 
 | ||||
|       .start-node-container { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
| 
 | ||||
|         .start-node-box { | ||||
|           display: flex; | ||||
|           justify-content: center; | ||||
|           align-items: center; | ||||
|           width: 90px; | ||||
|           height: 36px; | ||||
|           padding: 3px 4px; | ||||
|           color: #212121; | ||||
|           cursor: pointer; | ||||
|           background: #fafafa; | ||||
|           border-radius: 30px; | ||||
|           box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); | ||||
|           box-sizing: border-box; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // 结束节点包装 | ||||
|     .end-node-wrapper { | ||||
|       margin-bottom: 16px; | ||||
| 
 | ||||
|       .end-node-box { | ||||
|         display: flex; | ||||
|         width: 80px; | ||||
|         height: 36px; | ||||
|         color: #212121; | ||||
|         border: 2px solid #fafafa; | ||||
|         border-radius: 30px; | ||||
|         box-shadow: 0 1px 5px 0 rgb(10 30 65 / 8%); | ||||
|         box-sizing: border-box; | ||||
|         justify-content: center; | ||||
|         align-items: center; | ||||
| 
 | ||||
|         &.status-pass { | ||||
|           background-color: #a9da90; | ||||
|           border-color: #6bb63c; | ||||
|         } | ||||
| 
 | ||||
|         &.status-pass:hover { | ||||
|           border-color: #6bb63c; | ||||
|         } | ||||
| 
 | ||||
|         &.status-reject { | ||||
|           background-color: #f6e5e5; | ||||
|           border-color: #e47470; | ||||
|         } | ||||
| 
 | ||||
|         &.status-reject:hover { | ||||
|           border-color: #e47470; | ||||
|         } | ||||
| 
 | ||||
|         &.status-cancel { | ||||
|           background-color: #eaeaeb; | ||||
|           border-color: #919398; | ||||
|         } | ||||
| 
 | ||||
|         &.status-cancel:hover { | ||||
|           border-color: #919398; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
| 
 | ||||
|     // 可编辑的 title 输入框 | ||||
|     .editable-title-input { | ||||
|       height: 20px; | ||||
|       max-width: 145px; | ||||
|       margin-left: 4px; | ||||
|       font-size: 12px; | ||||
|       line-height: 20px; | ||||
|       border: 1px solid #d9d9d9; | ||||
|       border-radius: 4px; | ||||
|       transition: all 0.3s; | ||||
| 
 | ||||
|       &:focus { | ||||
|         border-color: #40a9ff; | ||||
|         outline: 0; | ||||
|         box-shadow: 0 0 0 2px rgb(24 144 255 / 20%); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
| 
 | ||||
| // iconfont 样式 | ||||
| @font-face { | ||||
|   font-family: "iconfont"; /* Project id 4495938 */ | ||||
|   src: url('iconfont.woff2?t=1737639517142') format('woff2'), | ||||
|        url('iconfont.woff?t=1737639517142') format('woff'), | ||||
|        url('iconfont.ttf?t=1737639517142') format('truetype'); | ||||
| } | ||||
| 
 | ||||
| .iconfont { | ||||
|   font-family: "iconfont" !important; | ||||
|   font-size: 16px; | ||||
|   font-style: normal; | ||||
|   -webkit-font-smoothing: antialiased; | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
| 
 | ||||
| .icon-trigger:before { | ||||
|   content: "\e6d3"; | ||||
| } | ||||
| 
 | ||||
| .icon-router:before { | ||||
|   content: "\e6b2"; | ||||
| } | ||||
| 
 | ||||
| .icon-delay:before { | ||||
|   content: "\e600"; | ||||
| } | ||||
| 
 | ||||
| .icon-start-user:before { | ||||
|   content: "\e679"; | ||||
| } | ||||
| 
 | ||||
| .icon-inclusive:before { | ||||
|   content: "\e602"; | ||||
| } | ||||
| 
 | ||||
| .icon-copy:before { | ||||
|   content: "\e7eb"; | ||||
| } | ||||
| 
 | ||||
| .icon-transactor:before { | ||||
|   content: "\e61c"; | ||||
| } | ||||
| 
 | ||||
| .icon-exclusive:before { | ||||
|   content: "\e717"; | ||||
| } | ||||
| 
 | ||||
| .icon-approve:before { | ||||
|   content: "\e715"; | ||||
| } | ||||
| 
 | ||||
| .icon-parallel:before { | ||||
|   content: "\e688"; | ||||
| } | ||||
| 
 | ||||
| .icon-async-child-process:before { | ||||
|   content: "\e6f2"; | ||||
| } | ||||
| 
 | ||||
| .icon-child-process:before { | ||||
|   content: "\e6c1"; | ||||
| } | ||||
|  | @ -0,0 +1 @@ | |||
| <svg width="22" height="22" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path fill="#FAFAFA" d="M0 0h22v22H0z"/><circle fill="#919BAE" cx="1" cy="1" r="1"/></g></svg> | ||||
| After Width: | Height: | Size: 192 B | 
|  | @ -29,6 +29,7 @@ import { getSimpleUserList } from '#/api/system/user'; | |||
| 
 | ||||
| import BasicInfo from './modules/basic-info.vue'; | ||||
| import FormDesign from './modules/form-design.vue'; | ||||
| import ProcessDesign from './modules/process-design.vue'; | ||||
| 
 | ||||
| defineOptions({ name: 'BpmModelCreate' }); | ||||
| 
 | ||||
|  | @ -68,6 +69,8 @@ const userStore = useUserStore(); | |||
| const basicInfoRef = ref<InstanceType<typeof BasicInfo>>(); | ||||
| // 表单设计组件引用 | ||||
| const formDesignRef = ref<InstanceType<typeof FormDesign>>(); | ||||
| // 流程设计组件引用 | ||||
| const processDesignRef = ref<InstanceType<typeof ProcessDesign>>(); | ||||
| 
 | ||||
| /** 步骤校验函数 */ | ||||
| const validateBasic = async () => { | ||||
|  | @ -81,7 +84,7 @@ const validateForm = async () => { | |||
| 
 | ||||
| /** 流程设计校验 */ | ||||
| const validateProcess = async () => { | ||||
|   // TODO | ||||
|   await processDesignRef.value?.validate(); | ||||
| }; | ||||
| 
 | ||||
| const currentStep = ref(-1); // 步骤控制。-1 用于,一开始全部不展示等当前页面数据初始化完成 | ||||
|  | @ -101,7 +104,7 @@ const formData: any = ref({ | |||
|   category: undefined, | ||||
|   icon: undefined, | ||||
|   description: '', | ||||
|   type: BpmModelType.BPMN, | ||||
|   type: BpmModelType.SIMPLE, | ||||
|   formType: BpmModelFormType.NORMAL, | ||||
|   formId: '', | ||||
|   formCustomCreatePath: '', | ||||
|  | @ -189,7 +192,7 @@ const initData = async () => { | |||
|   } else { | ||||
|     // 情况三:新增场景 | ||||
|     formData.value.startUserType = 0; // 全体 | ||||
|     formData.value.managerUserIds.push(userStore.userInfo?.userId); | ||||
|     formData.value.managerUserIds.push(userStore.userInfo?.id); | ||||
|   } | ||||
| 
 | ||||
|   // 获取表单列表 | ||||
|  | @ -351,6 +354,7 @@ const handleDeploy = async () => { | |||
| /** 步骤切换处理 */ | ||||
| const handleStepClick = async (index: number) => { | ||||
|   try { | ||||
|     console.warn('handleStepClick', index); | ||||
|     if (index !== 0) { | ||||
|       await validateBasic(); | ||||
|     } | ||||
|  | @ -400,7 +404,7 @@ onBeforeUnmount(() => { | |||
|   // 清理所有的引用 | ||||
|   basicInfoRef.value = undefined; | ||||
|   formDesignRef.value = undefined; | ||||
|   // processDesignRef.value = null; | ||||
|   processDesignRef.value = undefined; | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
|  | @ -485,7 +489,7 @@ onBeforeUnmount(() => { | |||
|             /> | ||||
|           </div> | ||||
|           <!-- 第二步:表单设计  --> | ||||
|           <div v-show="currentStep === 1" class="mx-auto w-4/6"> | ||||
|           <div v-if="currentStep === 1" class="mx-auto w-4/6"> | ||||
|             <FormDesign | ||||
|               v-model="formData" | ||||
|               :form-list="formList" | ||||
|  | @ -493,10 +497,15 @@ onBeforeUnmount(() => { | |||
|             /> | ||||
|           </div> | ||||
| 
 | ||||
|           <!-- 第三步:流程设计 TODO --> | ||||
|           <!-- 第三步:流程设计 --> | ||||
|           <ProcessDesign | ||||
|             v-if="currentStep === 2" | ||||
|             v-model="formData" | ||||
|             ref="processDesignRef" | ||||
|           /> | ||||
| 
 | ||||
|           <!-- 第四步:更多设置 TODO --> | ||||
|           <div v-show="currentStep === 3" class="mx-auto w-4/6"></div> | ||||
|           <div v-if="currentStep === 3" class="mx-auto w-4/6"></div> | ||||
|         </div> | ||||
|       </Card> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -65,7 +65,6 @@ const rules: Record<string, Rule[]> = { | |||
|   category: [{ required: true, message: '流程分类不能为空', trigger: 'blur' }], | ||||
|   type: [{ required: true, message: '流程类型不能为空', trigger: 'blur' }], | ||||
|   visible: [{ required: true, message: '是否可见不能为空', trigger: 'blur' }], | ||||
|   // TODO 这个的校验好像没有起作用 | ||||
|   managerUserIds: [ | ||||
|     { required: true, message: '流程管理员不能为空', trigger: 'blur' }, | ||||
|   ], | ||||
|  | @ -282,10 +281,12 @@ defineExpose({ validate }); | |||
|     </Form.Item> | ||||
|     <Form.Item label="流程类型" name="type" class="mb-5"> | ||||
|       <Radio.Group v-model:value="modelData.type"> | ||||
|         <!-- TODO BPMN 流程类型需要整合,暂时禁用 --> | ||||
|         <Radio | ||||
|           v-for="dict in getIntDictOptions(DICT_TYPE.BPM_MODEL_TYPE)" | ||||
|           :key="dict.value" | ||||
|           :value="dict.value" | ||||
|           :disabled="dict.value === 10" | ||||
|         > | ||||
|           {{ dict.label }} | ||||
|         </Radio> | ||||
|  |  | |||
|  | @ -0,0 +1,67 @@ | |||
| <script lang="ts" setup> | ||||
| import type { Ref } from 'vue'; | ||||
| 
 | ||||
| import { computed, inject, nextTick } from 'vue'; | ||||
| 
 | ||||
| import { BpmModelType } from '#/utils'; | ||||
| 
 | ||||
| // TODO BPM 流程模型设计器 BpmModelEditor 待整合 | ||||
| import SimpleModelDesign from './simple-model-design.vue'; | ||||
| 
 | ||||
| // 创建本地数据副本 | ||||
| const modelData = defineModel<any>(); | ||||
| 
 | ||||
| const processData = inject('processData') as Ref; | ||||
| 
 | ||||
| /** 表单校验 */ | ||||
| const validate = async () => { | ||||
|   // 获取最新的流程数据 | ||||
|   if (!processData.value) { | ||||
|     throw new Error('请设计流程'); | ||||
|   } | ||||
|   return true; | ||||
| }; | ||||
| /** 处理设计器保存成功 */ | ||||
| const handleDesignSuccess = async (data?: any) => { | ||||
|   if (data) { | ||||
|     // 创建新的对象以触发响应式更新 | ||||
|     const newModelData = { | ||||
|       ...modelData.value, | ||||
|       bpmnXml: modelData.value.type === BpmModelType.BPMN ? data : null, | ||||
|       simpleModel: modelData.value.type === BpmModelType.BPMN ? null : data, | ||||
|     }; | ||||
|     // 使用emit更新父组件的数据 | ||||
|     await nextTick(); | ||||
|     // 更新表单的模型数据部分 | ||||
|     modelData.value = newModelData; | ||||
|   } | ||||
| }; | ||||
| 
 | ||||
| /** 是否显示设计器 */ | ||||
| const showDesigner = computed(() => { | ||||
|   return Boolean(modelData.value?.key && modelData.value?.name); | ||||
| }); | ||||
| defineExpose({ | ||||
|   validate, | ||||
| }); | ||||
| </script> | ||||
| <template> | ||||
|   <div class="h-full"> | ||||
|     <!-- BPMN设计器 --> | ||||
|     <template v-if="modelData.type === BpmModelType.BPMN"> | ||||
|       <!-- TODO BPMN 流程设计器 --> | ||||
|     </template> | ||||
|     <!-- Simple设计器 --> | ||||
|     <template v-else> | ||||
|       <SimpleModelDesign | ||||
|         v-if="showDesigner" | ||||
|         :model-id="modelData.id" | ||||
|         :model-key="modelData.key" | ||||
|         :model-name="modelData.name" | ||||
|         :start-user-ids="modelData.startUserIds" | ||||
|         :start-dept-ids="modelData.startDeptIds" | ||||
|         @success="handleDesignSuccess" | ||||
|       /> | ||||
|     </template> | ||||
|   </div> | ||||
| </template> | ||||
|  | @ -0,0 +1,40 @@ | |||
| <script setup lang="ts"> | ||||
| import { ref } from 'vue'; | ||||
| 
 | ||||
| import ContentWrap from '#/components/content-wrap/content-wrap.vue'; | ||||
| import { SimpleProcessDesigner } from '#/components/simple-process-design'; | ||||
| 
 | ||||
| defineOptions({ name: 'SimpleModelDesign' }); | ||||
| 
 | ||||
| defineProps<{ | ||||
|   modelId?: string; | ||||
|   modelKey?: string; | ||||
|   modelName?: string; | ||||
|   startDeptIds?: number[]; | ||||
|   startUserIds?: number[]; | ||||
| }>(); | ||||
| 
 | ||||
| const emit = defineEmits(['success']); | ||||
| const designerRef = ref(); | ||||
| 
 | ||||
| // 修改成功回调 | ||||
| const handleSuccess = (data?: any) => { | ||||
|   if (data) { | ||||
|     emit('success', data); | ||||
|   } | ||||
| }; | ||||
| </script> | ||||
| <template> | ||||
|   <ContentWrap :body-style="{ padding: '20px 16px' }"> | ||||
|     <SimpleProcessDesigner | ||||
|       :model-id="modelId" | ||||
|       :model-key="modelKey" | ||||
|       :model-name="modelName" | ||||
|       @success="handleSuccess" | ||||
|       :start-user-ids="startUserIds" | ||||
|       :start-dept-ids="startDeptIds" | ||||
|       ref="designerRef" | ||||
|     /> | ||||
|   </ContentWrap> | ||||
| </template> | ||||
| <style lang="scss" scoped></style> | ||||
|  | @ -54,35 +54,39 @@ const columns = [ | |||
|     dataIndex: 'name', | ||||
|     key: 'name', | ||||
|     align: 'left' as const, | ||||
|     minWidth: 250, | ||||
|     ellipsis: true, | ||||
|     width: 250, | ||||
|   }, | ||||
|   { | ||||
|     title: '可见范围', | ||||
|     dataIndex: 'startUserIds', | ||||
|     key: 'startUserIds', | ||||
|     align: 'center' as const, | ||||
|     minWidth: 150, | ||||
|     ellipsis: true, | ||||
|     width: 150, | ||||
|   }, | ||||
|   { | ||||
|     title: '流程类型', | ||||
|     dataIndex: 'type', | ||||
|     key: 'type', | ||||
|     align: 'center' as const, | ||||
|     minWidth: 120, | ||||
|     ellipsis: true, | ||||
|     width: 120, | ||||
|   }, | ||||
|   { | ||||
|     title: '表单信息', | ||||
|     dataIndex: 'formType', | ||||
|     key: 'formType', | ||||
|     align: 'center' as const, | ||||
|     minWidth: 150, | ||||
|     ellipsis: true, | ||||
|     width: 150, | ||||
|   }, | ||||
|   { | ||||
|     title: '最后发布', | ||||
|     dataIndex: 'deploymentTime', | ||||
|     key: 'deploymentTime', | ||||
|     align: 'center' as const, | ||||
|     minWidth: 250, | ||||
|     width: 250, | ||||
|   }, | ||||
|   { | ||||
|     title: '操作', | ||||
|  | @ -316,6 +320,7 @@ const handleRenameSuccess = () => { | |||
|           :columns="columns" | ||||
|           :pagination="false" | ||||
|           :custom-row="customRow" | ||||
|           :scroll="{ x: '100%' }" | ||||
|           row-key="id" | ||||
|         > | ||||
|           <template #bodyCell="{ column, record }"> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 jason
						jason