From 3229f6a34f4982d1a551d86e68d313482e33f701 Mon Sep 17 00:00:00 2001 From: wujingjing <gersonwu@qq.com> Date: 星期三, 11 十二月 2024 14:37:43 +0800 Subject: [PATCH] 条件节点 --- src/components/vue-flow/ui/nodes/EndNode.vue | 4 src/components/vue-flow/VueFlowHelper.ts | 116 ++++++++++++ src/components/vue-flow/VueFlowConstant.ts | 3 src/components/vue-flow/ui/nodes/ConditionNode.vue | 284 +++++++++++++++++++++++++++++++ src/components/vue-flow/ui/edges/CustomEdge.vue | 23 ++ src/components/vue-flow/MainCanvas.vue | 19 + src/components/vue-flow/vueFlowEnum.ts | 40 ++++ src/components/vue-flow/ui/nodes/StartNode.vue | 34 ++- 8 files changed, 495 insertions(+), 28 deletions(-) diff --git a/src/components/vue-flow/MainCanvas.vue b/src/components/vue-flow/MainCanvas.vue index 203aeda..ad45f8d 100644 --- a/src/components/vue-flow/MainCanvas.vue +++ b/src/components/vue-flow/MainCanvas.vue @@ -1,6 +1,9 @@ <template> <div class="relative h-full w-full" id="main-canvas" @drop="handleOnDrop" @dragover="handleOnDragOver"> <VueFlow v-model="elements" :node-types="nodeTypes" :connection-mode="ConnectionMode.Loose"> + <template #edge-custom="customEdgeProps"> + <CustomEdge v-bind="customEdgeProps" /> + </template> <Controls /> <Background /> </VueFlow> @@ -10,16 +13,19 @@ <script setup lang="ts"> import { Background } from '@vue-flow/background'; import { Controls } from '@vue-flow/controls'; -import { Dimensions, Elements, useVueFlow, VueFlow, ConnectionMode } from '@vue-flow/core'; +import { Dimensions, Elements, useVueFlow, VueFlow, ConnectionMode, MarkerType } from '@vue-flow/core'; import { markRaw, nextTick, ref, watch } from 'vue'; import { Test_data } from './testData'; import StartNode from './ui/nodes/StartNode.vue'; import EndNode from './ui/nodes/EndNode.vue'; import { VueFlowHelper } from './VueFlowHelper'; import { NodeType, nodeTypeMap } from './vueFlowEnum'; +import ConditionNode from './ui/nodes/ConditionNode.vue'; +import CustomEdge from './ui/edges/CustomEdge.vue'; const nodeTypes = { start: markRaw(StartNode), end: markRaw(EndNode), + condition: markRaw(ConditionNode), // LLM: markRaw(LLMNode), // code: markRaw(CodeNode), // knowledge: markRaw(KnowledgeNode), @@ -31,18 +37,14 @@ { id: '1', type: 'start', - data: { - title: '寮�濮�', - }, + data: VueFlowHelper.getDefaultData(NodeType.Start), position: { x: 25, y: 400 }, }, { id: '2', type: 'end', - data: { - title: '缁撴潫', - }, + data: VueFlowHelper.getDefaultData(NodeType.End), position: { x: 1000, y: 400 }, }, ], @@ -56,6 +58,7 @@ }); onConnect((params) => { + addEdges(params); }); @@ -78,7 +81,7 @@ }); const newNode = { - id: (nodes.value.length + 1).toString(), + id: VueFlowHelper.genGeometryId(), type, position, label: nodeTypeMap[type], diff --git a/src/components/vue-flow/VueFlowConstant.ts b/src/components/vue-flow/VueFlowConstant.ts index 85d8d86..fcea352 100644 --- a/src/components/vue-flow/VueFlowConstant.ts +++ b/src/components/vue-flow/VueFlowConstant.ts @@ -1,3 +1,4 @@ export class VueFlowConstant { - static PARAMS_KEY = 'input'; + static GROUP_PARAMS_KEY = 'group_params'; + static PARAMS_KEY = 'params' } \ No newline at end of file diff --git a/src/components/vue-flow/VueFlowHelper.ts b/src/components/vue-flow/VueFlowHelper.ts index 79671ec..cbb0d0e 100644 --- a/src/components/vue-flow/VueFlowHelper.ts +++ b/src/components/vue-flow/VueFlowHelper.ts @@ -1,15 +1,51 @@ -import { HandleType, Position } from '@vue-flow/core'; -import { NodeType, nodeTypeMap } from './vueFlowEnum'; +import { HandleType } from '@vue-flow/core'; +import { v4 as uuid } from 'uuid'; +import { VueFlowConstant } from './VueFlowConstant'; +import { CompareOperation, ConditionOperator, NodeType, VarType, nodeTypeMap } from './vueFlowEnum'; +import { get } from 'lodash'; export class VueFlowHelper { + static genId() { + return uuid().slice(0, 8); + } + + static genGeometryId(){ + return uuid().slice(0, 12); + } + static getDefaultData = (type: NodeType) => { let data: any = { title: nodeTypeMap[type], }; switch (type) { case NodeType.Start: + data[VueFlowConstant.GROUP_PARAMS_KEY] = [ + { + [VueFlowConstant.PARAMS_KEY]: [ + { + key: 'var_list', + label: '', + type: 'var_list', + value: [], + }, + ], + }, + ]; break; - + case NodeType.Condition: + data[VueFlowConstant.GROUP_PARAMS_KEY] = [ + { + [VueFlowConstant.PARAMS_KEY]: [ + { + key: 'condition', + label: '', + type: 'condition', + value: [ConditionHelper.getDefaultConditionGroup()], + }, + ], + }, + ]; + break; default: break; } @@ -20,4 +56,78 @@ const orderSuffix = order == undefined ? '' : `__${order + ''}`; return `${node.id}__handle-${handleType}${orderSuffix}`; }; + + static getFieldValue = (data, key, index = 0) => { + let varList = []; + const group = data?.[VueFlowConstant.GROUP_PARAMS_KEY]; + if (group && group.length > 0) { + if (index !== null) { + const val = group?.[index]?.[VueFlowConstant.PARAMS_KEY]?.find((item) => item.key === key)?.value; + if (val) { + varList.push(val); + } + } else { + for (const item of group) { + if (item[VueFlowConstant.PARAMS_KEY].key === key) { + varList.push(item[VueFlowConstant.PARAMS_KEY].value); + } + } + } + } + if (varList.length === 0) { + return null; + } else if (varList.length === 1) { + return varList[0]; + } else { + return varList; + } + }; +} + +export class StartNodeHelper { + // static getDefaultData = () => { + // return { + // title: nodeTypeMap[NodeType.Start], + // }; + // }; + + static getVarList = (data) => { + const varList = data[VueFlowConstant.GROUP_PARAMS_KEY][0][VueFlowConstant.PARAMS_KEY].find( + (item) => item.key === 'condition' + ).value; + return varList; + }; +} + +export class ConditionHelper { + static getConditionItem = ( + left?: { + var: string; + label: string; + }, + right?: { + type: VarType; + value: string; + label: string; + }, + operation?: CompareOperation + ) => { + return { + id: VueFlowHelper.genId(), + left_var: left?.var ?? '', + left_label: left?.label ?? '', + comparison_operation: operation ?? '', + right_value_type: right?.type ?? VarType.Input, + right_value: right?.value ?? '', + right_label: right?.label ?? '', + }; + }; + + static getDefaultConditionGroup = () => { + return { + id: VueFlowHelper.genId(), + operator: ConditionOperator.And, + conditions: [], + }; + }; } diff --git a/src/components/vue-flow/ui/edges/CustomEdge.vue b/src/components/vue-flow/ui/edges/CustomEdge.vue new file mode 100644 index 0000000..5f3765a --- /dev/null +++ b/src/components/vue-flow/ui/edges/CustomEdge.vue @@ -0,0 +1,23 @@ +<script setup lang="ts"> +import { BezierEdge } from '@vue-flow/core'; + +// props were passed from the slot using `v-bind="customEdgeProps"` +const props = defineProps(['sourceX', 'sourceY', 'targetX', 'targetY', 'sourcePosition', 'targetPosition']); +</script> + +<script lang="ts"> +export default { + name: 'CustomEdge', +}; +</script> + +<template> + <BezierEdge + :source-x="sourceX" + :source-y="sourceY" + :target-x="targetX" + :target-y="targetY" + :source-position="sourcePosition" + :target-position="targetPosition" + /> +</template> diff --git a/src/components/vue-flow/ui/nodes/ConditionNode.vue b/src/components/vue-flow/ui/nodes/ConditionNode.vue new file mode 100644 index 0000000..6513543 --- /dev/null +++ b/src/components/vue-flow/ui/nodes/ConditionNode.vue @@ -0,0 +1,284 @@ +<template> + <div + class="w-max-[520px] border-2 rounded-lg border-solid border-gray-100 bg-white p-3 shadow-md relative hover:border-blue-500 group" + > + <Handle :id="leftId" type="target" :position="Position.Left" /> + <div + class="group-hover:visible invisible flex absolute divide-y-[1.5px] divide-solid divide-gray-100 rounded-lg right-0 -top-0.5 translate-y-[-100%]" + style="box-shadow: 0 0 15px #dbdee6" + > + <el-tooltip effect="dark" content="澶嶅埗" placement="top"> + <div + class="flex content-center items-center border-x-0 p-1 hover:bg-gray-200 active:bg-gray-300 cursor-pointer" + @click="handleClickDuplicateBtn" + > + <span class="ywifont !text-[20px] mb-1 p-1.5 ywicon-copy"></span> + </div> + </el-tooltip> + <el-tooltip effect="dark" content="鍒犻櫎" placement="top"> + <div + @click="clickDeleteBtn" + class="flex content-center items-center border-x-0 p-1 hover:bg-gray-200 active:bg-gray-300 hover:text-red-400 cursor-pointer" + > + <span class="ywifont !text-[20px] mb-1 p-1.5 ywicon-shanchu"></span> + </div> + </el-tooltip> + </div> + <div class="flex flex-col gap-y-2 min-w-[400px]"> + <div class="flex justify-between flex-0"> + <div class="flex items-center gap-x-2"> + <img src="/@/components/vue-flow/ui/assets/images/icon_Start.png" class="h-4 w-4" alt="Start icon" /> + <div class="flex flex-col gap-y-1"> + <p v-if="!titleIsEdit" class="text-xl font-bold text-gray-500" @click="titleIsEdit = true">{{ data.title }}</p> + <el-input v-else v-model="data.title" @blur="() => (titleIsEdit = false)"></el-input> + </div> + </div> + </div> + + <div class="flex-auto gap-y-2 flex-col flex nodrag"> + <div v-for="(item, index) in conditionGroupList" class="flex-column gap-3 relative group/conditionGroup"> + <div class="flex-items-center justify-between"> + <div class="text-lg font-bold">濡傛灉</div> + <div class="flex-items-center gap-3"> + <el-select v-model="item.operator" class="w-[130px]"> + <el-option + v-for="item in Object.keys(conditionOperatorMap)" + :key="item" + :value="item" + :label="conditionOperatorMap[item]" + ></el-option> + </el-select> + <!-- v-if="conditionGroupList.length > 1" --> + + <span + class="ywifont ywicon-shanchu text-red-400 invisible group-hover/conditionGroup:visible cursor-pointer" + @click="delConditionBranch(index)" + ></span> + </div> + </div> + + <div class="flex-column gap-y-2"> + <div v-for="(subItem, subIndex) in item.conditions" class="ml-5 flex-items-center gap-x-2 group/conditionItem"> + <el-tree-select + filterable + class="w-[120px] flex-0" + v-model="subItem.left_var" + :data="treeReferOptions" + node-key="id" + :clearable="true" + :accordion="false" + :expandNode="false" + :check-strictly="false" + placeholder="閫夋嫨鍙橀噺" + > + </el-tree-select> + <el-select v-model="subItem.comparison_operation" class="flex-0 w-[120px]" placeholder="閫夋嫨鏉′欢"> + <el-option + v-for="item in Object.keys(compareOperationMap)" + :key="item" + :value="item" + :label="compareOperationMap[item]" + ></el-option> + </el-select> + + <el-select v-model="subItem.right_value_type" class="flex-0 w-[90px]" placeholder="璇烽�夋嫨"> + <el-option v-for="item in Object.keys(varTypeMap)" :key="item" :value="item" :label="varTypeMap[item]"></el-option> + </el-select> + + <el-input + v-if="subItem.right_value_type === VarType.Input" + v-model="subItem.right_value" + class="w-[120px]" + placeholder="杈撳叆鍊�" + > + </el-input> + + <el-tree-select + v-else + filterable + class="w-[120px] flex-0" + v-model="subItem.right_value" + :data="treeReferOptions" + node-key="id" + :clearable="true" + :accordion="false" + :expandNode="false" + :check-strictly="false" + placeholder="閫夋嫨鍙橀噺" + > + </el-tree-select> + + <span + class="ywifont ywicon-shanchu text-red-400 invisible group-hover/conditionItem:visible cursor-pointer" + @click="delConditionItem(item, subIndex)" + ></span> + </div> + <el-button class="w-fit mt-3" type="primary" @click="addConditionItem(item)">娣诲姞鏉′欢</el-button> + </div> + <Handle :id="item.id" type="source" :position="Position.Right" /> + </div> + + <div class="flex-column gap-3 relative"> + <div class="flex-items-center justify-between"> + <div class="text-lg font-bold">鍚﹀垯</div> + <!-- <el-select class="w-[130px]"> + <el-option + v-for="item in Object.keys(conditionOperatorMap)" + :key="item" + :value="parseInt(item)" + :label="conditionOperatorMap[item]" + ></el-option> + </el-select> --> + </div> + + <div class="flex-column"> + <el-button @click="addConditionBranch" class="w-fit mt-3" type="success">娣诲姞鍒嗘敮</el-button> + </div> + <Handle :id="otherHandleId" type="source" :position="Position.Right" /> + </div> + </div> + </div> + <!-- <Handle :id="handleId" type="source" :position="Position.Right" /> --> + </div> + + <!-- <div> + <span>璧峰</span> + + <Handle type="source" :position="Position.Right" /> + </div> --> +</template> + +<script lang="ts" setup> +import { Handle, Position, useNode, useVueFlow } from '@vue-flow/core'; +import { ref, watchEffect } from 'vue'; + +import type { NodeProps } from '@vue-flow/core'; +import { computed } from 'vue'; +import { VueFlowConstant } from '../../VueFlowConstant'; +import { ConditionHelper, VueFlowHelper } from '../../VueFlowHelper'; +import { compareOperationMap, VarType, conditionOperatorMap, varTypeMap } from '../../vueFlowEnum'; +import { LLMNodeData, LLMNodeEvents } from './index'; +import { deepClone } from '/@/utils/other'; +defineProps<NodeProps<LLMNodeData, LLMNodeEvents>>(); + +const node = useNode(); +const leftId = VueFlowHelper.getHandleId(node, 'target'); +const { findNode } = useVueFlow(); +const referenceOptions = ref([]); +const otherHandleId = VueFlowHelper.genId(); +const treeReferOptions = computed(() => { + const result = []; + for (const item of referenceOptions.value) { + result.push({ + id: item.id, + label: item.groupName, + children: item.options.map((subItem, index) => { + return { + id: `${item.id}_${index}`, + label: subItem.label, + value: subItem.value, + }; + }), + }); + } + return result; +}); + +const travelConnectNode = (node, cb: Function) => { + if (node.connectedEdges && node.connectedEdges.value.length > 0) { + const filteredEdges = node.connectedEdges.value.filter((item) => item.target === node.id); + filteredEdges.map((edge) => { + const node = findNode(edge.source); + if (node) { + travelConnectNode(node, cb); + cb(node); + } + }); + } +}; +watchEffect(() => { + const result = []; + travelConnectNode(node, (item) => { + const currentItem = { + id: `option-${item.id}`, + groupName: item?.data.title ?? item?.label, + options: [], + }; + const varList = VueFlowHelper.getFieldValue(item.data,'var_list'); + if (varList) { + varList + .filter((item: any) => Boolean(item.name)) + .forEach((option: any) => { + currentItem.options.push({ + label: option.name, + value: option.name, + }); + }); + } else { + currentItem.options = []; + } + result.push(currentItem); + }); + referenceOptions.value = result; +}); + +const data = ref(node.node.data); +const conditionGroupList = ref(VueFlowHelper.getFieldValue(data.value,'condition')); +const titleIsEdit = ref(false); + +function handleClickAddBtn() { + if (!data.value[VueFlowConstant.GROUP_PARAMS_KEY]) { + data.value[VueFlowConstant.GROUP_PARAMS_KEY] = []; + } + + data.value[VueFlowConstant.GROUP_PARAMS_KEY].push({ + name: '', + description: '', + type: '', + isRequired: true, + }); +} + +function handleClickDeleteBtn(index: number) { + data.value[VueFlowConstant.GROUP_PARAMS_KEY].splice(index, 1); +} +const { removeNodes, nodes, addNodes } = useVueFlow(); + +function handleClickDuplicateBtn() { + const { type, position, data } = node.node; + const newNode = { + id: VueFlowHelper.genGeometryId(), + type, + position: { + x: position.x + 100, + y: position.y + 100, + }, + data: deepClone(data), + }; + addNodes(newNode); +} +const clickDeleteBtn = () => { + removeNodes(node.id); +}; + +const addConditionBranch = () => { + const conditionGroup = ConditionHelper.getDefaultConditionGroup(); + conditionGroupList.value.push(conditionGroup); +}; + +const delConditionBranch = (index) => { + conditionGroupList.value.splice(index, 1); + if (conditionGroupList.value.length === 0) { + addConditionBranch(); + } +}; + +const addConditionItem = (group) => { + const conditionGroup = ConditionHelper.getConditionItem(); + group.conditions.push(conditionGroup); +}; + +const delConditionItem = (group, index) => { + group.conditions.splice(index, 1); +}; +</script> diff --git a/src/components/vue-flow/ui/nodes/EndNode.vue b/src/components/vue-flow/ui/nodes/EndNode.vue index fce65bd..5c57725 100644 --- a/src/components/vue-flow/ui/nodes/EndNode.vue +++ b/src/components/vue-flow/ui/nodes/EndNode.vue @@ -62,13 +62,13 @@ const data = ref(node.node.data); const titleIsEdit = ref(false); -const parameterTable = computed(() => data.value[VueFlowConstant.PARAMS_KEY]); +const parameterTable = computed(() => data.value[VueFlowConstant.GROUP_PARAMS_KEY]); const { removeNodes, nodes, addNodes } = useVueFlow(); function handleClickDuplicateBtn() { const { type, position, data } = node.node; const newNode = { - id: (nodes.value.length + 1).toString(), + id: VueFlowHelper.genGeometryId(), type, position: { x: position.x + 100, diff --git a/src/components/vue-flow/ui/nodes/StartNode.vue b/src/components/vue-flow/ui/nodes/StartNode.vue index daec7bf..40fc09c 100644 --- a/src/components/vue-flow/ui/nodes/StartNode.vue +++ b/src/components/vue-flow/ui/nodes/StartNode.vue @@ -36,7 +36,7 @@ <div class="flex-auto gap-y-2 flex-col flex nodrag"> <div class="text-lg font-bold">鍙傛暟</div> - <el-table size="small" class="flex-auto" :data="parameterTable" border> + <el-table size="small" class="flex-auto" :data="varList" border> <el-table-column prop="name" width="90" label="鍙傛暟鍚�" fixed> <template #default="scope"> <el-input v-model="scope.row.name"></el-input> @@ -91,15 +91,15 @@ <script lang="ts" setup> import { Handle, Position, useNode, useVueFlow } from '@vue-flow/core'; -import { ref } from 'vue'; +import { ref, watchEffect } from 'vue'; import type { NodeProps } from '@vue-flow/core'; import { computed } from 'vue'; import { VueFlowConstant } from '../../VueFlowConstant'; +import { VueFlowHelper } from '../../VueFlowHelper'; import { parameterTypeMap } from '../../vueFlowEnum'; import { LLMNodeData, LLMNodeEvents } from './index'; import { deepClone } from '/@/utils/other'; -import { VueFlowHelper } from '../../VueFlowHelper'; defineProps<NodeProps<LLMNodeData, LLMNodeEvents>>(); @@ -107,14 +107,21 @@ const handleId = ref(VueFlowHelper.getHandleId(node.node, 'source')); const data = ref(node.node.data); -const titleIsEdit = ref(false); -const parameterTable = computed(() => data.value[VueFlowConstant.PARAMS_KEY]); -function handleClickAddBtn() { - if (!data.value[VueFlowConstant.PARAMS_KEY]) { - data.value[VueFlowConstant.PARAMS_KEY] = []; - } - data.value[VueFlowConstant.PARAMS_KEY].push({ +const getVarList = () =>{ + const varList = data.value[VueFlowConstant.GROUP_PARAMS_KEY][0][VueFlowConstant.PARAMS_KEY].find( + (item) => item.key === 'condition' + ).value; + return varList; +} + +const varList = ref(VueFlowHelper.getFieldValue(data.value,'var_list')); +const titleIsEdit = ref(false); + +function handleClickAddBtn() { + + + varList.value.push({ name: '', description: '', type: '', @@ -123,14 +130,14 @@ } function handleClickDeleteBtn(index: number) { - data.value[VueFlowConstant.PARAMS_KEY].splice(index, 1); + varList.value.splice(index, 1); } const { removeNodes, nodes, addNodes } = useVueFlow(); function handleClickDuplicateBtn() { const { type, position, data } = node.node; const newNode = { - id: (nodes.value.length + 1).toString(), + id: VueFlowHelper.genGeometryId(), type, position: { x: position.x + 100, @@ -143,4 +150,7 @@ const clickDeleteBtn = () => { removeNodes(node.id); }; + + + </script> diff --git a/src/components/vue-flow/vueFlowEnum.ts b/src/components/vue-flow/vueFlowEnum.ts index dabd789..85e7936 100644 --- a/src/components/vue-flow/vueFlowEnum.ts +++ b/src/components/vue-flow/vueFlowEnum.ts @@ -14,7 +14,7 @@ LLM = 'LLM', Start = 'start', End = 'end', - Condition='condition' + Condition = 'condition', } export const nodeTypeMap = { @@ -22,5 +22,41 @@ [NodeType.Start]: '寮�濮�', [NodeType.End]: '缁撴潫', [NodeType.Condition]: '鏉′欢鍒ゆ柇', - }; + +export const enum CompareOperation { + /** @description 澶т簬 */ + gt = 'gt', + /** @description 灏忎簬 */ + lt = 'lt', + /** @description 澶т簬鎴栫瓑浜� */ + gte = 'gte', + /** @description 灏忎簬鎴栫瓑浜� */ + lte = 'lte', +} +export const compareOperationMap = { + [CompareOperation.gt]: '>', + [CompareOperation.lt]: '<', + [CompareOperation.gte]: '鈮�', + [CompareOperation.lte]: '鈮�', +}; + +export const enum VarType { + Input = 'input', + Reference = 'reference', +} + +export const varTypeMap = { + [VarType.Input]: '杈撳叆', + [VarType.Reference]: '寮曠敤', +}; + +export const enum ConditionOperator { + And = 'and', + Or = 'or', +} + +export const conditionOperatorMap = { + [ConditionOperator.And]: '涓�', + [ConditionOperator.Or]: '鎴�', +}; \ No newline at end of file -- Gitblit v1.9.3