2026-02-08 15:59:14 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<Wrapper>
|
|
|
|
|
|
<a-layout class="bg-transparent" style="background: transparent">
|
|
|
|
|
|
<Header />
|
|
|
|
|
|
<a-layout class="ks-layout-body">
|
|
|
|
|
|
<div class="ks-model-builder-body">
|
|
|
|
|
|
<div class="ks-model-builder-left">
|
|
|
|
|
|
<TressCard
|
|
|
|
|
|
@select-tree="handleSelectTree"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<NodesCard
|
|
|
|
|
|
@drag-item-start="handleDragStart"
|
|
|
|
|
|
@drag-item-end="handleDragEnd"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="ks-model-builder-content">
|
|
|
|
|
|
<div class="ks-model-builder-actions">
|
|
|
|
|
|
<a-space>
|
|
|
|
|
|
<a-tooltip v-if="graph && currentTreeModelGraph" placement="top">
|
|
|
|
|
|
<template #title>
|
|
|
|
|
|
保存
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<a-popconfirm
|
|
|
|
|
|
title="确定保存?"
|
|
|
|
|
|
@confirm="handleSave"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-button class="ks-model-builder-save" size="small">
|
|
|
|
|
|
<CheckOutlined />
|
|
|
|
|
|
<span>保存</span>
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</a-popconfirm>
|
|
|
|
|
|
</a-tooltip>
|
|
|
|
|
|
</a-space>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 画布容器,添加拖放事件 -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
ref="canvas"
|
|
|
|
|
|
class="ks-model-builder-canvas"
|
|
|
|
|
|
@dragenter="handleDragEnter"
|
|
|
|
|
|
@dragleave="handleDragLeave"
|
|
|
|
|
|
@drop="handleDrop"
|
|
|
|
|
|
@dragover.prevent
|
|
|
|
|
|
></div>
|
|
|
|
|
|
<TeleportContainer />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<Properties
|
|
|
|
|
|
v-if="graph"
|
2026-02-08 16:01:21 +08:00
|
|
|
|
@update-element="handleUpdateElement"
|
2026-02-08 15:59:14 +08:00
|
|
|
|
:element="selectedNodeTaskElement"
|
|
|
|
|
|
:graph="graph as any"
|
2026-02-08 16:01:21 +08:00
|
|
|
|
:node="selectedModelNode as any" />
|
2026-02-08 15:59:14 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</a-layout>
|
|
|
|
|
|
</a-layout>
|
|
|
|
|
|
</Wrapper>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
|
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
|
|
|
|
import { message } from 'ant-design-vue';
|
|
|
|
|
|
import { getTeleport } from '@antv/x6-vue-shape';
|
2026-02-08 16:01:21 +08:00
|
|
|
|
import { Graph, Node,Edge, type NodeProperties } from '@antv/x6';
|
2026-02-08 15:59:14 +08:00
|
|
|
|
import { CheckCircleOutlined, CheckOutlined, RollbackOutlined, SaveOutlined } from '@ant-design/icons-vue';
|
|
|
|
|
|
import { Wrapper } from '@/components/wrapper';
|
|
|
|
|
|
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
|
|
|
|
|
import Header from './header.vue';
|
|
|
|
|
|
import Properties from './properties.vue';
|
|
|
|
|
|
import { useGraphCanvas } from './builder/hooks';
|
|
|
|
|
|
import { registerNodeElement } from './builder/register';
|
|
|
|
|
|
import type { NodeGraph, NodeTemplate, SettingTaskNodeElement, TreeModel, TreeModelGraph } from './types';
|
|
|
|
|
|
import { createTree, findOneTreeById, updateTree } from './api';
|
|
|
|
|
|
import { createTaskNodeElement, createTaskNodeElementFromTemplate, hasElements, hasRootElementNode, resolveNodeGraph } from './utils/node';
|
|
|
|
|
|
import TressCard from './trees-card.vue';
|
|
|
|
|
|
import NodesCard from './nodes-card.vue';
|
|
|
|
|
|
|
|
|
|
|
|
const TeleportContainer = defineComponent(getTeleport());
|
|
|
|
|
|
|
|
|
|
|
|
registerNodeElement();
|
|
|
|
|
|
|
|
|
|
|
|
export default defineComponent({
|
|
|
|
|
|
components: {
|
|
|
|
|
|
TressCard,
|
|
|
|
|
|
NodesCard,
|
|
|
|
|
|
Wrapper,
|
|
|
|
|
|
Header,
|
|
|
|
|
|
Properties,
|
|
|
|
|
|
SaveOutlined,
|
|
|
|
|
|
CheckCircleOutlined,
|
|
|
|
|
|
CheckOutlined,
|
|
|
|
|
|
RollbackOutlined,
|
|
|
|
|
|
TeleportContainer,
|
|
|
|
|
|
},
|
|
|
|
|
|
setup() {
|
|
|
|
|
|
const canvas = ref<HTMLDivElement | null>(null);
|
|
|
|
|
|
const graph = ref<Graph | null>(null);
|
|
|
|
|
|
const currentZoom = ref<number>(1);
|
|
|
|
|
|
const draggedNodeData = ref<NodeTemplate | null>(null);
|
|
|
|
|
|
const isDraggingOver = ref(false);
|
|
|
|
|
|
const currentTreeModelGraph = ref<TreeModelGraph | null>(null);
|
|
|
|
|
|
const selectedModelNode = ref<Node<NodeProperties> | null>(null);
|
|
|
|
|
|
const selectedNodeTaskElement = ref<SettingTaskNodeElement | null>(null);
|
2026-02-08 16:01:21 +08:00
|
|
|
|
const changed = ref<boolean>(false)
|
2026-02-08 15:59:14 +08:00
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
|
handleGraphEvent,
|
|
|
|
|
|
createCanvas,
|
|
|
|
|
|
zoomIn,
|
|
|
|
|
|
zoomOut,
|
|
|
|
|
|
fitToScreen,
|
|
|
|
|
|
centerContent,
|
|
|
|
|
|
resizeCanvas,
|
|
|
|
|
|
} = useGraphCanvas();
|
|
|
|
|
|
|
|
|
|
|
|
// 处理拖动开始
|
|
|
|
|
|
const handleDragStart = (nm: NodeTemplate) => {
|
|
|
|
|
|
draggedNodeData.value = nm;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理拖动结束
|
|
|
|
|
|
const handleDragEnd = () => {
|
|
|
|
|
|
isDraggingOver.value = false;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理拖动进入
|
|
|
|
|
|
const handleDragEnter = (e: DragEvent) => {
|
|
|
|
|
|
safePreventDefault(e);
|
|
|
|
|
|
safeStopPropagation(e);
|
|
|
|
|
|
isDraggingOver.value = true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理拖动离开
|
|
|
|
|
|
const handleDragLeave = (e: DragEvent) => {
|
|
|
|
|
|
safePreventDefault(e);
|
|
|
|
|
|
safeStopPropagation(e);
|
|
|
|
|
|
|
|
|
|
|
|
if (canvas.value && e.relatedTarget &&
|
|
|
|
|
|
typeof e.relatedTarget === 'object' &&
|
|
|
|
|
|
'nodeType' in e.relatedTarget) {
|
|
|
|
|
|
// 使用 Element 类型而不是 x6 的 Node 类型
|
|
|
|
|
|
if (!canvas.value.contains(e.relatedTarget as Element)) {
|
|
|
|
|
|
isDraggingOver.value = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理放置
|
|
|
|
|
|
const handleDrop = (e: DragEvent) => {
|
|
|
|
|
|
console.info('handleDrop', e);
|
|
|
|
|
|
safePreventDefault(e);
|
|
|
|
|
|
safeStopPropagation(e);
|
|
|
|
|
|
isDraggingOver.value = false;
|
|
|
|
|
|
|
|
|
|
|
|
if (!currentTreeModelGraph.value) {
|
|
|
|
|
|
message.error('请先选择或者创建行为树.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!graph.value || !canvas.value || !draggedNodeData.value) {
|
|
|
|
|
|
message.error('无法放置节点,缺少必要数据');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 获取拖动的数据
|
|
|
|
|
|
const template = draggedNodeData.value as NodeTemplate;
|
|
|
|
|
|
|
|
|
|
|
|
if (!hasElements(graph.value as Graph) && template.type !== 'root') {
|
|
|
|
|
|
message.error('请先添加根节点.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (hasRootElementNode(graph.value as Graph) && template.type === 'root') {
|
|
|
|
|
|
message.error('根节点已经存在.');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 计算相对于画布的位置(考虑缩放)
|
|
|
|
|
|
const rect = canvas.value.getBoundingClientRect();
|
|
|
|
|
|
const scale = currentZoom.value || 1;
|
|
|
|
|
|
const x = (e.clientX - rect.left) / scale;
|
|
|
|
|
|
const y = (e.clientY - rect.top) / scale;
|
|
|
|
|
|
|
|
|
|
|
|
console.log('放置节点:', { ...template, x, y });
|
|
|
|
|
|
|
|
|
|
|
|
// 创建节点数据
|
|
|
|
|
|
const settingTaskElement: SettingTaskNodeElement = createTaskNodeElementFromTemplate(template, { x, y });
|
|
|
|
|
|
// 创建节点
|
|
|
|
|
|
const settingTaskNode = createTaskNodeElement(settingTaskElement);
|
|
|
|
|
|
console.info('create settingTaskNode: ', settingTaskElement, settingTaskNode);
|
|
|
|
|
|
|
|
|
|
|
|
// 将节点添加到画布
|
|
|
|
|
|
graph.value?.addNode(settingTaskNode as any);
|
|
|
|
|
|
console.log('节点已添加到画布:', settingTaskNode.id);
|
|
|
|
|
|
|
|
|
|
|
|
// 重置拖动数据
|
|
|
|
|
|
draggedNodeData.value = null;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('放置节点时出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const createElements = () => {
|
2026-02-08 16:01:21 +08:00
|
|
|
|
if(graph.value && currentTreeModelGraph.value){
|
2026-02-08 15:59:14 +08:00
|
|
|
|
graph.value.clearCells();
|
2026-02-08 16:01:21 +08:00
|
|
|
|
setTimeout(()=> {
|
|
|
|
|
|
nextTick(()=> {
|
|
|
|
|
|
try{
|
|
|
|
|
|
if (currentTreeModelGraph.value && !currentTreeModelGraph.value?.graph){
|
2026-02-08 15:59:14 +08:00
|
|
|
|
currentTreeModelGraph.value.graph = {
|
|
|
|
|
|
nodes: [],
|
|
|
|
|
|
edges: [],
|
2026-02-08 16:01:21 +08:00
|
|
|
|
}
|
2026-02-08 15:59:14 +08:00
|
|
|
|
}
|
2026-02-08 16:01:21 +08:00
|
|
|
|
const nodes = currentTreeModelGraph.value?.graph?.nodes ?? [];
|
|
|
|
|
|
const edges = currentTreeModelGraph.value?.graph?.edges ?? [];
|
2026-02-08 15:59:14 +08:00
|
|
|
|
|
2026-02-08 16:01:21 +08:00
|
|
|
|
nodes.forEach(n=> {
|
2026-02-08 15:59:14 +08:00
|
|
|
|
const node = createTaskNodeElement(n);
|
|
|
|
|
|
graph.value?.addNode(node as Node);
|
2026-02-08 16:01:21 +08:00
|
|
|
|
})
|
|
|
|
|
|
edges.forEach(g=> {
|
|
|
|
|
|
graph.value?.addEdge( g as any);
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e){
|
|
|
|
|
|
console.warn('createElements',e)
|
2026-02-08 15:59:14 +08:00
|
|
|
|
}
|
2026-02-08 16:01:21 +08:00
|
|
|
|
})
|
|
|
|
|
|
}, 200)
|
2026-02-08 15:59:14 +08:00
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化X6画布
|
|
|
|
|
|
const initGraph = () => {
|
|
|
|
|
|
if (!canvas.value) {
|
|
|
|
|
|
console.error('画布容器不存在');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
graph.value = createCanvas(canvas.value);
|
|
|
|
|
|
console.log('画布初始化成功');
|
|
|
|
|
|
createElements();
|
|
|
|
|
|
|
|
|
|
|
|
// 监听缩放变化
|
|
|
|
|
|
handleGraphEvent('scale', ({ sx }: { sx: number }) => {
|
|
|
|
|
|
currentZoom.value = sx;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
handleGraphEvent('node:click', (args: any) => {
|
|
|
|
|
|
const node = args.node as Node<NodeProperties>;
|
|
|
|
|
|
const newElement = node.getData() as SettingTaskNodeElement;
|
|
|
|
|
|
|
|
|
|
|
|
selectedModelNode.value = node;
|
|
|
|
|
|
selectedNodeTaskElement.value = JSON.parse(JSON.stringify(newElement || {})) as SettingTaskNodeElement;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听节点鼠标事件,显示/隐藏连接点
|
|
|
|
|
|
handleGraphEvent('node:mouseenter', (_ctx: any) => {
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
handleGraphEvent('node:mouseleave', (_ctx: any) => {
|
|
|
|
|
|
});
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('初始化画布失败:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 监听窗口大小变化
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
resizeCanvas();
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const init = () => {
|
|
|
|
|
|
console.info('init');
|
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
initGraph();
|
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
|
console.log('节点挂载完成');
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleUpdateElement = (element: SettingTaskNodeElement) => {
|
|
|
|
|
|
// 更新选中的节点数据
|
|
|
|
|
|
if (selectedModelNode.value) {
|
|
|
|
|
|
selectedModelNode.value.replaceData(element);
|
|
|
|
|
|
}
|
|
|
|
|
|
console.info('handleUpdateElement', element);
|
|
|
|
|
|
// 更新本地引用
|
|
|
|
|
|
selectedNodeTaskElement.value = element;
|
2026-02-08 16:01:21 +08:00
|
|
|
|
changed.value = true
|
2026-02-08 15:59:14 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSelectTree = (treeModel: TreeModel) => {
|
|
|
|
|
|
console.error('handleSelectTree', treeModel);
|
|
|
|
|
|
findOneTreeById(treeModel.id).then(r => {
|
|
|
|
|
|
if (r.data) {
|
|
|
|
|
|
currentTreeModelGraph.value = r.data;
|
|
|
|
|
|
createElements();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(r.message ?? '行为树不存在.');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSave = () => {
|
|
|
|
|
|
const graphData: NodeGraph = resolveNodeGraph(graph.value as Graph);
|
|
|
|
|
|
console.info('handleSave', graphData);
|
|
|
|
|
|
if (!currentTreeModelGraph.value) {
|
|
|
|
|
|
message.error('当前决策树不存在');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const newModel: TreeModelGraph = {
|
|
|
|
|
|
...currentTreeModelGraph.value,
|
|
|
|
|
|
graph: graphData,
|
|
|
|
|
|
};
|
|
|
|
|
|
let res = null;
|
|
|
|
|
|
if (currentTreeModelGraph.value.id > 0) {
|
|
|
|
|
|
res = createTree(newModel);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res = updateTree(newModel);
|
|
|
|
|
|
}
|
|
|
|
|
|
res.then(r => {
|
|
|
|
|
|
if (r.code === 200) {
|
|
|
|
|
|
message.success(r.message ?? '操作成功.');
|
|
|
|
|
|
} else {
|
|
|
|
|
|
message.error(r.message ?? '操作失败.');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
init();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 清理
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
|
|
if (graph.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
graph.value.clearCells();
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('销毁画布时出错:', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
graph.value = null;
|
|
|
|
|
|
console.log('画布已销毁');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
currentTreeModelGraph,
|
|
|
|
|
|
selectedNodeTaskElement,
|
|
|
|
|
|
selectedModelNode,
|
|
|
|
|
|
graph,
|
|
|
|
|
|
canvas,
|
|
|
|
|
|
zoomIn,
|
|
|
|
|
|
zoomOut,
|
|
|
|
|
|
fitToScreen,
|
|
|
|
|
|
centerContent,
|
|
|
|
|
|
handleDragStart,
|
|
|
|
|
|
handleDragEnd,
|
|
|
|
|
|
handleDragEnter,
|
|
|
|
|
|
handleDragLeave,
|
|
|
|
|
|
handleDrop,
|
|
|
|
|
|
isDraggingOver,
|
|
|
|
|
|
handleSave,
|
|
|
|
|
|
handleUpdateElement,
|
|
|
|
|
|
handleSelectTree,
|
|
|
|
|
|
};
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|