Merge branch 'main' of ssh://18.167.251.121:10002/aidlab/FiDA_Front

This commit is contained in:
2026-02-09 15:03:22 +08:00
21 changed files with 797 additions and 195 deletions

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
import VersionTreeIndex from './versionTree/index.vue'
import GenerateSketch from './generateSketch/index.vue'
//const props = defineProps({
//})
//const emit = defineEmits([
@@ -13,6 +14,73 @@ const versionTreeData = ref({
return []
})
})
const generateData = ref({
list:[
{
id:'1',
type:'waiting',
},
{
id:'2',
type:'success',
img:'/img/success.png',
},
{
id:'3',
type:'success',
img:'/img/success.png',
},
{
id:'4',
type:'success',
img:'/img/success.png',
},
{
id:'5',
type:'success',
img:'/img/success.png',
},
{
id:'6',
type:'success',
img:'/img/success.png',
},
{
id:'7',
type:'success',
img:'/img/success.png',
},
{
id:'8',
type:'success',
img:'/img/success.png',
},
{
id:'9',
type:'success',
img:'/img/success.png',
},
]
})
const generateSketch = ()=>{
generateData.value.list.push(
{
id:'2',
type:'waiting',
img:'/img/success.png',
}
)
sketchRestore('2')
}
const sketchRestore = (id)=>{
generateData.value.list.forEach((item)=>{
if(item.id == id){
item.type = 'waiting'
}
})
}
onMounted(()=>{
})
onUnmounted(()=>{
@@ -22,8 +90,14 @@ const {} = toRefs(data);
</script>
<template>
<div class="homeNavBox">
<el-button type="primary" @click="versionTreeData.drawer = true">open Version Tree</el-button>
<div>
<el-button type="primary" @click="versionTreeData.drawer = true">open Version Tree</el-button>
<el-button type="primary" @click="generateSketch">Generate</el-button>
</div>
<VersionTreeIndex v-model:versionTreeData="versionTreeData" />
<div class="generateSketchBox">
<GenerateSketch v-model:generateData="generateData"></GenerateSketch>
</div>
</div>
</template>
<style lang="less" scoped>
@@ -31,5 +105,14 @@ const {} = toRefs(data);
width: 100%;
height: 100%;
position: relative;
display: flex;
> .generateSketchBox{
height: 100%;
width: 50%;
overflow-y: auto;
&::-webkit-scrollbar {
display: none;
}
}
}
</style>

View File

@@ -0,0 +1,164 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
const props = defineProps({
item: {
type: Object,
default: () => ({})
} as any,
})
const emit = defineEmits([
'sketchRestore','sketchDelete'
])
let data = reactive({
})
const morePopoverRef = ref(null)
const handleClick = ()=>{
morePopoverRef.value?.hide()
}
const sketchRestore = ()=>{
emit('sketchRestore')
handleClick()
}
const sketchDelete = ()=>{
emit('sketchDelete')
handleClick()
}
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="generateItem">
<div v-if="item.type === 'waiting'" class="waitingItem">
等待
<!-- <el-spinner style="display: inline-block;" /> -->
</div>
<div v-else-if="item.type === 'success'" class="successItem">
<img :src="item.img" alt="">
<div class="more">
<el-popover
width="10rem"
min-width="10rem"
ref="morePopoverRef"
trigger="click"
popper-class="moreBox"
placement="bottom-end"
>
<template #reference>
<span class="icon"><svg-icon name="more" size="25" /></span>
</template>
<template #default>
<div class="item" @click="sketchRestore">
<div class="icon">
<SvgIcon name="sketchRestore" size="14" />
</div>
<span>{{ $t('generateSketch.restore') }}</span>
</div>
<div class="item delete" @click="sketchDelete">
<div class="icon">
<SvgIcon name="sketchDelete" size="14" />
</div>
<span>{{ $t('generateSketch.delete') }}</span>
</div>
</template>
</el-popover>
</div>
<div class="edit" @click="$emit('edit')">
<span>{{ $t('generateSketch.edit') }}</span>
<div class="icon">
<SvgIcon name="generateSketchEdit" size="7" />
</div>
</div>
</div>
</div>
</template>
<style lang="less">
.el-popover.el-popper.moreBox{
border-radius: 1rem;
padding: .6rem .7rem;
min-width: 10rem;
.item{
display: flex;
cursor: pointer;
border-radius: .4rem;
align-items: center;
padding: .6rem .7rem;
margin-bottom: 1.2rem;
&.delete{
margin-bottom: 0rem;
color: #ff4747;
}
&:hover{
background-color: #f6f6f6;
}
> .icon{
margin-right: .7rem;
}
> span{
color: #000;
font-weight: 500;
font-size: 1.3rem;
line-height: 100%;
}
}
}
</style>
<style lang="less" scoped>
.generateItem{
width: 100%;
height: 100%;
position: relative;
> div{
position: relative;
}
> .waitingItem,.successItem{
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
}
> .successItem{
> .more{
position: absolute;
top: 1.2rem;
right: 1.2rem;
transition: all .3s;
border-radius: .3rem;
&:hover{
background-color: #e8e8e8;
}
.icon{
width: 2.4rem;
height: 2.4rem;
cursor: pointer;
}
}
> .edit{
position: absolute;
bottom: 1rem;
right: 1rem;
border-radius: 2rem;
border: 2px solid #e5e5e5;
background-color: #fff;
display: flex;
justify-content: center;
padding: .65rem 1.2rem;
cursor: pointer;
> span{
margin-right: .6rem;
font-weight: 500;
font-size: 1.4rem;
line-height: 100%;
}
}
}
}
</style>

View File

@@ -0,0 +1,52 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import GenerateItem from './generateItem.vue'
const props = defineProps({
generateData:{
type:Object,
default:()=>{
return {
list:[]
}
},
},
})
//const emit = defineEmits([
//])
let data = reactive({
})
onMounted(()=>{
})
onUnmounted(()=>{
})
defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="generateSketch">
<div v-for="item in generateData.list" :key="item.id" class="item">
<GenerateItem :item="item" />
</div>
</div>
</template>
<style lang="less" scoped>
.generateSketch{
width: 100%;
height: 100%;
position: relative;
display: flex;
flex-wrap: wrap;
align-content: flex-start;
gap: 1.2rem;
.item{
width: calc((100% - 1.2rem * 3) / 4);
//设置比例属性 1 / 1
aspect-ratio: 1 / 1;
background-color: #fff;
border-radius: 1.6rem;
overflow: hidden;
}
}
</style>

View File

@@ -36,7 +36,6 @@ const {} = toRefs(data);
<div class="infoTitle">{{ type == 'user'? $t('VersionTree.userRequest'): $t('VersionTree.generateResult') }}</div>
<div class="history">
Design a modern yellow sofa that combines comfort, elegance, and contemporary aesthetics. The sofa features a warm, soft yellow tone (mustard or light ochre), with a minimalist silhouette and clean lines. Upholstered in high-quality fabric with a subtle texture, offering a cozy and inviting feel. The seat is deep and plush, with generous cushioning for relaxation, while the backrest and armrests are softly rounded to enhance comfort.
Design a modern yellow sofa that combines comfort, elegance, and contemporary aesthetics. The sofa features a warm, soft yellow tone (mustard or light ochre), with a minimalist silhouette and clean lines. Upholstered in high-quality fabric with a subtle texture, offering a cozy and inviting feel. The seat is deep and plush, with generous cushioning for relaxation, while the backrest and armrests are softly rounded to enhance comfort.
</div>
</div>
</template>
@@ -91,12 +90,16 @@ const {} = toRefs(data);
flex: 1;
overflow-y: auto;
margin-left: 3rem;
scrollbar-width: thin;
scrollbar-color: #ababab #f1f5f9;
-ms-overflow-style: -ms-autohiding-scrollbar;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #ababab;
}
&::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 10px;
border-radius: 4px;
background: #d9d9d9;
}
}
}

