支持在通信节点右键菜单中挂载行为树

This commit is contained in:
2026-04-14 15:09:50 +08:00
parent 835bb56851
commit 2b2a11831d
3 changed files with 173 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import { HttpRequestClient } from '@/utils/request';
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest } from './types';
import type { PlatformWithComponentsResponse } from '../types';
import type { BasicResponse } from '@/types';
import type { BehaviorTree } from '../designer/tree';
const req = HttpRequestClient.create<BasicResponse>({
baseURL: '/api',
@@ -34,4 +35,14 @@ export const findPlatformWithComponents = (id: number): Promise<PlatformWithComp
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
};
// 获取场景下的所有行为树列表
export const getAllBehaviorTreesBySceneId = (sceneId: number): Promise<{ code: number; msg: string; data: BehaviorTree[] }> => {
return req.get<{ code: number; msg: string; data: BehaviorTree[] }>(`/system/scene/getAllTree/${sceneId}`);
};
// 更新行为树(挂载到平台)
export const updateBehaviorTree = (behaviorTree: BehaviorTree): Promise<BasicResponse> => {
return req.putJson<BasicResponse>(`/system/behaviortree`, behaviorTree);
};

View File

@@ -64,7 +64,7 @@ import { createGraphScenarioElement, createGraphTaskElementFromScenario } from '
import PlatformCard from './platform-card.vue';
import NodesCard from './nodes-card.vue';
import { findOneScenarioById, saveScenario } from './api';
import { findOneScenarioById, saveScenario, getAllBehaviorTreesBySceneId } from './api';
import { resolveConnectionRelation } from './relation';
const TeleportContainer = defineComponent(getTeleport());
@@ -211,7 +211,7 @@ export default defineComponent({
}
};
const handleSelect = (scenario: Scenario) => {
const handleSelect = async (scenario: Scenario) => {
let nodeGraph: GraphContainer | null = null;
try {
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
@@ -230,6 +230,27 @@ export default defineComponent({
relations: []
};
currentScenarioEditing.value = true;
// 将场景ID存储到graph对象中供子组件访问
if (graph.value) {
(graph.value as any).currentScenario = currentScenario.value;
// 加载该场景下的行为树列表
try {
const response = await getAllBehaviorTreesBySceneId(scenario.id);
if (response.code === 200 && response.data) {
(graph.value as any).behaviorTrees = response.data;
console.log(`加载场景${scenario.id}的行为树列表:`, response.data.length, '个');
} else {
(graph.value as any).behaviorTrees = [];
console.warn('获取行为树列表失败:', response.msg);
}
} catch (error) {
console.error('获取行为树列表失败:', error);
(graph.value as any).behaviorTrees = [];
}
}
createElements();
};
@@ -277,6 +298,12 @@ export default defineComponent({
nodes: [],
},
};
// 清空graph中的场景信息
if (graph.value) {
(graph.value as any).currentScenario = null;
}
currentGraph.value = {
edges: [],
nodes: [],

View File

@@ -1,5 +1,9 @@
<template>
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
<a-dropdown
:trigger="['contextmenu']"
:getPopupContainer="getPopupContainer"
@openChange="handleVisibleChange"
>
<a-card
:class="[
'ks-scenario-node',
@@ -55,6 +59,27 @@
<template #overlay>
<a-menu @click="handleMenuClick">
<a-sub-menu key="mount">
<template #icon>
<LinkOutlined />
</template>
<template #title>挂载</template>
<a-menu-item
v-for="tree in availableTrees"
:key="`tree-${tree.id}`"
:disabled="isTreeMounted(tree.id)"
@click="() => handleMountTree(tree)"
>
<template #icon>
<CheckOutlined v-if="isTreeMounted(tree.id)" />
</template>
{{ tree.name }}
</a-menu-item>
<a-menu-item v-if="availableTrees.length === 0" disabled>
暂无可用行为树
</a-menu-item>
</a-sub-menu>
<a-menu-divider />
<a-menu-item key="delete">
<template #icon>
<DeleteOutlined />
@@ -70,15 +95,20 @@
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
import { elementProps, type ModelElement } from '../graph';
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
import { DeleteOutlined, LinkOutlined, CheckOutlined, SettingOutlined } from '@ant-design/icons-vue';
import type { Graph } from '@antv/x6';
import { message } from 'ant-design-vue';
import { substring } from '@/utils/strings';
import { getAllBehaviorTreesBySceneId, updateBehaviorTree } from './api';
import type { BehaviorTree } from '../designer/tree';
export default defineComponent({
name: 'ModelElement',
components: {
SettingOutlined,
DeleteOutlined,
LinkOutlined,
CheckOutlined,
},
props: elementProps,
setup(_props) {
@@ -87,6 +117,17 @@ export default defineComponent({
);
const updateKey = ref(0);
const isMenuVisible = ref(false);
// 挂载行为树相关状态
const availableTrees = ref<BehaviorTree[]>([]);
// 获取 popup 容器
const getPopupContainer = () => {
if (typeof document !== 'undefined') {
return document.body;
}
return undefined;
};
// 获取画布实例
const getGraph = (): Graph | null => {
@@ -103,8 +144,35 @@ export default defineComponent({
updateKey.value++;
};
// 获取行为树名称
const getBehaviorTreeName = (treeId: number | undefined | null): string => {
if (!treeId) return '';
const tree = availableTrees.value.find(t => t.id === treeId);
return tree?.name || `行为树${treeId}`;
};
// 判断行为树是否已挂载到当前节点
const isTreeMounted = (treeId: number): boolean => {
if (!element.value) return false;
const currentTreeId = (element.value as any).behaviorTreeId as number | undefined;
return currentTreeId === treeId;
};
// 处理挂载行为树 - 当右键菜单打开时从graph中读取已缓存的行为树列表
const handleVisibleChange = (visible: boolean) => {
isMenuVisible.value = visible;
if (!visible || !element.value) return;
// 从graph对象中获取已缓存的行为树列表
const graph = _props.graph as any;
if (graph?.behaviorTrees) {
availableTrees.value = graph.behaviorTrees;
console.log('从缓存中读取行为树列表:', availableTrees.value.length, '个');
} else {
availableTrees.value = [];
console.warn('未找到缓存的行为树列表');
}
};
const handleMenuClick = ({ key }: { key: string }) => {
@@ -113,6 +181,37 @@ export default defineComponent({
}
};
// 处理挂载具体的行为树
const handleMountTree = async (tree: BehaviorTree) => {
if (!element.value) return;
try {
// 更新节点的behaviorTreeId属性
const updatedElement = { ...(element.value as any), behaviorTreeId: tree.id };
// 调用后端API更新行为树将platformId关联到该平台
const platformIdValue = (element.value as any).platformId as number | undefined;
const treeToUpdate = {
...tree,
platformId: platformIdValue ?? null
};
const updateResponse = await updateBehaviorTree(treeToUpdate);
if (updateResponse.code === 200) {
// 更新本地节点数据
if (_props.node) {
_props.node.setData(updatedElement);
}
message.success(`已成功挂载行为树: ${tree.name}`);
} else {
message.error(updateResponse.msg || '挂载失败');
}
} catch (error) {
console.error('挂载行为树失败:', error);
message.error('挂载行为树失败');
}
};
const handleDelete = () => {
if (!_props.node) return;
@@ -134,10 +233,37 @@ export default defineComponent({
onMounted(() => {
_props.node?.on('change:data', handleDataChange);
// 监听画布各种事件,操作时立即关闭菜单
const graph = getGraph();
if (graph) {
const closeMenuHandler = () => {
if (isMenuVisible.value) {
isMenuVisible.value = false;
}
};
// 监听多种可能导致菜单位置变化的事件
graph.on('pan', closeMenuHandler);
graph.on('translate', closeMenuHandler);
graph.on('scale', closeMenuHandler);
graph.on('zoom', closeMenuHandler);
graph.on('resize', closeMenuHandler);
}
});
onUnmounted(() => {
_props.node?.off('change:data', handleDataChange);
// 清理事件监听
const graph = getGraph();
if (graph) {
graph.off('pan');
graph.off('translate');
graph.off('scale');
graph.off('zoom');
graph.off('resize');
}
});
return {
@@ -145,6 +271,11 @@ export default defineComponent({
substring,
handleMenuClick,
handleVisibleChange,
availableTrees,
getBehaviorTreeName,
isTreeMounted,
handleMountTree,
getPopupContainer,
};
},
});