@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@antv/g6": "^5.0.49",
|
"@antv/g6": "^5.0.49",
|
||||||
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
"@antv/x6": "^3.1.2",
|
"@antv/x6": "^3.1.2",
|
||||||
"@antv/x6-vue-shape": "^3.0.2",
|
"@antv/x6-vue-shape": "^3.0.2",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export function resolveConnectionRelation(graph: Graph): PlatformRelation[] {
|
|||||||
// 过滤无效/临时边
|
// 过滤无效/临时边
|
||||||
const validEdges = edges.filter(edge => {
|
const validEdges = edges.filter(edge => {
|
||||||
// 过滤临时边(X6 拖拽连线时生成的未完成边)
|
// 过滤临时边(X6 拖拽连线时生成的未完成边)
|
||||||
const isTempEdge = edge?.attr('line/stroke') === 'transparent' || edge.id.includes('temp');
|
const isTempEdge = edge?.attr('line/stroke') === 'transparent' || String(edge.id).includes('temp');
|
||||||
if (isTempEdge) {
|
if (isTempEdge) {
|
||||||
tempEdgeIds.add(edge.id);
|
tempEdgeIds.add(edge.id);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -10,9 +10,54 @@
|
|||||||
import { HttpRequestClient } from '@/utils/request';
|
import { HttpRequestClient } from '@/utils/request';
|
||||||
import type { NodeTemplatesResponse } from './template';
|
import type { NodeTemplatesResponse } from './template';
|
||||||
import type { BehaviorTree, BehaviorTreeDetailsResponse, BehaviorTreePageResponse, BehaviorTreeRequest } from './tree';
|
import type { BehaviorTree, BehaviorTreeDetailsResponse, BehaviorTreePageResponse, BehaviorTreeRequest } from './tree';
|
||||||
import type { BasicResponse } from '@/types';
|
import type { BasicResponse, PageableResponse } from '@/types';
|
||||||
import type { PlatformListableResponse } from '../types';
|
import type { PlatformListableResponse } from '../types';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export interface TreeTemplateRow {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
name: string;
|
||||||
|
multiable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeNodeInstanceRow {
|
||||||
|
id: number;
|
||||||
|
treeId: number;
|
||||||
|
templateId: number;
|
||||||
|
instanceName: string;
|
||||||
|
isRoot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeConnectionRow {
|
||||||
|
id: number;
|
||||||
|
treeId: number;
|
||||||
|
parentNodeId: number;
|
||||||
|
childNodeId: number;
|
||||||
|
orderIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeTemplateParameterDefRow {
|
||||||
|
id: number;
|
||||||
|
templateId: number;
|
||||||
|
paramKey: string;
|
||||||
|
dataType: string;
|
||||||
|
defaultValue: string;
|
||||||
|
description: string;
|
||||||
|
templateType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TreeNodeParameterRow {
|
||||||
|
id: number;
|
||||||
|
treeId: number;
|
||||||
|
nodeInstanceId: number;
|
||||||
|
paramDefId: number;
|
||||||
|
value: string;
|
||||||
|
groupIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const req = HttpRequestClient.create<BasicResponse>({
|
const req = HttpRequestClient.create<BasicResponse>({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
});
|
});
|
||||||
@@ -20,6 +65,20 @@ const req = HttpRequestClient.create<BasicResponse>({
|
|||||||
export const findNodeTemplates = (): Promise<NodeTemplatesResponse> => {
|
export const findNodeTemplates = (): Promise<NodeTemplatesResponse> => {
|
||||||
return req.get('/system/nodetemplate/all');
|
return req.get('/system/nodetemplate/all');
|
||||||
};
|
};
|
||||||
|
export const findTemplateParameterDefs = (query={pageSize: 1000, pageNum: 1}): Promise<PageableResponse<TreeTemplateParameterDefRow>> => {
|
||||||
|
return req.get('/system/templateparameterdef/list', query);
|
||||||
|
}
|
||||||
|
export const findNodeConnections = (query: {treeId: number}): Promise<PageableResponse<TreeConnectionRow>> => {
|
||||||
|
return req.get('/system/nodeconnection/list', {pageSize: 1000, pageNum: 1, ...query});
|
||||||
|
};
|
||||||
|
export const findNodeParameters = (query: {treeId: number}): Promise<PageableResponse<TreeNodeParameterRow>> => {
|
||||||
|
return req.get('/system/nodeparameter/list', {pageSize: 1000, pageNum: 1, ...query});
|
||||||
|
}
|
||||||
|
export const findTreeNodeInstances = (query: {treeId: number}): Promise<PageableResponse<TreeNodeInstanceRow>> => {
|
||||||
|
return req.get('/system/treenodeinstance/list', {pageSize: 1000, pageNum: 1, ...query});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const findTreesByQuery = (query: Partial<BehaviorTreeRequest> = {}): Promise<BehaviorTreePageResponse> => {
|
export const findTreesByQuery = (query: Partial<BehaviorTreeRequest> = {}): Promise<BehaviorTreePageResponse> => {
|
||||||
return req.get<BehaviorTreePageResponse>('/system/behaviortree/list', query);
|
return req.get<BehaviorTreePageResponse>('/system/behaviortree/list', query);
|
||||||
|
|||||||
@@ -7,11 +7,13 @@
|
|||||||
<div class="ks-model-builder-left">
|
<div class="ks-model-builder-left">
|
||||||
<ScenariosCard
|
<ScenariosCard
|
||||||
ref="scenariosCardRef"
|
ref="scenariosCardRef"
|
||||||
|
:scenarioId="currentScenarioId"
|
||||||
@select-scenario="handleSelectScenario"
|
@select-scenario="handleSelectScenario"
|
||||||
/>
|
/>
|
||||||
<TressCard
|
<TressCard
|
||||||
ref="treesCardRef"
|
ref="treesCardRef"
|
||||||
:scenarioId="currentScenarioId"
|
:scenarioId="currentScenarioId"
|
||||||
|
:platformId="currentPlatformId"
|
||||||
@create-tree="handleCreateTree"
|
@create-tree="handleCreateTree"
|
||||||
@select-tree="handleSelectTree"
|
@select-tree="handleSelectTree"
|
||||||
/>
|
/>
|
||||||
@@ -76,6 +78,7 @@ import Properties from './properties.vue';
|
|||||||
import type { NodeDragTemplate } from './template';
|
import type { NodeDragTemplate } from './template';
|
||||||
import type { BehaviorTree } from './tree';
|
import type { BehaviorTree } from './tree';
|
||||||
import { createGraphTaskElementFromTemplate } from './utils';
|
import { createGraphTaskElementFromTemplate } from './utils';
|
||||||
|
import { resolveBehaviorTreeGraph } from './reverse-tree/tree-graph-resolver';
|
||||||
|
|
||||||
import { createGraphTaskElement, createLineOptions, type GraphContainer, type GraphTaskElement, hasElements, hasRootElementNode, resolveGraph, useGraphCanvas } from '../graph';
|
import { createGraphTaskElement, createLineOptions, type GraphContainer, type GraphTaskElement, hasElements, hasRootElementNode, resolveGraph, useGraphCanvas } from '../graph';
|
||||||
import { registerNodeElement } from './register';
|
import { registerNodeElement } from './register';
|
||||||
@@ -268,102 +271,6 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectTree = (tree: BehaviorTree) => {
|
|
||||||
destroyGraph();
|
|
||||||
currentPlatformId.value = null;
|
|
||||||
|
|
||||||
console.info('handleSelectTree', tree);
|
|
||||||
findOneTreeById(tree.id).then(r => {
|
|
||||||
if (r.data) {
|
|
||||||
let nodeGraph: GraphContainer | null = null;
|
|
||||||
try {
|
|
||||||
nodeGraph = JSON.parse(r.data?.xmlContent as unknown as string) as unknown as GraphContainer;
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('parse error,cause:', e);
|
|
||||||
}
|
|
||||||
if (!nodeGraph) {
|
|
||||||
nodeGraph = {
|
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
currentBehaviorTree.value = {
|
|
||||||
...r.data,
|
|
||||||
graph: nodeGraph,
|
|
||||||
};
|
|
||||||
currentTreeEditing.value = true;
|
|
||||||
|
|
||||||
// 加载下属平台
|
|
||||||
loadSubPlatforms(r.data.platformId);
|
|
||||||
|
|
||||||
nextTick(() => {
|
|
||||||
initGraph();
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
message.error(r.msg ?? '行为树不存在.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createElements = () => {
|
|
||||||
clearGraph();
|
|
||||||
nextTick(() => {
|
|
||||||
if (currentBehaviorTree.value?.graph && graph.value) {
|
|
||||||
if (currentBehaviorTree.value?.graph.nodes) {
|
|
||||||
currentBehaviorTree.value?.graph.nodes.forEach(ele => {
|
|
||||||
const node = createGraphTaskElement(ele as GraphTaskElement);
|
|
||||||
console.info('create node: ', ele);
|
|
||||||
// 将节点添加到画布
|
|
||||||
graph.value?.addNode(node as Node);
|
|
||||||
});
|
|
||||||
fitToScreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 然后添加所有边,确保包含桩点信息
|
|
||||||
currentBehaviorTree.value?.graph.edges.forEach(edgeData => {
|
|
||||||
graph.value?.addEdge({
|
|
||||||
...edgeData,
|
|
||||||
...createLineOptions(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const STORAGE_KEY_SCENARIO = 'designer_from_scenario_id';
|
|
||||||
|
|
||||||
const resolveQuery = ()=> {
|
|
||||||
console.log(currentRoute);
|
|
||||||
|
|
||||||
let scenarioId = Number(currentRoute.query.scenario);
|
|
||||||
if (!isNaN(scenarioId) && scenarioId > 0) {
|
|
||||||
currentScenarioId.value = scenarioId;
|
|
||||||
fromScenarioPage.value = true;
|
|
||||||
sessionStorage.setItem(STORAGE_KEY_SCENARIO, String(scenarioId));
|
|
||||||
} else {
|
|
||||||
// 尝试从 sessionStorage 恢复(页面刷新或 SPA 内部跳转后 query 参数丢失的情况)
|
|
||||||
const stored = sessionStorage.getItem(STORAGE_KEY_SCENARIO);
|
|
||||||
const storedId = Number(stored);
|
|
||||||
if (stored && !isNaN(storedId) && storedId > 0) {
|
|
||||||
currentScenarioId.value = storedId;
|
|
||||||
fromScenarioPage.value = true;
|
|
||||||
} else {
|
|
||||||
fromScenarioPage.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let platformId = Number(currentRoute.query.platform);
|
|
||||||
if (!isNaN(platformId)) {
|
|
||||||
currentPlatformId.value = platformId;
|
|
||||||
} else {
|
|
||||||
currentPlatformId.value = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理选择场景
|
|
||||||
const handleSelectScenario = (scenario: any) => {
|
|
||||||
currentScenarioId.value = scenario.id;
|
|
||||||
};
|
|
||||||
|
|
||||||
const initGraphConfig = (_graph?: GraphContainer) => {
|
const initGraphConfig = (_graph?: GraphContainer) => {
|
||||||
const graph: GraphContainer = _graph ? _graph : {
|
const graph: GraphContainer = _graph ? _graph : {
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -384,6 +291,96 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createElements = () => {
|
||||||
|
clearGraph();
|
||||||
|
nextTick(() => {
|
||||||
|
if (currentBehaviorTree.value?.graph && graph.value) {
|
||||||
|
if (currentBehaviorTree.value?.graph.nodes) {
|
||||||
|
currentBehaviorTree.value?.graph.nodes.forEach(ele => {
|
||||||
|
const node = createGraphTaskElement(ele as GraphTaskElement);
|
||||||
|
// 将节点添加到画布
|
||||||
|
graph.value?.addNode(node as Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
// 然后添加所有边,确保包含桩点信息
|
||||||
|
currentBehaviorTree.value?.graph.edges.forEach(edgeData => {
|
||||||
|
graph.value?.addEdge({
|
||||||
|
...edgeData,
|
||||||
|
...createLineOptions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
fitToScreen();
|
||||||
|
|
||||||
|
console.info('create elements: ', currentBehaviorTree.value?.graph);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyBehaviorTree = (tree: BehaviorTree) => {
|
||||||
|
destroyGraph();
|
||||||
|
currentPlatformId.value = tree.platformId ?? null;
|
||||||
|
currentBehaviorTree.value = tree;
|
||||||
|
currentTreeEditing.value = true;
|
||||||
|
selectedModelNode.value = null;
|
||||||
|
selectedNodeTaskElement.value = null;
|
||||||
|
loadSubPlatforms(currentPlatformId.value);
|
||||||
|
console.log('currentBehaviorTree.value: ', tree);
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
initGraph();
|
||||||
|
console.log('initGraphTree: ', tree);
|
||||||
|
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectTree = (tree: BehaviorTree) => {
|
||||||
|
console.info('handleSelectTree', tree);
|
||||||
|
findOneTreeById(tree.id).then(r => {
|
||||||
|
if (r.data) {
|
||||||
|
resolveBehaviorTreeGraph(r.data.id, r.data.xmlContent).then(nodeGraph => {
|
||||||
|
applyBehaviorTree({
|
||||||
|
...r.data,
|
||||||
|
graph: nodeGraph,
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
console.error('resolve tree graph error:', error);
|
||||||
|
message.error('加载行为树图失败');
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
message.error(r.msg ?? '行为树不存在.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveQuery = ()=> {
|
||||||
|
console.log(currentRoute);
|
||||||
|
if (!currentRoute.query.scenario) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let scenarioId = Number(currentRoute.query.scenario);
|
||||||
|
if (!isNaN(scenarioId)) {
|
||||||
|
currentScenarioId.value = scenarioId;
|
||||||
|
fromScenarioPage.value = true;
|
||||||
|
} else {
|
||||||
|
fromScenarioPage.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let platformId = Number(currentRoute.query.platform);
|
||||||
|
if (!isNaN(platformId)) {
|
||||||
|
currentPlatformId.value = platformId;
|
||||||
|
} else {
|
||||||
|
currentPlatformId.value = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理选择场景
|
||||||
|
const handleSelectScenario = (scenario: any) => {
|
||||||
|
currentScenarioId.value = scenario.id;
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateTree = () => {
|
const handleCreateTree = () => {
|
||||||
destroyGraph();
|
destroyGraph();
|
||||||
initGraphConfig();
|
initGraphConfig();
|
||||||
@@ -407,9 +404,17 @@ export default defineComponent({
|
|||||||
try {
|
try {
|
||||||
graph.value = createCanvas(canvas.value);
|
graph.value = createCanvas(canvas.value);
|
||||||
console.log('画布初始化成功');
|
console.log('画布初始化成功');
|
||||||
|
if (!currentBehaviorTree.value) {
|
||||||
initGraphConfig();
|
initGraphConfig();
|
||||||
|
}
|
||||||
createElements();
|
createElements();
|
||||||
|
|
||||||
|
graph.value?.on('edge:click', (args: any) => {
|
||||||
|
const edge = args.edge;
|
||||||
|
console.info('点击了连线:', args);
|
||||||
|
// 这里可以添加选中连线的逻辑,比如显示属性面板等
|
||||||
|
});
|
||||||
|
|
||||||
// 监听缩放变化
|
// 监听缩放变化
|
||||||
handleGraphEvent('scale', ({ sx }: { sx: number }) => {
|
handleGraphEvent('scale', ({ sx }: { sx: number }) => {
|
||||||
currentZoom.value = sx;
|
currentZoom.value = sx;
|
||||||
@@ -451,7 +456,6 @@ export default defineComponent({
|
|||||||
const init = () => {
|
const init = () => {
|
||||||
console.info('init');
|
console.info('init');
|
||||||
|
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
initGraph();
|
initGraph();
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
@@ -461,8 +465,6 @@ export default defineComponent({
|
|||||||
|
|
||||||
if (currentPlatformId.value) {
|
if (currentPlatformId.value) {
|
||||||
findOneTreeByPlatformId(currentPlatformId.value).then(r => {
|
findOneTreeByPlatformId(currentPlatformId.value).then(r => {
|
||||||
console.log(r);
|
|
||||||
|
|
||||||
if (r.data) {
|
if (r.data) {
|
||||||
handleSelectTree(r.data);
|
handleSelectTree(r.data);
|
||||||
} else {
|
} else {
|
||||||
@@ -488,7 +490,6 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleGoback = ()=> {
|
const handleGoback = ()=> {
|
||||||
sessionStorage.removeItem(STORAGE_KEY_SCENARIO);
|
|
||||||
window.location.href = `/app/decision/communication?scenario=${currentScenarioId.value}`
|
window.location.href = `/app/decision/communication?scenario=${currentScenarioId.value}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -542,6 +543,7 @@ export default defineComponent({
|
|||||||
return {
|
return {
|
||||||
nodeCommands,
|
nodeCommands,
|
||||||
currentScenarioId,
|
currentScenarioId,
|
||||||
|
currentPlatformId,
|
||||||
platforms,
|
platforms,
|
||||||
subPlatforms,
|
subPlatforms,
|
||||||
scenariosCardRef,
|
scenariosCardRef,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
</a-space>
|
</a-space>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="port port-in" data-port="in-0" magnet="passive">
|
<div class="port port-in" data-port="in-0" port="in-0" magnet="passive">
|
||||||
<div class="triangle-left"></div>
|
<div class="triangle-left"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full ks-designer-node-text">
|
<div class="w-full ks-designer-node-text">
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
</p>
|
</p>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div class="port port-out" data-port="out-0" magnet="active">
|
<div class="port port-out" data-port="out-0" port="out-0" magnet="active">
|
||||||
<div class="triangle-right" ></div>
|
<div class="triangle-right" ></div>
|
||||||
</div>
|
</div>
|
||||||
</a-card>
|
</a-card>
|
||||||
|
|||||||
@@ -72,8 +72,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref } from 'vue';
|
import { defineComponent, onMounted, ref } from 'vue';
|
||||||
import type { NodeDragTemplate, NodeTemplate } from './template';
|
import type { NodeDragTemplate, NodeTemplate } from './template';
|
||||||
import { findNodeTemplates } from './api';
|
|
||||||
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
||||||
|
import { loadNodeTemplatesOnce } from './template-metadata-loader';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
emits: ['drag-item-start', 'drag-item-end'],
|
emits: ['drag-item-start', 'drag-item-end'],
|
||||||
@@ -95,7 +95,7 @@ export default defineComponent({
|
|||||||
conditionTemplates.value = [];
|
conditionTemplates.value = [];
|
||||||
actionsTemplates.value = [];
|
actionsTemplates.value = [];
|
||||||
|
|
||||||
findNodeTemplates().then(r => {
|
loadNodeTemplatesOnce().then(r => {
|
||||||
templateData.value = r.data;
|
templateData.value = r.data;
|
||||||
if (r.data) {
|
if (r.data) {
|
||||||
r.data.forEach(tpl => {
|
r.data.forEach(tpl => {
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
<a-select :placeholder="`请选择${setting.description}`"
|
<a-select :placeholder="`请选择${setting.description}`"
|
||||||
allow-clear
|
allow-clear
|
||||||
v-else-if="setting.paramKey === 'command'" v-model:value="setting.defaultValue">
|
v-else-if="isNodeCommandParameter(setting.paramKey as string | null | undefined)" v-model:value="setting.defaultValue">
|
||||||
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
|
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-input v-else v-model:value="setting.defaultValue"
|
<a-input v-else v-model:value="setting.defaultValue"
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
<a-select :placeholder="`请选择${setting.description}`"
|
<a-select :placeholder="`请选择${setting.description}`"
|
||||||
allow-clear
|
allow-clear
|
||||||
v-else-if="setting.paramKey === 'command'" v-model:value="setting.defaultValue">
|
v-else-if="isNodeCommandParameter(setting.paramKey as string | null | undefined)" v-model:value="setting.defaultValue">
|
||||||
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
|
<a-select-option v-for="pl in nodeCommands" :value="pl.command">{{pl.chineseName}}</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
|
<a-input v-else v-model:value="setting.defaultValue" :placeholder="setting.description" size="small" />
|
||||||
@@ -209,6 +209,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
emits: ['update-element', 'update-tree'],
|
emits: ['update-element', 'update-tree'],
|
||||||
setup(props, { emit }) {
|
setup(props, { emit }) {
|
||||||
|
const isNodeCommandParameter = (paramKey: string | null | undefined): boolean => {
|
||||||
|
return ['command', 'should_task'].includes(paramKey ?? '');
|
||||||
|
};
|
||||||
|
|
||||||
const platforms = ref<Platform[]>(props.platforms ?? []);
|
const platforms = ref<Platform[]>(props.platforms ?? []);
|
||||||
const subPlatforms = ref<Platform[]>(props.subPlatforms ?? []);
|
const subPlatforms = ref<Platform[]>(props.subPlatforms ?? []);
|
||||||
const nodeCommands = ref<NodeCommand[]>(props.nodeCommands ?? []);
|
const nodeCommands = ref<NodeCommand[]>(props.nodeCommands ?? []);
|
||||||
@@ -448,6 +452,7 @@ export default defineComponent({
|
|||||||
groupedParameters,
|
groupedParameters,
|
||||||
getPlatformTabName,
|
getPlatformTabName,
|
||||||
getAvailablePlatforms,
|
getAvailablePlatforms,
|
||||||
|
isNodeCommandParameter,
|
||||||
actionSpaceColumns,
|
actionSpaceColumns,
|
||||||
activeTopTabsKey,
|
activeTopTabsKey,
|
||||||
activeBottomTabsKey,
|
activeBottomTabsKey,
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import type { GraphContainer } from '../../graph';
|
||||||
|
import { findNodeConnections, findNodeParameters, findTreeNodeInstances } from '../api';
|
||||||
|
import type { TreeTemplateRow } from '../api';
|
||||||
|
import { buildGraphFromReverseTreeRows } from './reverse-tree';
|
||||||
|
import type { ReverseTreeRows } from './reverse-tree';
|
||||||
|
import { loadNodeTemplatesOnce, loadTemplateParameterDefsOnce } from '../template-metadata-loader';
|
||||||
|
|
||||||
|
// 已反演过的树直接返回缓存图,无需重复请求
|
||||||
|
const graphCache = new Map<number, GraphContainer>();
|
||||||
|
|
||||||
|
export const loadReverseTreeGraph = async (treeId: number): Promise<GraphContainer | null> => {
|
||||||
|
const cached = graphCache.get(treeId);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模板类接口只请求一次,树级接口每次切树请求
|
||||||
|
const [nodeTemplatesResponse, templateParameterDefs, instancesResponse, connectionsResponse] =
|
||||||
|
await Promise.all([
|
||||||
|
loadNodeTemplatesOnce(),
|
||||||
|
loadTemplateParameterDefsOnce(),
|
||||||
|
findTreeNodeInstances({ treeId }),
|
||||||
|
findNodeConnections({ treeId }),
|
||||||
|
]);
|
||||||
|
const parametersResponse = await findNodeParameters({ treeId });
|
||||||
|
|
||||||
|
const rows: ReverseTreeRows = {
|
||||||
|
templates: (nodeTemplatesResponse.data ?? []).map(t => ({
|
||||||
|
id: t.id,
|
||||||
|
type: t.type ?? 'action',
|
||||||
|
name: t.name ?? '',
|
||||||
|
multiable: t.multiable,
|
||||||
|
})) as TreeTemplateRow[],
|
||||||
|
templateParameterDefs: templateParameterDefs,
|
||||||
|
nodeInstances: instancesResponse.rows ?? [],
|
||||||
|
connections: connectionsResponse.rows ?? [],
|
||||||
|
nodeParameters: parametersResponse.rows ?? [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const graph = buildGraphFromReverseTreeRows(treeId, rows);
|
||||||
|
if (graph) {
|
||||||
|
graphCache.set(treeId, graph);
|
||||||
|
}
|
||||||
|
return graph;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const invalidateReverseTreeCache = (treeId: number): void => {
|
||||||
|
graphCache.delete(treeId);
|
||||||
|
};
|
||||||
267
modeler/src/views/decision/designer/reverse-tree/reverse-tree.ts
Normal file
267
modeler/src/views/decision/designer/reverse-tree/reverse-tree.ts
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
import type { ElementParameter, GraphContainer, GraphEdgeElement, GraphTaskElement } from '../../graph';
|
||||||
|
import type {
|
||||||
|
TreeConnectionRow as ReverseTreeConnectionRow,
|
||||||
|
TreeNodeInstanceRow as ReverseTreeNodeInstanceRow,
|
||||||
|
TreeNodeParameterRow as ReverseTreeNodeParameterRow,
|
||||||
|
TreeTemplateParameterDefRow as ReverseTreeTemplateParameterDefRow,
|
||||||
|
TreeTemplateRow as ReverseTreeTemplateRow,
|
||||||
|
} from '../api';
|
||||||
|
|
||||||
|
export interface ReverseTreeRows {
|
||||||
|
templates: ReverseTreeTemplateRow[];
|
||||||
|
nodeInstances: ReverseTreeNodeInstanceRow[];
|
||||||
|
connections: ReverseTreeConnectionRow[];
|
||||||
|
templateParameterDefs: ReverseTreeTemplateParameterDefRow[];
|
||||||
|
nodeParameters: ReverseTreeNodeParameterRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const NODE_WIDTH = 250;
|
||||||
|
const CONTROL_HEIGHT = 60;
|
||||||
|
const ACTION_HEIGHT = 120;
|
||||||
|
const ROOT_X = 80;
|
||||||
|
const ROOT_Y = 50;
|
||||||
|
const LEVEL_Y_SPACING = 120;
|
||||||
|
const SIBLING_X_SPACING = 280;
|
||||||
|
|
||||||
|
const createNodeKey = (treeId: number, nodeId: number): string => {
|
||||||
|
return `reverse-tree-${treeId}-node-${nodeId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createEdgeKey = (treeId: number, edgeId: number): string => {
|
||||||
|
return `reverse-tree-${treeId}-edge-${edgeId}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createBehaviorTreeEdgeEndpoints = (sourceCell: string, targetCell: string) => {
|
||||||
|
return {
|
||||||
|
source: {
|
||||||
|
cell: sourceCell,
|
||||||
|
port: 'out-0',
|
||||||
|
},
|
||||||
|
target: {
|
||||||
|
cell: targetCell,
|
||||||
|
port: 'in-0',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveGroup = (type: string): 'control' | 'condition' | 'action' => {
|
||||||
|
if (type === 'root' || type === 'parallel' || type === 'select' || type === 'sequence') {
|
||||||
|
return 'control';
|
||||||
|
}
|
||||||
|
if (type === 'condition') {
|
||||||
|
return 'condition';
|
||||||
|
}
|
||||||
|
return 'action';
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTemplateType = (type: string): string => {
|
||||||
|
if (type === 'select') {
|
||||||
|
return 'selector';
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveNodeHeight = (group: 'control' | 'condition' | 'action'): number => {
|
||||||
|
return group === 'action' ? ACTION_HEIGHT : CONTROL_HEIGHT;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findRootNodeId = (
|
||||||
|
instances: ReverseTreeNodeInstanceRow[],
|
||||||
|
connections: ReverseTreeConnectionRow[],
|
||||||
|
): number | null => {
|
||||||
|
const rootNode = instances.find(item => item.isRoot === 1);
|
||||||
|
if (rootNode) {
|
||||||
|
return rootNode.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childIds = new Set(connections.map(item => item.childNodeId));
|
||||||
|
const topNode = instances.find(item => !childIds.has(item.id));
|
||||||
|
return topNode?.id ?? null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildGraphFromReverseTreeRows = (
|
||||||
|
treeId: number,
|
||||||
|
rows: ReverseTreeRows,
|
||||||
|
): GraphContainer | null => {
|
||||||
|
const templates = rows.templates;
|
||||||
|
const templateParameterDefs = rows.templateParameterDefs;
|
||||||
|
const nodeParameters = rows.nodeParameters;
|
||||||
|
const instances = rows.nodeInstances.filter(item => item.treeId === treeId);
|
||||||
|
const connections = rows.connections
|
||||||
|
.filter(item => item.treeId === treeId)
|
||||||
|
.sort((left, right) => left.orderIndex - right.orderIndex);
|
||||||
|
const treeNodeParameters = nodeParameters.filter(item => item.treeId === treeId);
|
||||||
|
|
||||||
|
if (instances.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateMap = new Map(templates.map(item => [item.id, item]));
|
||||||
|
const templateParameterDefsMap = new Map<number, ReverseTreeTemplateParameterDefRow[]>();
|
||||||
|
const instanceMap = new Map(instances.map(item => [item.id, item]));
|
||||||
|
const childrenMap = new Map<number, ReverseTreeConnectionRow[]>();
|
||||||
|
const nodeParametersMap = new Map<number, ReverseTreeNodeParameterRow[]>();
|
||||||
|
|
||||||
|
templateParameterDefs.forEach(definition => {
|
||||||
|
const currentDefinitions = templateParameterDefsMap.get(definition.templateId) ?? [];
|
||||||
|
currentDefinitions.push(definition);
|
||||||
|
currentDefinitions.sort((left, right) => left.id - right.id);
|
||||||
|
templateParameterDefsMap.set(definition.templateId, currentDefinitions);
|
||||||
|
});
|
||||||
|
|
||||||
|
treeNodeParameters.forEach(parameter => {
|
||||||
|
const currentParameters = nodeParametersMap.get(parameter.nodeInstanceId) ?? [];
|
||||||
|
currentParameters.push(parameter);
|
||||||
|
currentParameters.sort((left, right) => {
|
||||||
|
if (left.groupIndex !== right.groupIndex) {
|
||||||
|
return left.groupIndex - right.groupIndex;
|
||||||
|
}
|
||||||
|
return left.id - right.id;
|
||||||
|
});
|
||||||
|
nodeParametersMap.set(parameter.nodeInstanceId, currentParameters);
|
||||||
|
});
|
||||||
|
|
||||||
|
connections.forEach(connection => {
|
||||||
|
const siblingConnections = childrenMap.get(connection.parentNodeId) ?? [];
|
||||||
|
siblingConnections.push(connection);
|
||||||
|
siblingConnections.sort((left, right) => left.orderIndex - right.orderIndex);
|
||||||
|
childrenMap.set(connection.parentNodeId, siblingConnections);
|
||||||
|
});
|
||||||
|
|
||||||
|
const rootNodeId = findRootNodeId(instances, connections);
|
||||||
|
if (!rootNodeId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const leafCountCache = new Map<number, number>();
|
||||||
|
const countLeaves = (nodeId: number): number => {
|
||||||
|
const cachedCount = leafCountCache.get(nodeId);
|
||||||
|
if (cachedCount) {
|
||||||
|
return cachedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = childrenMap.get(nodeId) ?? [];
|
||||||
|
if (children.length === 0) {
|
||||||
|
leafCountCache.set(nodeId, 1);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalLeaves = children.reduce((total, connection) => {
|
||||||
|
return total + countLeaves(connection.childNodeId);
|
||||||
|
}, 0);
|
||||||
|
leafCountCache.set(nodeId, totalLeaves);
|
||||||
|
return totalLeaves;
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodes: GraphTaskElement[] = [];
|
||||||
|
const edges: GraphEdgeElement[] = [];
|
||||||
|
|
||||||
|
const buildNodeParameters = (instance: ReverseTreeNodeInstanceRow): ElementParameter[] => {
|
||||||
|
const parameterDefinitions = templateParameterDefsMap.get(instance.templateId) ?? [];
|
||||||
|
if (parameterDefinitions.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterRows = nodeParametersMap.get(instance.id) ?? [];
|
||||||
|
const groupIndexes = parameterRows.length > 0
|
||||||
|
? Array.from(new Set(parameterRows.map(item => item.groupIndex))).sort((left, right) => left - right)
|
||||||
|
: [0];
|
||||||
|
|
||||||
|
return groupIndexes.flatMap(groupIndex => {
|
||||||
|
return parameterDefinitions.map(definition => {
|
||||||
|
const parameterRow = parameterRows.find(item => item.paramDefId === definition.id && item.groupIndex === groupIndex);
|
||||||
|
return {
|
||||||
|
id: definition.id,
|
||||||
|
templateId: definition.templateId,
|
||||||
|
paramKey: definition.paramKey,
|
||||||
|
dataType: definition.dataType,
|
||||||
|
defaultValue: parameterRow?.value ?? definition.defaultValue,
|
||||||
|
description: definition.description,
|
||||||
|
templateType: definition.templateType,
|
||||||
|
groupIndex,
|
||||||
|
} as ElementParameter;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const walk = (nodeId: number, startX: number, y: number): number => {
|
||||||
|
const instance = instanceMap.get(nodeId);
|
||||||
|
if (!instance) {
|
||||||
|
return startX;
|
||||||
|
}
|
||||||
|
|
||||||
|
const template = templateMap.get(instance.templateId);
|
||||||
|
const templateType = template?.type ?? 'action';
|
||||||
|
const group = resolveGroup(templateType);
|
||||||
|
const parameters = buildNodeParameters(instance);
|
||||||
|
const leafCount = countLeaves(nodeId);
|
||||||
|
const totalWidth = leafCount * SIBLING_X_SPACING;
|
||||||
|
const centerX = startX + totalWidth / 2 - SIBLING_X_SPACING / 2;
|
||||||
|
const currentNode: GraphTaskElement = {
|
||||||
|
id: instance.id,
|
||||||
|
key: createNodeKey(treeId, instance.id),
|
||||||
|
type: 'task',
|
||||||
|
template: instance.templateId,
|
||||||
|
templateType: resolveTemplateType(templateType),
|
||||||
|
name: instance.instanceName || template?.name || `节点 ${instance.id}`,
|
||||||
|
category: group,
|
||||||
|
group,
|
||||||
|
description: instance.instanceName || template?.name || `节点 ${instance.id}`,
|
||||||
|
multiable: template?.multiable === true,
|
||||||
|
order: 0,
|
||||||
|
position: {
|
||||||
|
x: Math.round(centerX),
|
||||||
|
y: Math.round(y),
|
||||||
|
},
|
||||||
|
width: NODE_WIDTH,
|
||||||
|
height: resolveNodeHeight(group),
|
||||||
|
inputs: null,
|
||||||
|
outputs: null,
|
||||||
|
parameters,
|
||||||
|
variables: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes.push(currentNode);
|
||||||
|
|
||||||
|
const children = childrenMap.get(nodeId) ?? [];
|
||||||
|
let childStartX = startX;
|
||||||
|
children.forEach(connection => {
|
||||||
|
const childLeafCount = countLeaves(connection.childNodeId);
|
||||||
|
walk(connection.childNodeId, childStartX, y + LEVEL_Y_SPACING + currentNode.height);
|
||||||
|
|
||||||
|
const childInstance = instanceMap.get(connection.childNodeId);
|
||||||
|
const edge: GraphEdgeElement = {
|
||||||
|
id: connection.id,
|
||||||
|
key: createEdgeKey(treeId, connection.id),
|
||||||
|
...createBehaviorTreeEdgeEndpoints(
|
||||||
|
currentNode.key as string,
|
||||||
|
createNodeKey(treeId, connection.childNodeId),
|
||||||
|
),
|
||||||
|
attrs: {},
|
||||||
|
router: {},
|
||||||
|
connector: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
edges.push(edge);
|
||||||
|
|
||||||
|
const currentEdges = ((currentNode as GraphTaskElement & { edges?: GraphEdgeElement[] }).edges ?? []).slice();
|
||||||
|
currentEdges.push({
|
||||||
|
...edge,
|
||||||
|
sourceName: currentNode.name,
|
||||||
|
targetName: childInstance?.instanceName ?? childInstance?.id ?? connection.childNodeId,
|
||||||
|
});
|
||||||
|
(currentNode as GraphTaskElement & { edges?: GraphEdgeElement[] }).edges = currentEdges;
|
||||||
|
|
||||||
|
childStartX += childLeafCount * SIBLING_X_SPACING;
|
||||||
|
});
|
||||||
|
|
||||||
|
return centerX;
|
||||||
|
};
|
||||||
|
|
||||||
|
walk(rootNodeId, ROOT_X, ROOT_Y);
|
||||||
|
|
||||||
|
return {
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import type { GraphContainer } from '../../graph';
|
||||||
|
import { loadReverseTreeGraph } from './reverse-tree-loader';
|
||||||
|
|
||||||
|
const EMPTY_GRAPH: GraphContainer = {
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBehaviorTreeGraph = (xmlContent: string | null | undefined): GraphContainer | null => {
|
||||||
|
if (!xmlContent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(xmlContent) as GraphContainer;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('parse behavior tree graph error:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hasBehaviorTreeNodes = (graph: GraphContainer | null | undefined): boolean => {
|
||||||
|
return Boolean(graph?.nodes && graph.nodes.length > 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadBehaviorTreeGraphById = async (treeId: number): Promise<GraphContainer> => {
|
||||||
|
return (await loadReverseTreeGraph(treeId)) ?? { ...EMPTY_GRAPH };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveBehaviorTreeGraph = async (
|
||||||
|
treeId: number,
|
||||||
|
xmlContent: string | null | undefined,
|
||||||
|
): Promise<GraphContainer> => {
|
||||||
|
// void xmlContent;
|
||||||
|
|
||||||
|
// 如需恢复动态选择逻辑,可切回下面这段:
|
||||||
|
const savedGraph = parseBehaviorTreeGraph(xmlContent);
|
||||||
|
if (hasBehaviorTreeNodes(savedGraph)) {
|
||||||
|
return savedGraph as GraphContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return loadBehaviorTreeGraphById(treeId);
|
||||||
|
};
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a-list :data-source="scenarios || []" size="small" style="min-height: 25vh">
|
<a-list :data-source="scenarios || []" size="small" style="min-height: 25vh">
|
||||||
<template #renderItem="{ item }">
|
<template #renderItem="{ item }">
|
||||||
<a-list-item :class="{ 'ks-item-selected': selectedId === item.id }" @click="()=> handleSelect(item)">
|
<a-list-item :class="{ 'ks-item-selected': scenarioId === item.id }" @click="()=> handleSelect(item)">
|
||||||
<a-flex>
|
<a-flex>
|
||||||
<a-tooltip placement="bottom">
|
<a-tooltip placement="bottom">
|
||||||
<template #title>
|
<template #title>
|
||||||
@@ -38,13 +38,16 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, ref } from 'vue';
|
import { defineComponent, onMounted, ref, watch, type PropType } from 'vue';
|
||||||
import { CopyOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
import { CopyOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
||||||
import type { Scenario, ScenarioRequest } from '../communication/types';
|
import type { Scenario, ScenarioRequest } from '../communication/types';
|
||||||
import { findScenarioByQuery } from '../communication/api';
|
import { findScenarioByQuery } from '../communication/api';
|
||||||
import { substring } from '@/utils/strings';
|
import { substring } from '@/utils/strings';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
props: {
|
||||||
|
scenarioId: Number as PropType<number>,
|
||||||
|
},
|
||||||
emits: ['select-scenario'],
|
emits: ['select-scenario'],
|
||||||
components: {
|
components: {
|
||||||
CopyOutlined,
|
CopyOutlined,
|
||||||
@@ -60,7 +63,6 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
const activeKey = ref<number>(0);
|
const activeKey = ref<number>(0);
|
||||||
const totalScenarios = ref<number>(0);
|
const totalScenarios = ref<number>(0);
|
||||||
const selectedId = ref<number | null>(null);
|
|
||||||
|
|
||||||
const loadScenarios = (cb?: () => void) => {
|
const loadScenarios = (cb?: () => void) => {
|
||||||
findScenarioByQuery(scenarioQuery.value).then(r => {
|
findScenarioByQuery(scenarioQuery.value).then(r => {
|
||||||
@@ -77,7 +79,6 @@ export default defineComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (record: Scenario) => {
|
const handleSelect = (record: Scenario) => {
|
||||||
selectedId.value = record.id;
|
|
||||||
emit('select-scenario', record);
|
emit('select-scenario', record);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -85,9 +86,13 @@ export default defineComponent({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadScenarios(() => {
|
loadScenarios(() => {
|
||||||
if(scenarios.value.length > 0){
|
if (_props.scenarioId) {
|
||||||
selectedId.value = scenarios.value[0]!.id;
|
return;
|
||||||
emit('select-scenario', scenarios.value[0]);
|
}
|
||||||
|
// 默认选中第一个场景
|
||||||
|
const selectedScenario = scenarios.value[0];
|
||||||
|
if (selectedScenario) {
|
||||||
|
emit('select-scenario', selectedScenario);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -97,7 +102,6 @@ export default defineComponent({
|
|||||||
totalScenarios,
|
totalScenarios,
|
||||||
substring,
|
substring,
|
||||||
activeKey,
|
activeKey,
|
||||||
selectedId,
|
|
||||||
scenarios,
|
scenarios,
|
||||||
scenarioQuery,
|
scenarioQuery,
|
||||||
loadScenarios,
|
loadScenarios,
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { findNodeTemplates, findTemplateParameterDefs } from './api';
|
||||||
|
import type { TreeTemplateParameterDefRow } from './api';
|
||||||
|
import type { NodeTemplatesResponse } from './template';
|
||||||
|
|
||||||
|
// 模板列表:全局只请求一次
|
||||||
|
let nodeTemplatesPromise: Promise<NodeTemplatesResponse> | null = null;
|
||||||
|
|
||||||
|
// 模板参数定义:全局只请求一次
|
||||||
|
let templateParameterDefsPromise: Promise<TreeTemplateParameterDefRow[]> | null = null;
|
||||||
|
|
||||||
|
export const loadNodeTemplatesOnce = (): Promise<NodeTemplatesResponse> => {
|
||||||
|
if (!nodeTemplatesPromise) {
|
||||||
|
nodeTemplatesPromise = findNodeTemplates().catch(err => {
|
||||||
|
// 请求失败时清除缓存,下次可以重试
|
||||||
|
nodeTemplatesPromise = null;
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodeTemplatesPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTemplateParameterDefsOnce = (): Promise<TreeTemplateParameterDefRow[]> => {
|
||||||
|
if (!templateParameterDefsPromise) {
|
||||||
|
templateParameterDefsPromise = findTemplateParameterDefs()
|
||||||
|
.then(r => r.rows ?? [])
|
||||||
|
.catch(err => {
|
||||||
|
templateParameterDefsPromise = null;
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return templateParameterDefsPromise;
|
||||||
|
};
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<a-list :data-source="behaviorTrees || []" size="small" style="min-height: 25vh">
|
<a-list :data-source="behaviorTrees || []" size="small" style="min-height: 25vh">
|
||||||
<template #renderItem="{ item }">
|
<template #renderItem="{ item }">
|
||||||
<a-list-item :class="{ 'ks-item-selected': selectedId === item.id }" @click="()=> handleSelect(item)">
|
<a-list-item :class="{ 'ks-item-selected': platformId === item.id || selectedId === item.id }" @click="()=> handleSelect(item)">
|
||||||
<a-flex>
|
<a-flex>
|
||||||
<a-tooltip placement="bottom">
|
<a-tooltip placement="bottom">
|
||||||
<template #title>
|
<template #title>
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { defineComponent, ref, watch } from 'vue';
|
import { defineComponent, onMounted, ref, watch } from 'vue';
|
||||||
import { CheckOutlined, CopyOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue';
|
import { CheckOutlined, CopyOutlined, DeleteOutlined, EditFilled, PlusOutlined } from '@ant-design/icons-vue';
|
||||||
import type { BehaviorTree, BehaviorTreeRequest } from './tree';
|
import type { BehaviorTree, BehaviorTreeRequest } from './tree';
|
||||||
import { copyTree, deleteOneTreeById, findTreesByQuery } from './api';
|
import { copyTree, deleteOneTreeById, findTreesByQuery } from './api';
|
||||||
@@ -66,7 +66,8 @@ import { substring } from '@/utils/strings';
|
|||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
scenarioId:Number as PropType<number>,
|
scenarioId: Number as PropType<number | undefined>,
|
||||||
|
platformId: Number as PropType<number | null>,
|
||||||
},
|
},
|
||||||
emits: ['select-tree', 'create-tree'],
|
emits: ['select-tree', 'create-tree'],
|
||||||
components: {
|
components: {
|
||||||
@@ -76,38 +77,48 @@ export default defineComponent({
|
|||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
EditFilled,
|
EditFilled,
|
||||||
},
|
},
|
||||||
setup(_props, { emit }) {
|
setup(props, { emit }) {
|
||||||
const behaviorTrees = ref<BehaviorTree[]>([]);
|
const behaviorTrees = ref<BehaviorTree[]>([]);
|
||||||
const behaviorTreeQuery = ref<Partial<BehaviorTreeRequest>>({
|
const behaviorTreeQuery = ref<Partial<BehaviorTreeRequest>>({
|
||||||
name: null,
|
name: null,
|
||||||
pageNum: 1,
|
pageNum: 1,
|
||||||
pageSize: 8,
|
pageSize: 8,
|
||||||
scenarioId: _props.scenarioId,
|
scenarioId: props.scenarioId,
|
||||||
});
|
});
|
||||||
const activeKey = ref<number>(1);
|
const activeKey = ref<number>(1);
|
||||||
const totalTress = ref<number>(0);
|
const totalTress = ref<number>(0);
|
||||||
const selectedId = ref<number | null>(null);
|
const selectedId = ref<number | null>(null);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => _props.scenarioId,
|
() => props.scenarioId,
|
||||||
() => {
|
() => {
|
||||||
selectedId.value = null;
|
if (!props.scenarioId) {
|
||||||
if (!_props.scenarioId) {
|
|
||||||
behaviorTrees.value = [];
|
behaviorTrees.value = [];
|
||||||
totalTress.value = 0;
|
totalTress.value = 0;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
behaviorTreeQuery.value.pageNum = 1;
|
behaviorTreeQuery.value.pageNum = 1;
|
||||||
behaviorTreeQuery.value.scenarioId = _props.scenarioId;
|
behaviorTreeQuery.value.scenarioId = props.scenarioId;
|
||||||
loadTress();
|
loadTress(() => {
|
||||||
|
if (props.platformId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 默认选中第一个行为树
|
||||||
|
const selectedTree = behaviorTrees.value[0];
|
||||||
|
if (selectedTree) {
|
||||||
|
selectedId.value = selectedTree.id;
|
||||||
|
emit('select-tree', selectedTree);
|
||||||
|
}
|
||||||
|
});
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
function loadTress(){
|
function loadTress(cb?: () => void) {
|
||||||
findTreesByQuery(behaviorTreeQuery.value).then(r => {
|
findTreesByQuery(behaviorTreeQuery.value).then(r => {
|
||||||
behaviorTrees.value = r.rows;
|
behaviorTrees.value = r.rows;
|
||||||
totalTress.value = r.total ?? 0;
|
totalTress.value = r.total ?? 0;
|
||||||
|
if (cb) cb();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -100,8 +100,8 @@ export interface ModelElement extends GraphBaseElement {
|
|||||||
export interface GraphEdgeElement {
|
export interface GraphEdgeElement {
|
||||||
id: number;
|
id: number;
|
||||||
key: NullableString;
|
key: NullableString;
|
||||||
source: NullableString;
|
source: NullableString | Record<string, any>;
|
||||||
target: NullableString;
|
target: NullableString | Record<string, any>;
|
||||||
attrs: Record<any, any>;
|
attrs: Record<any, any>;
|
||||||
router: Record<any, any>;
|
router: Record<any, any>;
|
||||||
connector: any;
|
connector: any;
|
||||||
|
|||||||
@@ -58,14 +58,12 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
|||||||
if (graph.value) {
|
if (graph.value) {
|
||||||
clearGraph();
|
clearGraph();
|
||||||
|
|
||||||
// 等待 Vue 完成卸载
|
// 同步销毁 Graph 实例,避免异步导致的竞态条件
|
||||||
nextTick(() => {
|
graph.value.dispose();
|
||||||
graph.value?.dispose(); // 销毁 Graph 实例
|
|
||||||
graph.value = null;
|
graph.value = null;
|
||||||
if (container.value) {
|
if (container.value) {
|
||||||
container.value.innerHTML = ''; // 清空容器内容
|
container.value.innerHTML = ''; // 清空容器内容
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else if (container.value) {
|
} else if (container.value) {
|
||||||
container.value.innerHTML = '';
|
container.value.innerHTML = '';
|
||||||
}
|
}
|
||||||
@@ -222,7 +220,8 @@ export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
|||||||
|
|
||||||
// 监听节点选中事件
|
// 监听节点选中事件
|
||||||
graph.value.on('node:selected', ({ node }) => {
|
graph.value.on('node:selected', ({ node }) => {
|
||||||
console.info('node select', node);
|
console.info('node select', node,node.getData());
|
||||||
|
|
||||||
emitGraphEvent('node:selected', node);
|
emitGraphEvent('node:selected', node);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user