feat: optimize Vue Flow workflow editor
This commit is contained in:
parent
320d1a9d92
commit
a30903dd70
@ -214,13 +214,11 @@ export const getInputNumber = (nodeId) => {
|
|||||||
|
|
||||||
export const removeSwitchEdge = (nodeId, sourceAnchorId) => {
|
export const removeSwitchEdge = (nodeId, sourceAnchorId) => {
|
||||||
const { edges } = lf.getGraphData();
|
const { edges } = lf.getGraphData();
|
||||||
const edge = edges.find(item => {
|
const matchedEdges = edges.filter(item => {
|
||||||
return item.sourceNodeId === nodeId && item.sourceAnchorId === sourceAnchorId
|
return item.sourceNodeId === nodeId && item.sourceAnchorId === sourceAnchorId
|
||||||
})
|
})
|
||||||
|
|
||||||
if (edge) {
|
matchedEdges.forEach(edge => lf.deleteEdge(edge.id))
|
||||||
lf.deleteEdge(edge.id);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const convertToTree = (data) => {
|
export const convertToTree = (data) => {
|
||||||
|
|||||||
@ -1,76 +1,242 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="component-library">
|
||||||
<el-collapse v-model="activeName" accordion>
|
<div class="library-header">
|
||||||
<el-collapse-item v-for="(item, index) in collapseList" :title="item.collapseTitle" :name="index" class="collapseItem">
|
<div class="library-heading">
|
||||||
<!-- node.type -->
|
<div>
|
||||||
<div v-for="node in item.nodeList" class="node" draggable="true" @dragstart="handleDragCustom($event, node)">
|
<div class="library-title">组件库</div>
|
||||||
<div class="title_container">
|
<div class="library-subtitle">{{ totalCount }} 个可用组件</div>
|
||||||
<img class="title_img" :src="node.icon" alt="">
|
</div>
|
||||||
{{ node.name }}
|
</div>
|
||||||
</div>
|
<el-input v-model="keyword" clearable placeholder="搜索组件" :prefix-icon="Search" />
|
||||||
<div class="node_desc">{{ node.desc }}</div>
|
|
||||||
</div>
|
|
||||||
</el-collapse-item>
|
|
||||||
</el-collapse>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { ref } from 'vue'
|
|
||||||
import { collapseList } from '../config'
|
|
||||||
|
|
||||||
const activeName = ref(0)
|
<div class="library-content">
|
||||||
|
<el-collapse v-if="filteredGroups.length" v-model="activeNames">
|
||||||
|
<el-collapse-item
|
||||||
|
v-for="item in filteredGroups"
|
||||||
|
:key="item.collapseTitle"
|
||||||
|
:name="item.collapseTitle"
|
||||||
|
class="collapse-item"
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<div class="category-title">
|
||||||
|
<span>{{ item.collapseTitle }}</span>
|
||||||
|
<span class="category-count">{{ item.nodeList.length }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="node-list">
|
||||||
|
<div
|
||||||
|
v-for="node in item.nodeList"
|
||||||
|
:key="`${node.type}-${node.action || node.name}`"
|
||||||
|
class="node-item"
|
||||||
|
draggable="true"
|
||||||
|
@dragstart="handleDragCustom($event, node)"
|
||||||
|
>
|
||||||
|
<div class="node-icon"><img :src="node.icon" alt="" /></div>
|
||||||
|
<div class="node-content">
|
||||||
|
<div class="node-name">{{ node.name }}</div>
|
||||||
|
<div class="node-desc">{{ node.desc }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="drag-handle"><Rank /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
<el-empty v-else :image-size="72" description="没有匹配的组件" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, ref, watch } from "vue";
|
||||||
|
import { Rank, Search } from "@element-plus/icons-vue";
|
||||||
|
import { collapseList } from "../config";
|
||||||
|
|
||||||
|
const keyword = ref("");
|
||||||
|
const activeNames = ref(collapseList[0] ? [collapseList[0].collapseTitle] : []);
|
||||||
|
const totalCount = computed(() => collapseList.reduce((total, group) => total + group.nodeList.length, 0));
|
||||||
|
const filteredGroups = computed(() => {
|
||||||
|
const query = keyword.value.trim().toLowerCase();
|
||||||
|
if (!query) return collapseList;
|
||||||
|
return collapseList
|
||||||
|
.map((group) => ({
|
||||||
|
...group,
|
||||||
|
nodeList: group.nodeList.filter((node) => (
|
||||||
|
[node.name, node.desc, node.action, node.type]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some((value) => String(value).toLowerCase().includes(query))
|
||||||
|
)),
|
||||||
|
}))
|
||||||
|
.filter((group) => group.nodeList.length > 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(keyword, (value) => {
|
||||||
|
if (value.trim()) activeNames.value = filteredGroups.value.map((group) => group.collapseTitle);
|
||||||
|
});
|
||||||
|
|
||||||
const handleDragCustom = (event, node) => {
|
const handleDragCustom = (event, node) => {
|
||||||
// event.dataTransfer.setData("nodeType", type);
|
event.dataTransfer.effectAllowed = "copy";
|
||||||
event.dataTransfer.setData("flowNode", JSON.stringify(node));
|
event.dataTransfer.setData("flowNode", JSON.stringify(node));
|
||||||
}
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
:deep(.collapseItem) {
|
.component-library {
|
||||||
.el-collapse-item__header {
|
display: flex;
|
||||||
padding-left: 20px;
|
height: 100%;
|
||||||
font-size: 14px;
|
min-height: 0;
|
||||||
}
|
flex-direction: column;
|
||||||
.el-collapse-item__wrap {
|
color: #1f2937;
|
||||||
background-color: transparent;
|
}
|
||||||
|
|
||||||
.el-collapse-item__content {
|
.library-header {
|
||||||
display: flex;
|
padding: 16px 14px 14px;
|
||||||
flex-direction: column;
|
border-bottom: 1px solid #e4e8ef;
|
||||||
align-items: center;
|
background: #fff;
|
||||||
padding-bottom: 0;
|
}
|
||||||
|
|
||||||
.node {
|
.library-heading {
|
||||||
width: 220px;
|
display: flex;
|
||||||
height: auto;
|
align-items: center;
|
||||||
min-height: 80px;
|
justify-content: space-between;
|
||||||
background: #fffc;
|
margin-bottom: 12px;
|
||||||
border-radius: 8px;
|
}
|
||||||
margin: 12px 0;
|
|
||||||
box-shadow: 0 5px 15px rgba(0,0,0,3%);
|
|
||||||
|
|
||||||
.title_container {
|
.library-title {
|
||||||
display: flex;
|
font-size: 15px;
|
||||||
align-items: center;
|
font-weight: 650;
|
||||||
height: 40px;
|
}
|
||||||
padding: 8px 12px;
|
|
||||||
margin-top: 4px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #0c0d0e;
|
|
||||||
|
|
||||||
.title_img {
|
.library-subtitle {
|
||||||
width: 24px;
|
margin-top: 2px;
|
||||||
margin-right: 12px;
|
color: #8a94a6;
|
||||||
}
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node_desc {
|
.library-content {
|
||||||
padding: 0 12px;
|
min-height: 0;
|
||||||
font-size: 12px;
|
flex: 1;
|
||||||
color: #737a87;
|
overflow-y: auto;
|
||||||
}
|
scrollbar-width: thin;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
:deep(.el-collapse) {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.collapse-item) {
|
||||||
|
.el-collapse-item__header {
|
||||||
|
height: 46px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-bottom-color: #e8ebf0;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-collapse-item__wrap {
|
||||||
|
border-bottom-color: #e8ebf0;
|
||||||
|
background: #f8fafc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-collapse-item__content {
|
||||||
|
padding: 8px 10px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-title {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-count {
|
||||||
|
display: grid;
|
||||||
|
min-width: 22px;
|
||||||
|
height: 20px;
|
||||||
|
padding: 0 6px;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid #dfe4ec;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: #7a8597;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-item {
|
||||||
|
display: grid;
|
||||||
|
min-height: 62px;
|
||||||
|
grid-template-columns: 34px minmax(0, 1fr) 18px;
|
||||||
|
gap: 9px;
|
||||||
|
align-items: center;
|
||||||
|
padding: 9px 8px 9px 10px;
|
||||||
|
border: 1px solid #e1e6ed;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: grab;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #f2a071;
|
||||||
|
box-shadow: 0 5px 14px rgba(31, 41, 55, 0.08);
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active { cursor: grabbing; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon {
|
||||||
|
display: grid;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-content { min-width: 0; }
|
||||||
|
|
||||||
|
.node-name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #202938;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-desc {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #7a8597;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
color: #a8b0bf;
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -137,8 +137,8 @@ const { formType, currentList, propPath, depth, isFirstLevel, endDepth, parentPa
|
|||||||
default: 2
|
default: 2
|
||||||
},
|
},
|
||||||
parentPath: {
|
parentPath: {
|
||||||
type: String,
|
type: Array,
|
||||||
default: ''
|
default: () => []
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,176 +1,185 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="showState !== 'NORMAL'" class="node-state" :class="`node-state--${showState.toLowerCase()}`">
|
||||||
<div class="state__container" :class="`nodeState-${props.state}`">
|
<div class="node-state__summary">
|
||||||
<div class="context" v-show="showState !== 'NORMAL'">
|
<el-icon class="node-state__icon">
|
||||||
<div class="state">
|
<SuccessFilled v-if="props.state === 'SUCCESS'" />
|
||||||
<el-icon>
|
<CircleCloseFilled v-else-if="['FAILED', 'STOPPED'].includes(props.state)" />
|
||||||
<SuccessFilled v-if="props.state === 'SUCCESS'" />
|
<VideoPause v-else-if="props.state === 'PAUSED'" />
|
||||||
<CircleCloseFilled v-if="['FAILED', 'STOPPED'].includes(props.state)" />
|
<Loading v-else />
|
||||||
<VideoPause v-if="props.state === 'PAUSED'" />
|
</el-icon>
|
||||||
<Loading v-if="props.state === 'RUNNING'" />
|
<span class="node-state__text">{{ stateText }}</span>
|
||||||
</el-icon>
|
<span class="node-state__runtime">{{ props.runtimes }} ms</span>
|
||||||
<div class="text">{{ getStateText() }}</div>
|
|
||||||
<el-tag :type="props.state === 'SUCCESS' ? 'success' : 'danger'">
|
|
||||||
{{ props.runtimes }}ms
|
|
||||||
</el-tag>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="resultText"
|
|
||||||
v-popover="popoverRef"
|
|
||||||
@click="popoverVisible = !popoverVisible"
|
|
||||||
>
|
|
||||||
{{ popoverVisible ? "隐藏结果" : "展示结果" }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<el-popover
|
<el-popover
|
||||||
ref="popoverRef"
|
ref="popoverRef"
|
||||||
virtual-triggering
|
trigger="click"
|
||||||
persistent
|
|
||||||
placement="right-start"
|
placement="right-start"
|
||||||
:visible="popoverVisible"
|
:width="480"
|
||||||
:teleported="false"
|
:teleported="true"
|
||||||
popper-class="popover__container"
|
popper-class="flow-result-popover"
|
||||||
|
@show="popoverVisible = true"
|
||||||
|
@hide="popoverVisible = false"
|
||||||
>
|
>
|
||||||
<template #default>
|
<template #reference>
|
||||||
<div class="params__container input">
|
<button type="button" class="node-state__result nodrag nopan" @mousedown.stop @click.stop>
|
||||||
<div class="paramsTitle">输入</div>
|
<el-icon><View /></el-icon>
|
||||||
<div>
|
<span>{{ popoverVisible ? "收起结果" : "查看结果" }}</span>
|
||||||
<slot name="input"></slot>
|
</button>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="params__container output" v-if="slots.output">
|
|
||||||
<div class="paramsTitle">输出</div>
|
|
||||||
<div>
|
|
||||||
<slot name="output"></slot>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<div class="result-section">
|
||||||
|
<div class="result-section__title">输入</div>
|
||||||
|
<div class="result-section__content"><slot name="input" /></div>
|
||||||
|
</div>
|
||||||
|
<div v-if="slots.output" class="result-section result-section--output">
|
||||||
|
<div class="result-section__title">输出</div>
|
||||||
|
<div class="result-section__content"><slot name="output" /></div>
|
||||||
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="js">
|
|
||||||
import { ref, useSlots, watch } from 'vue'
|
<script setup>
|
||||||
import { SuccessFilled, CircleCloseFilled, Loading, VideoPause } from '@element-plus/icons-vue'
|
import { computed, ref, useSlots, watch } from "vue";
|
||||||
|
import { CircleCloseFilled, Loading, SuccessFilled, VideoPause, View } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
state: {
|
state: { type: String, default: "NORMAL" },
|
||||||
type: String,
|
runtimes: { type: Number, default: 0 },
|
||||||
default: 'NORMAL'
|
});
|
||||||
},
|
|
||||||
runtimes: {
|
|
||||||
type: Number,
|
|
||||||
default: 0
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const slots = useSlots()
|
|
||||||
|
|
||||||
const getStateText = () => {
|
|
||||||
const map = {
|
|
||||||
SUCCESS: '成功',
|
|
||||||
RUNNING: '运行中',
|
|
||||||
FAILED: '失败',
|
|
||||||
PAUSED: '已暂停',
|
|
||||||
STOPPED: '已停止',
|
|
||||||
NORMAL: ''
|
|
||||||
}
|
|
||||||
return map[props.state]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
const popoverRef = ref();
|
const popoverRef = ref();
|
||||||
const popoverVisible = ref(false);
|
const popoverVisible = ref(false);
|
||||||
|
const showState = ref("NORMAL");
|
||||||
|
const stateText = computed(() => ({
|
||||||
|
SUCCESS: "执行成功",
|
||||||
|
RUNNING: "执行中",
|
||||||
|
FAILED: "执行失败",
|
||||||
|
PAUSED: "已暂停",
|
||||||
|
STOPPED: "已停止",
|
||||||
|
NORMAL: "",
|
||||||
|
}[props.state] || props.state));
|
||||||
|
|
||||||
const showState = ref('NORMAL')
|
watch(() => props.state, (value) => {
|
||||||
|
showState.value = value;
|
||||||
watch(() => props.state, (newVal) => {
|
|
||||||
showState.value = newVal;
|
|
||||||
}, { immediate: true });
|
}, { immediate: true });
|
||||||
|
|
||||||
const closePopover = () => {
|
const closePopover = () => {
|
||||||
showState.value = 'NORMAL';
|
showState.value = "NORMAL";
|
||||||
popoverVisible.value = false;
|
popoverVisible.value = false;
|
||||||
|
popoverRef.value?.hide?.();
|
||||||
};
|
};
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.state__container {
|
.node-state {
|
||||||
display: none;
|
display: flex;
|
||||||
margin-bottom: 8px;
|
min-height: 34px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
margin: -4px -4px 10px;
|
||||||
|
padding: 5px 7px 5px 9px;
|
||||||
|
border: 1px solid #dbe3ee;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #f7f9fc;
|
||||||
|
color: #526071;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.context {
|
.node-state__summary {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
min-width: 0;
|
||||||
justify-content: space-between;
|
align-items: center;
|
||||||
padding: 4px;
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-state__icon { font-size: 16px; }
|
||||||
|
.node-state__text { font-weight: 600; white-space: nowrap; }
|
||||||
|
|
||||||
|
.node-state__runtime {
|
||||||
|
padding-left: 6px;
|
||||||
|
border-left: 1px solid currentColor;
|
||||||
|
opacity: 0.72;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-state__result {
|
||||||
|
display: flex;
|
||||||
|
height: 26px;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 7px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
color: inherit;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-state--success {
|
||||||
|
border-color: #c8ead5;
|
||||||
|
background: #effaf3;
|
||||||
|
color: #1e7d49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-state--failed,
|
||||||
|
.node-state--stopped {
|
||||||
|
border-color: #f1cecb;
|
||||||
|
background: #fff3f2;
|
||||||
|
color: #c33b32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-state--paused {
|
||||||
|
border-color: #d9dee7;
|
||||||
|
background: #f3f5f8;
|
||||||
|
color: #657084;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-state--running {
|
||||||
|
border-color: #cbdcf8;
|
||||||
|
background: #f0f6ff;
|
||||||
|
color: #2f6fca;
|
||||||
|
|
||||||
|
.node-state__icon { animation: rotate-animation 1.2s linear infinite; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rotate-animation {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.flow-result-popover {
|
||||||
|
max-width: min(480px, calc(100vw - 32px));
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.result-section {
|
||||||
|
padding: 14px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section--output { border-top: 1px solid #e8ebf0; }
|
||||||
|
|
||||||
|
.result-section__title {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-section__content {
|
||||||
|
max-height: 280px;
|
||||||
|
overflow: auto;
|
||||||
|
color: #475467;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|
||||||
.state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resultText {
|
|
||||||
color: #1664ff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-SUCCESS {
|
|
||||||
display: block;
|
|
||||||
background-color: #eef9f1;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #309256;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-FAILED {
|
|
||||||
display: block;
|
|
||||||
background-color: #fdf5f5;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #ee3f38;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-STOPPED {
|
|
||||||
display: block;
|
|
||||||
background-color: #fdf5f5;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #ee3f38;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-PAUSED {
|
|
||||||
display: block;
|
|
||||||
background-color: #cbcbcb;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-RUNNING {
|
|
||||||
display: block;
|
|
||||||
background-color: #f4f7ff;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
color: #68a2e4;
|
|
||||||
font-size: 16px;
|
|
||||||
animation: rotateAnimation 2s linear infinite;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,271 +1,299 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="title__container">
|
<div class="node-title">
|
||||||
<div class="title">
|
<div class="node-title__main">
|
||||||
<div class="left">
|
<div class="node-title__identity">
|
||||||
<img :src="imageUrl()" alt="" />
|
<div class="node-title__icon">
|
||||||
<div class="text__container">
|
<img v-if="props.icon" :src="props.icon" alt="" />
|
||||||
<div class="text__title" v-if="showTextTitle">
|
<el-icon v-else><Operation /></el-icon>
|
||||||
<span class="text">{{ localName }}</span>
|
</div>
|
||||||
<el-icon class="editIcon" @click="startEditing"><EditPen/></el-icon>
|
|
||||||
|
<div class="node-title__text">
|
||||||
|
<div v-if="!isEditing" class="node-title__name-row">
|
||||||
|
<span class="node-title__name" :title="localName">{{ localName }}</span>
|
||||||
|
<span v-if="typeLabel" class="node-title__type">{{ typeLabel }}</span>
|
||||||
|
<el-tooltip content="重命名" placement="top">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="node-icon-button node-title__edit nodrag nopan"
|
||||||
|
aria-label="重命名节点"
|
||||||
|
@mousedown.stop
|
||||||
|
@click.stop="startEditing"
|
||||||
|
>
|
||||||
|
<el-icon><EditPen /></el-icon>
|
||||||
|
</button>
|
||||||
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="input_name_container" v-else >
|
|
||||||
|
<div v-else class="node-title__editor nodrag nopan" @mousedown.stop @click.stop>
|
||||||
<el-input
|
<el-input
|
||||||
|
ref="nameInputRef"
|
||||||
v-model="localName"
|
v-model="localName"
|
||||||
@focus="handleInputFocus"
|
maxlength="40"
|
||||||
@keydown="handleInputKeydown"
|
@keydown.enter.prevent="confirmNodeName"
|
||||||
@click.stop
|
@keydown.esc.prevent="cancelNodeName"
|
||||||
@mousedown.stop
|
/>
|
||||||
/>
|
<button type="button" class="node-icon-button is-confirm" aria-label="确认名称" @click="confirmNodeName">
|
||||||
<el-button
|
|
||||||
class="input_name_select"
|
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
@click="confirmNodeName"
|
|
||||||
>
|
|
||||||
<el-icon><Select /></el-icon>
|
<el-icon><Select /></el-icon>
|
||||||
</el-button>
|
</button>
|
||||||
<el-button
|
<button type="button" class="node-icon-button" aria-label="取消重命名" @click="cancelNodeName">
|
||||||
class="input_name_closeBold"
|
|
||||||
circle
|
|
||||||
size="small"
|
|
||||||
@click="cancelNodeName"
|
|
||||||
>
|
|
||||||
<el-icon><CloseBold /></el-icon>
|
<el-icon><CloseBold /></el-icon>
|
||||||
</el-button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="right">
|
|
||||||
<el-tooltip
|
<div class="node-title__actions nodrag nopan" @mousedown.stop>
|
||||||
class="box-item"
|
<el-tooltip v-if="canExecute" content="执行当前节点" placement="top">
|
||||||
effect="dark"
|
<button type="button" class="node-icon-button is-run" aria-label="执行当前节点" @click.stop="execute">
|
||||||
content="执行"
|
<el-icon><VideoPlay /></el-icon>
|
||||||
placement="top"
|
</button>
|
||||||
>
|
|
||||||
<el-button v-if="nodeType && nodeType !=='NONE'" circle @click="execute">
|
|
||||||
<el-icon>
|
|
||||||
<VideoPlay />
|
|
||||||
</el-icon>
|
|
||||||
</el-button>
|
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-tooltip
|
<el-tooltip :content="isSelectArea ? '解散分组' : '删除节点'" placement="top">
|
||||||
class="box-item"
|
<button
|
||||||
effect="dark"
|
type="button"
|
||||||
content="删除"
|
class="node-icon-button is-danger"
|
||||||
placement="top"
|
:aria-label="isSelectArea ? '解散分组' : '删除节点'"
|
||||||
>
|
@click.stop="deleteNode"
|
||||||
<el-button circle @click="deleteNode">
|
>
|
||||||
<el-icon><Delete /></el-icon>
|
<el-icon><FolderRemove v-if="isSelectArea" /><Delete v-else /></el-icon>
|
||||||
</el-button>
|
</button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subTitle">{{ props.nodeDesc }}</div>
|
|
||||||
|
<div v-if="props.nodeDesc" class="node-title__description" :title="props.nodeDesc">
|
||||||
|
{{ props.nodeDesc }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="js">
|
|
||||||
import { onUnmounted, ref } from 'vue'
|
<script setup>
|
||||||
import { EditPen, Select, CloseBold, Delete, VideoPlay } from '@element-plus/icons-vue'
|
import { computed, nextTick, onMounted, ref, watch } from "vue";
|
||||||
import { ElMessageBox } from 'element-plus'
|
import { CloseBold, Delete, EditPen, FolderRemove, Operation, Select, VideoPlay } from "@element-plus/icons-vue";
|
||||||
import { computed } from 'vue'
|
import { ElMessageBox } from "element-plus";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
icon: {
|
icon: { type: String, default: "" },
|
||||||
type: String,
|
nodeId: { type: String, default: "" },
|
||||||
default: ''
|
nodeProperties: { type: Object, default: () => ({}) },
|
||||||
},
|
nodeDesc: { type: String, default: "" },
|
||||||
nodeId: {
|
nodeName: { type: String, default: "" },
|
||||||
type: String,
|
nodeType: { type: String, default: "" },
|
||||||
default: ''
|
});
|
||||||
},
|
|
||||||
nodeProperties: {
|
|
||||||
type: Object,
|
|
||||||
default: () => {}
|
|
||||||
},
|
|
||||||
nodeDesc: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
nodeName: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
nodeType: {
|
|
||||||
type: String,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const emits = defineEmits(['setNodeName'])
|
const emits = defineEmits(["setNodeName"]);
|
||||||
|
const isEditing = ref(false);
|
||||||
|
const localName = ref("");
|
||||||
|
const nameInputRef = ref();
|
||||||
|
const isSelectArea = computed(() => props.nodeType === "selectArea");
|
||||||
|
const canExecute = computed(() => Boolean(props.nodeType) && !["NONE", "selectArea"].includes(props.nodeType));
|
||||||
|
const typeLabel = computed(() => {
|
||||||
|
if (!props.nodeType || ["NONE", "selectArea"].includes(props.nodeType)) return "";
|
||||||
|
return { EDGE: "设备", LLM: "模型", AE: "评估" }[props.nodeType] || props.nodeType;
|
||||||
|
});
|
||||||
|
|
||||||
const showTextTitle = ref(true)
|
const sourceName = () => props.nodeProperties?.name || props.nodeName || "未命名节点";
|
||||||
const localName = ref('')
|
|
||||||
|
|
||||||
// 开始编辑
|
watch(sourceName, (value) => {
|
||||||
const startEditing = () => {
|
if (!isEditing.value) localName.value = value;
|
||||||
showTextTitle.value = false
|
});
|
||||||
}
|
|
||||||
|
const startEditing = async () => {
|
||||||
|
isEditing.value = true;
|
||||||
|
await nextTick();
|
||||||
|
nameInputRef.value?.focus?.();
|
||||||
|
nameInputRef.value?.select?.();
|
||||||
|
};
|
||||||
|
|
||||||
const confirmNodeName = () => {
|
const confirmNodeName = () => {
|
||||||
emits('setNodeName', localName.value)
|
const name = localName.value.trim();
|
||||||
showTextTitle.value = true
|
if (!name) return;
|
||||||
}
|
localName.value = name;
|
||||||
|
emits("setNodeName", name);
|
||||||
|
isEditing.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
const cancelNodeName = () => {
|
const cancelNodeName = () => {
|
||||||
localName.value = props.nodeProperties?.name || props.nodeName
|
localName.value = sourceName();
|
||||||
showTextTitle.value = true
|
isEditing.value = false;
|
||||||
}
|
};
|
||||||
|
|
||||||
const deleteNode = () => {
|
const deleteNode = async () => {
|
||||||
ElMessageBox.confirm(
|
try {
|
||||||
'是否删除该节点?',
|
await ElMessageBox.confirm(
|
||||||
'警告',
|
isSelectArea.value
|
||||||
{
|
? "解散后内部节点和连线会保留,是否继续?"
|
||||||
confirmButtonText: '确定',
|
: "删除后相关连线也会一并移除,是否继续?",
|
||||||
cancelButtonText: '取消',
|
isSelectArea.value ? "解散分组" : "删除节点",
|
||||||
type: 'warning',
|
{
|
||||||
}
|
confirmButtonText: isSelectArea.value ? "解散" : "删除",
|
||||||
)
|
cancelButtonText: "取消",
|
||||||
.then(() => {
|
confirmButtonClass: "el-button--danger",
|
||||||
if (props.nodeType === 'selectArea') {
|
type: "warning",
|
||||||
const children = lf.getNodeModelById(props.nodeId).children
|
});
|
||||||
lf.getNodeModelById(props.nodeId).children = []
|
if (isSelectArea.value) window.lf?.ungroup(props.nodeId);
|
||||||
children.forEach(item => {
|
else window.lf?.deleteNode(props.nodeId);
|
||||||
lf.getNodeModelById(item).draggable = true
|
emitter.emit("deleteNode", props.nodeId);
|
||||||
})
|
} catch (error) {
|
||||||
}
|
// Cancelled by the user.
|
||||||
lf.deleteNode(props.nodeId);
|
}
|
||||||
emitter.emit('deleteNode', props.nodeId);
|
};
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const execute = () => {
|
const execute = () => {
|
||||||
const myEvent = new CustomEvent('singleNodeExecution', {
|
document.dispatchEvent(new CustomEvent("singleNodeExecution", {
|
||||||
bubbles: false, // 是否冒泡(默认 false)
|
bubbles: false,
|
||||||
cancelable: false, // 是否可被 preventDefault() 取消(默认 false)
|
cancelable: false,
|
||||||
detail: { ...props.nodeProperties } // 自定义数据(任意类型,通过 event.detail 读取)
|
detail: { ...props.nodeProperties },
|
||||||
});
|
}));
|
||||||
document.dispatchEvent(myEvent);
|
};
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputFocus = (e) => {
|
|
||||||
// 阻止事件冒泡到流程画布
|
|
||||||
e.stopPropagation()
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleInputKeydown = (e) => {
|
|
||||||
// 阻止事件冒泡到流程节点,避免被画布快捷键拦截
|
|
||||||
e.stopPropagation();
|
|
||||||
// 可选:明确放行 Ctrl+V(增强兼容性)
|
|
||||||
if ((e.ctrlKey || e.metaKey) && e.key === "v") {
|
|
||||||
e.returnValue = true; // 允许默认粘贴行为
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageUrl = () => {
|
|
||||||
return new URL(props.icon, import.meta.url).href;
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
localName.value = props.nodeProperties?.name || props.nodeName
|
localName.value = sourceName();
|
||||||
})
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("deleteNode");
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.title__container {
|
.node-title { min-width: 0; }
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.left {
|
.node-title__main {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center; // 添加垂直居中
|
min-height: 38px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
img {
|
.node-title__identity {
|
||||||
width: 24px;
|
display: flex;
|
||||||
height: 24px;
|
min-width: 0;
|
||||||
}
|
flex: 1;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.text__container {
|
.node-title__icon {
|
||||||
.text__title {
|
display: grid;
|
||||||
display: flex;
|
width: 36px;
|
||||||
align-items: center;
|
height: 36px;
|
||||||
|
flex: 0 0 36px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: var(--node-accent, #475569);
|
||||||
|
|
||||||
.text {
|
img {
|
||||||
font-size: 16px;
|
width: 22px;
|
||||||
font-weight: bold;
|
height: 22px;
|
||||||
margin: 0 12px;
|
object-fit: contain;
|
||||||
}
|
|
||||||
|
|
||||||
.editIcon {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input_name_container {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.el-input {
|
|
||||||
margin-left: 12px;
|
|
||||||
width: 120px;
|
|
||||||
// 确保输入框有更高的 z-index
|
|
||||||
position: relative;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
// 覆盖 Element Plus 的默认样式,确保焦点状态明显
|
|
||||||
:deep(.el-input__wrapper) {
|
|
||||||
box-shadow: 0 0 0 1px #409eff !important;
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
box-shadow: 0 0 0 2px #409eff !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.input_name_select {
|
|
||||||
color: #00b42a;
|
|
||||||
margin-left: 4px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input_name_closeBold {
|
|
||||||
color: #f53f3f;
|
|
||||||
margin-left: 4px;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
.el-button {
|
|
||||||
border: none;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
color: #737a87;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加全局样式,确保输入框焦点不被其他元素干扰
|
.node-title__text { min-width: 0; flex: 1; }
|
||||||
:global(.lf-node) {
|
|
||||||
.el-input__inner {
|
.node-title__name-row {
|
||||||
user-select: text !important;
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title__name {
|
||||||
|
overflow: hidden;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 22px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title__type {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border: 1px solid #dfe4ec;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #697386;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title__edit {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
flex: 0 0 24px;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title__actions {
|
||||||
|
display: flex;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 4px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.16s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title:hover,
|
||||||
|
.node-title:focus-within {
|
||||||
|
.node-title__actions,
|
||||||
|
.node-title__edit { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-icon-button {
|
||||||
|
display: grid;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: #7b8494;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #dfe4ec;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #344054;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-run:hover {
|
||||||
|
border-color: #b7dfc7;
|
||||||
|
background: #effaf3;
|
||||||
|
color: #168447;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger:hover {
|
||||||
|
border-color: #f2c5c2;
|
||||||
|
background: #fff3f2;
|
||||||
|
color: #d93025;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-confirm { color: #168447; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title__editor {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(100px, 1fr) 28px 28px;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-title__description {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #7a8494;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: none) {
|
||||||
|
.node-title__actions,
|
||||||
|
.node-title__edit { opacity: 1; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,9 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="props.drawer"
|
:model-value="props.drawer"
|
||||||
custom-class="custom-drawer-header"
|
custom-class="flow-params-drawer"
|
||||||
:title="props.data.properties?.name || '参数配置'"
|
:title="props.data.properties?.name || '参数配置'"
|
||||||
class="no-header-margin"
|
class="flow-params-drawer"
|
||||||
|
size="min(560px, 92vw)"
|
||||||
|
append-to-body
|
||||||
|
destroy-on-close
|
||||||
|
:close-on-click-modal="false"
|
||||||
:before-close="confirmSave"
|
:before-close="confirmSave"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
@ -17,7 +21,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { emitter } from '@/utils/eventBus'
|
import { emitter } from '@/utils/eventBus'
|
||||||
|
|
||||||
// 导入各子组件
|
// 导入各子组件
|
||||||
@ -52,6 +56,8 @@ const currentComponent = computed(() => {
|
|||||||
const confirmSave = async () => {
|
const confirmSave = async () => {
|
||||||
if (paramsComponentRef.value && typeof paramsComponentRef.value.validateAndSave === 'function') {
|
if (paramsComponentRef.value && typeof paramsComponentRef.value.validateAndSave === 'function') {
|
||||||
await paramsComponentRef.value.validateAndSave()
|
await paramsComponentRef.value.validateAndSave()
|
||||||
|
} else {
|
||||||
|
emits('close')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,7 +83,35 @@ const handleSaveError = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.no-header-margin .el-drawer__header {
|
.flow-params-drawer {
|
||||||
margin-bottom: 0 !important;
|
.el-drawer__header {
|
||||||
|
min-height: 62px;
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
padding: 0 20px;
|
||||||
|
border-bottom: 1px solid #e4e8ef;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer__body {
|
||||||
|
padding: 0 20px 24px;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-collapse {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-collapse-item__header {
|
||||||
|
height: 50px;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form__container {
|
||||||
|
padding: 4px 0 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-drawer
|
<el-drawer
|
||||||
v-model="props.drawer"
|
:model-value="props.drawer"
|
||||||
title="试运行"
|
title="试运行"
|
||||||
:before-close="handleClose"
|
:before-close="handleClose"
|
||||||
>
|
>
|
||||||
@ -60,7 +60,7 @@
|
|||||||
</el-drawer>
|
</el-drawer>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch, onUnmounted } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { getStartNodeFormData, formatTableData } from '@/utils/flow'
|
import { getStartNodeFormData, formatTableData } from '@/utils/flow'
|
||||||
// import { Plus, Minus } from '@element-plus/icons-vue'
|
// import { Plus, Minus } from '@element-plus/icons-vue'
|
||||||
import { flowExecuteTrial } from '@/api/device/flow'
|
import { flowExecuteTrial } from '@/api/device/flow'
|
||||||
@ -80,7 +80,7 @@ const tableData = ref([])
|
|||||||
const emits = defineEmits(['close', 'changeState'])
|
const emits = defineEmits(['close', 'changeState'])
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
ruleFormRef.value.resetFields()
|
ruleFormRef.value?.resetFields()
|
||||||
emits('close')
|
emits('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,9 +120,7 @@ const confirm = () => {
|
|||||||
ElMessage.error(res.msg)
|
ElMessage.error(res.msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch(() => {})
|
||||||
console.log(err)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const terminalIdOptions = ref([])
|
const terminalIdOptions = ref([])
|
||||||
@ -140,7 +138,4 @@ const handleGetTerminalGroup = async (row) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off('contentChange')
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
@ -203,7 +203,6 @@ const initData = () => {
|
|||||||
...transformedData,
|
...transformedData,
|
||||||
outputParams
|
outputParams
|
||||||
};
|
};
|
||||||
console.log('RecognizeNodeData', JSON.stringify(RecognizeNodeData.value))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
@ -230,7 +230,6 @@ const initData = () => {
|
|||||||
...transformedData,
|
...transformedData,
|
||||||
outputParams
|
outputParams
|
||||||
};
|
};
|
||||||
console.log('sdAgentNodeData', sdAgentNodeData.value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
|
|||||||
18
src/views/flow/composables/useFlowEvents.js
Normal file
18
src/views/flow/composables/useFlowEvents.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { onUnmounted } from "vue";
|
||||||
|
import { emitter } from "@/utils/eventBus";
|
||||||
|
|
||||||
|
export const useFlowEvents = () => {
|
||||||
|
const listeners = [];
|
||||||
|
|
||||||
|
const onFlowEvent = (type, handler) => {
|
||||||
|
emitter.on(type, handler);
|
||||||
|
listeners.push([type, handler]);
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
listeners.forEach(([type, handler]) => emitter.off(type, handler));
|
||||||
|
listeners.length = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return { onFlowEvent };
|
||||||
|
};
|
||||||
@ -2,23 +2,38 @@
|
|||||||
<div class="page">
|
<div class="page">
|
||||||
<el-container class="page__container">
|
<el-container class="page__container">
|
||||||
<el-header v-if="!flowInfoData.isLog">
|
<el-header v-if="!flowInfoData.isLog">
|
||||||
<div>
|
<div class="flow-heading">
|
||||||
<div>{{ flowInfoData.name }}</div>
|
<div class="flow-heading__name">{{ flowInfoData.name || "未命名流程" }}</div>
|
||||||
<div>{{ stateMap[flowState] }}</div>
|
<div class="flow-status" :class="`flow-status--${flowState}`">
|
||||||
|
<span class="flow-status__dot"></span>
|
||||||
|
<span>{{ stateMap[flowState] }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn__container">
|
<div class="btn__container">
|
||||||
<div class="btn">
|
<div class="btn flow-actions">
|
||||||
<el-tag style="margin-right: 12px;" v-if="['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
|
<el-tag v-if="hasRunResult && ['testRunError', 'testRunFinish'].includes(flowState)" :type="flowState === 'testRunFinish' ? 'success' : 'danger'">
|
||||||
{{ totalRunningTime }}ms
|
{{ totalRunningTime }}ms
|
||||||
</el-tag>
|
</el-tag>
|
||||||
<el-button v-if="['testRunError', 'testRunFinish', 'stop'].includes(flowState)" @click="clearRun">清除上一次运行结果</el-button>
|
<el-button v-if="hasRunResult" @click="clearRun">
|
||||||
<el-button @click="group">分组</el-button>
|
<el-icon><Delete /></el-icon>清除结果
|
||||||
<el-button v-if="flowState !== 'testRunning'" @click="testRun">试运行</el-button>
|
</el-button>
|
||||||
<el-button v-if="flowState === 'testRunning'" @click="flowPauseFn">暂停</el-button>
|
<el-button :type="groupSelectionPending ? 'primary' : 'default'" @click="group">
|
||||||
<el-button v-if="flowState === 'pause'" @click="flowResumeFn">恢复</el-button>
|
<el-icon><CollectionTag /></el-icon>{{ groupSelectionPending ? "取消分组" : "分组" }}
|
||||||
<el-button v-if="['testRunning', 'pause'].includes(flowState)" @click="flowStopFn">终止</el-button>
|
</el-button>
|
||||||
|
<el-button v-if="flowState !== 'testRunning'" @click="testRun">
|
||||||
|
<el-icon><VideoPlay /></el-icon>试运行
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="flowState === 'testRunning'" @click="flowPauseFn">
|
||||||
|
<el-icon><VideoPause /></el-icon>暂停
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="flowState === 'pause'" @click="flowResumeFn">
|
||||||
|
<el-icon><VideoPlay /></el-icon>恢复
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="['testRunning', 'pause'].includes(flowState)" @click="flowStopFn">
|
||||||
|
<el-icon><CircleClose /></el-icon>终止
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn">
|
<div class="btn publish-actions">
|
||||||
<el-tooltip
|
<el-tooltip
|
||||||
class="box-item"
|
class="box-item"
|
||||||
effect="dark"
|
effect="dark"
|
||||||
@ -26,9 +41,9 @@
|
|||||||
placement="top"
|
placement="top"
|
||||||
v-if="flowState !== 'testRunFinish'"
|
v-if="flowState !== 'testRunFinish'"
|
||||||
>
|
>
|
||||||
<el-button type="primary" :disabled="true">发布</el-button>
|
<el-button type="primary" :disabled="true"><el-icon><Upload /></el-icon>发布</el-button>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
<el-button v-else @click="deployFlow" type="primary">发布</el-button>
|
<el-button v-else @click="deployFlow" type="primary"><el-icon><Upload /></el-icon>发布</el-button>
|
||||||
|
|
||||||
<el-popover
|
<el-popover
|
||||||
:width="260"
|
:width="260"
|
||||||
@ -36,9 +51,7 @@
|
|||||||
popper-class="node-relationship-network-popover"
|
popper-class="node-relationship-network-popover"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<el-button style="margin-right: 16px"
|
<el-button circle title="节点关系" aria-label="节点关系"><el-icon><Share /></el-icon></el-button>
|
||||||
><el-icon><Location /></el-icon
|
|
||||||
></el-button>
|
|
||||||
</template>
|
</template>
|
||||||
<div>
|
<div>
|
||||||
<div class="title">节点关系网</div>
|
<div class="title">节点关系网</div>
|
||||||
@ -71,6 +84,18 @@
|
|||||||
<Aside />
|
<Aside />
|
||||||
</el-aside>
|
</el-aside>
|
||||||
<el-main>
|
<el-main>
|
||||||
|
<div class="canvas-toolbar">
|
||||||
|
<el-tooltip v-if="!flowStore.disableForm" content="撤销" placement="bottom">
|
||||||
|
<el-button aria-label="撤销" @click="lf.undo()"><el-icon><RefreshLeft /></el-icon></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<el-tooltip v-if="!flowStore.disableForm" content="重做" placement="bottom">
|
||||||
|
<el-button aria-label="重做" @click="lf.redo()"><el-icon><RefreshRight /></el-icon></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
<span v-if="!flowStore.disableForm" class="canvas-toolbar__divider"></span>
|
||||||
|
<el-tooltip content="适应视图" placement="bottom">
|
||||||
|
<el-button aria-label="适应视图" @click="fitView"><el-icon><FullScreen /></el-icon></el-button>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
<VueFlow
|
<VueFlow
|
||||||
id="main-flow"
|
id="main-flow"
|
||||||
class="flow__container"
|
class="flow__container"
|
||||||
@ -82,9 +107,10 @@
|
|||||||
:nodes-connectable="!flowStore.disableForm"
|
:nodes-connectable="!flowStore.disableForm"
|
||||||
:edges-updatable="!flowStore.disableForm"
|
:edges-updatable="!flowStore.disableForm"
|
||||||
:elements-selectable="true"
|
:elements-selectable="true"
|
||||||
|
:elevate-nodes-on-select="false"
|
||||||
:delete-key-code="null"
|
:delete-key-code="null"
|
||||||
:multi-selection-key-code="['Control', 'Meta']"
|
:multi-selection-key-code="['Control', 'Meta']"
|
||||||
:selection-key-code="groupSelectionPending ? true : 'Shift'"
|
:selection-key-code="groupSelectionPending"
|
||||||
:pan-on-drag="!groupSelectionPending"
|
:pan-on-drag="!groupSelectionPending"
|
||||||
:zoom-on-double-click="false"
|
:zoom-on-double-click="false"
|
||||||
:default-edge-options="defaultEdgeOptions"
|
:default-edge-options="defaultEdgeOptions"
|
||||||
@ -122,7 +148,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<ParamsDrawer
|
<ParamsDrawer
|
||||||
:drawer="showParamsDrawer"
|
v-if="showParamsDrawer"
|
||||||
|
:drawer="true"
|
||||||
:data="paramsDrawerData"
|
:data="paramsDrawerData"
|
||||||
@close="showParamsDrawer = false"
|
@close="showParamsDrawer = false"
|
||||||
/>
|
/>
|
||||||
@ -213,13 +240,14 @@ import "@vue-flow/controls/dist/style.css";
|
|||||||
import FlowNode from "./vue-flow/FlowNode.vue";
|
import FlowNode from "./vue-flow/FlowNode.vue";
|
||||||
import FlowEdge from "./vue-flow/FlowEdge.vue";
|
import FlowEdge from "./vue-flow/FlowEdge.vue";
|
||||||
import { createFlowFacade } from "./vue-flow/adapter";
|
import { createFlowFacade } from "./vue-flow/adapter";
|
||||||
|
import { useFlowEvents } from "./composables/useFlowEvents";
|
||||||
import TestRun from "./components/TestRun.vue";
|
import TestRun from "./components/TestRun.vue";
|
||||||
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
import ParamsDrawer from "./components/ParamsDrawer.vue";
|
||||||
import Aside from "./components/Aside.vue";
|
import Aside from "./components/Aside.vue";
|
||||||
import { ElMessage } from "element-plus";
|
import { ElMessage } from "element-plus";
|
||||||
import { useFlowStore } from "@/store/modules/flow";
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import { recursiveFilter, newEdge, convertToTree } from "@/utils/flow";
|
import { convertToTree } from "@/utils/flow";
|
||||||
import {
|
import {
|
||||||
flowDeploy,
|
flowDeploy,
|
||||||
flowView,
|
flowView,
|
||||||
@ -229,13 +257,26 @@ import {
|
|||||||
flowAction,
|
flowAction,
|
||||||
} from "@/api/device/flow";
|
} from "@/api/device/flow";
|
||||||
import { getDetect } from "@/api/test/detect";
|
import { getDetect } from "@/api/test/detect";
|
||||||
import { Location } from "@element-plus/icons-vue";
|
import {
|
||||||
|
CircleClose,
|
||||||
|
CollectionTag,
|
||||||
|
Delete,
|
||||||
|
FullScreen,
|
||||||
|
Location,
|
||||||
|
RefreshLeft,
|
||||||
|
RefreshRight,
|
||||||
|
Share,
|
||||||
|
Upload,
|
||||||
|
VideoPause,
|
||||||
|
VideoPlay,
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
const flowContainerRef = ref(null);
|
const flowContainerRef = ref(null);
|
||||||
const lfRef = ref(null);
|
const lfRef = ref(null);
|
||||||
const vueFlow = useVueFlow("main-flow");
|
const vueFlow = useVueFlow("main-flow");
|
||||||
const lf = createFlowFacade(vueFlow);
|
const lf = createFlowFacade(vueFlow);
|
||||||
const loopChildDragContexts = new Map();
|
const loopChildDragContexts = new Map();
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
window.lf = lf;
|
window.lf = lf;
|
||||||
|
|
||||||
const defaultEdgeOptions = {
|
const defaultEdgeOptions = {
|
||||||
@ -372,9 +413,6 @@ const handleNodeDragStop = (payload) => {
|
|||||||
if (loopNode) lf.addToGroup(loopNode.id, vueNode.id);
|
if (loopNode) lf.addToGroup(loopNode.id, vueNode.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
const graph = lf.getGraphData();
|
|
||||||
const groups = recursiveFilter(graph.nodes, node.id, ["loop", "selectArea"]);
|
|
||||||
operationEdge(groups);
|
|
||||||
lf.markChanged();
|
lf.markChanged();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -460,11 +498,30 @@ const flowPauseFn = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const totalRunningTime = ref(0);
|
const totalRunningTime = ref(0);
|
||||||
|
const hasRunResult = ref(false);
|
||||||
|
let flowPollTimer = null;
|
||||||
|
let flowDisposed = false;
|
||||||
|
|
||||||
|
const clearFlowPollTimer = () => {
|
||||||
|
if (flowPollTimer !== null) {
|
||||||
|
window.clearTimeout(flowPollTimer);
|
||||||
|
flowPollTimer = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleFlowView = (currentInstId, taskId = null, delay = 2000) => {
|
||||||
|
clearFlowPollTimer();
|
||||||
|
flowPollTimer = window.setTimeout(() => {
|
||||||
|
flowPollTimer = null;
|
||||||
|
if (!flowDisposed) loopFlowView(currentInstId, taskId);
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
const flowResumeFn = async () => {
|
const flowResumeFn = async () => {
|
||||||
const res = await flowResume(instId.value);
|
const res = await flowResume(instId.value);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
flowState.value = "testRunning";
|
flowState.value = "testRunning";
|
||||||
|
clearFlowPollTimer();
|
||||||
loopFlowView(instId.value);
|
loopFlowView(instId.value);
|
||||||
ElMessage.success(res.msg);
|
ElMessage.success(res.msg);
|
||||||
}
|
}
|
||||||
@ -473,7 +530,9 @@ const flowResumeFn = async () => {
|
|||||||
const flowStopFn = async () => {
|
const flowStopFn = async () => {
|
||||||
const res = await flowStop(instId.value);
|
const res = await flowStop(instId.value);
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
|
clearFlowPollTimer();
|
||||||
flowState.value = "stop";
|
flowState.value = "stop";
|
||||||
|
hasRunResult.value = true;
|
||||||
flowStore.updateDisableForm(false);
|
flowStore.updateDisableForm(false);
|
||||||
ElMessage.success(res.msg);
|
ElMessage.success(res.msg);
|
||||||
}
|
}
|
||||||
@ -504,8 +563,23 @@ const initFlowData = {
|
|||||||
edges: [],
|
edges: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hasSameEdge = (edge, ignoredId) => lf.getGraphData().edges.some((item) => (
|
||||||
|
item.id !== ignoredId
|
||||||
|
&& item.sourceNodeId === edge.sourceNodeId
|
||||||
|
&& item.targetNodeId === edge.targetNodeId
|
||||||
|
&& item.sourceAnchorId === edge.sourceAnchorId
|
||||||
|
&& item.targetAnchorId === edge.targetAnchorId
|
||||||
|
));
|
||||||
|
|
||||||
const validEdge = (data) => {
|
const validEdge = (data) => {
|
||||||
const sourceAnchorArr = lf.getNodeModelById(data.sourceNodeId).anchors;
|
const sourceModel = lf.getNodeModelById(data.sourceNodeId);
|
||||||
|
const targetModel = lf.getNodeModelById(data.targetNodeId);
|
||||||
|
if (!sourceModel || !targetModel) {
|
||||||
|
lf.deleteEdge(data.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceAnchorArr = sourceModel.anchors;
|
||||||
const [sourceName] = sourceAnchorArr
|
const [sourceName] = sourceAnchorArr
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
return item.id === data.sourceAnchorId;
|
return item.id === data.sourceAnchorId;
|
||||||
@ -514,7 +588,7 @@ const validEdge = (data) => {
|
|||||||
return item.name;
|
return item.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
const targetAnchorArr = lf.getNodeModelById(data.targetNodeId).anchors;
|
const targetAnchorArr = targetModel.anchors;
|
||||||
const [targetName] = targetAnchorArr
|
const [targetName] = targetAnchorArr
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
return item.id === data.targetAnchorId;
|
return item.id === data.targetAnchorId;
|
||||||
@ -523,7 +597,13 @@ const validEdge = (data) => {
|
|||||||
return item.name;
|
return item.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!sourceName || !targetName) {
|
||||||
|
lf.deleteEdge(data.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceName === "right" && targetName === "left") {
|
if (sourceName === "right" && targetName === "left") {
|
||||||
|
if (hasSameEdge(data, data.id)) lf.deleteEdge(data.id);
|
||||||
return;
|
return;
|
||||||
} else if (
|
} else if (
|
||||||
(sourceName === "right" && targetName === "right") ||
|
(sourceName === "right" && targetName === "right") ||
|
||||||
@ -536,6 +616,7 @@ const validEdge = (data) => {
|
|||||||
const targetNodeId = data.sourceNodeId;
|
const targetNodeId = data.sourceNodeId;
|
||||||
const targetAnchorId = data.sourceAnchorId;
|
const targetAnchorId = data.sourceAnchorId;
|
||||||
lf.deleteEdge(data.id);
|
lf.deleteEdge(data.id);
|
||||||
|
if (hasSameEdge({ sourceNodeId, targetNodeId, sourceAnchorId, targetAnchorId })) return;
|
||||||
lf.addEdge({
|
lf.addEdge({
|
||||||
type: "bezier",
|
type: "bezier",
|
||||||
sourceNodeId,
|
sourceNodeId,
|
||||||
@ -553,8 +634,10 @@ const changeState = (value) => {
|
|||||||
isOpen.value = false;
|
isOpen.value = false;
|
||||||
flowStore.updateDisableForm(true);
|
flowStore.updateDisableForm(true);
|
||||||
flowState.value = value.type;
|
flowState.value = value.type;
|
||||||
|
hasRunResult.value = false;
|
||||||
|
|
||||||
instId.value = value.instId;
|
instId.value = value.instId;
|
||||||
|
clearFlowPollTimer();
|
||||||
loopFlowView(value.instId);
|
loopFlowView(value.instId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -567,8 +650,10 @@ const loopFlowView = async (instId, taskId = null) => {
|
|||||||
params.taskId = taskId;
|
params.taskId = taskId;
|
||||||
}
|
}
|
||||||
const res = await flowView(params);
|
const res = await flowView(params);
|
||||||
|
if (flowDisposed) return;
|
||||||
if (res.code === 200) {
|
if (res.code === 200) {
|
||||||
const arr = res.data || [];
|
const arr = res.data || [];
|
||||||
|
if (arr.length > 0) hasRunResult.value = true;
|
||||||
arr.forEach((item) => {
|
arr.forEach((item) => {
|
||||||
emitter.emit("changeNodeState", item);
|
emitter.emit("changeNodeState", item);
|
||||||
});
|
});
|
||||||
@ -577,9 +662,7 @@ const loopFlowView = async (instId, taskId = null) => {
|
|||||||
arr[arr.length - 1]?.nodeType !== "END" &&
|
arr[arr.length - 1]?.nodeType !== "END" &&
|
||||||
!["FAILED", "STOPPED"].includes(arr[arr.length - 1]?.status)
|
!["FAILED", "STOPPED"].includes(arr[arr.length - 1]?.status)
|
||||||
) {
|
) {
|
||||||
setTimeout(() => {
|
scheduleFlowView(instId);
|
||||||
loopFlowView(instId);
|
|
||||||
}, 2000);
|
|
||||||
} else if (
|
} else if (
|
||||||
arr[arr.length - 1]?.nodeType === "END" &&
|
arr[arr.length - 1]?.nodeType === "END" &&
|
||||||
arr[arr.length - 1]?.status === "SUCCESS"
|
arr[arr.length - 1]?.status === "SUCCESS"
|
||||||
@ -632,19 +715,9 @@ const loadFlowData = async (id, lf) => {
|
|||||||
const res = await getDetect(id);
|
const res = await getDetect(id);
|
||||||
if (res.data && res.data.flowData) {
|
if (res.data && res.data.flowData) {
|
||||||
const flowData = JSON.parse(res.data.flowData);
|
const flowData = JSON.parse(res.data.flowData);
|
||||||
const selectArea = [];
|
|
||||||
flowData.nodes.forEach((item) => {
|
|
||||||
if (item.type === "selectArea") {
|
|
||||||
selectArea.push(...item.children);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
lf.render(flowData);
|
lf.render(flowData);
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
selectArea.forEach((item) => {
|
|
||||||
lf.getNodeModelById(item).draggable = false;
|
|
||||||
});
|
|
||||||
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
|
oldFlowData = JSON.parse(JSON.stringify(lf.getGraphData()));
|
||||||
lf.fitView();
|
lf.fitView();
|
||||||
});
|
});
|
||||||
@ -686,9 +759,7 @@ const justLoadFlow = () => {
|
|||||||
|
|
||||||
if (flowInfoData.value.isLog) {
|
if (flowInfoData.value.isLog) {
|
||||||
flowStore.updateDisableForm(true);
|
flowStore.updateDisableForm(true);
|
||||||
setTimeout(() => {
|
scheduleFlowView(flowInfoData.value.instId, flowInfoData.value.taskId, 1000);
|
||||||
loopFlowView(flowInfoData.value.instId, flowInfoData.value.taskId);
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -713,13 +784,17 @@ const initFlow = () => {
|
|||||||
const groupSelectionPending = ref(false);
|
const groupSelectionPending = ref(false);
|
||||||
|
|
||||||
const createGroup = (selectedNodes) => {
|
const createGroup = (selectedNodes) => {
|
||||||
const nodes = selectedNodes.filter((item) => !["selectArea", "loop"].includes(item.type));
|
const nodes = selectedNodes.filter((item) => (
|
||||||
if (!nodes.length) {
|
!["selectArea", "loop"].includes(item.type)
|
||||||
|
&& !lf.getNodeModelById(item.id)?.parentId
|
||||||
|
));
|
||||||
|
if (nodes.length < 2) {
|
||||||
|
ElMessage.warning("请至少选择两个未分组节点");
|
||||||
groupSelectionPending.value = false;
|
groupSelectionPending.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PADDING = 200;
|
const INSET = { left: 52, top: 120, right: 52, bottom: 52 };
|
||||||
let minX = Infinity;
|
let minX = Infinity;
|
||||||
let minY = Infinity;
|
let minY = Infinity;
|
||||||
let maxX = -Infinity;
|
let maxX = -Infinity;
|
||||||
@ -730,32 +805,38 @@ const createGroup = (selectedNodes) => {
|
|||||||
const model = lf.getNodeModelById(item.id);
|
const model = lf.getNodeModelById(item.id);
|
||||||
const width = model?.width || 372;
|
const width = model?.width || 372;
|
||||||
const height = model?.height || 180;
|
const height = model?.height || 180;
|
||||||
minX = Math.min(minX, item.x - width / 2);
|
const left = Number.isFinite(model?.left) ? model.left : item.x - width / 2;
|
||||||
minY = Math.min(minY, item.y - height / 2);
|
const top = Number.isFinite(model?.top) ? model.top : item.y - height / 2;
|
||||||
maxX = Math.max(maxX, item.x + width / 2);
|
minX = Math.min(minX, left);
|
||||||
maxY = Math.max(maxY, item.y + height / 2);
|
minY = Math.min(minY, top);
|
||||||
|
maxX = Math.max(maxX, left + width);
|
||||||
|
maxY = Math.max(maxY, top + height);
|
||||||
children.push(item.id);
|
children.push(item.id);
|
||||||
if (model) model.draggable = false;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
lf.addNode({
|
const width = maxX - minX + INSET.left + INSET.right;
|
||||||
|
const height = maxY - minY + INSET.top + INSET.bottom;
|
||||||
|
lf.groupNodes({
|
||||||
type: "selectArea",
|
type: "selectArea",
|
||||||
x: minX + (maxX - minX) / 2,
|
x: minX - INSET.left + width / 2,
|
||||||
y: minY + (maxY - minY) / 2,
|
y: minY - INSET.top + height / 2,
|
||||||
properties: {
|
properties: {
|
||||||
name: "分组",
|
name: "分组",
|
||||||
action: "NONE",
|
action: "NONE",
|
||||||
width: maxX - minX + PADDING,
|
width,
|
||||||
height: maxY - minY + PADDING,
|
height,
|
||||||
},
|
},
|
||||||
children,
|
}, children);
|
||||||
});
|
|
||||||
lf.clearSelectElements();
|
lf.clearSelectElements();
|
||||||
groupSelectionPending.value = false;
|
groupSelectionPending.value = false;
|
||||||
nextTick(() => lf.fitView());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const group = () => {
|
const group = () => {
|
||||||
|
if (groupSelectionPending.value) {
|
||||||
|
groupSelectionPending.value = false;
|
||||||
|
lf.clearSelectElements();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { nodes } = lf.getSelectElements();
|
const { nodes } = lf.getSelectElements();
|
||||||
if (nodes.length) createGroup(nodes);
|
if (nodes.length) createGroup(nodes);
|
||||||
else groupSelectionPending.value = true;
|
else groupSelectionPending.value = true;
|
||||||
@ -847,6 +928,8 @@ const clearRun = () => {
|
|||||||
node.closePopover();
|
node.closePopover();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
hasRunResult.value = false;
|
||||||
|
totalRunningTime.value = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeleteKey = (event) => {
|
const handleDeleteKey = (event) => {
|
||||||
@ -871,15 +954,6 @@ const handleDeleteKey = (event) => {
|
|||||||
if (nodes.length) {
|
if (nodes.length) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
nodes.forEach((node) => {
|
nodes.forEach((node) => {
|
||||||
if (node.type === "selectArea") {
|
|
||||||
const model = lf.getNodeModelById(node.id);
|
|
||||||
const children = Array.from(model?.children || []);
|
|
||||||
model.children = [];
|
|
||||||
children.forEach((childId) => {
|
|
||||||
const child = lf.getNodeModelById(childId);
|
|
||||||
if (child) child.draggable = true;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
lf.deleteNode(node.id);
|
lf.deleteNode(node.id);
|
||||||
emitter.emit("deleteNode", node.id);
|
emitter.emit("deleteNode", node.id);
|
||||||
});
|
});
|
||||||
@ -900,12 +974,12 @@ onMounted(() => {
|
|||||||
registerBeforeUnload();
|
registerBeforeUnload();
|
||||||
window.addEventListener("keydown", handleDeleteKey);
|
window.addEventListener("keydown", handleDeleteKey);
|
||||||
|
|
||||||
emitter.on("openParamsDrawer", (node) => {
|
onFlowEvent("openParamsDrawer", (node) => {
|
||||||
showParamsDrawer.value = true;
|
showParamsDrawer.value = true;
|
||||||
paramsDrawerData.value = node;
|
paramsDrawerData.value = node;
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("throwAnError", (data) => {
|
onFlowEvent("throwAnError", (data) => {
|
||||||
// 处理错误抛出逻辑
|
// 处理错误抛出逻辑
|
||||||
const { id, hasError, type } = data;
|
const { id, hasError, type } = data;
|
||||||
let element
|
let element
|
||||||
@ -935,7 +1009,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("deleteNode", (nodeId) => {
|
onFlowEvent("deleteNode", (nodeId) => {
|
||||||
const index = nodeErrorList.value.findIndex((item) => item === nodeId);
|
const index = nodeErrorList.value.findIndex((item) => item === nodeId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
nodeErrorList.value.splice(index, 1);
|
nodeErrorList.value.splice(index, 1);
|
||||||
@ -943,18 +1017,11 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
const operationEdge = (arr) => {
|
|
||||||
arr.forEach((item) => {
|
|
||||||
newEdge(item.id);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
flowDisposed = true;
|
||||||
|
clearFlowPollTimer();
|
||||||
loopChildDragContexts.clear();
|
loopChildDragContexts.clear();
|
||||||
emitter.off("changeNodeState");
|
if (window.lf === lf) delete window.lf;
|
||||||
emitter.off("openParamsDrawer");
|
|
||||||
emitter.off("throwAnError");
|
|
||||||
emitter.off("deleteNode");
|
|
||||||
|
|
||||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||||
window.removeEventListener("keydown", handleDeleteKey);
|
window.removeEventListener("keydown", handleDeleteKey);
|
||||||
@ -981,12 +1048,90 @@ $flow-canvas-cursor: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org
|
|||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
border-bottom: 1px solid #e3e7ee;
|
border-bottom: 1px solid #e3e7ee;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
gap: 18px;
|
||||||
|
|
||||||
|
.flow-heading {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-heading__name {
|
||||||
|
overflow: hidden;
|
||||||
|
max-width: 320px;
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 650;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-status {
|
||||||
|
display: flex;
|
||||||
|
height: 24px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border: 1px solid #dfe4ec;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #687386;
|
||||||
|
font-size: 11px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-status__dot {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-status--release,
|
||||||
|
.flow-status--testRunFinish {
|
||||||
|
border-color: #c8ead5;
|
||||||
|
background: #effaf3;
|
||||||
|
color: #1e7d49;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-status--testRunning,
|
||||||
|
.flow-status--updateData {
|
||||||
|
border-color: #cbdcf8;
|
||||||
|
background: #f0f6ff;
|
||||||
|
color: #2f6fca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-status--testRunError,
|
||||||
|
.flow-status--stop {
|
||||||
|
border-color: #f1cecb;
|
||||||
|
background: #fff3f2;
|
||||||
|
color: #c33b32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flow-status--pause {
|
||||||
|
border-color: #ead9b8;
|
||||||
|
background: #fff9eb;
|
||||||
|
color: #9a6700;
|
||||||
|
}
|
||||||
|
|
||||||
.btn__container {
|
.btn__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
margin-right: 12px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.el-button + .el-button { margin-left: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-actions {
|
||||||
|
padding-left: 10px;
|
||||||
|
border-left: 1px solid #e4e8ef;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -997,16 +1142,56 @@ $flow-canvas-cursor: url("data:image/svg+xml,%3Csvg%20xmlns=%27http://www.w3.org
|
|||||||
|
|
||||||
.el-main {
|
.el-main {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-aside {
|
.el-aside {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
width: 260px;
|
width: 272px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-right: 1px solid #e3e7ee;
|
border-right: 1px solid #e3e7ee;
|
||||||
background: #f8fafc;
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.canvas-toolbar {
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
top: 14px;
|
||||||
|
left: 14px;
|
||||||
|
z-index: 20;
|
||||||
|
align-items: center;
|
||||||
|
padding: 4px;
|
||||||
|
border: 1px solid #dfe4ec;
|
||||||
|
border-radius: 7px;
|
||||||
|
background: rgba(255, 255, 255, 0.96);
|
||||||
|
box-shadow: 0 6px 18px rgba(31, 41, 55, 0.1);
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: transparent;
|
||||||
|
color: #566174;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button + .el-button { margin-left: 2px; }
|
||||||
|
|
||||||
|
.el-button:hover {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.canvas-toolbar__divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 0 4px;
|
||||||
|
background: #e1e6ed;
|
||||||
|
}
|
||||||
|
|
||||||
.flow__container {
|
.flow__container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
@ -1,246 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="node__container" >
|
<div class="node__container">
|
||||||
<!-- 状态栏 -->
|
<div class="terminal-node">
|
||||||
<div class="title__container">
|
<div class="terminal-node__icon"><img src="../../icon/end.svg" alt="" /></div>
|
||||||
<div class="title">
|
<div class="terminal-node__content">
|
||||||
<img src="../../icon/end.svg" alt="">
|
<div class="terminal-node__title">流程结束</div>
|
||||||
<span class="text">end</span>
|
<div class="terminal-node__description">结束当前工作流并输出最终结果</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subTitle">工作流的终止节点,结束整个流程</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineProps({
|
||||||
const props = defineProps({
|
|
||||||
model: Object,
|
model: Object,
|
||||||
graphModel: Object,
|
graphModel: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
text: String,
|
text: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.node__container {
|
|
||||||
width: 300px;
|
|
||||||
height: auto;
|
|
||||||
background: #fff;
|
|
||||||
padding: 12px;
|
|
||||||
cursor: default;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
box-shadow: 0 5px 15px 0#00000008;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.state__container {
|
|
||||||
display: none;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.context {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resultText {
|
|
||||||
color: #1664ff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-success {
|
|
||||||
display: block;
|
|
||||||
background-color: #eef9f1;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #309256;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-error{
|
|
||||||
display: block;
|
|
||||||
background-color: #fdf5f5;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #ee3f38;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-running{
|
|
||||||
display: block;
|
|
||||||
background-color: #f4f7ff;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
color: #68a2e4;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title__container {
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
color: #737a87;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.input__container {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #fafbfc;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-radius: 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
|
|
||||||
.title {
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.left {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.tag {
|
|
||||||
width: 3px;
|
|
||||||
height: 16px;
|
|
||||||
background: #1664ff;
|
|
||||||
border-radius: 0 4px 4px 0;
|
|
||||||
margin-right: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0c0d0e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
.addFormItem {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form__container {
|
|
||||||
margin: 12px 0;
|
|
||||||
|
|
||||||
.sub-properties {
|
|
||||||
margin-left: 10px;
|
|
||||||
.zw {
|
|
||||||
margin-left: 15px;
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: -4px;
|
|
||||||
width: 10px;
|
|
||||||
border-left: 1px solid gray;
|
|
||||||
border-bottom: 1px solid gray;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.gSon-properties {
|
|
||||||
margin-left: 10px;
|
|
||||||
.zw {
|
|
||||||
margin-left: 15px;
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: -4px;
|
|
||||||
width: 10px;
|
|
||||||
border-left: 1px solid gray;
|
|
||||||
border-bottom: 1px solid gray;
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
background-color: transparent;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.deleteBtn {
|
|
||||||
margin-bottom: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.addBtn {
|
|
||||||
margin-bottom: 22px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-row) {
|
|
||||||
align-items: end;
|
|
||||||
|
|
||||||
.el-input {
|
|
||||||
--el-input-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-select {
|
|
||||||
--el-select-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-cascader {
|
|
||||||
--el-form-inline-content-width: 140px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.popover__container {
|
|
||||||
min-width: 300px !important;
|
|
||||||
|
|
||||||
.params__container {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.paramsTitle {
|
|
||||||
color: #0c0d0e;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,9 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="group__container">
|
<div class="group__container">
|
||||||
<NodeTitle :icon="group" :nodeId="props.model.id" :nodeProperties="props.properties" :nodeName="props.properties?.name || '分组'" nodeType="selectArea" nodeDesc="将一些组件进行分组" />
|
<NodeTitle
|
||||||
|
:icon="group"
|
||||||
|
:nodeId="props.model.id"
|
||||||
|
:nodeProperties="props.properties"
|
||||||
|
:nodeName="props.properties?.name || '分组'"
|
||||||
|
nodeType="selectArea"
|
||||||
|
nodeDesc="组织相关流程节点"
|
||||||
|
@setNodeName="setNodeName"
|
||||||
|
/>
|
||||||
|
<div class="group-summary">
|
||||||
|
<span>分组区域</span>
|
||||||
|
<span class="group-summary__count">{{ props.childCount || 0 }} 个节点</span>
|
||||||
|
</div>
|
||||||
<div class="child__container">
|
<div class="child__container">
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -17,13 +28,21 @@ const props = defineProps({
|
|||||||
graphModel: Object,
|
graphModel: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
nowTime: Number,
|
nowTime: Number,
|
||||||
|
childCount: Number,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emits = defineEmits(["contentChange"]);
|
||||||
|
|
||||||
|
const setNodeName = (name) => {
|
||||||
|
const properties = lf.getProperties(props.model.id);
|
||||||
|
lf.setProperties(props.model.id, { ...properties, name });
|
||||||
|
emits("contentChange");
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
lf.setProperties(props.model.id, {
|
if (props.properties?.action !== "NONE") {
|
||||||
...props.properties,
|
lf.setProperties(props.model.id, { ...props.properties, action: "NONE" });
|
||||||
action: "NONE"
|
}
|
||||||
});
|
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@ -31,25 +50,67 @@ onMounted(() => {
|
|||||||
.group__container {
|
.group__container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #fff;
|
background: rgba(255, 255, 255, 0.96);
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 12px;
|
border-radius: 8px;
|
||||||
border: 2px solid white;
|
border: 1px solid #b8c5d8;
|
||||||
box-shadow: 0 5px 15px 0#00000008;
|
box-shadow: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
width: 5px;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
background: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: #8fa0b8;
|
||||||
|
box-shadow: 0 8px 24px rgba(31, 41, 55, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-summary {
|
||||||
|
display: flex;
|
||||||
|
height: 30px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-summary__count {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 1px solid #d9e0ea;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #f8fafc;
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
.child__container {
|
.child__container {
|
||||||
min-height: 80px;
|
min-height: 80px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #f6f8fa;
|
background-color: #f6f8fb;
|
||||||
border-radius: 8px;
|
background-image: radial-gradient(#d6dde8 0.7px, transparent 0.7px);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
border: 1px dashed #aeb9c9;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-row {
|
.el-row {
|
||||||
|
|||||||
@ -40,8 +40,6 @@
|
|||||||
:nodeType="props.properties.nodeType || 'NONE'"
|
:nodeType="props.properties.nodeType || 'NONE'"
|
||||||
:nodeName="props.properties.name"
|
:nodeName="props.properties.name"
|
||||||
:nodeDesc="props.properties.desc"
|
:nodeDesc="props.properties.desc"
|
||||||
:zoom-state="nodeZoom"
|
|
||||||
|
|
||||||
@setNodeName="setNodeName"
|
@setNodeName="setNodeName"
|
||||||
/>
|
/>
|
||||||
<!-- @zoom="zoom" -->
|
<!-- @zoom="zoom" -->
|
||||||
@ -49,13 +47,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -88,7 +88,7 @@ const closePopover = () => {
|
|||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -110,7 +110,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -119,19 +119,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -10,23 +10,23 @@
|
|||||||
</template> -->
|
</template> -->
|
||||||
</NodeState>
|
</NodeState>
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
<div class="title__container">
|
<div class="terminal-node">
|
||||||
<div class="title">
|
<div class="terminal-node__icon"><img src="../../icon/start.svg" alt="" /></div>
|
||||||
<div class="left">
|
<div class="terminal-node__content">
|
||||||
<img src="../../icon/start.svg" alt="" />
|
<div class="terminal-node__title">流程开始</div>
|
||||||
<span class="text">Start</span>
|
<div class="terminal-node__description">接收流程输入并启动工作流</div>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="subTitle">工作流的起始节点,用于设定启动工作流需要的信息</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
@ -49,7 +49,7 @@ const closePopover = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -71,7 +71,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -81,7 +81,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
@ -89,69 +89,6 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.node__container {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
|
|
||||||
.title__container {
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
.left {
|
|
||||||
img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.right {
|
|
||||||
.el-button {
|
|
||||||
border: none;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
color: #737a87;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.popover__container {
|
|
||||||
min-width: 300px !important;
|
|
||||||
|
|
||||||
.params__container {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.paramsTitle {
|
|
||||||
color: #0c0d0e;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||
@ -46,12 +46,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -84,7 +86,7 @@ const closePopover = () => {
|
|||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -106,7 +108,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -115,19 +117,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -25,20 +25,21 @@
|
|||||||
:nodeProperties="props.properties"
|
:nodeProperties="props.properties"
|
||||||
:nodeName="props.properties.name"
|
:nodeName="props.properties.name"
|
||||||
:nodeDesc="props.properties.desc"
|
:nodeDesc="props.properties.desc"
|
||||||
:zoom-state="nodeZoom"
|
|
||||||
@setNodeName="setNodeName"
|
@setNodeName="setNodeName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, onUnmounted } from "vue";
|
import { ref, reactive, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -69,7 +70,7 @@ const closePopover = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -90,7 +91,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -99,19 +100,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -40,21 +40,21 @@
|
|||||||
:nodeType="props.properties.nodeType || 'NONE'"
|
:nodeType="props.properties.nodeType || 'NONE'"
|
||||||
:nodeName="props.properties.name"
|
:nodeName="props.properties.name"
|
||||||
:nodeDesc="props.properties.desc"
|
:nodeDesc="props.properties.desc"
|
||||||
:zoom-state="nodeZoom"
|
|
||||||
@zoom="zoom"
|
|
||||||
@setNodeName="setNodeName"
|
@setNodeName="setNodeName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -86,7 +86,7 @@ const closePopover = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -108,7 +108,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -117,19 +117,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@
|
|||||||
/>
|
/>
|
||||||
<div class="loop-body-title">
|
<div class="loop-body-title">
|
||||||
<span>循环体</span>
|
<span>循环体</span>
|
||||||
<span class="loop-body-count">{{ childCount }} 个节点</span>
|
<span class="loop-body-count">{{ actualChildCount }} 个节点</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="child__container"
|
class="child__container"
|
||||||
@ -30,21 +30,21 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import loop from "../../icon/loop.svg";
|
import loop from "../../icon/loop.svg";
|
||||||
import { addNewEdge } from "@/utils/flow";
|
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { emitter } from "@/utils/eventBus";
|
||||||
import { computed, onUnmounted, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
graphModel: Object,
|
graphModel: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
nowTime: Number,
|
nowTime: Number,
|
||||||
|
childCount: Number,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
const emits = defineEmits(["bindRef", "addToGroup", "contentChange"]);
|
const emits = defineEmits(["bindRef", "addToGroup", "contentChange"]);
|
||||||
const isDragOver = ref(false);
|
const isDragOver = ref(false);
|
||||||
const childCount = computed(() => props.model?.children?.size || 0);
|
const actualChildCount = computed(() => props.childCount ?? props.model?.children?.size ?? 0);
|
||||||
|
|
||||||
const handleDragover = (e) => {
|
const handleDragover = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -82,9 +82,6 @@ const handleDrop = (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lf.addToGroup(props.model.id, node.id, point.canvasOverlayPosition);
|
lf.addToGroup(props.model.id, node.id, point.canvasOverlayPosition);
|
||||||
setTimeout(() => {
|
|
||||||
addNewEdge(props.model.id);
|
|
||||||
}, 50);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNodeName = (name) => {
|
const setNodeName = (name) => {
|
||||||
@ -96,10 +93,6 @@ const setNodeName = (name) => {
|
|||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("openParamsDrawer");
|
|
||||||
})
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.group__container {
|
.group__container {
|
||||||
@ -118,6 +111,8 @@ onUnmounted(() => {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||||
|
|
||||||
|
&:hover { border-color: #aeb9c9; }
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -142,8 +137,8 @@ onUnmounted(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
height: 28px;
|
height: 30px;
|
||||||
margin-top: 2px;
|
margin-top: 4px;
|
||||||
color: #344054;
|
color: #344054;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -152,7 +147,8 @@ onUnmounted(() => {
|
|||||||
.loop-body-count {
|
.loop-body-count {
|
||||||
padding: 2px 8px;
|
padding: 2px 8px;
|
||||||
color: #667085;
|
color: #667085;
|
||||||
background: #eef1f6;
|
border: 1px solid #dfe4ec;
|
||||||
|
background: #f8fafc;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@ -161,14 +157,14 @@ onUnmounted(() => {
|
|||||||
.child__container {
|
.child__container {
|
||||||
min-height: 300px;
|
min-height: 300px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background-color: #f8fafc;
|
background-color: #f7f9fc;
|
||||||
border: 1px dashed #b8c2d2;
|
border: 1px dashed #aeb9c9;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
transition: background-color 0.18s ease, border-color 0.18s ease;
|
transition: background-color 0.18s ease, border-color 0.18s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--dragover .child__container {
|
&--dragover .child__container {
|
||||||
background-color: #fff7f2;
|
background-color: #fff8f3;
|
||||||
border-color: #ff6b35;
|
border-color: #ff6b35;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,13 +32,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -69,7 +71,7 @@ const closePopover = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -90,7 +92,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -99,20 +101,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -32,13 +32,15 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -69,7 +71,7 @@ const closePopover = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -90,7 +92,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -99,20 +101,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -25,21 +25,21 @@
|
|||||||
:nodeProperties="props.properties"
|
:nodeProperties="props.properties"
|
||||||
:nodeName="props.properties.name"
|
:nodeName="props.properties.name"
|
||||||
:nodeDesc="props.properties.desc"
|
:nodeDesc="props.properties.desc"
|
||||||
:zoom-state="nodeZoom"
|
|
||||||
@zoom="zoom"
|
|
||||||
@setNodeName="setNodeName"
|
@setNodeName="setNodeName"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, onUnmounted } from "vue";
|
import { ref, onMounted } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import "vue3-json-viewer/dist/index.css";
|
import "vue3-json-viewer/dist/index.css";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
import IPlayer from "@/components/IPlayer/index.vue";
|
import IPlayer from "@/components/IPlayer/index.vue";
|
||||||
|
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
properties: Object,
|
properties: Object,
|
||||||
@ -70,7 +70,7 @@ const closePopover = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -91,7 +91,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -100,20 +100,13 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("setProperties", (data) => {
|
onFlowEvent("setProperties", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
emits("contentChange");
|
emits("contentChange");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
emitter.off("changeNodeState");
|
|
||||||
emitter.off("contentChange");
|
|
||||||
emitter.off("setProperties");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -1,145 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="node__container" >
|
<div class="node__container">
|
||||||
<!-- 状态栏 -->
|
<div class="terminal-node">
|
||||||
<div class="title__container">
|
<div class="terminal-node__icon"><img src="../../icon/end.svg" alt="" /></div>
|
||||||
<div class="title">
|
<div class="terminal-node__content">
|
||||||
<img src="../../icon/end.svg" alt="">
|
<div class="terminal-node__title">单次循环结束</div>
|
||||||
<span class="text">单次循环结束</span>
|
<div class="terminal-node__description">结束循环体内的本次执行</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subTitle">循环体内部工作流的终止节点,结束循环体内部的单次流程</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineProps({ model: Object, graphModel: Object, properties: Object });
|
||||||
const props = defineProps({
|
|
||||||
model: Object,
|
|
||||||
graphModel: Object,
|
|
||||||
properties: Object
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
@keyframes rotateAnimation {
|
|
||||||
from {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.node__container {
|
|
||||||
width: 300px;
|
|
||||||
height: auto;
|
|
||||||
background: #fff;
|
|
||||||
padding: 12px;
|
|
||||||
cursor: default;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px solid white;
|
|
||||||
box-shadow: 0 5px 15px 0#00000008;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.state__container {
|
|
||||||
display: none;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.context {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resultText {
|
|
||||||
color: #1664ff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-success {
|
|
||||||
display: block;
|
|
||||||
background-color: #eef9f1;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #309256;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-error{
|
|
||||||
display: block;
|
|
||||||
background-color: #fdf5f5;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
font-size: 16px;
|
|
||||||
color: #ee3f38;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.nodeState-running{
|
|
||||||
display: block;
|
|
||||||
background-color: #f4f7ff;
|
|
||||||
|
|
||||||
.el-icon {
|
|
||||||
color: #68a2e4;
|
|
||||||
font-size: 16px;
|
|
||||||
animation: rotateAnimation 2s linear infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title__container {
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
color: #737a87;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.popover__container {
|
|
||||||
min-width: 300px !important;
|
|
||||||
|
|
||||||
.params__container {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.paramsTitle {
|
|
||||||
color: #0c0d0e;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -1,106 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="node__container" >
|
<div class="node__container">
|
||||||
<!-- 状态栏 -->
|
<div class="terminal-node">
|
||||||
<div class="title__container">
|
<div class="terminal-node__icon"><img src="../../icon/start.svg" alt="" /></div>
|
||||||
<div class="title">
|
<div class="terminal-node__content">
|
||||||
<img src="../../icon/end.svg" alt="">
|
<div class="terminal-node__title">单次循环开始</div>
|
||||||
<span class="text">单次循环开始</span>
|
<div class="terminal-node__description">开始循环体内的一次执行</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="subTitle">循环体内部工作流的开始节点,开始循环体内部的单次流程</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
defineProps({ model: Object, graphModel: Object, properties: Object });
|
||||||
const props = defineProps({
|
|
||||||
model: Object,
|
|
||||||
graphModel: Object,
|
|
||||||
properties: Object
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
|
|
||||||
|
|
||||||
.node__container {
|
|
||||||
width: 300px;
|
|
||||||
height: auto;
|
|
||||||
background: #fff;
|
|
||||||
padding: 12px;
|
|
||||||
cursor: default;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px solid white;
|
|
||||||
box-shadow: 0 5px 15px 0#00000008;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.state__container {
|
|
||||||
display: none;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
|
|
||||||
.context {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 4px;
|
|
||||||
font-size: 12px;
|
|
||||||
|
|
||||||
.state {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
.text {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.resultText {
|
|
||||||
color: #1664ff;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.title__container {
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.subTitle {
|
|
||||||
color: #737a87;
|
|
||||||
font-size: 12px;
|
|
||||||
margin: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.popover__container {
|
|
||||||
min-width: 300px !important;
|
|
||||||
|
|
||||||
.params__container {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
|
|
||||||
.paramsTitle {
|
|
||||||
color: #0c0d0e;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -47,7 +47,9 @@
|
|||||||
<div
|
<div
|
||||||
class="condition"
|
class="condition"
|
||||||
v-for="(params, index) in formData.nodeParams"
|
v-for="(params, index) in formData.nodeParams"
|
||||||
|
:key="params.id"
|
||||||
:id="params.id"
|
:id="params.id"
|
||||||
|
:data-branch-anchor-id="params.id"
|
||||||
>
|
>
|
||||||
<div class="title__container">
|
<div class="title__container">
|
||||||
<div class="left">
|
<div class="left">
|
||||||
@ -82,7 +84,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form__container">
|
<div class="form__container">
|
||||||
<el-row v-for="(property, pIndex) in params.list">
|
<el-row v-for="(property, pIndex) in params.list" :key="`${params.id}-${pIndex}`">
|
||||||
<el-form-item
|
<el-form-item
|
||||||
:label="pIndex === 0 ? '引用变量' : ''"
|
:label="pIndex === 0 ? '引用变量' : ''"
|
||||||
:prop="`nodeParams.${index}.list.${pIndex}.nameType`"
|
:prop="`nodeParams.${index}.list.${pIndex}.nameType`"
|
||||||
@ -251,22 +253,26 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<div class="else" :id="''.concat(props.model.id, '_else')">
|
<div
|
||||||
|
class="else"
|
||||||
|
:id="''.concat(props.model.id, '_else')"
|
||||||
|
:data-branch-anchor-id="''.concat(props.model.id, '_else')"
|
||||||
|
>
|
||||||
<div class="space"></div>
|
<div class="space"></div>
|
||||||
<div class="title">Else</div>
|
<div class="title">Else</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
|
import { nextTick, onMounted, onUnmounted, reactive, ref, toRaw, watch } from "vue";
|
||||||
import NodeTitle from "../../components/NodeTitle.vue";
|
import NodeTitle from "../../components/NodeTitle.vue";
|
||||||
import NodeState from "../../components/NodeState.vue";
|
import NodeState from "../../components/NodeState.vue";
|
||||||
import switchSvg from "../../icon/switch.svg";
|
import switchSvg from "../../icon/switch.svg";
|
||||||
import { v4 as randomUUID } from "uuid";
|
import { v4 as randomUUID } from "uuid";
|
||||||
import { useFlowStore } from "@/store/modules/flow";
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
import { Plus, Minus } from "@element-plus/icons-vue";
|
import { Plus, Minus } from "@element-plus/icons-vue";
|
||||||
import { getInput, addNewEdge, removeSwitchEdge } from "@/utils/flow";
|
import { getInput, removeSwitchEdge } from "@/utils/flow";
|
||||||
import { emitter } from "@/utils/eventBus";
|
import { useFlowEvents } from "../../composables/useFlowEvents";
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
model: Object,
|
model: Object,
|
||||||
@ -280,19 +286,21 @@ const emits = defineEmits([
|
|||||||
"removeAnchor",
|
"removeAnchor",
|
||||||
"bindRef",
|
"bindRef",
|
||||||
"changeAnchor",
|
"changeAnchor",
|
||||||
|
"syncAnchors",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const switchRef = ref();
|
const switchRef = ref();
|
||||||
const dynamicForm = ref();
|
const dynamicForm = ref();
|
||||||
const flowStore = useFlowStore();
|
const flowStore = useFlowStore();
|
||||||
|
const rules = reactive({});
|
||||||
|
const { onFlowEvent } = useFlowEvents();
|
||||||
|
|
||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
nodeParams: props.properties?.conditions || [],
|
nodeParams: props.properties?.conditions || [],
|
||||||
});
|
});
|
||||||
|
|
||||||
const addFormItem = () => {
|
const addFormItem = () => {
|
||||||
const rect = switchRef.value.getBoundingClientRect();
|
addCondition(0);
|
||||||
addCondition(rect.height - 60);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteFormItem = (id, index) => {
|
const deleteFormItem = (id, index) => {
|
||||||
@ -303,7 +311,7 @@ const deleteFormItem = (id, index) => {
|
|||||||
emits("removeAnchor", id);
|
emits("removeAnchor", id);
|
||||||
removeSwitchEdge(props.model.id, id);
|
removeSwitchEdge(props.model.id, id);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
changeFormItemHeight();
|
scheduleAnchorSync();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -338,12 +346,8 @@ const addCondition = (height) => {
|
|||||||
});
|
});
|
||||||
emits("addAnchor", height, id);
|
emits("addAnchor", height, id);
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
addNewEdge(props.model.id);
|
|
||||||
}, 50);
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
changeFormItemHeight();
|
scheduleAnchorSync();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -357,7 +361,7 @@ const addListItem = (pIndex, cIndex) => {
|
|||||||
quote: "",
|
quote: "",
|
||||||
});
|
});
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
changeFormItemHeight();
|
scheduleAnchorSync();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -367,18 +371,34 @@ const removeListItem = (pIndex, cIndex) => {
|
|||||||
}
|
}
|
||||||
formData.nodeParams[pIndex].list.splice(cIndex, 1);
|
formData.nodeParams[pIndex].list.splice(cIndex, 1);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
changeFormItemHeight();
|
scheduleAnchorSync();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeFormItemHeight = () => {
|
let anchorSyncFrame = null;
|
||||||
formData.nodeParams.forEach((item) => {
|
let resizeObserver = null;
|
||||||
const node = document.getElementById(item.id);
|
|
||||||
emits("changeAnchor", node.offsetTop, item.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
const node = document.getElementById("".concat(props.model.id, "_else"));
|
const syncAnchorPositions = () => {
|
||||||
emits("changeAnchor", node.offsetTop, "".concat(props.model.id, "_else"));
|
const root = switchRef.value;
|
||||||
|
if (!root) return;
|
||||||
|
const rootRect = root.getBoundingClientRect();
|
||||||
|
const scale = root.offsetWidth > 0 ? rootRect.width / root.offsetWidth : 1;
|
||||||
|
const positions = [...root.querySelectorAll("[data-branch-anchor-id]")].map((element) => {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
id: element.dataset.branchAnchorId,
|
||||||
|
height: Math.round(((rect.top - rootRect.top + rect.height / 2) / scale) * 100) / 100,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
emits("syncAnchors", positions);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scheduleAnchorSync = () => {
|
||||||
|
if (anchorSyncFrame !== null) cancelAnimationFrame(anchorSyncFrame);
|
||||||
|
anchorSyncFrame = requestAnimationFrame(() => {
|
||||||
|
anchorSyncFrame = null;
|
||||||
|
syncAnchorPositions();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const setNodeProperties = async () => {
|
const setNodeProperties = async () => {
|
||||||
@ -438,6 +458,7 @@ watch(
|
|||||||
addCondition(100);
|
addCondition(100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
nextTick(scheduleAnchorSync);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
immediate: true,
|
immediate: true,
|
||||||
@ -471,11 +492,16 @@ const handleInputKeydown = (e) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
emits("addAnchor", 280, "".concat(props.model.id, "_else"));
|
emits("addAnchor", 0, "".concat(props.model.id, "_else"));
|
||||||
|
|
||||||
emits("bindRef", dynamicForm.value);
|
emits("bindRef", dynamicForm.value);
|
||||||
|
nextTick(() => {
|
||||||
|
scheduleAnchorSync();
|
||||||
|
resizeObserver = new ResizeObserver(scheduleAnchorSync);
|
||||||
|
if (switchRef.value) resizeObserver.observe(switchRef.value);
|
||||||
|
});
|
||||||
|
|
||||||
emitter.on("changeNodeState", (data) => {
|
onFlowEvent("changeNodeState", (data) => {
|
||||||
if (data.nodeId === props.model.id) {
|
if (data.nodeId === props.model.id) {
|
||||||
nodeOperatingStatus.value = data.status;
|
nodeOperatingStatus.value = data.status;
|
||||||
if (data.endTime && data.startTime) {
|
if (data.endTime && data.startTime) {
|
||||||
@ -498,7 +524,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emitter.on("contentChange", (data) => {
|
onFlowEvent("contentChange", (data) => {
|
||||||
if (data.id === props.model.id) {
|
if (data.id === props.model.id) {
|
||||||
nodeOperatingStatus.value = "NORMAL";
|
nodeOperatingStatus.value = "NORMAL";
|
||||||
inputJsonData.value = {};
|
inputJsonData.value = {};
|
||||||
@ -510,60 +536,83 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
emitter.off("changeNodeState");
|
resizeObserver?.disconnect();
|
||||||
emitter.off("contentChange");
|
if (anchorSyncFrame !== null) cancelAnimationFrame(anchorSyncFrame);
|
||||||
});
|
});
|
||||||
|
|
||||||
defineExpose({ closePopover })
|
defineExpose({ closePopover })
|
||||||
</script>
|
</script>
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.node__container {
|
.node__container {
|
||||||
width: 720px;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
padding: 12px;
|
padding: 16px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
border-radius: 12px;
|
border: 1px solid #dfe4ec;
|
||||||
border: 2px solid white;
|
border-left: 4px solid var(--node-accent, #b7791f);
|
||||||
box-shadow: 0 5px 15px 0#00000008;
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(31, 41, 55, 0.09);
|
||||||
position: relative;
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||||||
|
|
||||||
.condition_title_container {
|
.condition_title_container {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
margin-top: 14px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #e8ebf0;
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.space {
|
.space {
|
||||||
width: 4px;
|
width: 3px;
|
||||||
height: 14px;
|
height: 15px;
|
||||||
background: rgb(22, 100, 255);
|
background: var(--node-accent, #b7791f);
|
||||||
border-radius: 0px 4px 4px 0px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 14px;
|
color: #344054;
|
||||||
font-weight: 500;
|
font-size: 13px;
|
||||||
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-button {
|
.el-button {
|
||||||
border: none;
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0;
|
||||||
|
border-color: #dfe4ec;
|
||||||
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.condition {
|
.condition {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
margin-bottom: 10px;
|
margin: 8px 0;
|
||||||
background-color: #fafbfc;
|
padding: 11px 12px 5px;
|
||||||
padding: 8px 16px;
|
border: 1px solid #e4e8ef;
|
||||||
border-radius: 8px;
|
border-radius: 7px;
|
||||||
|
background-color: #f8fafc;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 17px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: -18px;
|
||||||
|
border-top: 1px solid var(--node-accent, #b7791f);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.title__container {
|
.title__container {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -575,45 +624,58 @@ defineExpose({ closePopover })
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
.space {
|
.space {
|
||||||
width: 4px;
|
width: 3px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background: rgb(22, 100, 255);
|
background: #3978c5;
|
||||||
border-radius: 0px 4px 4px 0px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 14px;
|
color: #344054;
|
||||||
font-weight: bold;
|
font-size: 12px;
|
||||||
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.withOr {
|
.withOr {
|
||||||
margin-bottom: 12px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 9px 0 7px;
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
font-size: 14px;
|
color: #7a8494;
|
||||||
color: #737a87;
|
font-size: 12px;
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.el-select) { width: 120px !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.form__container) {
|
:deep(.form__container) {
|
||||||
.el-form-item {
|
.el-form-item {
|
||||||
margin-right: 20px;
|
margin-right: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-input {
|
.el-input {
|
||||||
width: 92px;
|
width: 105px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-select {
|
.el-select {
|
||||||
width: 92px;
|
width: 105px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-cascader {
|
.el-cascader {
|
||||||
width: 92px;
|
width: 105px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -621,21 +683,35 @@ defineExpose({ closePopover })
|
|||||||
.else {
|
.else {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #fafbfc;
|
min-height: 40px;
|
||||||
padding: 8px 16px;
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #e4e8ef;
|
||||||
|
border-radius: 7px;
|
||||||
|
background-color: #f8fafc;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: "";
|
||||||
|
width: 17px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
right: -18px;
|
||||||
|
border-top: 1px solid var(--node-accent, #b7791f);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.space {
|
.space {
|
||||||
width: 4px;
|
width: 3px;
|
||||||
height: 14px;
|
height: 14px;
|
||||||
background: rgb(22, 100, 255);
|
background: #697386;
|
||||||
border-radius: 0px 4px 4px 0px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
font-size: 14px;
|
color: #344054;
|
||||||
font-weight: 500;
|
font-size: 12px;
|
||||||
font-weight: bold;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,6 +24,8 @@ import { BaseEdge, EdgeLabelRenderer, getBezierPath } from "@vue-flow/core";
|
|||||||
import { Delete } from "@element-plus/icons-vue";
|
import { Delete } from "@element-plus/icons-vue";
|
||||||
import { useFlowStore } from "@/store/modules/flow";
|
import { useFlowStore } from "@/store/modules/flow";
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false });
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
id: String,
|
id: String,
|
||||||
sourceX: Number,
|
sourceX: Number,
|
||||||
@ -49,7 +51,7 @@ const pathData = computed(() => getBezierPath({
|
|||||||
}));
|
}));
|
||||||
const edgePath = computed(() => pathData.value[0]);
|
const edgePath = computed(() => pathData.value[0]);
|
||||||
const deleteActionStyle = computed(() => ({
|
const deleteActionStyle = computed(() => ({
|
||||||
transform: `translate(-50%, -50%) translate(${pathData.value[1]}px, ${pathData.value[2] - 24}px)`,
|
transform: `translate(-50%, -50%) translate(${pathData.value[1]}px, ${pathData.value[2]}px)`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const deleteEdge = () => {
|
const deleteEdge = () => {
|
||||||
@ -70,12 +72,12 @@ const deleteEdge = () => {
|
|||||||
height: 30px;
|
height: 30px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
border: 1px solid #f2b8b5;
|
border: 1px solid #efb8b4;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
color: #d93025;
|
color: #d93025;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-shadow: 0 4px 12px rgba(20, 33, 61, 0.18);
|
box-shadow: 0 4px 12px rgba(31, 41, 55, 0.2);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: #d93025;
|
border-color: #d93025;
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
type="target"
|
type="target"
|
||||||
:position="Position.Left"
|
:position="Position.Left"
|
||||||
:connectable="connectable && !flowStore.disableForm"
|
:connectable="connectable && !flowStore.disableForm"
|
||||||
|
:is-valid-connection="isValidLoopConnection"
|
||||||
class="flow-handle flow-handle--target"
|
class="flow-handle flow-handle--target"
|
||||||
/>
|
/>
|
||||||
<div class="flow-node-content" :class="{ 'flow-node-card': !isGroup && flowType !== 'branch' }">
|
<div class="flow-node-content" :class="{ 'flow-node-card': !isGroup && flowType !== 'branch' }">
|
||||||
@ -18,11 +19,13 @@
|
|||||||
:model="model"
|
:model="model"
|
||||||
:graph-model="model"
|
:graph-model="model"
|
||||||
:properties="properties"
|
:properties="properties"
|
||||||
|
:child-count="isGroup ? childNodeCount : undefined"
|
||||||
@content-change="contentChanged"
|
@content-change="contentChanged"
|
||||||
@bind-ref="bindComponent"
|
@bind-ref="bindComponent"
|
||||||
@add-anchor="addAnchor"
|
@add-anchor="addAnchor"
|
||||||
@remove-anchor="removeAnchor"
|
@remove-anchor="removeAnchor"
|
||||||
@change-anchor="changeAnchor"
|
@change-anchor="changeAnchor"
|
||||||
|
@sync-anchors="syncBranchAnchors"
|
||||||
@add-to-group="addToGroup"
|
@add-to-group="addToGroup"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -34,8 +37,9 @@
|
|||||||
type="source"
|
type="source"
|
||||||
:position="Position.Right"
|
:position="Position.Right"
|
||||||
:connectable="connectable && !flowStore.disableForm"
|
:connectable="connectable && !flowStore.disableForm"
|
||||||
|
:is-valid-connection="isValidLoopConnection"
|
||||||
class="flow-handle flow-handle--source flow-handle--branch"
|
class="flow-handle flow-handle--source flow-handle--branch"
|
||||||
:style="{ top: `${Math.max(76, Number(anchor.height || 0) + 60)}px` }"
|
:style="{ top: `${Math.max(40, Number(anchor.height || 0))}px` }"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
@ -45,6 +49,7 @@
|
|||||||
type="source"
|
type="source"
|
||||||
:position="Position.Right"
|
:position="Position.Right"
|
||||||
:connectable="connectable && !flowStore.disableForm"
|
:connectable="connectable && !flowStore.disableForm"
|
||||||
|
:is-valid-connection="isValidLoopConnection"
|
||||||
class="flow-handle flow-handle--source"
|
class="flow-handle flow-handle--source"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -102,6 +107,9 @@ const flowType = computed(() => props.data.flowType);
|
|||||||
const properties = computed(() => props.data.properties || {});
|
const properties = computed(() => props.data.properties || {});
|
||||||
const nodeComponent = computed(() => componentMap[flowType.value] || ServiceNode);
|
const nodeComponent = computed(() => componentMap[flowType.value] || ServiceNode);
|
||||||
const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value));
|
const isGroup = computed(() => ["loop", "selectArea"].includes(flowType.value));
|
||||||
|
const childNodeCount = computed(() => (
|
||||||
|
(vueFlow.getNodes.value || []).filter((node) => node.parentNode === props.id).length
|
||||||
|
));
|
||||||
const showTarget = computed(() => !["start", "subStart", "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 showSource = computed(() => !["end", "stopLoop", "subEnd", "selectArea", "branch"].includes(flowType.value));
|
||||||
const targetHandleId = computed(() => flowType.value === "branch" ? `${props.id}_entry` : `${props.id}_3`);
|
const targetHandleId = computed(() => flowType.value === "branch" ? `${props.id}_entry` : `${props.id}_3`);
|
||||||
@ -120,6 +128,17 @@ const branchAnchors = computed(() => {
|
|||||||
});
|
});
|
||||||
const model = computed(() => window.lf?.getNodeModelById(props.id));
|
const model = computed(() => window.lf?.getNodeModelById(props.id));
|
||||||
|
|
||||||
|
const getLoopScope = (nodeId) => {
|
||||||
|
const node = vueFlow.findNode(nodeId);
|
||||||
|
const parent = node?.parentNode ? vueFlow.findNode(node.parentNode) : null;
|
||||||
|
return parent?.data?.flowType === "loop" ? parent.id : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const isValidLoopConnection = (connection) => {
|
||||||
|
if (!connection.source || !connection.target || connection.source === connection.target) return false;
|
||||||
|
return getLoopScope(connection.source) === getLoopScope(connection.target);
|
||||||
|
};
|
||||||
|
|
||||||
const syncAnchors = (anchors) => {
|
const syncAnchors = (anchors) => {
|
||||||
window.lf?.setProperties(props.id, { ...window.lf.getProperties(props.id), anchor: anchors });
|
window.lf?.setProperties(props.id, { ...window.lf.getProperties(props.id), anchor: anchors });
|
||||||
vueFlow.updateNodeInternals([props.id]);
|
vueFlow.updateNodeInternals([props.id]);
|
||||||
@ -135,6 +154,26 @@ const addAnchor = (height, anchorId) => {
|
|||||||
|
|
||||||
const removeAnchor = (anchorId) => syncAnchors(branchAnchors.value.filter((anchor) => anchor.id !== anchorId));
|
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 changeAnchor = (height, anchorId) => syncAnchors(branchAnchors.value.map((anchor) => anchor.id === anchorId ? { ...anchor, height } : anchor));
|
||||||
|
const syncBranchAnchors = (positions = []) => {
|
||||||
|
if (!positions.length) return;
|
||||||
|
const positionMap = new Map(positions.map((item) => [item.id, Number(item.height || 0)]));
|
||||||
|
const current = [...(properties.value.anchor || [])];
|
||||||
|
let changed = false;
|
||||||
|
const anchors = current.map((anchor) => {
|
||||||
|
if (!positionMap.has(anchor.id)) return anchor;
|
||||||
|
const height = positionMap.get(anchor.id);
|
||||||
|
positionMap.delete(anchor.id);
|
||||||
|
if (Math.abs(Number(anchor.height || 0) - height) < 0.5) return anchor;
|
||||||
|
changed = true;
|
||||||
|
return { ...anchor, height };
|
||||||
|
});
|
||||||
|
positionMap.forEach((height, id) => {
|
||||||
|
anchors.push({ id, name: "right", height });
|
||||||
|
changed = true;
|
||||||
|
});
|
||||||
|
if (changed) syncAnchors(anchors);
|
||||||
|
else vueFlow.updateNodeInternals([props.id]);
|
||||||
|
};
|
||||||
const contentChanged = () => {
|
const contentChanged = () => {
|
||||||
vueFlow.updateNodeInternals([props.id]);
|
vueFlow.updateNodeInternals([props.id]);
|
||||||
window.lf?.markChanged();
|
window.lf?.markChanged();
|
||||||
@ -147,25 +186,47 @@ onMounted(() => bindComponent(componentRef.value));
|
|||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.flow-node-shell {
|
.flow-node-shell {
|
||||||
|
--node-accent: #4f6b8a;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
&--start,
|
||||||
|
&--subStart { --node-accent: #239565; }
|
||||||
|
|
||||||
|
&--end,
|
||||||
|
&--subEnd,
|
||||||
|
&--stopLoop { --node-accent: #d45252; }
|
||||||
|
|
||||||
|
&--branch { --node-accent: #b7791f; }
|
||||||
|
&--loop,
|
||||||
|
&--currentLoop { --node-accent: #df6b35; }
|
||||||
|
&--http,
|
||||||
|
&--code { --node-accent: #3978c5; }
|
||||||
|
&--sdAgent,
|
||||||
|
&--recognize { --node-accent: #7a5ab8; }
|
||||||
|
|
||||||
&--loop,
|
&--loop,
|
||||||
&--selectArea {
|
&--selectArea {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--loop.flow-node-shell--selected .flow-node-content {
|
&:hover .flow-node-card {
|
||||||
box-shadow: none;
|
border-color: #cbd3df;
|
||||||
|
box-shadow: 0 10px 26px rgba(31, 41, 55, 0.11);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--selected .flow-node-content {
|
&--selected .flow-node-card {
|
||||||
outline: none;
|
border-color: #2f80ed;
|
||||||
border: 2px solid #2f80ed;
|
box-shadow: 0 0 0 2px rgba(47, 128, 237, 0.2), 0 10px 26px rgba(31, 41, 55, 0.12);
|
||||||
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.1);
|
}
|
||||||
border-radius: 8px;
|
|
||||||
|
&--selected.flow-node-shell--branch :deep(.node__container),
|
||||||
|
&--selected.flow-node-shell--loop :deep(.group__container),
|
||||||
|
&--selected.flow-node-shell--selectArea :deep(.group__container) {
|
||||||
|
border-color: #2f80ed;
|
||||||
|
box-shadow: 0 0 0 2px rgba(47, 128, 237, 0.18);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,25 +239,92 @@ onMounted(() => bindComponent(componentRef.value));
|
|||||||
.flow-node-card {
|
.flow-node-card {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
padding: 12px;
|
padding: 13px 14px 13px 16px;
|
||||||
|
position: relative;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #e4e8ef;
|
border: 1px solid #dfe4ec;
|
||||||
|
border-left: 4px solid var(--node-accent);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 8px 24px rgba(20, 33, 61, 0.08);
|
box-shadow: 0 7px 20px rgba(31, 41, 55, 0.08);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
transition: border-color 0.16s ease, box-shadow 0.16s ease;
|
||||||
|
|
||||||
|
:deep(.node__container) {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.terminal-node) {
|
||||||
|
display: flex;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.terminal-node__icon) {
|
||||||
|
display: grid;
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
flex: 0 0 38px;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.terminal-node__icon img) {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.terminal-node__content) { min-width: 0; }
|
||||||
|
|
||||||
|
:deep(.terminal-node__title) {
|
||||||
|
color: #1f2937;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 650;
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.terminal-node__description) {
|
||||||
|
overflow: hidden;
|
||||||
|
margin-top: 3px;
|
||||||
|
color: #7a8494;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 16px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flow-handle {
|
.flow-handle {
|
||||||
width: 14px;
|
width: 12px;
|
||||||
height: 14px;
|
height: 12px;
|
||||||
border: 3px solid #fff;
|
border: 2px solid #fff;
|
||||||
background: #ff6b35;
|
background: var(--node-accent);
|
||||||
box-shadow: 0 0 0 1px #ff6b35;
|
box-shadow: 0 0 0 1px var(--node-accent), 0 2px 5px rgba(31, 41, 55, 0.18);
|
||||||
z-index: 8;
|
z-index: 10;
|
||||||
|
transition: transform 0.14s ease, box-shadow 0.14s ease;
|
||||||
|
|
||||||
&--target { left: -7px; }
|
&:hover {
|
||||||
&--source { right: -7px; }
|
box-shadow: 0 0 0 3px rgba(47, 128, 237, 0.18), 0 2px 6px rgba(31, 41, 55, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--target {
|
||||||
|
left: -6px;
|
||||||
|
background: #fff;
|
||||||
|
border-color: var(--node-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--source { right: -6px; }
|
||||||
&--branch { transform: translate(50%, -50%); }
|
&--branch { transform: translate(50%, -50%); }
|
||||||
|
|
||||||
|
&--branch:hover { transform: translate(50%, -50%) scale(1.18); }
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -8,6 +8,7 @@ const GROUP_TYPES = new Set(["loop", "selectArea"]);
|
|||||||
const TARGET_ONLY_TYPES = new Set(["end", "stopLoop", "subEnd"]);
|
const TARGET_ONLY_TYPES = new Set(["end", "stopLoop", "subEnd"]);
|
||||||
const SOURCE_ONLY_TYPES = new Set(["start", "subStart"]);
|
const SOURCE_ONLY_TYPES = new Set(["start", "subStart"]);
|
||||||
const LOOP_BODY_INSET = { left: 24, top: 112, right: 24, bottom: 24 };
|
const LOOP_BODY_INSET = { left: 24, top: 112, right: 24, bottom: 24 };
|
||||||
|
const GROUP_BODY_INSET = { left: 24, top: 120, right: 24, bottom: 24 };
|
||||||
const LOOP_MIN_SIZE = { width: 520, height: 360 };
|
const LOOP_MIN_SIZE = { width: 520, height: 360 };
|
||||||
|
|
||||||
const loopChildExtent = () => ({
|
const loopChildExtent = () => ({
|
||||||
@ -20,6 +21,16 @@ const loopChildExtent = () => ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const groupChildExtent = () => ({
|
||||||
|
range: "parent",
|
||||||
|
padding: [
|
||||||
|
GROUP_BODY_INSET.top,
|
||||||
|
GROUP_BODY_INSET.right,
|
||||||
|
GROUP_BODY_INSET.bottom,
|
||||||
|
GROUP_BODY_INSET.left,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const nodeSize = (node) => {
|
const nodeSize = (node) => {
|
||||||
const width = Number(node.properties?.width || node.width);
|
const width = Number(node.properties?.width || node.width);
|
||||||
const height = Number(node.properties?.height || node.height);
|
const height = Number(node.properties?.height || node.height);
|
||||||
@ -37,19 +48,28 @@ const nodeSize = (node) => {
|
|||||||
const edgeId = () => `edge_${uuid().replace(/-/g, "")}`;
|
const edgeId = () => `edge_${uuid().replace(/-/g, "")}`;
|
||||||
const nodeId = () => uuid().replace(/-/g, "");
|
const nodeId = () => uuid().replace(/-/g, "");
|
||||||
|
|
||||||
const toVueNode = (node) => {
|
const toVueNode = (node, sizeOverride, positionOverride) => {
|
||||||
const normalized = clone(node);
|
const normalized = clone(node);
|
||||||
normalized.id ||= nodeId();
|
normalized.id ||= nodeId();
|
||||||
normalized.properties ||= {};
|
normalized.properties ||= {};
|
||||||
const size = nodeSize(normalized);
|
const overrideWidth = Number(sizeOverride?.width);
|
||||||
|
const overrideHeight = Number(sizeOverride?.height);
|
||||||
|
const size = Number.isFinite(overrideWidth) && overrideWidth > 0
|
||||||
|
&& Number.isFinite(overrideHeight) && overrideHeight > 0
|
||||||
|
? { width: overrideWidth, height: overrideHeight }
|
||||||
|
: nodeSize(normalized);
|
||||||
const center = {
|
const center = {
|
||||||
x: Number(normalized.x) || 0,
|
x: Number(normalized.x) || 0,
|
||||||
y: Number(normalized.y) || 0,
|
y: Number(normalized.y) || 0,
|
||||||
};
|
};
|
||||||
const position = {
|
const overrideX = Number(positionOverride?.x);
|
||||||
x: center.x - size.width / 2,
|
const overrideY = Number(positionOverride?.y);
|
||||||
y: center.y - size.height / 2,
|
const position = Number.isFinite(overrideX) && Number.isFinite(overrideY)
|
||||||
};
|
? { x: overrideX, y: overrideY }
|
||||||
|
: {
|
||||||
|
x: center.x - size.width / 2,
|
||||||
|
y: center.y - size.height / 2,
|
||||||
|
};
|
||||||
|
|
||||||
const dimensions = GROUP_TYPES.has(normalized.type)
|
const dimensions = GROUP_TYPES.has(normalized.type)
|
||||||
? { width: size.width, height: size.height }
|
? { width: size.width, height: size.height }
|
||||||
@ -106,9 +126,16 @@ const toVueEdge = (edge) => {
|
|||||||
|
|
||||||
const attachChildrenToGroups = (nodes) => {
|
const attachChildrenToGroups = (nodes) => {
|
||||||
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
const declaredParents = new Map();
|
||||||
|
|
||||||
|
nodes.filter((node) => GROUP_TYPES.has(node.data.flowType)).forEach((group) => {
|
||||||
|
(group.data.children || []).forEach((childId) => {
|
||||||
|
if (!declaredParents.has(childId)) declaredParents.set(childId, group.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
nodes.forEach((child) => {
|
nodes.forEach((child) => {
|
||||||
const parentId = child.data.properties?.parentId;
|
const parentId = child.data.properties?.parentId || declaredParents.get(child.id);
|
||||||
const parent = nodeLookup.get(parentId);
|
const parent = nodeLookup.get(parentId);
|
||||||
if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return;
|
if (!parent || !GROUP_TYPES.has(parent.data.flowType) || GROUP_TYPES.has(child.data.flowType)) return;
|
||||||
|
|
||||||
@ -122,10 +149,12 @@ const attachChildrenToGroups = (nodes) => {
|
|||||||
: child.position.y - parent.position.y,
|
: child.position.y - parent.position.y,
|
||||||
};
|
};
|
||||||
child.parentNode = parentId;
|
child.parentNode = parentId;
|
||||||
child.extent = isLoop ? loopChildExtent() : "parent";
|
child.extent = isLoop ? loopChildExtent() : groupChildExtent();
|
||||||
child.expandParent = false;
|
child.expandParent = false;
|
||||||
child.position = relativePosition;
|
child.position = relativePosition;
|
||||||
child.zIndex = 2;
|
child.zIndex = 5;
|
||||||
|
child.data.properties = { ...child.data.properties, parentId };
|
||||||
|
parent.data.children = Array.from(new Set([...(parent.data.children || []), child.id]));
|
||||||
});
|
});
|
||||||
|
|
||||||
nodes.filter((node) => node.data.flowType === "loop").forEach((group) => {
|
nodes.filter((node) => node.data.flowType === "loop").forEach((group) => {
|
||||||
@ -148,10 +177,20 @@ const attachChildrenToGroups = (nodes) => {
|
|||||||
return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode)));
|
return nodes.sort((left, right) => Number(Boolean(left.parentNode)) - Number(Boolean(right.parentNode)));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const toVueFlowData = (graph = {}) => ({
|
export const toVueFlowData = (graph = {}, options = {}) => {
|
||||||
nodes: attachChildrenToGroups((graph.nodes || []).map(toVueNode)),
|
const nodes = attachChildrenToGroups((graph.nodes || []).map((node) => (
|
||||||
edges: (graph.edges || []).map(toVueEdge),
|
toVueNode(node, options.nodeSizes?.get(node.id), options.nodePositions?.get(node.id))
|
||||||
});
|
)));
|
||||||
|
const nodeLookup = new Map(nodes.map((node) => [node.id, node]));
|
||||||
|
const edges = (graph.edges || []).map((edge) => {
|
||||||
|
const vueEdge = toVueEdge(edge);
|
||||||
|
const source = nodeLookup.get(vueEdge.source);
|
||||||
|
const target = nodeLookup.get(vueEdge.target);
|
||||||
|
if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4;
|
||||||
|
return vueEdge;
|
||||||
|
});
|
||||||
|
return { nodes, edges };
|
||||||
|
};
|
||||||
|
|
||||||
export const createFlowFacade = (store) => {
|
export const createFlowFacade = (store) => {
|
||||||
const handlers = new Map();
|
const handlers = new Map();
|
||||||
@ -177,9 +216,20 @@ export const createFlowFacade = (store) => {
|
|||||||
: (node.position || node.computedPosition);
|
: (node.position || node.computedPosition);
|
||||||
result.id = node.id;
|
result.id = node.id;
|
||||||
result.type = node.data.flowType;
|
result.type = node.data.flowType;
|
||||||
result.x = absolutePosition.x + size.width / 2;
|
if (GROUP_TYPES.has(result.type)) {
|
||||||
result.y = absolutePosition.y + size.height / 2;
|
result.x = absolutePosition.x + size.width / 2;
|
||||||
|
result.y = absolutePosition.y + size.height / 2;
|
||||||
|
} else {
|
||||||
|
const originalCenter = node.data.originalCenter || {
|
||||||
|
x: absolutePosition.x + size.width / 2,
|
||||||
|
y: absolutePosition.y + size.height / 2,
|
||||||
|
};
|
||||||
|
const originalPosition = node.data.originalPosition || absolutePosition;
|
||||||
|
result.x = originalCenter.x + absolutePosition.x - originalPosition.x;
|
||||||
|
result.y = originalCenter.y + absolutePosition.y - originalPosition.y;
|
||||||
|
}
|
||||||
result.properties = clone(node.data.properties || {});
|
result.properties = clone(node.data.properties || {});
|
||||||
|
if (parent?.data?.flowType === "selectArea") delete result.properties.parentId;
|
||||||
if (GROUP_TYPES.has(result.type)) {
|
if (GROUP_TYPES.has(result.type)) {
|
||||||
result.properties.width = size.width;
|
result.properties.width = size.width;
|
||||||
result.properties.height = size.height;
|
result.properties.height = size.height;
|
||||||
@ -342,6 +392,9 @@ export const createFlowFacade = (store) => {
|
|||||||
get y() { return backendNode(node).y; },
|
get y() { return backendNode(node).y; },
|
||||||
get width() { return node.dimensions?.width || node.data.size?.width || 0; },
|
get width() { return node.dimensions?.width || node.data.size?.width || 0; },
|
||||||
get height() { return node.dimensions?.height || node.data.size?.height || 0; },
|
get height() { return node.dimensions?.height || node.data.size?.height || 0; },
|
||||||
|
get left() { return getNodeRect(node).x; },
|
||||||
|
get top() { return getNodeRect(node).y; },
|
||||||
|
get parentId() { return node.parentNode || null; },
|
||||||
get anchors() { return getAnchors(node); },
|
get anchors() { return getAnchors(node); },
|
||||||
get children() { return new Set(node.data.children || []); },
|
get children() { return new Set(node.data.children || []); },
|
||||||
set children(value) {
|
set children(value) {
|
||||||
@ -390,18 +443,44 @@ export const createFlowFacade = (store) => {
|
|||||||
render(graph, options = {}) {
|
render(graph, options = {}) {
|
||||||
historySuspended = true;
|
historySuspended = true;
|
||||||
loopLayoutStates.clear();
|
loopLayoutStates.clear();
|
||||||
const data = toVueFlowData(graph);
|
const nodeSizes = options.preserveNodeSizes === true
|
||||||
store.setNodes(data.nodes);
|
? new Map(getNodes().map((node) => [node.id, {
|
||||||
store.setEdges(data.edges);
|
width: Number(node.dimensions?.width || node.width || node.data.size?.width || 0),
|
||||||
nextTick(() => {
|
height: Number(node.dimensions?.height || node.height || node.data.size?.height || 0),
|
||||||
store.updateNodeInternals();
|
}]))
|
||||||
|
: options.nodeSizes;
|
||||||
|
const nodePositions = options.preserveNodePositions === true
|
||||||
|
? new Map(getNodes().map((node) => {
|
||||||
|
const rect = getNodeRect(node);
|
||||||
|
return [node.id, { x: rect.x, y: rect.y }];
|
||||||
|
}))
|
||||||
|
: options.nodePositions;
|
||||||
|
const data = toVueFlowData(graph, { nodeSizes, nodePositions });
|
||||||
|
const finishRender = () => {
|
||||||
historySuspended = false;
|
historySuspended = false;
|
||||||
if (options.resetHistory !== false) {
|
if (options.resetHistory !== false) {
|
||||||
history = [];
|
history = [];
|
||||||
historyIndex = -1;
|
historyIndex = -1;
|
||||||
recordHistory();
|
recordHistory();
|
||||||
}
|
}
|
||||||
});
|
nextTick(() => store.updateNodeInternals());
|
||||||
|
};
|
||||||
|
const applyData = () => {
|
||||||
|
store.setNodes(data.nodes);
|
||||||
|
store.setEdges(data.edges);
|
||||||
|
finishRender();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.replaceNodes === true) {
|
||||||
|
// Changing parentNode is structural in Vue Flow. Unmount in one tick
|
||||||
|
// and remount in the next so node wrappers and edges cannot retain
|
||||||
|
// different graph-node instances for the same id.
|
||||||
|
store.setEdges([]);
|
||||||
|
store.setNodes([]);
|
||||||
|
nextTick(applyData);
|
||||||
|
} else {
|
||||||
|
applyData();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
addNode(node) {
|
addNode(node) {
|
||||||
const vueNode = toVueNode(node);
|
const vueNode = toVueNode(node);
|
||||||
@ -411,6 +490,10 @@ export const createFlowFacade = (store) => {
|
|||||||
},
|
},
|
||||||
deleteNode(id) {
|
deleteNode(id) {
|
||||||
const target = store.findNode(id);
|
const target = store.findNode(id);
|
||||||
|
if (target?.data?.flowType === "selectArea") {
|
||||||
|
facade.ungroup(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const parentId = target?.parentNode;
|
const parentId = target?.parentNode;
|
||||||
const childIds = target?.data?.children || [];
|
const childIds = target?.data?.children || [];
|
||||||
const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0;
|
const removeChildren = target?.data?.flowType === "loop" && childIds.length > 0;
|
||||||
@ -432,6 +515,9 @@ export const createFlowFacade = (store) => {
|
|||||||
},
|
},
|
||||||
addEdge(edge) {
|
addEdge(edge) {
|
||||||
const vueEdge = toVueEdge(edge);
|
const vueEdge = toVueEdge(edge);
|
||||||
|
const source = store.findNode(vueEdge.source);
|
||||||
|
const target = store.findNode(vueEdge.target);
|
||||||
|
if (source?.parentNode || target?.parentNode) vueEdge.zIndex = 4;
|
||||||
store.addEdges(vueEdge);
|
store.addEdges(vueEdge);
|
||||||
changed();
|
changed();
|
||||||
return backendEdge(vueEdge);
|
return backendEdge(vueEdge);
|
||||||
@ -508,12 +594,75 @@ export const createFlowFacade = (store) => {
|
|||||||
childData.x = dropCenter.x;
|
childData.x = dropCenter.x;
|
||||||
childData.y = dropCenter.y;
|
childData.y = dropCenter.y;
|
||||||
}
|
}
|
||||||
facade.render(graph, { resetHistory: false });
|
facade.render(graph, {
|
||||||
|
resetHistory: false,
|
||||||
|
replaceNodes: true,
|
||||||
|
preserveNodeSizes: true,
|
||||||
|
preserveNodePositions: true,
|
||||||
|
});
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
store.updateNodeInternals([groupId, childId]);
|
store.updateNodeInternals([groupId, childId]);
|
||||||
changed();
|
changed();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
groupNodes(groupNode, childIds = []) {
|
||||||
|
const graph = facade.getGraphData();
|
||||||
|
const selectedIds = new Set(childIds);
|
||||||
|
const children = graph.nodes.filter((node) => (
|
||||||
|
selectedIds.has(node.id)
|
||||||
|
&& !GROUP_TYPES.has(node.type)
|
||||||
|
&& !store.findNode(node.id)?.parentNode
|
||||||
|
));
|
||||||
|
if (!children.length) return null;
|
||||||
|
|
||||||
|
const group = clone(groupNode);
|
||||||
|
group.id ||= nodeId();
|
||||||
|
group.type = "selectArea";
|
||||||
|
group.properties = { ...(group.properties || {}), action: "NONE" };
|
||||||
|
group.children = children.map((node) => node.id);
|
||||||
|
children.forEach((node) => {
|
||||||
|
node.properties = { ...(node.properties || {}), parentId: group.id };
|
||||||
|
});
|
||||||
|
graph.nodes.push(group);
|
||||||
|
facade.render(graph, {
|
||||||
|
resetHistory: false,
|
||||||
|
replaceNodes: true,
|
||||||
|
preserveNodeSizes: true,
|
||||||
|
preserveNodePositions: true,
|
||||||
|
});
|
||||||
|
nextTick(() => {
|
||||||
|
store.updateNodeInternals([group.id, ...group.children]);
|
||||||
|
changed();
|
||||||
|
});
|
||||||
|
return clone(group);
|
||||||
|
},
|
||||||
|
ungroup(groupId) {
|
||||||
|
const graph = facade.getGraphData();
|
||||||
|
const group = graph.nodes.find((node) => node.id === groupId && node.type === "selectArea");
|
||||||
|
if (!group) return false;
|
||||||
|
const childIds = new Set([
|
||||||
|
...(group.children || []),
|
||||||
|
...graph.nodes.filter((node) => node.properties?.parentId === groupId).map((node) => node.id),
|
||||||
|
]);
|
||||||
|
graph.nodes = graph.nodes.filter((node) => node.id !== groupId);
|
||||||
|
graph.nodes.forEach((node) => {
|
||||||
|
if (!childIds.has(node.id)) return;
|
||||||
|
const properties = { ...(node.properties || {}) };
|
||||||
|
delete properties.parentId;
|
||||||
|
node.properties = properties;
|
||||||
|
});
|
||||||
|
facade.render(graph, {
|
||||||
|
resetHistory: false,
|
||||||
|
replaceNodes: true,
|
||||||
|
preserveNodeSizes: true,
|
||||||
|
preserveNodePositions: true,
|
||||||
|
});
|
||||||
|
nextTick(() => {
|
||||||
|
store.updateNodeInternals([...childIds]);
|
||||||
|
changed();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
prepareLoopChildDrag(childId) {
|
prepareLoopChildDrag(childId) {
|
||||||
const child = store.findNode(childId);
|
const child = store.findNode(childId);
|
||||||
const group = child?.parentNode ? store.findNode(child.parentNode) : null;
|
const group = child?.parentNode ? store.findNode(child.parentNode) : null;
|
||||||
@ -551,7 +700,7 @@ export const createFlowFacade = (store) => {
|
|||||||
if (historyIndex <= 0) return false;
|
if (historyIndex <= 0) return false;
|
||||||
historyIndex -= 1;
|
historyIndex -= 1;
|
||||||
const snapshot = clone(history[historyIndex].graph);
|
const snapshot = clone(history[historyIndex].graph);
|
||||||
facade.render(snapshot, { resetHistory: false });
|
facade.render(snapshot, { resetHistory: false, replaceNodes: true });
|
||||||
nextTick(() => emit("history:change", { data: facade.getGraphData() }));
|
nextTick(() => emit("history:change", { data: facade.getGraphData() }));
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
@ -559,7 +708,7 @@ export const createFlowFacade = (store) => {
|
|||||||
if (historyIndex >= history.length - 1) return false;
|
if (historyIndex >= history.length - 1) return false;
|
||||||
historyIndex += 1;
|
historyIndex += 1;
|
||||||
const snapshot = clone(history[historyIndex].graph);
|
const snapshot = clone(history[historyIndex].graph);
|
||||||
facade.render(snapshot, { resetHistory: false });
|
facade.render(snapshot, { resetHistory: false, replaceNodes: true });
|
||||||
nextTick(() => emit("history:change", { data: facade.getGraphData() }));
|
nextTick(() => emit("history:change", { data: facade.getGraphData() }));
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user