View File

@@ -4,8 +4,10 @@ import VersionDetail from './versionDetail.vue'
import ChatHistory from './chatHistory.vue'
//const props = defineProps({
//})
//const emit = defineEmits([
//])
const emit = defineEmits([
'versionRestore',
'versionDelete',
])
const detailData = ref({
id:1,
versionDetail:{
@@ -30,7 +32,11 @@ defineExpose({})
<template>
<div class="detailBox">
<div class="versionDetail">
<VersionDetail :versionDetail="detailData.versionDetail"></VersionDetail>
<VersionDetail
:versionDetail="detailData.versionDetail"
@versionRestore="()=>emit('versionRestore')"
@versionDelete="emit('versionDelete')"
></VersionDetail>
</div>
<div class="useInput">
<ChatHistory type="user"></ChatHistory>

View File

@@ -8,10 +8,28 @@ const props = defineProps({
default: () => ({})
}
})
//const emit = defineEmits([
//])
const emit = defineEmits([
'versionRestore',
'versionDelete',
])
let data = reactive({
})
const morePopoverRef = ref(null)
const handleClick = ()=>{
morePopoverRef.value?.hide()
}
const versionRestore = ()=>{
emit('versionRestore')
handleClick()
}
const versionDelete = ()=>{
emit('versionDelete')
handleClick()
}
onMounted(()=>{
})
onUnmounted(()=>{
@@ -25,7 +43,41 @@ const {} = toRefs(data);
<span class="titleText">
{{ $t('VersionTree.versionInformation') }}
</span>
<span class="icon"><svg-icon name="more" size="25" /></span>
<el-popover
width="24rem"
ref="morePopoverRef"
trigger="click"
popper-class="moreBox"
placement="bottom-end"
>
<template #reference>
<span class="icon"><svg-icon name="more" size="25" /></span>
</template>
<template #default>
<div class="topBtn">
<div class="item" @click="versionRestore">
<div class="icon">
<SvgIcon name="versionRestore" size="18" />
</div>
<span>{{ $t('VersionTree.restore') }}</span>
</div>
<!-- <div class="item" @click="$emit('versionNewChat')">
<div class="icon">
<SvgIcon name="versionNewChat" size="18" />
</div>
<span>{{ $t('VersionTree.newChat') }}</span>
</div> -->
</div>
<div class="bottomBtn">
<div class="item" @click="versionDelete">
<div class="icon">
<SvgIcon name="versionDelete" size="18" />
</div>
<span>{{ $t('VersionTree.delete') }}</span>
</div>
</div>
</template>
</el-popover>
</div>
<div class="version">{{versionDetail.version}}</div>
<div class="time marBott1">{{versionDetail.versionTime}}</div>
@@ -33,26 +85,71 @@ const {} = toRefs(data);
<div class="time gray">{{versionDetail.versionSketchTime}}</div>
</div>
</template>
<style lang="less">
.el-popover.el-popper.moreBox{
border-radius: .8rem;
padding: 0;
.topBtn,.bottomBtn{
display: flex;
flex-direction: column;
justify-content: center;
padding: .8rem;
.item{
display: flex;
padding: 0rem 1rem;
cursor: pointer;
line-height: 3.2rem;
border-radius: .4rem;
height: 3.2rem;
align-items: center;
&:hover{
background-color: #f6f6f6;
}
> .icon{
margin-right: 1rem;
}
> span{
color: #000;
font-weight: 500;
font-size: 1.5rem;
line-height: 100%;
}
}
}
.topBtn{
border-bottom: 1px solid #d4d4d4;
}
}
</style>
<style lang="less" scoped>
.versionDetail{
width: 100%;
height: 100%;
position: relative;
> .title{
line-height: 2rem;
line-height: 3.2rem;
font-size: 1.4rem;
color: #000;
display: flex;
justify-content: space-between;
height: 2rem;
margin-bottom: 2.8rem;
height: 3.2rem;
margin-bottom: 2rem;
> .titleText{
opacity: .5;
font-weight: 600;
font-family: 'SemiBold';
}
> .icon{
.icon{
color: #5a5a5a;
width: 3.6rem;
height: 100%;
border-radius: .6rem;
cursor: pointer;
transition: all .3s;
&:hover{
background-color: #E5E5E5;
}
}
}
> .version{

View File

@@ -2,6 +2,9 @@
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
import Tree from './tree/index.vue'
import Detail from './detail/index.vue'
import { versionsList } from './tools/versionsData'
import { findAndAddChild, findAndRemoveChild } from './tools/tools'
const props = defineProps({
versionTreeData:{
type:Object,
@@ -15,13 +18,51 @@ const props = defineProps({
})
//const emit = defineEmits([
//])
const treeRef = ref(null)
const treeKey = ref(0)
const treeState = ref(true)//
const selectItem = ref({})
const selectItem:any = ref({})
const openTree = ()=>{
treeState.value = !treeState.value
const openTree = (state)=>{
treeState.value = state
}
const versionRestore = ()=>{
let id = ''
if(selectItem.value?.child?.length > 0){
function findMaxForYourFormat(items) {
let max = 0
for (const item of items) {
// 直接分割并取最后一部分
const parts = item.id.split('-')
const lastNumber = parseInt(parts[parts.length - 1], 10)
if (lastNumber > max) {
max = lastNumber
}
}
return max
}
id = `${selectItem.value?.id}-${findMaxForYourFormat(selectItem.value?.child) + 1}`
}else{
id = `${selectItem.value?.id}-1`
}
let addObj = {
id,
name:`V${id}`
}
findAndAddChild(versionsList, selectItem.value?.id, addObj)
selectItem.value = {...addObj}
treeKey.value++
}
const versionDelete = (versionDetail)=>{
if(!selectItem.value.id)return
findAndRemoveChild(versionsList, selectItem.value.id)
treeKey.value++
}
let data = reactive({
@@ -50,16 +91,32 @@ const {} = toRefs(data);
</div>
</div>
</div>
<div style="display: flex;" class="expandBtnBox">
<el-button class="expandBtn" @click="openTree" style="width: 5rem;">+</el-button>
<el-button class="expandBtn" @click="openTree" style="width: 5rem;">-</el-button>
<div class="expandBtnBox">
<div class="btn" @click="openTree(true)">
<div class="bg left" :class="{'active':treeState}"></div>
<span>{{ $t('VersionTree.linearNodeTree') }}</span>
</div>
<div class="btn" @click="openTree(false)">
<div class="bg right" :class="{'active':!treeState}"></div>
<span>{{ $t('VersionTree.branchingNodeTree') }}</span>
</div>
</div>
<div class="versionTreeBox">
<div class="tree">
<Tree :treeState="treeState" v-model:selectItem="selectItem"></Tree>
<Tree
ref="treeRef"
:versionsList="versionsList"
:treeState="treeState"
v-model:selectItem="selectItem"
:key="treeKey"
></Tree>
</div>
<div class="detail">
<Detail v-model:selectItem="selectItem"></Detail>
<Detail
v-model:selectItem="selectItem"
@versionRestore="versionRestore"
@versionDelete="versionDelete"
></Detail>
</div>
</div>
</el-drawer>
@@ -71,7 +128,7 @@ const {} = toRefs(data);
--treeItem-width: 5.4rem;
--treeItem-height: 5.4rem;
--treeItem-raduis: 50%;
--treeItem-border: 2px solid #000;
--treeItem-border: 2px solid #C1C1C1;
--treeItem-background: #ffffff;
--treeItem-active-background: #e6e6e6;
@@ -100,7 +157,43 @@ const {} = toRefs(data);
}
}
.expandBtnBox{
padding: .4rem .7rem;
border-radius: 1.1rem;
background-color: #f3f3f3;
display: flex;
margin-left: auto;
margin-top: 2.1rem;
margin-right: 3rem;
> .btn{
padding: .6rem .5rem;
position: relative;
cursor: pointer;
> .bg{
position: absolute;
background-color: #fff;
border-radius: .6rem;
width: 0;
height: 100%;
transition: all .3s;
top: 0;
&.active{
width: 100%;
}
&.left{
right: 0;
}
&.right{
left: 0;
}
}
> span{
position: relative;
font-size: 1.3rem;
line-height: 1.8rem;
font-weight: 500;
letter-spacing: -0.08px;
}
}
}
.versionTreeBox{
flex: 1;

View File

@@ -36,7 +36,10 @@ export function useLayout() {
// if you need width+height of nodes for your layout, you can use the dimensions property of the internal node (`GraphNode` type)
const graphNode = findNode(node.id)
dagreGraph.setNode(node.id, { width: graphNode.dimensions.width || 150, height: graphNode.dimensions.height || 50 })
dagreGraph.setNode(node.id, {
width: graphNode.dimensions.width || 150,
height: graphNode.dimensions.height || 50
})
}
for (const edge of edges) {
@@ -53,11 +56,11 @@ export function useLayout() {
let targetPosition, sourcePosition
switch (layoutDirection) {
case 'BT': // 从上到下 (Top to Bottom)
targetPosition = Position.Bottom // 目标节点连接点在下方
targetPosition = Position.Bottom // 目标节点连接点在下方
sourcePosition = Position.Top // 源节点连接点在上方
break
case 'TB': // 从下到上 (Bottom to Top)
targetPosition = Position.Top // 目标节点连接点在上方
targetPosition = Position.Top // 目标节点连接点在上方
sourcePosition = Position.Bottom // 源节点连接点在下方
break
case 'LR': // 从左到右 (Left to Right)
@@ -77,10 +80,66 @@ export function useLayout() {
...node,
targetPosition,
sourcePosition,
position: { x: nodeWithPosition.x, y: nodeWithPosition.y },
position: { x: nodeWithPosition.x, y: nodeWithPosition.y }
}
})
}
return { graph, layout, previousDirection }
}
}
/**
* 递归查找指定ID的节点并添加子节点
* @param {Array} items - 要搜索的数组
* @param {string} targetId - 要查找的节点ID
* @param {Object} newChild - 要添加的新子节点
* @returns {boolean} 是否成功添加
*/
export function findAndAddChild(items, targetId, newChild) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
// 如果找到目标节点
if (item.id === targetId) {
// 初始化child数组如果不存在
if (!item.child) {
item.child = []
}
// 添加新子节点
item.child.push(newChild)
return true
}
// 递归搜索子节点
if (item.child && item.child.length > 0) {
const found = findAndAddChild(item.child, targetId, newChild)
if (found) return true
}
}
return false
}
/**
* 递归删除指定ID的节点
* @param {Array} items - 要搜索的数组
* @param {string} targetId - 要删除的节点ID
* @returns {boolean} 是否成功删除
*/
export function findAndRemoveChild(items, targetId) {
for (let i = 0; i < items.length; i++) {
const item = items[i]
// 如果找到目标节点,从当前数组中删除
if (item.id === targetId) {
items.splice(i, 1)
return true
}
// 递归搜索子节点
if (item.child && item.child.length > 0) {
const found = findAndRemoveChild(item.child, targetId)
if (found) return true
}
}
return false
}

View File

@@ -10,19 +10,15 @@ export const versionsList = [
{
id: '1-1-1',
name:'V1-1-1',
}
]
},
{
id: '1-2',
name:'V1-2',
child:[
{
id: '1-2-1',
name:'V1-2-1',
},{
id: '1-2-2',
name:'V1-2-2',
child:[
{
id: '1-1-1-1',
name:'V1-1-1-1',
},{
id: '1-1-1-2',
name:'V1-1-1-2',
},
]
}
]
},

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, reactive, toRefs, watch } from "vue";
import { ref, onMounted, onUnmounted, reactive, toRefs, watch, nextTick } from "vue";
import view1Item from './view1Item.vue'
import view2 from './view2/index.vue'
import { versionsList } from './view2/tools/versionsData'
const props = defineProps({
versionsList: {
type: Array,
default: () => []
},
treeState:{
default:false,
},
@@ -19,6 +22,8 @@ const emit = defineEmits([
let data = reactive({
})
const view1Ref = ref(null)
const isLoad = ref(false)
const treeStateTime = ref(true)
@@ -35,13 +40,7 @@ const pushView2Item = (item)=>{
}
const treeList = ref([
{
name:'P1',
},{
name:'V1',
},{
name:'V1-1',
}
])
function traverseArray(items, callback) {
@@ -53,22 +52,44 @@ function traverseArray(items, callback) {
}
}
}
const initialize = ()=>{
isLoad.value = false
setSelectItem(versionsList[0])
treeList.value = []
traverseArray(versionsList, (item, index) => {
treeList.value.push({id: null,name:'index',})
traverseArray(props.versionsList, (item, index) => {
treeList.value.push(item)
})
isLoad.value = true
if(!props.selectItem?.id)setSelectItem(treeList.value[treeList.value.length - 1])
}
const setSelectItem = (item)=>{
console.log(item[0])
if(!item.id)return
emit('update:selectItem', {...item})
}
// 滚动到选中项
const scrollToActive = ()=>{
nextTick(() => {
const activeEl = view1Ref.value?.querySelector('.active')
console.log(activeEl)
activeEl.scrollIntoView({
behavior: 'smooth',
block: 'nearest'
})
})
}
watch(()=>treeStateTime.value,(newVal,oldVal)=>{
if((props.treeState + '') == 'false'){
scrollToActive()
}
})
watch(()=>props.selectItem,(newVal,oldVal)=>{
scrollToActive()
},{immediate: true})
onMounted(()=>{
initialize()
})
@@ -79,8 +100,8 @@ const {} = toRefs(data);
</script>
<template>
<div class="tree" v-show="treeStateTime" v-if="isLoad">
<div v-show="!treeState" class="box view1">
<view1Item v-for="item in treeList" :key="item.name" :item="item" @click="emit('selectItem', item)"></view1Item>
<div v-show="!treeState" class="box view1" ref="view1Ref">
<view1Item v-for="item in treeList" :key="item.name" :selectItem="props.selectItem" :item="item" @click="setSelectItem(item)"></view1Item>
</div>
<div v-show="treeState" class="box view2">
<view2

View File

@@ -4,7 +4,11 @@ const props = defineProps({
item: {
type: Object,
default: () => ({})
}
},
selectItem: {
type: Object,
default: () => {},
},
})
//const emit = defineEmits([
//])
@@ -18,7 +22,7 @@ defineExpose({})
const {} = toRefs(data);
</script>
<template>
<div class="btn">
<div class="btn" :class="{'active': item.id === props.selectItem?.id}">
{{ item.name }}
</div>
</template>

View File

@@ -5,7 +5,7 @@ import { VueFlow, useVueFlow } from '@vue-flow/core'
import SpecialEdge from './speciaiEdge.vue'
import InputNode from './InputNode.vue'//主
import SecondaryNode from './secondaryNode.vue'//分支
import { useLayout } from './tools/tools'
import { useLayout } from '../../tools/tools'
const props = defineProps({
selectItem: {
type: Object,
@@ -49,14 +49,15 @@ async function layoutGraph(direction) {
}
const push = (item)=>{
if(nodes.value.length == 0){
if(!item.id){
nodes.value.push({ id: '0', type: 'InputNode', class: 'custom-node', position })
}else{
let className = `custom-node item${item.id.replace(/-/g, "_")}`
let id = item.id
let source = edges.value.length == 0?'0':item.id.slice(0, -2)
nodes.value.push({id,type:'SecondaryNode',class:className,position,data:item})
edges.value.push({ id, target:id, source, type: 'smoothstep' })
}
let className = `custom-node item${item.id.replace(/-/g, "_")}`
let id = item.id
let source = edges.value.length == 0?'0':item.id.slice(0, -2)
nodes.value.push({id,type:'SecondaryNode',class:className,position,data:item})
edges.value.push({ id, target:id, source, type: 'smoothstep' })
}
const initialized = ()=>{
@@ -129,7 +130,7 @@ defineExpose({push})
}
:deep(.custom-node){
.node{
--vf-handle: #000;
--vf-handle: #c1c1c1;
--vf-node-color: #000;
--vf-box-shadow: #000;
font-size: 1.2rem;

View File

@@ -20,8 +20,8 @@ const props = defineProps<{
<div class="node" :class="{active:props.selectItem.id == props.data.id}">
<Handle type="target" id="Top" :position="Position.Top" />
<Handle type="source" id="Bottom" :position="Position.Bottom" />
<!-- <Handle type="target" id="Bottom" :position="Position.Bottom" />
<Handle type="source" id="Top" :position="Position.Top" /> -->
<!-- <Handle type="source" id="Right" :position="Position.Right" />
<Handle type="target" id="Left" :position="Position.Left" /> -->
<div>{{ props.data.id }}</div>
</div>
</template>