Initial commit

This commit is contained in:
libertyspy
2026-02-08 17:57:40 +08:00
parent 294e5d687e
commit 82fcedfa97
15 changed files with 548 additions and 290 deletions

View File

@@ -29,11 +29,11 @@ export const findOneTreeById = (id: number): Promise<BehaviorTreeDetailsResponse
export const updateTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => { export const updateTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => {
return req.postJson('/system/behaviortree/${id}', rt); return req.postJson(`/system/behaviortree`, rt);
}; };
export const createTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => { export const createTree = (rt: Partial<BehaviorTree>): Promise<BasicResponse> => {
return req.putJson('/system/behaviortree/${id}', rt); return req.putJson(`/system/behaviortree`, rt);
}; };

View File

@@ -8,7 +8,20 @@
*/ */
import type { NullableString } from '@/types'; import type { NullableString } from '@/types';
import type { NodeSetting } from './parameter'; import type { NodeSetting } from '@/views/decision/types';
export interface DraggableElement {
id: number | null,
key?: NullableString,
name: NullableString,
description: NullableString,
category: NullableString,
draggable: boolean,
parent?: DraggableElement,
children: DraggableElement[]
[key: string]: unknown;
}
export type ElementStatus = 'default' | 'success' | 'failed' | 'running' | string | null export type ElementStatus = 'default' | 'success' | 'failed' | 'running' | string | null
@@ -51,6 +64,8 @@ export interface TaskNodeElement extends BaseElement {
variables: ElementVariable[]; variables: ElementVariable[];
parameters: Record<any, any>; parameters: Record<any, any>;
children?: TaskNodeElement[],
[key: string]: unknown; [key: string]: unknown;
} }

View File

@@ -7,9 +7,10 @@
* that was distributed with this source code. * that was distributed with this source code.
*/ */
import { Clipboard, Edge, Graph, History, Keyboard, Path, Selection, Snapline, Transform } from '@antv/x6'; import { Edge, Graph, Path, Selection } from '@antv/x6';
import type { BaseElement } from '../types'; import type { ModelElement } from './element';
import type { Connecting } from '@antv/x6/lib/graph/options'; import type { Connecting } from '@antv/x6/lib/graph/options';
import {createLineOptions} from './line'
Graph.registerConnector( Graph.registerConnector(
'sequenceFlowConnector', 'sequenceFlowConnector',
@@ -33,30 +34,30 @@ Graph.registerConnector(
true, true,
); );
export const createGraphConnectingAttributes = (): Partial<Connecting> => { export const createGraphConnectingAttributes = (): Partial<Connecting> => {
const lineOptions = createLineOptions();
return { return {
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附 snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
highlight: true, // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。 highlight: true, // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
connector: 'sequenceFlowConnector', connector: 'smooth',
connectionPoint: 'boundary', // 指定连接点,默认值为 boundary。 connectionPoint: 'anchor', // 指定连接点,默认值为 boundary。
anchor: 'center', anchor: 'center',
router: 'manhattan',
// connector: {
// name: 'rounded',
// args: {
// radius: 8,
// },
// },
// connectionPoint: 'anchor',
// validateMagnet({ magnet }) { // validateMagnet({ magnet }) {
// return magnet.getAttribute('port-group') !== 'top' // return magnet.getAttribute('port-group') !== 'top'
// }, // },
// 验证连接 // 验证连接
createEdge(this: Graph) {
const edge: Edge = this.createEdge({
shape: 'edge',
...lineOptions, // 应用动画配置
attrs: lineOptions.attrs,
animation: lineOptions.animation,
markup: lineOptions.markup,
})
return edge;
},
validateConnection(this: Graph, { sourceCell, targetCell }) { validateConnection(this: Graph, { sourceCell, targetCell }) {
console.error('validateConnection'); console.error('validateConnection');
if (!sourceCell || !targetCell) return false; if (!sourceCell || !targetCell) return false;
@@ -67,21 +68,28 @@ export const createGraphConnectingAttributes = (): Partial<Connecting> => {
} }
// const sourceData = sourceCell.getData() as GraphElement; // const sourceData = sourceCell.getData() as GraphElement;
const targetData = targetCell.getData() as BaseElement; const targetData = targetCell.getData() as ModelElement;
// 根节点不能作为子节点 // 根节点不能作为子节点
if (targetData.type === 'root') { if (targetData.type === 'startEvent') {
return false; return false;
} }
// 检查是否已存在相同连接(保留原有逻辑 // 4. 新增核心逻辑:检查源节点是否已有出边(已连接其他节点
const edges: Edge[] = this.getEdges(); // const hasOutgoingEdge = this.getOutgoingEdges(sourceCell);
const existingConnection = edges.find(edge => // if (hasOutgoingEdge && hasOutgoingEdge.length > 1) {
edge.getSourceCell() === sourceCell && // return false;
edge.getTargetCell() === targetCell, // }
);
return !existingConnection; // 检查是否已存在相同连接
// const edges: Edge[] = this.getEdges();
// const existingConnection = edges.find(edge =>
// edge.getSourceCell() === sourceCell &&
// edge.getTargetCell() === targetCell,
// );
//
// return !existingConnection;
return true;
}, },
}; };
}; };
@@ -103,10 +111,10 @@ export const createGraphCanvas = (container: HTMLDivElement, readonly: boolean =
}, },
mousewheel: { mousewheel: {
enabled: true, enabled: true,
zoomAtMousePosition: true,
modifiers: 'ctrl', modifiers: 'ctrl',
factor: 1.1,
maxScale: 1.5,
minScale: 0.5, minScale: 0.5,
maxScale: 3,
}, },
highlighting: { highlighting: {
magnetAdsorbed: { magnetAdsorbed: {
@@ -162,16 +170,7 @@ export const createGraphCanvas = (container: HTMLDivElement, readonly: boolean =
modifiers: 'shift', modifiers: 'shift',
rubberband: true, rubberband: true,
}), }),
).use( );
new Transform({
resizing: false,
rotating: false,
}),
)
.use(new Snapline())
.use(new Keyboard())
.use(new Clipboard())
.use(new History());
return graph; return graph;

