feat: [BPM 工作流] Simple 模型 - 路由分支节点
							parent
							
								
									059df5b4ca
								
							
						
					
					
						commit
						4a796b7e9b
					
				|  | @ -0,0 +1,290 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | import type { Ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import type { RouterSetting, SimpleFlowNode } from '../../consts'; | ||||||
|  | 
 | ||||||
|  | import { inject, ref } from 'vue'; | ||||||
|  | 
 | ||||||
|  | import { useVbenDrawer } from '@vben/common-ui'; | ||||||
|  | import { IconifyIcon } from '@vben/icons'; | ||||||
|  | 
 | ||||||
|  | import { | ||||||
|  |   Button, | ||||||
|  |   Card, | ||||||
|  |   Col, | ||||||
|  |   Form, | ||||||
|  |   FormItem, | ||||||
|  |   Input, | ||||||
|  |   message, | ||||||
|  |   Row, | ||||||
|  |   Select, | ||||||
|  |   SelectOption, | ||||||
|  | } from 'ant-design-vue'; | ||||||
|  | 
 | ||||||
|  | import { ConditionType, NodeType } from '../../consts'; | ||||||
|  | import { useNodeName, useWatchNode } from '../../helpers'; | ||||||
|  | import Condition from './modules/condition.vue'; | ||||||
|  | 
 | ||||||
|  | defineOptions({ | ||||||
|  |   name: 'RouterNodeConfig', | ||||||
|  | }); | ||||||
|  | const props = defineProps({ | ||||||
|  |   flowNode: { | ||||||
|  |     type: Object as () => SimpleFlowNode, | ||||||
|  |     required: true, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | const processNodeTree = inject<Ref<SimpleFlowNode>>('processNodeTree'); | ||||||
|  | 
 | ||||||
|  | // 当前节点 | ||||||
|  | const currentNode = useWatchNode(props); | ||||||
|  | // 节点名称 | ||||||
|  | const { nodeName, showInput, clickIcon, blurEvent } = useNodeName( | ||||||
|  |   NodeType.ROUTER_BRANCH_NODE, | ||||||
|  | ); | ||||||
|  | const routerGroups = ref<RouterSetting[]>([]); | ||||||
|  | const nodeOptions = ref<any[]>([]); | ||||||
|  | const conditionRef = ref<any[]>([]); | ||||||
|  | const formRef = ref(); | ||||||
|  | 
 | ||||||
|  | /** 保存配置 */ | ||||||
|  | const saveConfig = async () => { | ||||||
|  |   // 校验路由分支选择 | ||||||
|  |   const routeIdValid = await formRef.value.validate().catch(() => false); | ||||||
|  |   if (!routeIdValid) { | ||||||
|  |     message.warning('请配置路由目标节点'); | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // 校验条件规则 | ||||||
|  |   let valid = true; | ||||||
|  |   for (const item of conditionRef.value) { | ||||||
|  |     if (item && !(await item.validate())) { | ||||||
|  |       valid = false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   if (!valid) return false; | ||||||
|  |   const showText = getShowText(); | ||||||
|  |   if (!showText) return false; | ||||||
|  |   currentNode.value.name = nodeName.value!; | ||||||
|  |   currentNode.value.showText = showText; | ||||||
|  |   currentNode.value.routerGroups = routerGroups.value; | ||||||
|  |   drawerApi.close(); | ||||||
|  |   return true; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 使用 useVbenDrawer | ||||||
|  | const [Drawer, drawerApi] = useVbenDrawer({ | ||||||
|  |   title: nodeName.value, | ||||||
|  |   class: 'w-[630px]', | ||||||
|  |   onCancel: () => { | ||||||
|  |     drawerApi.close(); | ||||||
|  |   }, | ||||||
|  |   onConfirm: saveConfig, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | // 显示路由分支节点配置, 由父组件调用 | ||||||
|  | const openDrawer = (node: SimpleFlowNode) => { | ||||||
|  |   nodeOptions.value = []; | ||||||
|  |   getRouterNode(processNodeTree?.value); | ||||||
|  |   routerGroups.value = []; | ||||||
|  |   nodeName.value = node.name; | ||||||
|  |   if (node.routerGroups) { | ||||||
|  |     routerGroups.value = node.routerGroups; | ||||||
|  |   } | ||||||
|  |   drawerApi.open(); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const getShowText = () => { | ||||||
|  |   if ( | ||||||
|  |     !routerGroups.value || | ||||||
|  |     !Array.isArray(routerGroups.value) || | ||||||
|  |     routerGroups.value.length <= 0 | ||||||
|  |   ) { | ||||||
|  |     message.warning('请配置路由!'); | ||||||
|  |     return ''; | ||||||
|  |   } | ||||||
|  |   for (const route of routerGroups.value) { | ||||||
|  |     if (!route.nodeId || !route.conditionType) { | ||||||
|  |       message.warning('请完善路由配置项!'); | ||||||
|  |       return ''; | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       route.conditionType === ConditionType.EXPRESSION && | ||||||
|  |       !route.conditionExpression | ||||||
|  |     ) { | ||||||
|  |       message.warning('请完善路由配置项!'); | ||||||
|  |       return ''; | ||||||
|  |     } | ||||||
|  |     if (route.conditionType === ConditionType.RULE) { | ||||||
|  |       for (const condition of route.conditionGroups.conditions) { | ||||||
|  |         for (const rule of condition.rules) { | ||||||
|  |           if (!rule.leftSide || !rule.rightSide) { | ||||||
|  |             message.warning('请完善路由配置项!'); | ||||||
|  |             return ''; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return `${routerGroups.value.length}条路由分支`; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const addRouterGroup = () => { | ||||||
|  |   routerGroups.value.push({ | ||||||
|  |     nodeId: undefined, | ||||||
|  |     conditionType: ConditionType.RULE, | ||||||
|  |     conditionExpression: '', | ||||||
|  |     conditionGroups: { | ||||||
|  |       and: true, | ||||||
|  |       conditions: [ | ||||||
|  |         { | ||||||
|  |           and: true, | ||||||
|  |           rules: [ | ||||||
|  |             { | ||||||
|  |               opCode: '==', | ||||||
|  |               leftSide: undefined, | ||||||
|  |               rightSide: '', | ||||||
|  |             }, | ||||||
|  |           ], | ||||||
|  |         }, | ||||||
|  |       ], | ||||||
|  |     }, | ||||||
|  |   }); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | const deleteRouterGroup = (index: number) => { | ||||||
|  |   routerGroups.value.splice(index, 1); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 递归获取所有节点 | ||||||
|  | const getRouterNode = (node: any) => { | ||||||
|  |   // TODO 最好还需要满足以下要求 | ||||||
|  |   // 并行分支、包容分支内部节点不能跳转到外部节点 | ||||||
|  |   // 条件分支节点可以向上跳转到外部节点 | ||||||
|  |   while (true) { | ||||||
|  |     if (!node) break; | ||||||
|  |     if ( | ||||||
|  |       node.type !== NodeType.ROUTER_BRANCH_NODE && | ||||||
|  |       node.type !== NodeType.CONDITION_NODE | ||||||
|  |     ) { | ||||||
|  |       nodeOptions.value.push({ | ||||||
|  |         label: node.name, | ||||||
|  |         value: node.id, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     if (!node.childNode || node.type === NodeType.END_EVENT_NODE) { | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     if (node.conditionNodes && node.conditionNodes.length > 0) { | ||||||
|  |       node.conditionNodes.forEach((item: any) => { | ||||||
|  |         getRouterNode(item); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     node = node.childNode; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | defineExpose({ openDrawer }); // 暴露方法给父组件 | ||||||
|  | </script> | ||||||
|  | <template> | ||||||
|  |   <Drawer> | ||||||
|  |     <template #title> | ||||||
|  |       <div class="flex items-center"> | ||||||
|  |         <Input | ||||||
|  |           v-if="showInput" | ||||||
|  |           type="text" | ||||||
|  |           class="mr-2 w-48" | ||||||
|  |           @blur="blurEvent()" | ||||||
|  |           v-model:value="nodeName" | ||||||
|  |           :placeholder="nodeName" | ||||||
|  |         /> | ||||||
|  |         <div | ||||||
|  |           v-else | ||||||
|  |           class="flex cursor-pointer items-center" | ||||||
|  |           @click="clickIcon()" | ||||||
|  |         > | ||||||
|  |           {{ nodeName }} | ||||||
|  |           <IconifyIcon class="ml-1" icon="ep:edit-pen" /> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     </template> | ||||||
|  | 
 | ||||||
|  |     <Form ref="formRef" :model="{ routerGroups }"> | ||||||
|  |       <Card | ||||||
|  |         :body-style="{ padding: '10px' }" | ||||||
|  |         class="mt-4" | ||||||
|  |         v-for="(item, index) in routerGroups" | ||||||
|  |         :key="index" | ||||||
|  |       > | ||||||
|  |         <template #title> | ||||||
|  |           <div class="flex h-16 w-full items-center justify-between"> | ||||||
|  |             <div class="flex items-center font-normal"> | ||||||
|  |               <span class="font-medium">路由{{ index + 1 }}</span> | ||||||
|  |               <FormItem | ||||||
|  |                 class="mb-0 ml-4 inline-block w-[180px]" | ||||||
|  |                 :name="['routerGroups', index, 'nodeId']" | ||||||
|  |                 :rules="{ | ||||||
|  |                   required: true, | ||||||
|  |                   message: '路由目标节点不能为空', | ||||||
|  |                   trigger: 'change', | ||||||
|  |                 }" | ||||||
|  |               > | ||||||
|  |                 <Select | ||||||
|  |                   v-model:value="item.nodeId" | ||||||
|  |                   placeholder="请选择路由目标节点" | ||||||
|  |                   allow-clear | ||||||
|  |                 > | ||||||
|  |                   <SelectOption | ||||||
|  |                     v-for="node in nodeOptions" | ||||||
|  |                     :key="node.value" | ||||||
|  |                     :value="node.value" | ||||||
|  |                   > | ||||||
|  |                     {{ node.label }} | ||||||
|  |                   </SelectOption> | ||||||
|  |                 </Select> | ||||||
|  |               </FormItem> | ||||||
|  |             </div> | ||||||
|  |             <Button | ||||||
|  |               v-if="routerGroups.length > 1" | ||||||
|  |               shape="circle" | ||||||
|  |               class="flex items-center justify-center" | ||||||
|  |               @click="deleteRouterGroup(index)" | ||||||
|  |             > | ||||||
|  |               <template #icon> | ||||||
|  |                 <IconifyIcon icon="ep:close" /> | ||||||
|  |               </template> | ||||||
|  |             </Button> | ||||||
|  |           </div> | ||||||
|  |         </template> | ||||||
|  |         <Condition | ||||||
|  |           :ref="(el) => (conditionRef[index] = el)" | ||||||
|  |           :model-value="routerGroups[index] as Record<string, any>" | ||||||
|  |           @update:model-value="(val) => (routerGroups[index] = val)" | ||||||
|  |         /> | ||||||
|  |       </Card> | ||||||
|  |     </Form> | ||||||
|  | 
 | ||||||
|  |     <Row class="mt-4"> | ||||||
|  |       <Col :span="24"> | ||||||
|  |         <Button | ||||||
|  |           class="flex items-center p-0" | ||||||
|  |           type="link" | ||||||
|  |           @click="addRouterGroup" | ||||||
|  |         > | ||||||
|  |           <template #icon> | ||||||
|  |             <IconifyIcon icon="ep:setting" /> | ||||||
|  |           </template> | ||||||
|  |           新增路由分支 | ||||||
|  |         </Button> | ||||||
|  |       </Col> | ||||||
|  |     </Row> | ||||||
|  | 
 | ||||||
|  |     <template #footer> | ||||||
|  |       <div class="flex justify-end space-x-2"> | ||||||
|  |         <Button type="primary" @click="saveConfig">确 定</Button> | ||||||
|  |         <Button @click="drawerApi.close">取 消</Button> | ||||||
|  |       </div> | ||||||
|  |     </template> | ||||||
|  |   </Drawer> | ||||||
|  | </template> | ||||||
|  | @ -0,0 +1,115 @@ | ||||||
|  | <script setup lang="ts"> | ||||||
|  | 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 RouterNodeConfig from '../nodes-config/router-node-config.vue'; | ||||||
|  | import NodeHandler from './node-handler.vue'; | ||||||
|  | 
 | ||||||
|  | defineOptions({ name: 'RouterNode' }); | ||||||
|  | 
 | ||||||
|  | const props = defineProps({ | ||||||
|  |   flowNode: { | ||||||
|  |     type: Object as () => SimpleFlowNode, | ||||||
|  |     required: true, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
|  | // 定义事件,更新父组件 | ||||||
|  | const emits = defineEmits<{ | ||||||
|  |   'update:flowNode': [node: SimpleFlowNode | undefined]; | ||||||
|  | }>(); | ||||||
|  | // 是否只读 | ||||||
|  | const readonly = inject<Boolean>('readonly'); | ||||||
|  | // 监控节点的变化 | ||||||
|  | const currentNode = useWatchNode(props); | ||||||
|  | // 节点名称编辑 | ||||||
|  | const { showInput, blurEvent, clickTitle } = useNodeName2( | ||||||
|  |   currentNode, | ||||||
|  |   NodeType.ROUTER_BRANCH_NODE, | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | const nodeSetting = ref(); | ||||||
|  | // 打开节点配置 | ||||||
|  | const openNodeConfig = () => { | ||||||
|  |   if (readonly) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   nodeSetting.value.openDrawer(currentNode.value); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | // 删除节点。更新当前节点为孩子节点 | ||||||
|  | const deleteNode = () => { | ||||||
|  |   emits('update:flowNode', currentNode.value.childNode); | ||||||
|  | }; | ||||||
|  | </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 router-node"> | ||||||
|  |             <span class="iconfont icon-router"></span> | ||||||
|  |           </div> | ||||||
|  |           <Input | ||||||
|  |             v-if="!readonly && showInput" | ||||||
|  |             type="text" | ||||||
|  |             class="editable-title-input" | ||||||
|  |             @blur="blurEvent()" | ||||||
|  |             v-model="currentNode.name" | ||||||
|  |             :placeholder="currentNode.name" | ||||||
|  |           /> | ||||||
|  |           <div v-else class="node-title" @click="clickTitle"> | ||||||
|  |             {{ currentNode.name }} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |         <div class="node-content" @click="openNodeConfig"> | ||||||
|  |           <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.ROUTER_BRANCH_NODE) }} | ||||||
|  |           </div> | ||||||
|  |           <IconifyIcon v-if="!readonly" icon="ep:arrow-right-bold" /> | ||||||
|  |         </div> | ||||||
|  |         <div v-if="!readonly" class="node-toolbar"> | ||||||
|  |           <div class="toolbar-icon"> | ||||||
|  |             <IconifyIcon | ||||||
|  |               color="#0089ff" | ||||||
|  |               icon="ep:circle-close-filled" | ||||||
|  |               @click="deleteNode" | ||||||
|  |             /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  | 
 | ||||||
|  |       <!-- 传递子节点给添加节点组件。会在子节点前面添加节点 --> | ||||||
|  |       <NodeHandler | ||||||
|  |         v-if="currentNode" | ||||||
|  |         v-model:child-node="currentNode.childNode" | ||||||
|  |         :current-node="currentNode" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|  |     <RouterNodeConfig | ||||||
|  |       v-if="!readonly && currentNode" | ||||||
|  |       ref="nodeSetting" | ||||||
|  |       :flow-node="currentNode" | ||||||
|  |     /> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | <style lang="scss" scoped></style> | ||||||
|  | @ -9,6 +9,7 @@ import EndEventNode from './nodes/end-event-node.vue'; | ||||||
| import ExclusiveNode from './nodes/exclusive-node.vue'; | import ExclusiveNode from './nodes/exclusive-node.vue'; | ||||||
| import InclusiveNode from './nodes/inclusive-node.vue'; | import InclusiveNode from './nodes/inclusive-node.vue'; | ||||||
| import ParallelNode from './nodes/parallel-node.vue'; | import ParallelNode from './nodes/parallel-node.vue'; | ||||||
|  | import RouterNode from './nodes/router-node.vue'; | ||||||
| import StartUserNode from './nodes/start-user-node.vue'; | import StartUserNode from './nodes/start-user-node.vue'; | ||||||
| import TriggerNode from './nodes/trigger-node.vue'; | import TriggerNode from './nodes/trigger-node.vue'; | ||||||
| import UserTaskNode from './nodes/user-task-node.vue'; | import UserTaskNode from './nodes/user-task-node.vue'; | ||||||
|  | @ -116,11 +117,11 @@ const recursiveFindParentNode = ( | ||||||
|     @update:flow-node="handleModelValueUpdate" |     @update:flow-node="handleModelValueUpdate" | ||||||
|   /> |   /> | ||||||
|   <!-- 路由分支节点 --> |   <!-- 路由分支节点 --> | ||||||
|   <!-- <RouterNode |   <RouterNode | ||||||
|     v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE" |     v-if="currentNode && currentNode.type === NodeType.ROUTER_BRANCH_NODE" | ||||||
|     :flow-node="currentNode" |     :flow-node="currentNode" | ||||||
|     @update:flow-node="handleModelValueUpdate" |     @update:flow-node="handleModelValueUpdate" | ||||||
|   /> --> |   /> | ||||||
|   <!-- 触发器节点 --> |   <!-- 触发器节点 --> | ||||||
|   <TriggerNode |   <TriggerNode | ||||||
|     v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE" |     v-if="currentNode && currentNode.type === NodeType.TRIGGER_NODE" | ||||||
|  |  | ||||||
|  | @ -564,7 +564,7 @@ export type RouterSetting = { | ||||||
|   conditionExpression: string; |   conditionExpression: string; | ||||||
|   conditionGroups: ConditionGroup; |   conditionGroups: ConditionGroup; | ||||||
|   conditionType: ConditionType; |   conditionType: ConditionType; | ||||||
|   nodeId: string; |   nodeId: string | undefined; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue
	
	 jason
						jason