203 lines
6.5 KiB
Vue
203 lines
6.5 KiB
Vue
|
|
<template>
|
||
|
|
<div
|
||
|
|
class="flow-node-shell"
|
||
|
|
:class="[`flow-node-shell--${flowType}`, { 'flow-node-shell--selected': selected }]"
|
||
|
|
>
|
||
|
|
<Handle
|
||
|
|
v-if="showTarget"
|
||
|
|
:id="targetHandleId"
|
||
|
|
type="target"
|
||
|
|
:position="Position.Left"
|
||
|
|
:connectable="connectable && !flowStore.disableForm"
|
||
|
|
class="flow-handle flow-handle--target"
|
||
|
|
/>
|
||
|
|
<div class="flow-node-content" :class="{ 'flow-node-card': !isGroup && flowType !== 'branch' }">
|
||
|
|
<component
|
||
|
|
:is="nodeComponent"
|
||
|
|
ref="componentRef"
|
||
|
|
:model="model"
|
||
|
|
:graph-model="model"
|
||
|
|
:properties="properties"
|
||
|
|
@content-change="contentChanged"
|
||
|
|
@bind-ref="bindComponent"
|
||
|
|
@add-anchor="addAnchor"
|
||
|
|
@remove-anchor="removeAnchor"
|
||
|
|
@change-anchor="changeAnchor"
|
||
|
|
@add-to-group="addToGroup"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<template v-if="flowType === 'branch'">
|
||
|
|
<template v-for="anchor in branchAnchors" :key="anchor.id">
|
||
|
|
<Handle
|
||
|
|
:id="anchor.id"
|
||
|
|
type="source"
|
||
|
|
:position="Position.Right"
|
||
|
|
:connectable="connectable && !flowStore.disableForm"
|
||
|
|
class="flow-handle flow-handle--source flow-handle--branch"
|
||
|
|
:style="{ top: `${Math.max(76, Number(anchor.height || 0) + 60)}px` }"
|
||
|
|
/>
|
||
|
|
</template>
|
||
|
|
</template>
|
||
|
|
<Handle
|
||
|
|
v-else-if="showSource"
|
||
|
|
:id="`${id}_1`"
|
||
|
|
type="source"
|
||
|
|
:position="Position.Right"
|
||
|
|
:connectable="connectable && !flowStore.disableForm"
|
||
|
|
class="flow-handle flow-handle--source"
|
||
|
|
/>
|
||
|
|
</div>
|
||
|
|
</template>
|
||
|
|
|
||
|
|
<script setup>
|
||
|
|
import { computed, onMounted, ref } from "vue";
|
||
|
|
import { Handle, Position, useVueFlow } from "@vue-flow/core";
|
||
|
|
import { useFlowStore } from "@/store/modules/flow";
|
||
|
|
import StartNode from "../nodes/common/startNode.vue";
|
||
|
|
import EndNode from "../nodes/common/endNode.vue";
|
||
|
|
import ServiceNode from "../nodes/common/serviceNode.vue";
|
||
|
|
import SelectAreaNode from "../nodes/common/selectAreaNode.vue";
|
||
|
|
import LoopNode from "../nodes/function/loop.vue";
|
||
|
|
import BranchNode from "../nodes/function/switch.vue";
|
||
|
|
import StopLoopNode from "../nodes/function/stopLoop.vue";
|
||
|
|
import SubStartNode from "../nodes/function/subStart.vue";
|
||
|
|
import SubEndNode from "../nodes/function/subEnd.vue";
|
||
|
|
import SleepNode from "../nodes/function/sleep.vue";
|
||
|
|
import HttpNode from "../nodes/function/httpNode.vue";
|
||
|
|
import CodeNode from "../nodes/function/codeNode.vue";
|
||
|
|
import CurrentLoopNode from "../nodes/function/currentLoop.vue";
|
||
|
|
import SdAgentNode from "../nodes/function/sdAgent.vue";
|
||
|
|
import RecognizeNode from "../nodes/function/recognize.vue";
|
||
|
|
|
||
|
|
const props = defineProps({
|
||
|
|
id: String,
|
||
|
|
data: Object,
|
||
|
|
selected: Boolean,
|
||
|
|
connectable: [Boolean, Number, String, Function],
|
||
|
|
});
|
||
|
|
|
||
|
|
const componentMap = {
|
||
|
|
start: StartNode,
|
||
|
|
end: EndNode,
|
||
|
|
serviceNode: ServiceNode,
|
||
|
|
selectArea: SelectAreaNode,
|
||
|
|
loop: LoopNode,
|
||
|
|
branch: BranchNode,
|
||
|
|
stopLoop: StopLoopNode,
|
||
|
|
subStart: SubStartNode,
|
||
|
|
subEnd: SubEndNode,
|
||
|
|
sleep: SleepNode,
|
||
|
|
http: HttpNode,
|
||
|
|
code: CodeNode,
|
||
|
|
currentLoop: CurrentLoopNode,
|
||
|
|
sdAgent: SdAgentNode,
|
||
|
|
recognize: RecognizeNode,
|
||
|
|
};
|
||
|
|
|
||
|
|
const flowStore = useFlowStore();
|
||
|
|
const vueFlow = useVueFlow("main-flow");
|
||
|
|
const componentRef = ref();
|
||
|
|
const flowType = computed(() => props.data.flowType);
|
||
|
|
const properties = computed(() => props.data.properties || {});
|
||
|
|
const nodeComponent = computed(() => componentMap[flowType.value] || ServiceNode);
|
||
|
|
const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value));
|
||
|
|
const showTarget = computed(() => !["start", "subStart", "selectArea"].includes(flowType.value));
|
||
|
|
const showSource = computed(() => !["end", "stopLoop", "subEnd", "selectArea", "branch"].includes(flowType.value));
|
||
|
|
const targetHandleId = computed(() => flowType.value === "branch" ? `${props.id}_entry` : `${props.id}_3`);
|
||
|
|
const branchAnchors = computed(() => {
|
||
|
|
const reservedIds = new Set([`${props.id}_1`, `${props.id}_3`, `${props.id}_entry`]);
|
||
|
|
const conditionDefs = properties.value.nodeParams || properties.value.conditions || [];
|
||
|
|
const conditionIds = new Set(conditionDefs.map((condition) => condition?.id).filter(Boolean));
|
||
|
|
const elseId = `${props.id}_else`;
|
||
|
|
const seen = new Set();
|
||
|
|
return (properties.value.anchor || []).filter((anchor) => {
|
||
|
|
if (!anchor?.id || reservedIds.has(anchor.id) || seen.has(anchor.id)) return false;
|
||
|
|
if (conditionIds.size && anchor.id !== elseId && !conditionIds.has(anchor.id)) return false;
|
||
|
|
seen.add(anchor.id);
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
const model = computed(() => window.lf?.getNodeModelById(props.id));
|
||
|
|
|
||
|
|
const syncAnchors = (anchors) => {
|
||
|
|
window.lf?.setProperties(props.id, { ...window.lf.getProperties(props.id), anchor: anchors });
|
||
|
|
vueFlow.updateNodeInternals([props.id]);
|
||
|
|
};
|
||
|
|
|
||
|
|
const addAnchor = (height, anchorId) => {
|
||
|
|
const anchors = [...branchAnchors.value];
|
||
|
|
if (!anchors.some((anchor) => anchor.id === anchorId)) {
|
||
|
|
anchors.push({ id: anchorId, name: "right", height });
|
||
|
|
syncAnchors(anchors);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const removeAnchor = (anchorId) => syncAnchors(branchAnchors.value.filter((anchor) => anchor.id !== anchorId));
|
||
|
|
const changeAnchor = (height, anchorId) => syncAnchors(branchAnchors.value.map((anchor) => anchor.id === anchorId ? { ...anchor, height } : anchor));
|
||
|
|
const contentChanged = () => {
|
||
|
|
vueFlow.updateNodeInternals([props.id]);
|
||
|
|
window.lf?.markChanged();
|
||
|
|
};
|
||
|
|
const bindComponent = (instance) => window.lf?.registerComponentInstance(props.id, instance);
|
||
|
|
const addToGroup = (childId, dropCenter) => window.lf?.addToGroup(props.id, childId, dropCenter);
|
||
|
|
|
||
|
|
onMounted(() => bindComponent(componentRef.value));
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<style scoped lang="scss">
|
||
|
|
.flow-node-shell {
|
||
|
|
width: 100%;
|
||
|
|
min-height: 100%;
|
||
|
|
position: relative;
|
||
|
|
box-sizing: border-box;
|
||
|
|
|
||
|
|
&--loop,
|
||
|
|
&--selectArea {
|
||
|
|
height: 100%;
|
||
|
|
}
|
||
|
|
|
||
|
|
&--loop.flow-node-shell--selected .flow-node-content {
|
||
|
|
box-shadow: none;
|
||
|
|
}
|
||
|
|
|
||
|
|
&--selected .flow-node-content {
|
||
|
|
outline: none;
|
||
|
|
border: 2px solid #2f80ed;
|
||
|
|
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.1);
|
||
|
|
border-radius: 8px;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
.flow-node-content {
|
||
|
|
width: 100%;
|
||
|
|
height: 100%;
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
.flow-node-card {
|
||
|
|
width: 100%;
|
||
|
|
height: auto;
|
||
|
|
padding: 12px;
|
||
|
|
background: #fff;
|
||
|
|
border: 1px solid #e4e8ef;
|
||
|
|
border-radius: 8px;
|
||
|
|
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.08);
|
||
|
|
box-sizing: border-box;
|
||
|
|
}
|
||
|
|
|
||
|
|
.flow-handle {
|
||
|
|
width: 14px;
|
||
|
|
height: 14px;
|
||
|
|
border: 3px solid #fff;
|
||
|
|
background: #ff6b35;
|
||
|
|
box-shadow: 0 0 0 1px #ff6b35;
|
||
|
|
z-index: 8;
|
||
|
|
|
||
|
|
&--target { left: -7px; }
|
||
|
|
&--source { right: -7px; }
|
||
|
|
&--branch { transform: translate(50%, -50%); }
|
||
|
|
}
|
||
|
|
|
||
|
|
</style>
|