View File

@@ -8,10 +8,12 @@
*/ */
import { computed, type ComputedRef, ref, type Ref } from 'vue'; import { computed, type ComputedRef, ref, type Ref } from 'vue';
import { type Dom, Graph } from '@antv/x6'; import { type Dom, Graph, Node } from '@antv/x6';
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type'; import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
import { createGraphCanvas } from './graph'; import { createGraphCanvas } from './graph';
import { EventListener } from '@/utils/event'; import { EventListener } from '@/utils/event';
import type { ModelElement } from './element';
// import {createLineOptions} from './line'
export interface UseGraphCanvas { export interface UseGraphCanvas {
container: Ref<HTMLDivElement | null>; container: Ref<HTMLDivElement | null>;
@@ -85,7 +87,7 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
const cells = graph.value?.getSelectedCells(); const cells = graph.value?.getSelectedCells();
if (cells && cells.length) { if (cells && cells.length) {
graph.value?.removeCells(cells); graph.value?.removeCells(cells);
// 通知父节点更新状态 // 通知父组件更新状态
emitGraphEvent('cells:removed', cells); emitGraphEvent('cells:removed', cells);
} }
} }
@@ -104,7 +106,15 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
// 监听连线删除 // 监听连线删除
graph.value.on('edge:removed', ({ edge }) => { graph.value.on('edge:removed', ({ edge }) => {
emitGraphEvent('cells:removed', edge); // 添加此行 emitGraphEvent('edge:removed', edge); // 添加此行
});
graph.value.on('edge:added', ({ edge }) => {
// const lineOptions = createLineOptions();
// edge.setAttrs(lineOptions.attrs);
// edge.set(lineOptions.animation);
// edge.setMarkup(lineOptions.markup);
emitGraphEvent('edge:added', edge); // 添加此行
}); });
graph.value.on('edge:contextmenu', (ctx) => { graph.value.on('edge:contextmenu', (ctx) => {
@@ -132,28 +142,28 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
}); });
graph.value.on('edge:connected', ({ edge }) => { graph.value.on('edge:connected', ({ edge }) => {
// const sourceNode = edge.getSourceCell() as Node; const sourceNode = edge.getSourceCell() as Node;
// const targetNode = edge.getTargetCell() as Node; const targetNode = edge.getTargetCell() as Node;
// if (sourceNode && targetNode) { if (sourceNode && targetNode) {
// const sourceData = sourceNode.getData() as TaskNodeElement; const sourceData = sourceNode.getData() as ModelElement;
// const targetData = targetNode.getData() as TaskNodeElement; const targetData = targetNode.getData() as ModelElement;
//
// // 将连线存储到节点数据中 // 将连线存储到节点数据中
// const sourceEdges = sourceData.edges || []; const sourceEdges = sourceData.edges || [];
// const existingEdge = sourceEdges.find(e => e.targetKey === targetNode.id); const existingEdge = sourceEdges.find(e => e.targetKey === targetNode.id);
//
// if (!existingEdge) { if (!existingEdge) {
// sourceEdges.push({ sourceEdges.push({
// key: edge.id, key: edge.id,
// sourceKey: sourceNode.id, sourceKey: sourceNode.id,
// sourceName: sourceData.name, sourceName: sourceData.name,
// targetKey: targetNode.id, targetKey: targetNode.id,
// targetName: targetData.name, targetName: targetData.name,
// }); });
// sourceNode.setData({ ...sourceData, edges: sourceEdges }); sourceNode.replaceData({ ...sourceData, edges: sourceEdges });
// } }
// } }
edge.attr('line/stroke', '#3b82f6'); // 显式设置颜色 edge.attr('line/stroke', '#3b82f6'); // 显式设置颜色
edge.attr('line/strokeWidth', 2); // 显式设置宽度 edge.attr('line/strokeWidth', 2); // 显式设置宽度

View File

@@ -0,0 +1,47 @@
/*
* This file is part of the kernelstudio package.
*
* (c) 2014-2026 zlin <admin@kernelstudio.com>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/
import { Shape } from '@antv/x6';
export const createLineOptions = (): any => {
return {
markup: [
{
tagName: 'circle',
selector: 'marker',
attrs: {
stroke: 'none',
r: 3,
},
},
...Shape.Edge.getMarkup() as any,
],
attrs: {
line: {
stroke: '#5da0df',
strokeWidth: 2,
strokeDasharray: ' ',
strokeDashoffset: 0,
},
marker: {
fill: '#5da0df',
atConnectionRatio: 0,
},
},
animation: [
[
{ 'attrs/marker/atConnectionRatio': 1 },
{
duration: 2000,
iterations: Infinity,
},
],
],
}
}

View File

@@ -1,13 +1,74 @@
<template> <template>
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange"> <a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
<a-card :class="['ks-designer-node', `ks-designer-node-${element?.type}`]" hoverable> <a-card
:class="[
'ks-designer-node',
`ks-designer-${element?.category ?? 'model'}-node`
]"
hoverable
>
<template #title> <template #title>
{{ element?.name ?? '-' }} <a-space>
<span class="ks-designer-node-icon"></span>
<span class="ks-designer-node-title">{{ element?.name ?? '-' }}</span>
</a-space>
</template> </template>
<!-- 节点内容区域 -->
<div class="w-full"> <div class="w-full">
<p>{{ element?.description ?? '-' }}</p> <div class="ks-designer-node-content">
<div
v-for="(item, index) in element?.element?.children || []"
:key="item.id || index"
class="ks-designer-node-row"
>
<div
:data-port="`in-${item.id || index}`"
:title="`入桩: ${item.name}`"
class="port port-in"
magnet="passive"
></div>
<!-- child名称 -->
<div class="ks-designer-node-name">
{{ item.name }}
</div>
<!-- 右侧出桩只能作为连线源 -->
<div
:data-port="`out-${item.id || index}`"
:title="`出桩: ${item.name}`"
class="port port-out"
magnet="active"
></div>
</div>
<div v-if="!(element?.element?.children && element?.element?.children?.length > 0)" class="ks-designer-node-row">
<div class="port port-in" data-port="in-0" magnet="passive"></div>
<div class="ks-designer-node-name" v-if="element?.category !== 'component'">
{{ element?.name ?? '-' }}
</div>
<div class="ks-designer-node-name" v-else>
<p>隐藏纬度: {{ element?.parameters?.hiddenLatitude ?? '-' }}</p>
<p>激活函数: {{ element?.parameters?.activationFunction ?? '-' }}</p>
</div>
<div class="port port-out" data-port="out-0" magnet="active"></div>
</div>
</div>
</div> </div>
<!-- <div class="w-full" v-else>-->
<!-- <div class="ks-designer-node-content">-->
<!-- <div class="port port-in" data-port="in-0" magnet="passive"></div>-->
<!-- <div class="ks-designer-node-name">-->
<!-- <p>隐藏纬度: {{ element?.parameters?.hiddenLatitude ?? '-' }}</p>-->
<!-- <p>激活函数: {{ element?.parameters?.activationFunction ?? '-' }}</p>-->
<!-- </div>-->
<!-- <div class="port port-out" data-port="out-0" magnet="active"></div>-->
<!-- </div>-->
<!-- </div>-->
</a-card> </a-card>
<template #overlay> <template #overlay>
<a-menu @click="handleMenuClick"> <a-menu @click="handleMenuClick">
<a-menu-item key="delete"> <a-menu-item key="delete">
@@ -24,37 +85,36 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue'; import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import { elementProps } from './props'; import { elementProps } from './props';
import type { SettingTaskNodeElement } from '../types'; import type { ModelElement } from './element';
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue'; import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
import type { Graph } from '@antv/x6'; import type { Graph } from '@antv/x6';
export default defineComponent({ export default defineComponent({
name: 'SettingTaskNodeElement', name: 'ModelElement',
components: { components: {
SettingOutlined, SettingOutlined,
DeleteOutlined, DeleteOutlined,
}, },
props: elementProps, props: elementProps,
setup(_props) { setup(_props) {
// 初始化 element保证类型是 SettingTaskNodeElement 或 null const element = ref<ModelElement | null>(
const element = ref<SettingTaskNodeElement | null>( _props.node ? (_props.node.getData() as ModelElement) : null,
_props.node ? (_props.node.getData() as SettingTaskNodeElement) : null,
); );
const updateKey = ref(0); const updateKey = ref(0);
const isMenuVisible = ref(false); const isMenuVisible = ref(false);
// 获取节点所在的画布实例 // 获取画布实例
const getGraph = (): Graph | null => { const getGraph = (): Graph | null => {
return _props.graph as Graph || null; return _props.graph as Graph || null;
}; };
// 监听节点数据变化
const handleDataChange = () => { const handleDataChange = () => {
if (_props.node) { if (_props.node) {
element.value = _props.node.getData() as SettingTaskNodeElement; element.value = _props.node.getData() as ModelElement;
} else { } else {
element.value = null; element.value = null;
} }
console.info('handleDataChange', element.value);
updateKey.value++; updateKey.value++;
}; };
@@ -74,36 +134,24 @@ export default defineComponent({
const graph = getGraph(); const graph = getGraph();
if (graph) { if (graph) {
try { try {
// 获取与该节点关联的所有 // 先删除关联
const connectedEdges = graph.getConnectedEdges(_props.node); const connectedEdges = graph.getConnectedEdges(_props.node);
connectedEdges.forEach(edge => graph.removeEdge(edge));
// 先移除关联的边 // 再删除节点
connectedEdges.forEach(edge => {
graph.removeEdge(edge);
});
// 再移除节点本身
graph.removeNode(_props.node); graph.removeNode(_props.node);
console.info(`节点 ${_props.node.id} 已删除`); console.info(`节点 ${_props.node.id} 已删除`);
} catch (error) { } catch (error) {
console.error('删除节点失败:', error); console.error('删除节点失败:', error);
} }
} else {
console.error('无法获取 Graph 实例');
} }
// 关闭右键菜单
isMenuVisible.value = false; isMenuVisible.value = false;
}; };
onMounted(() => { onMounted(() => {
console.info('node onMounted')
_props.node?.on('change:data', handleDataChange); _props.node?.on('change:data', handleDataChange);
}); });
onUnmounted(() => { onUnmounted(() => {
console.info('node onUnmounted')
_props.node?.off('change:data', handleDataChange); _props.node?.off('change:data', handleDataChange);
}); });
@@ -117,71 +165,168 @@ export default defineComponent({
</script> </script>
<style lang="less"> <style lang="less">
.x6-widget-selection-box {
border: 1px dashed #7a6986;
box-shadow: 2px 2px 5px #000000;
}
.ks-designer-node { .ks-designer-node {
background: #1b3875; background: linear-gradient(150deg, #093866 1%, #1f69b3 55%);
//background: url('@/assets/icons/bg-node.png') center / 100% 100%;
border: 0; border: 0;
border-radius: 2px; border-radius: 8px;
width: 100%; width: 100%;
height: 100%; height: 100%;
cursor: pointer; cursor: pointer;
position: relative;
&:hover {
box-shadow: 0 1px 2px -2px rgb(0 0 0), 0 3px 6px 0 rgb(0 0 0 / 60%), 0 5px 12px 4px rgb(0 0 0 / 30%);
}
.ant-card-head { .ant-card-head {
border: 0; border: 0;
height: 30px; height: 38px;
min-height: 30px; min-height: 38px;
border-radius: 0; border-radius: 0;
color: #ddd; color: #ddd;
font-size: 12px; font-size: 12px;
font-weight: normal; font-weight: normal;
padding: 0 15px; padding: 0 20px;
}
.ks-designer-node-icon {
width: 15px;
height: 15px;
display: block;
position: absolute;
left: 8px;
top: 13px;
background: url('@/assets/icons/model-4.svg') center / 100% 100%;
}
.ks-designer-node-title {
font-size: 13px;
} }
.ant-card-body { .ant-card-body {
color: #fff; color: #fff;
height: calc(100% - 30px); height: calc(100% - 38px);
background: #24417e;
border-radius: 0; border-radius: 0;
font-size: 12px; font-size: 12px;
padding: 15px !important; padding: 8px 15px;
//overflow: hidden; overflow-y: auto;
//white-space: nowrap; border-top: 1px solid #195693;
//text-overflow: ellipsis;
} }
//&.ks-designer-node-root{ &.ks-designer-task-node {
//background: #645525; background: linear-gradient(150deg, #20421b 1%, #4a6646 55%);
//.ant-card-body{
// background: #726334; .ant-card-body {
//} border-top: 1px solid #466741;
//} }
&.ks-designer-node-select {
background: #255464; .ks-designer-node-icon {
.ant-card-body{ background: url('@/assets/icons/m-02.png') center / 100% 100%;
background: #1c4654;
} }
} }
&.ks-designer-node-precondition, &.ks-designer-input-node {
&.ks-designer-node-parallel, background: linear-gradient(150deg, #083058 1%, #1e5d9b 55%);
&.ks-designer-node-sequence{
background: #4c5a9d; .ant-card-body {
.ant-card-body{ border-top: 1px solid #105ca7;
background: #3f4d8d; }
.ks-designer-node-icon {
background: url('@/assets/icons/icon-model-input.png') center / 100% 100%;
} }
} }
&.ks-designer-node-action{
background: #645525; &.ks-designer-action-node {
.ant-card-body{ background: linear-gradient(150deg, #343207 1%, #485010 55%);
background: #726334;
.ant-card-body {
border-top: 1px solid #59550e;
} }
.ks-designer-node-icon {
background: url('@/assets/icons/bg-fk-point.png') center / 100% 100%;
}
}
&.ks-designer-component-node {
background: linear-gradient(150deg, #06226b 1%, #1a43a7 55%);
.ant-card-body {
border-top: 1px solid #26448c;
}
}
&.ks-designer-control-node {
background: linear-gradient(150deg, #1d4f32 1%, #326a5d 55%);
.ant-card-body {
border-top: 1px solid #326a5d;
}
.ks-designer-node-icon {
background: url('@/assets/icons/bg-model-builder-card-title.png') center / 100% 100%;
}
}
// 连接桩容器样式
.ks-designer-node-content {
width: 100%;
display: flex;
flex-direction: column;
gap: 4px; // 每个child行之间的间距
}
// 每个child行包含左右桩+文本)
.ks-designer-node-row {
width: 100%;
display: flex;
align-items: center;
position: relative;
min-height: 24px; // 固定行高,保证桩对齐
}
// 连接桩基础样式
.port {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: crosshair;
flex-shrink: 0;
box-shadow: 0 0 0 2px rgb(74 114 214 / 80%);
z-index: 10; // 确保桩在最上层
// X6 标记为可连线的磁体
magnet: true;
}
// 左侧入桩样式
.port-in {
background-color: #093866; // 青色:入桩
margin-right: 8px; // 与文本的间距
//border: 1px solid #093866;
// X6 只能作为连线目标(入)
magnet: passive;
box-shadow: none;
width: 20px;
height: 20px;
display: block;
background: url('@/assets/icons/point.svg') center / 100% 100%;
}
// 右侧出桩样式
.port-out {
margin-left: 8px; // 与文本的间距
margin-right: 5px;
// X6 只能作为连线源(出)
magnet: active;
box-shadow: none;
width: 20px;
height: 20px;
display: block;
background: url('@/assets/icons/arrow-right.svg') center / 100% 100%;
}
// 节点文本样式
.ks-designer-node-name {
flex: 1; // 占满中间空间
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
} }
} }
</style> </style>

View File

@@ -12,11 +12,11 @@ export const createPort = (name: string = 'top', args: Record<any, any> = { dx:
position: { name: name, args: args }, position: { name: name, args: args },
attrs: { attrs: {
circle: { circle: {
r: 3, // 大小 r: 4, // 大小
magnet: true, magnet: true,
stroke: '#999', // 边框颜色 stroke: '#1b5e9f', // 边框颜色
strokeWidth: 1, // 边框大小 strokeWidth: 1, // 边框大小
fill: '#ffffff', // 填充颜色 fill: '#3578bf', // 填充颜色
style: { style: {
visibility: 'visible', // 是否可见 visibility: 'visible', // 是否可见
}, },
@@ -25,19 +25,27 @@ export const createPort = (name: string = 'top', args: Record<any, any> = { dx:
}; };
}; };
export const createPorts = () => { export const createPorts = (top: boolean = true, right: boolean = true, bottom: boolean = true, left: boolean = true) => {
const groups: any = {};
const items: any = [];
if (top) {
groups['top'] = createPort('top');
items.push({ group: 'top', id: 'top' });
}
if (right) {
groups['right'] = createPort('right');
items.push({ group: 'right', id: 'right' });
}
if (bottom) {
groups['bottom'] = createPort('bottom');
items.push({ group: 'bottom', id: 'bottom' });
}
if (left) {
groups['left'] = createPort('left');
items.push({ group: 'left', id: 'left' });
}
return { return {
groups: { groups: groups,
top: createPort('top'), items: items,
right: createPort('right'),
bottom: createPort('bottom'),
left: createPort('left'),
},
items: [
{ group: 'top', id: 'top' },
{ group: 'right', id: 'right' },
{ group: 'bottom', id: 'bottom' },
{ group: 'left', id: 'left' },
],
}; };
}; };

View File

@@ -9,7 +9,7 @@
import { Graph, Node } from '@antv/x6'; import { Graph, Node } from '@antv/x6';
import { type ExtractPropTypes, type PropType } from 'vue'; import { type ExtractPropTypes, type PropType } from 'vue';
import type { BaseElement } from '../types'; import type { ModelElement } from './element';
export const elementProps = { export const elementProps = {
node: { node: {
@@ -21,7 +21,7 @@ export const elementProps = {
required: true, required: true,
}, },
element: { element: {
type: Object as PropType<BaseElement>, type: Object as PropType<ModelElement>,
required: false, required: false,
}, },
}; };

View File

@@ -7,14 +7,21 @@
* that was distributed with this source code. * that was distributed with this source code.
*/ */
/*
* This file is part of the kernelstudio package.
*
* (c) 2014-2025 zlin <admin@kernelstudio.com>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/
import { register } from '@antv/x6-vue-shape'; import { register } from '@antv/x6-vue-shape';
import ModelElement from './node.vue'; import ModelElement from './node.vue';
import { createPorts } from './ports';
export const registerNodeElement = (type: string = 'task') => { export const registerNodeElement = () => {
console.info('registerNodeElement'); console.info('registerNodeElement');
register({ register({
shape: type, shape: 'node',
component: ModelElement, component: ModelElement,
width: 120, width: 120,
attrs: { attrs: {
@@ -29,6 +36,14 @@ export const registerNodeElement = (type: string = 'task') => {
dragging: { dragging: {
enabled: true, enabled: true,
}, },
ports: createPorts(), // 配置端口识别规则,
portMarkup: [
{
tagName: 'div',
selector: 'port-body',
},
],
// 告诉 X6 如何识别 Vue 组件内的端口
portAttribute: 'data-port',
}); });
}; };

View File

@@ -0,0 +1,109 @@
/*
* This file is part of the kernelstudio package.
*
* (c) 2014-2026 zlin <admin@kernelstudio.com>
*
* For the full copyright and license information, please view the LICENSE file
* that was distributed with this source code.
*/
import type { EdgeNodeElement, NodeGraph, TaskNodeElement } from './element';
import { Edge, Graph, Node } from '@antv/x6';
export const defaultHeight: Record<string, number> = {
component: 110,
};
export const createModelNode = (element: TaskNodeElement, width: number = 250, height: number = 120): any => {
let realHeight = defaultHeight[element.category as string];
if (!realHeight) {
realHeight = 120;
}
return {
shape: 'node',
id: element.key,
position: {
x: element.position?.x || 0,
y: element.position?.y || 0,
},
size: {
width: width,
height: realHeight,
},
attrs: {
label: {
text: element.name,
},
},
data: element,
};
};
export const resolveNodeTaskElements = (graph: Graph): TaskNodeElement[] => {
const taskElements: TaskNodeElement[] = [];
if (graph) {
const nodes = graph?.getNodes() as Node[];
if (nodes) {
nodes.forEach(node => {
const nodeData = node.getData() as TaskNodeElement;
const newElement = {
...nodeData,
key: node.id,
position: node.getPosition(),
width: node.getSize().width,
height: node.getSize().height,
};
taskElements.push(newElement);
});
}
}
return taskElements;
};
export const resolveNodeEdgeElements = (graph: Graph): EdgeNodeElement[] => {
const edgeElements: EdgeNodeElement[] = [];
if (graph) {
const graphEdges = graph?.getEdges() ?? [] as Edge[];
if (graphEdges) {
graphEdges.forEach(edge => {
const nodeData = edge.getData() as TaskNodeElement;
edgeElements.push({
id: nodeData?.id ?? 0,
key: edge.id,
type: 'edge',
status: nodeData?.status,
source: edge.getSource() ? edge.getSource() as unknown as string : null,
target: edge.getSource() ? edge.getTarget() as unknown as string : null,
attrs: edge.getAttrs() ?? {},
router: edge.getRouter() ?? {},
connector: edge.getConnector() ?? null,
});
});
}
}
return edgeElements;
};
export const resolveNodeGraph = (graph: Graph): NodeGraph => {
const nodes: TaskNodeElement[] = resolveNodeTaskElements(graph);
const edges: EdgeNodeElement[] = resolveNodeEdgeElements(graph);
return {
nodes,
edges,
};
};
export const hasElements = (graph: Graph): boolean => {
if (graph) {
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
return taskElements.length > 0;
}
return false;
};
export const hasRootElementNode = (graph: Graph): boolean => {
if (graph) {
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
return taskElements.filter(e => e.type === 'root').length === 1;
}
return false;
};

View File

@@ -68,11 +68,14 @@ import Header from './header.vue';
import Properties from './properties.vue'; import Properties from './properties.vue';
import { useGraphCanvas } from './builder/hooks'; import { useGraphCanvas } from './builder/hooks';
import { registerNodeElement } from './builder/register'; import { registerNodeElement } from './builder/register';
import type { BehaviorTree, EdgeNodeElement, NodeGraph, NodeTemplate, SettingTaskNodeElement } from './types'; import type { BehaviorTree, NodeTemplate } from './types';
import type { NodeGraph, SettingTaskNodeElement, TaskNodeElement } from './builder/element';
import { createTree, findOneTreeById, updateTree } from './api'; import { createTree, findOneTreeById, updateTree } from './api';
import { createTaskNodeElement, createTaskNodeElementFromTemplate, hasElements, hasRootElementNode, resolveNodeGraph } from './utils/node'; import { createModelNode, hasElements, hasRootElementNode, resolveNodeGraph } from './builder/utils';
import { createTaskNodeElementFromTemplate } from './utils/node';
import TressCard from './trees-card.vue'; import TressCard from './trees-card.vue';
import NodesCard from './nodes-card.vue'; import NodesCard from './nodes-card.vue';
import { createLineOptions } from '@/views/decision/builder/line.ts';
const TeleportContainer = defineComponent(getTeleport()); const TeleportContainer = defineComponent(getTeleport());
@@ -187,7 +190,7 @@ export default defineComponent({
// 创建节点数据 // 创建节点数据
const settingTaskElement: SettingTaskNodeElement = createTaskNodeElementFromTemplate(template, { x, y }); const settingTaskElement: SettingTaskNodeElement = createTaskNodeElementFromTemplate(template, { x, y });
// 创建节点 // 创建节点
const settingTaskNode = createTaskNodeElement(settingTaskElement); const settingTaskNode = createModelNode(settingTaskElement);
console.info('create settingTaskNode: ', settingTaskElement, settingTaskNode); console.info('create settingTaskNode: ', settingTaskElement, settingTaskNode);
// 将节点添加到画布 // 将节点添加到画布
@@ -205,12 +208,23 @@ export default defineComponent({
console.error('handleSelectTree', tree); console.error('handleSelectTree', tree);
findOneTreeById(tree.id).then(r => { findOneTreeById(tree.id).then(r => {
if (r.data) { if (r.data) {
currentBehaviorTree.value = r.data; let nodeGraph: NodeGraph | null = null;
try { try {
currentBehaviorTree.value.graph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as NodeGraph; nodeGraph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as NodeGraph;
} catch (e: any) { } catch (e: any) {
console.error('parse error,cause:', e); console.error('parse error,cause:', e);
} }
if (!nodeGraph) {
nodeGraph = {
nodes: [],
edges: [],
};
}
currentBehaviorTree.value = {
...r.data,
graph: nodeGraph,
};
console.error(currentBehaviorTree.value);
createElements(); createElements();
} else { } else {
message.error(r.msg ?? '行为树不存在.'); message.error(r.msg ?? '行为树不存在.');
@@ -219,32 +233,30 @@ export default defineComponent({
}; };
const createElements = () => { const createElements = () => {
if (!currentBehaviorTree.value?.graph) { nextTick(() => {
currentBehaviorTree.value?.graph = { graph.value?.clearCells();
nodes: [], if (currentBehaviorTree.value?.graph && graph.value) {
edges: [], if (currentBehaviorTree.value?.graph.nodes) {
}; currentBehaviorTree.value?.graph.nodes.forEach(ele => {
} const node = createModelNode(ele as TaskNodeElement);
if(graph.value && currentNodeGraph.value){ console.info('create node: ', ele);
graph.value.clearCells(); // 将节点添加到画布
setTimeout(()=> { graph.value?.addNode(node as Node);
nextTick(()=> { });
try{ }
const nodes: EdgeNodeElement[] = currentBehaviorTree.value?.graph?.nodes ?? []; if (currentBehaviorTree.value?.graph.edges) {
const edges: EdgeNodeElement[] = currentBehaviorTree.value?.graph?.edges ?? []; // 然后添加所有边,确保包含桩点信息
nodes.forEach((n: any) => { setTimeout(() => {
const node = createTaskNodeElement(n); currentBehaviorTree.value?.graph.edges.forEach(edgeData => {
graph.value?.addNode(node as Node); graph.value?.addEdge({
}) ...edgeData,
edges.forEach((g: any) => { ...createLineOptions(),
graph.value?.addEdge( g as any); });
}) });
} catch (e){ }, 100); // 延迟一会儿,免得连线错位
console.warn('createElements',e) }
} }
}) });
}, 200)
}
}; };
// 初始化X6画布 // 初始化X6画布

View File

@@ -10,4 +10,3 @@
export * from './tree' export * from './tree'
export * from './template' export * from './template'
export * from './parameter' export * from './parameter'
export * from './node'

View File

@@ -7,7 +7,7 @@
* that was distributed with this source code. * that was distributed with this source code.
*/ */
import type { ApiDataResponse, ApiPaginationQuery, NullableString } from '@/types'; import type { ApiDataResponse, NullableString } from '@/types';
export interface NodeTemplate { export interface NodeTemplate {
id: number; id: number;
@@ -19,16 +19,6 @@ export interface NodeTemplate {
englishName: NullableString; englishName: NullableString;
} }
export interface NodeTemplateData {
templates: NodeTemplate[];
total: number;
}
export interface NodeTemplateQuery extends ApiPaginationQuery {
include_params: boolean;
type: NullableString;
}
export interface NodeTemplatesResponse extends ApiDataResponse<NodeTemplate[]> { export interface NodeTemplatesResponse extends ApiDataResponse<NodeTemplate[]> {
} }

View File

@@ -8,7 +8,7 @@
*/ */
import type { ApiDataResponse, NullableString, PageableResponse } from '@/types'; import type { ApiDataResponse, NullableString, PageableResponse } from '@/types';
import type { NodeGraph } from './node'; import type { NodeGraph } from '../builder/element';
export interface BehaviorTree { export interface BehaviorTree {
id: number, id: number,

View File

@@ -7,9 +7,9 @@
* that was distributed with this source code. * that was distributed with this source code.
*/ */
import { Edge, Graph, Node } from '@antv/x6'; import type { NodeTemplate } from '../types';
import type { EdgeNodeElement, NodeGraph, NodeTemplate, SettingTaskNodeElement, TaskNodeElement, TaskNodeRect } from '../types'; import type { SettingTaskNodeElement, TaskNodeRect } from '../builder/element';
import { generateKey } from '@/utils/strings.ts'; import { generateKey } from '@/utils/strings';
export const createTaskNodeElementFromTemplate = ( export const createTaskNodeElementFromTemplate = (
template: NodeTemplate, template: NodeTemplate,
@@ -31,7 +31,7 @@ export const createTaskNodeElementFromTemplate = (
}, },
width: realRect.width, width: realRect.width,
height: realRect.height, height: realRect.height,
settings: JSON.parse(JSON.stringify(template.parameter_defs ?? [])), // settings: JSON.parse(JSON.stringify(template.parameter_defs ?? [])),
inputs: null, inputs: null,
outputs: null, outputs: null,
parameters: {}, parameters: {},
@@ -53,94 +53,3 @@ export const createTaskNodeElementFromTemplate = (
], ],
} as SettingTaskNodeElement; } as SettingTaskNodeElement;
}; };
export const createTaskNodeElement = (element: TaskNodeElement, width: number = 200, height: number = 100): any => {
return {
shape: 'task',
id: element.key,
position: {
x: element.position?.x || 0,
y: element.position?.y || 0,
},
size: {
width: element.width ?? width,
height: element.height ?? height,
},
attrs: {
label: {
text: element.name,
},
},
data: element,
};
};
export const resolveNodeTaskElements = (graph: Graph): TaskNodeElement[] => {
const taskElements: TaskNodeElement[] = [];
if (graph) {
const nodes = graph?.getNodes() as Node[];
if (nodes) {
nodes.forEach(node => {
const nodeData = node.getData() as TaskNodeElement;
const newElement = {
...nodeData,
key: node.id,
position: node.getPosition(),
width: node.getSize().width,
height: node.getSize().height,
};
taskElements.push(newElement);
});
}
}
return taskElements;
};
export const resolveNodeEdgeElements = (graph: Graph): EdgeNodeElement[] => {
const edgeElements: EdgeNodeElement[] = [];
if (graph) {
const graphEdges = graph?.getEdges() ?? [] as Edge[];
if (graphEdges) {
graphEdges.forEach(edge => {
const nodeData = edge.getData() as TaskNodeElement;
edgeElements.push({
id: nodeData?.id ?? 0,
key: edge.id,
type: 'edge',
status: nodeData?.status,
source: edge.getSource() ? edge.getSource() as unknown as string : null,
target: edge.getSource() ? edge.getTarget() as unknown as string : null,
attrs: edge.getAttrs() ?? {},
router: edge.getRouter() ?? {},
connector: edge.getConnector() ?? null,
});
});
}
}
return edgeElements;
};
export const resolveNodeGraph = (graph: Graph): NodeGraph => {
const nodes: TaskNodeElement[] = resolveNodeTaskElements(graph);
const edges: EdgeNodeElement[] = resolveNodeEdgeElements(graph);
return {
nodes,
edges,
};
};
export const hasElements = (graph: Graph): boolean => {
if (graph) {
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
return taskElements.length > 0;
}
return false;
}
export const hasRootElementNode = (graph: Graph): boolean => {
if (graph) {
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
return taskElements.filter(e => e.type === 'root').length === 1;
}
return false;
};