Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -8,9 +8,10 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { HttpRequestClient } from '@/utils/request';
|
import { HttpRequestClient } from '@/utils/request';
|
||||||
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest } from './types';
|
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest, CommunicationRelationsResponse } from './types';
|
||||||
import type { PlatformWithComponentsResponse } from '../types';
|
import type { PlatformWithComponentsResponse } from '../types';
|
||||||
import type { BasicResponse } from '@/types';
|
import type { BasicResponse } from '@/types';
|
||||||
|
import type { BehaviorTree } from '../designer/tree';
|
||||||
|
|
||||||
const req = HttpRequestClient.create<BasicResponse>({
|
const req = HttpRequestClient.create<BasicResponse>({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
@@ -32,6 +33,25 @@ export const findPlatformWithComponents = (id: number): Promise<PlatformWithComp
|
|||||||
return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`);
|
return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取场景的所有通信关系
|
||||||
|
* @param id 场景ID
|
||||||
|
* @returns 通信关系列表
|
||||||
|
*/
|
||||||
|
export const findRelations = (id: number): Promise<CommunicationRelationsResponse> => {
|
||||||
|
return req.get<CommunicationRelationsResponse>(`/system/scene/getAllRelation/${id}`);
|
||||||
|
};
|
||||||
|
|
||||||
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
|
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
|
||||||
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
|
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);
|
||||||
|
};
|
||||||
@@ -21,6 +21,14 @@
|
|||||||
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
|
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
|
||||||
<div class="ks-model-builder-actions">
|
<div class="ks-model-builder-actions">
|
||||||
<a-space>
|
<a-space>
|
||||||
|
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" style="width: auto;" size="small" @click="handleGenerateRandom">
|
||||||
|
<ThunderboltOutlined />
|
||||||
|
<span>随机生成</span>
|
||||||
|
</a-button>
|
||||||
|
<a-button v-if="graph && currentScenario && currentScenario.id > 0" class="ks-model-builder-save" style="width: auto;" size="small" @click="handleLoadFromBackend">
|
||||||
|
<DatabaseOutlined />
|
||||||
|
<span>从后端加载</span>
|
||||||
|
</a-button>
|
||||||
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" size="small" @click="handleSave">
|
<a-button v-if="graph && currentScenario" class="ks-model-builder-save" size="small" @click="handleSave">
|
||||||
<CheckOutlined />
|
<CheckOutlined />
|
||||||
<span>保存</span>
|
<span>保存</span>
|
||||||
@@ -50,22 +58,24 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { getTeleport } from '@antv/x6-vue-shape';
|
import { getTeleport } from '@antv/x6-vue-shape';
|
||||||
import { Graph, Node, type NodeProperties } from '@antv/x6';
|
import { Graph, Node, type NodeProperties } from '@antv/x6';
|
||||||
import { CheckCircleOutlined, CheckOutlined, RollbackOutlined, SaveOutlined } from '@ant-design/icons-vue';
|
import { CheckCircleOutlined, CheckOutlined, DatabaseOutlined, RollbackOutlined, SaveOutlined, ThunderboltOutlined } from '@ant-design/icons-vue';
|
||||||
import { Wrapper } from '@/components/wrapper';
|
import { Wrapper } from '@/components/wrapper';
|
||||||
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
||||||
import Header from '../header.vue';
|
import Header from '../header.vue';
|
||||||
|
|
||||||
import type { Scenario } from './types';
|
import type { Scenario } from './types';
|
||||||
import type { PlatformWithComponents } from '../types';
|
import type { PlatformWithComponents } from '../types';
|
||||||
import { createLineOptions, type GraphContainer, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
|
import { createLineOptions, type GraphContainer, type GraphEdgeElement, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
|
||||||
|
|
||||||
import { registerScenarioElement } from './register';
|
import { registerScenarioElement } from './register';
|
||||||
import { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
|
import { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
|
||||||
|
|
||||||
import PlatformCard from './platform-card.vue';
|
import PlatformCard from './platform-card.vue';
|
||||||
import NodesCard from './nodes-card.vue';
|
import NodesCard from './nodes-card.vue';
|
||||||
import { findOneScenarioById, saveScenario } from './api';
|
import { findOneScenarioById, saveScenario, findRelations, getAllBehaviorTreesBySceneId } from './api';
|
||||||
import { resolveConnectionRelation } from './relation';
|
import { resolveConnectionRelation } from './relation';
|
||||||
|
import { generateRandomCommunicationData } from './random-data-generator';
|
||||||
|
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
||||||
|
|
||||||
const TeleportContainer = defineComponent(getTeleport());
|
const TeleportContainer = defineComponent(getTeleport());
|
||||||
|
|
||||||
@@ -81,6 +91,8 @@ export default defineComponent({
|
|||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CheckOutlined,
|
CheckOutlined,
|
||||||
RollbackOutlined,
|
RollbackOutlined,
|
||||||
|
ThunderboltOutlined,
|
||||||
|
DatabaseOutlined,
|
||||||
TeleportContainer,
|
TeleportContainer,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
@@ -211,25 +223,95 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelect = (scenario: Scenario) => {
|
const handleSelect = async (scenario: Scenario) => {
|
||||||
let nodeGraph: GraphContainer | null = null;
|
let nodeGraph: GraphContainer | null = null;
|
||||||
try {
|
try {
|
||||||
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
|
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('parse error,cause:', e);
|
console.error('parse error,cause:', e);
|
||||||
}
|
}
|
||||||
if (!nodeGraph) {
|
|
||||||
nodeGraph = {
|
// 设置当前场景
|
||||||
nodes: [],
|
|
||||||
edges: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
currentScenario.value = {
|
currentScenario.value = {
|
||||||
...scenario,
|
...scenario,
|
||||||
graph: nodeGraph,
|
graph: nodeGraph || { nodes: [], edges: [] },
|
||||||
relations: []
|
relations: []
|
||||||
};
|
};
|
||||||
|
console.log('选中场景:', currentScenario.value);
|
||||||
|
|
||||||
currentScenarioEditing.value = true;
|
currentScenarioEditing.value = true;
|
||||||
|
|
||||||
|
// 并行加载通信关系和行为树列表
|
||||||
|
if (scenario.id > 0) {
|
||||||
|
try {
|
||||||
|
// 1. 加载通信关系(如果没有已保存的图数据)
|
||||||
|
if (!nodeGraph) {
|
||||||
|
message.loading({ content: '正在加载通信关系...', key: 'loading-relations' });
|
||||||
|
const response = await findRelations(scenario.id);
|
||||||
|
|
||||||
|
console.log('API完整响应:', response);
|
||||||
|
|
||||||
|
// 解析API响应(支持多种格式)
|
||||||
|
let relations: any[] = [];
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
relations = response.data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).data)) {
|
||||||
|
relations = (response.data as any).data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).rows)) {
|
||||||
|
relations = (response.data as any).rows;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).list)) {
|
||||||
|
relations = (response.data as any).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析后的通信关系数量:', relations.length);
|
||||||
|
|
||||||
|
if (relations.length > 0) {
|
||||||
|
// 字段名标准化(驼峰转下划线)
|
||||||
|
const normalizedRelations = relations.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
command_platform: item.commandPlatform || item.command_platform,
|
||||||
|
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
|
||||||
|
command_comm: item.commandComm || item.command_comm,
|
||||||
|
subordinate_comm: item.subordinateComm || item.subordinate_comm,
|
||||||
|
scenary_id: item.scenaryId || item.scenary_id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('标准化后的第一条记录:', normalizedRelations[0]);
|
||||||
|
|
||||||
|
// 转换为图数据
|
||||||
|
const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
|
||||||
|
console.log('转换后的图数据:', convertedGraph);
|
||||||
|
|
||||||
|
// 更新当前场景的图数据
|
||||||
|
currentScenario.value.graph = convertedGraph;
|
||||||
|
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
|
||||||
|
|
||||||
|
message.success({ content: `成功加载 ${normalizedRelations.length} 条通信关系`, key: 'loading-relations' });
|
||||||
|
} else {
|
||||||
|
message.warning({ content: '该场景暂无通信关系数据', key: 'loading-relations' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 加载行为树列表并缓存到graph对象
|
||||||
|
const treesResponse = await getAllBehaviorTreesBySceneId(scenario.id);
|
||||||
|
if (treesResponse.code === 200 && treesResponse.data) {
|
||||||
|
console.log('[communication] 行为树列表加载完成:', treesResponse.data.length, '个');
|
||||||
|
// 将行为树列表存储到graph对象中供node.vue使用
|
||||||
|
if (graph.value) {
|
||||||
|
(graph.value as any).behaviorTrees = treesResponse.data;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('[communication] 行为树列表加载失败或为空');
|
||||||
|
if (graph.value) {
|
||||||
|
(graph.value as any).behaviorTrees = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从后端加载数据失败:', error);
|
||||||
|
message.error({ content: '加载数据失败', key: 'loading-relations' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
createElements();
|
createElements();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -261,6 +343,7 @@ export default defineComponent({
|
|||||||
}, 100); // 延迟一会儿,免得连线错位
|
}, 100); // 延迟一会儿,免得连线错位
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -277,6 +360,12 @@ export default defineComponent({
|
|||||||
nodes: [],
|
nodes: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 清空graph中的场景信息
|
||||||
|
if (graph.value) {
|
||||||
|
(graph.value as any).currentScenario = null;
|
||||||
|
}
|
||||||
|
|
||||||
currentGraph.value = {
|
currentGraph.value = {
|
||||||
edges: [],
|
edges: [],
|
||||||
nodes: [],
|
nodes: [],
|
||||||
@@ -415,6 +504,193 @@ export default defineComponent({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 随机生成节点流图
|
||||||
|
const handleGenerateRandom = () => {
|
||||||
|
if (!graph.value) {
|
||||||
|
message.error('画布未初始化');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 生成随机数据
|
||||||
|
const { records, graph: randomGraph } = generateRandomCommunicationData(30);
|
||||||
|
|
||||||
|
console.log('生成的随机数据:', records);
|
||||||
|
console.log('转换后的图数据:', randomGraph);
|
||||||
|
|
||||||
|
// 清空现有内容
|
||||||
|
graph.value.clearCells();
|
||||||
|
|
||||||
|
// 设置当前场景
|
||||||
|
if (!currentScenario.value) {
|
||||||
|
currentScenario.value = {
|
||||||
|
id: 0,
|
||||||
|
name: `随机场景_${Date.now()}`,
|
||||||
|
description: '自动生成的测试场景',
|
||||||
|
communicationGraph: null,
|
||||||
|
relations: [],
|
||||||
|
graph: randomGraph,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
currentScenario.value.graph = randomGraph;
|
||||||
|
currentScenario.value.communicationGraph = JSON.stringify(randomGraph);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 渲染节点
|
||||||
|
setTimeout(() => {
|
||||||
|
if (randomGraph.nodes) {
|
||||||
|
randomGraph.nodes.forEach(ele => {
|
||||||
|
const node = createGraphScenarioElement(ele as GraphTaskElement);
|
||||||
|
graph.value?.addNode(node as Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟添加边,确保节点已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
if (randomGraph.edges) {
|
||||||
|
randomGraph.edges.forEach(edgeData => {
|
||||||
|
graph.value?.addEdge({
|
||||||
|
...edgeData,
|
||||||
|
...createLineOptions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动适应视图
|
||||||
|
fitToScreen();
|
||||||
|
|
||||||
|
message.success(`已生成 ${randomGraph.nodes.length} 个节点和 ${randomGraph.edges.length} 条连接线`);
|
||||||
|
}, 100);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('随机生成时出错:', error);
|
||||||
|
message.error('生成失败,请重试');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 从后端加载平台数据并转换为通信关系图(当前使用模拟数据)
|
||||||
|
const handleLoadFromBackend = async () => {
|
||||||
|
if (!graph.value || !currentScenario.value) {
|
||||||
|
message.error('请先选择场景');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
|
||||||
|
|
||||||
|
// 调用真实API获取通信关系
|
||||||
|
console.log(`正在从后端加载场景 ${currentScenario.value.id} 的通信关系...`);
|
||||||
|
const response = await findRelations(currentScenario.value.id);
|
||||||
|
|
||||||
|
console.log('API完整响应:', response);
|
||||||
|
console.log('response.data类型:', typeof response.data, Array.isArray(response.data) ? 'Array' : 'Object');
|
||||||
|
|
||||||
|
// API返回的是 CommunicationRelationRecord[],与 CommunicationRecord 结构兼容
|
||||||
|
// 处理可能的多种返回格式
|
||||||
|
let relations: any[] = [];
|
||||||
|
if (Array.isArray(response.data)) {
|
||||||
|
relations = response.data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).data)) {
|
||||||
|
relations = (response.data as any).data;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).rows)) {
|
||||||
|
relations = (response.data as any).rows;
|
||||||
|
} else if (response.data && Array.isArray((response.data as any).list)) {
|
||||||
|
relations = (response.data as any).list;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('解析后的通信关系数量:', relations.length);
|
||||||
|
if (relations.length > 0) {
|
||||||
|
console.log('第一条记录:', JSON.stringify(relations[0], null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 后端返回的是驼峰命名,需要转换为下划线命名以匹配前端类型
|
||||||
|
const normalizedRelations = relations.map((item: any) => ({
|
||||||
|
id: item.id,
|
||||||
|
command_platform: item.commandPlatform || item.command_platform,
|
||||||
|
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
|
||||||
|
command_comm: item.commandComm || item.command_comm,
|
||||||
|
subordinate_comm: item.subordinateComm || item.subordinate_comm,
|
||||||
|
scenary_id: item.scenaryId || item.scenary_id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
console.log('标准化后的第一条记录:', normalizedRelations[0]);
|
||||||
|
|
||||||
|
if (normalizedRelations.length === 0) {
|
||||||
|
console.warn('API未返回任何通信关系数据,使用模拟数据作为fallback');
|
||||||
|
// Fallback到模拟数据(保留以便测试)
|
||||||
|
relations.push(
|
||||||
|
{ id: 6, command_platform: 'chief', subordinate_platform: 'task1_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 7, command_platform: 'chief', subordinate_platform: 'task2_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 8, command_platform: 'chief', subordinate_platform: 'task3_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 9, command_platform: 'task1_commander', subordinate_platform: 'platform1', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 10, command_platform: 'task1_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 11, command_platform: 'task1_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 12, command_platform: 'task1_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 13, command_platform: 'task1_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 14, command_platform: 'task2_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 15, command_platform: 'task2_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 16, command_platform: 'task2_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 17, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 18, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 19, command_platform: 'task3_commander', subordinate_platform: 'platform7', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
{ id: 20, command_platform: 'task3_commander', subordinate_platform: 'platform8', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('最终使用的通信记录:', normalizedRelations);
|
||||||
|
|
||||||
|
// 使用数据进行转换
|
||||||
|
const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
|
||||||
|
|
||||||
|
console.log('转换后的图数据:', convertedGraph);
|
||||||
|
|
||||||
|
// 清空现有内容
|
||||||
|
graph.value.clearCells();
|
||||||
|
|
||||||
|
// 更新当前场景
|
||||||
|
currentScenario.value.graph = convertedGraph;
|
||||||
|
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
|
||||||
|
|
||||||
|
// 渲染节点
|
||||||
|
setTimeout(() => {
|
||||||
|
if (convertedGraph.nodes) {
|
||||||
|
convertedGraph.nodes.forEach(ele => {
|
||||||
|
const node = createGraphScenarioElement(ele as GraphTaskElement);
|
||||||
|
graph.value?.addNode(node as Node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 延迟添加边,确保节点已渲染
|
||||||
|
setTimeout(() => {
|
||||||
|
if (convertedGraph.edges) {
|
||||||
|
convertedGraph.edges.forEach(edgeData => {
|
||||||
|
graph.value?.addEdge({
|
||||||
|
...edgeData,
|
||||||
|
...createLineOptions(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动适应视图
|
||||||
|
fitToScreen();
|
||||||
|
|
||||||
|
message.success({
|
||||||
|
content: `已从后端加载 ${convertedGraph.nodes.length} 个平台和 ${convertedGraph.edges.length} 条连接关系`,
|
||||||
|
key: 'loading'
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('从后端加载时出错:', error);
|
||||||
|
message.error({
|
||||||
|
content: error instanceof Error ? error.message : '加载失败,请重试',
|
||||||
|
key: 'loading'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
@@ -444,6 +720,8 @@ export default defineComponent({
|
|||||||
handleDrop,
|
handleDrop,
|
||||||
isDraggingOver,
|
isDraggingOver,
|
||||||
handleSave,
|
handleSave,
|
||||||
|
handleGenerateRandom,
|
||||||
|
handleLoadFromBackend,
|
||||||
handleUpdateElement,
|
handleUpdateElement,
|
||||||
handleSelect,
|
handleSelect,
|
||||||
};
|
};
|
||||||
|
|||||||
1049
modeler/src/views/decision/communication/data-converter.ts
Normal file
1049
modeler/src/views/decision/communication/data-converter.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
|
<a-dropdown
|
||||||
|
:trigger="['contextmenu']"
|
||||||
|
:getPopupContainer="getPopupContainer"
|
||||||
|
@openChange="handleVisibleChange"
|
||||||
|
>
|
||||||
<a-card
|
<a-card
|
||||||
:class="[
|
:class="[
|
||||||
'ks-scenario-node',
|
'ks-scenario-node',
|
||||||
@@ -55,6 +59,27 @@
|
|||||||
|
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="handleMenuClick">
|
<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">
|
<a-menu-item key="delete">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
@@ -70,15 +95,20 @@
|
|||||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { elementProps, type ModelElement } from '../graph';
|
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 type { Graph } from '@antv/x6';
|
||||||
|
import { message } from 'ant-design-vue';
|
||||||
import { substring } from '@/utils/strings';
|
import { substring } from '@/utils/strings';
|
||||||
|
import { getAllBehaviorTreesBySceneId, updateBehaviorTree } from './api';
|
||||||
|
import type { BehaviorTree } from '../designer/tree';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'ModelElement',
|
name: 'ModelElement',
|
||||||
components: {
|
components: {
|
||||||
SettingOutlined,
|
SettingOutlined,
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
|
LinkOutlined,
|
||||||
|
CheckOutlined,
|
||||||
},
|
},
|
||||||
props: elementProps,
|
props: elementProps,
|
||||||
setup(_props) {
|
setup(_props) {
|
||||||
@@ -88,6 +118,17 @@ export default defineComponent({
|
|||||||
const updateKey = ref(0);
|
const updateKey = ref(0);
|
||||||
const isMenuVisible = ref(false);
|
const isMenuVisible = ref(false);
|
||||||
|
|
||||||
|
// 挂载行为树相关状态
|
||||||
|
const availableTrees = ref<BehaviorTree[]>([]);
|
||||||
|
|
||||||
|
// 获取 popup 容器
|
||||||
|
const getPopupContainer = () => {
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
return document.body;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
// 获取画布实例
|
// 获取画布实例
|
||||||
const getGraph = (): Graph | null => {
|
const getGraph = (): Graph | null => {
|
||||||
return _props.graph as Graph || null;
|
return _props.graph as Graph || null;
|
||||||
@@ -103,8 +144,35 @@ export default defineComponent({
|
|||||||
updateKey.value++;
|
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) => {
|
const handleVisibleChange = (visible: boolean) => {
|
||||||
isMenuVisible.value = visible;
|
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 }) => {
|
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 = () => {
|
const handleDelete = () => {
|
||||||
if (!_props.node) return;
|
if (!_props.node) return;
|
||||||
|
|
||||||
@@ -134,10 +233,37 @@ export default defineComponent({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
_props.node?.on('change:data', handleDataChange);
|
_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(() => {
|
onUnmounted(() => {
|
||||||
_props.node?.off('change:data', handleDataChange);
|
_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 {
|
return {
|
||||||
@@ -145,6 +271,11 @@ export default defineComponent({
|
|||||||
substring,
|
substring,
|
||||||
handleMenuClick,
|
handleMenuClick,
|
||||||
handleVisibleChange,
|
handleVisibleChange,
|
||||||
|
availableTrees,
|
||||||
|
getBehaviorTreeName,
|
||||||
|
isTreeMounted,
|
||||||
|
handleMountTree,
|
||||||
|
getPopupContainer,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
/*
|
||||||
|
* 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 { GraphContainer } from '../graph';
|
||||||
|
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机的通信关系数据用于测试
|
||||||
|
* @param nodeCount 节点数量(默认5-8个)
|
||||||
|
* @param edgeDensity 边的密度(0.3-0.7之间,表示连接概率)
|
||||||
|
* @returns 随机生成的通信关系记录和对应的GraphContainer
|
||||||
|
*/
|
||||||
|
export const generateRandomCommunicationData = (
|
||||||
|
nodeCount: number = Math.floor(Math.random() * 4) + 5, // 5-8个节点
|
||||||
|
edgeDensity: number = 0.5
|
||||||
|
): { records: CommunicationRecord[]; graph: GraphContainer } => {
|
||||||
|
// 生成随机平台名称
|
||||||
|
const platformTypes = ['指挥中心', '雷达站', '导弹阵地', '预警机', '战斗机', '驱逐舰', '潜艇', '电子战飞机'];
|
||||||
|
const platforms: string[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < nodeCount; i++) {
|
||||||
|
const baseName = platformTypes[i % platformTypes.length];
|
||||||
|
const suffix = Math.floor(i / platformTypes.length) > 0 ? `-${Math.floor(i / platformTypes.length) + 1}` : '';
|
||||||
|
platforms.push(`${baseName}${suffix}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成随机通信关系 - 改进版:更符合实际指挥层级
|
||||||
|
const records: CommunicationRecord[] = [];
|
||||||
|
let recordId = 1;
|
||||||
|
|
||||||
|
// 确定根节点(指挥中心)
|
||||||
|
const rootPlatformName = platforms.find(p => p.includes('指挥')) || platforms[0] || '默认平台';
|
||||||
|
const rootIndex = platforms.indexOf(rootPlatformName);
|
||||||
|
|
||||||
|
if (rootIndex === -1) {
|
||||||
|
console.warn('未找到根节点,使用第一个平台');
|
||||||
|
return { records: [], graph: { nodes: [], edges: [] } };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一层:指挥中心直接管理的单位(通常是主要作战单元)
|
||||||
|
// 选择2-4个作为一级下属
|
||||||
|
const firstLevelCount = Math.min(Math.max(2, Math.floor(platforms.length / 2)), 4);
|
||||||
|
const firstLevelIndices: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < platforms.length && firstLevelIndices.length < firstLevelCount; i++) {
|
||||||
|
if (i !== rootIndex) {
|
||||||
|
firstLevelIndices.push(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 建立第一层连接:指挥中心 -> 一级下属
|
||||||
|
firstLevelIndices.forEach(idx => {
|
||||||
|
const subordinatePlatform = platforms[idx];
|
||||||
|
if (subordinatePlatform) {
|
||||||
|
records.push({
|
||||||
|
id: recordId++,
|
||||||
|
command_platform: rootPlatformName,
|
||||||
|
subordinate_platform: subordinatePlatform,
|
||||||
|
command_comm: '加密指挥链路',
|
||||||
|
subordinate_comm: '接收端',
|
||||||
|
scenary_id: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第二层:一级下属可以有二级下属
|
||||||
|
const remainingIndices = platforms.map((_, i) => i).filter(i =>
|
||||||
|
i !== rootIndex && !firstLevelIndices.includes(i)
|
||||||
|
);
|
||||||
|
|
||||||
|
remainingIndices.forEach(idx => {
|
||||||
|
// 随机选择一个一级下属作为父节点
|
||||||
|
const parentIdx = firstLevelIndices[Math.floor(Math.random() * firstLevelIndices.length)];
|
||||||
|
|
||||||
|
if (parentIdx !== undefined) {
|
||||||
|
const parentPlatform = platforms[parentIdx];
|
||||||
|
const childPlatform = platforms[idx];
|
||||||
|
|
||||||
|
if (parentPlatform && childPlatform) {
|
||||||
|
records.push({
|
||||||
|
id: recordId++,
|
||||||
|
command_platform: parentPlatform,
|
||||||
|
subordinate_platform: childPlatform,
|
||||||
|
command_comm: Math.random() > 0.5 ? '战术数据链' : '无线通信',
|
||||||
|
subordinate_comm: '双向通信',
|
||||||
|
scenary_id: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 第三层:添加少量横向协同连接(不超过总边数的20%)
|
||||||
|
const maxCrossLinks = Math.floor(records.length * 0.2);
|
||||||
|
let crossLinkCount = 0;
|
||||||
|
|
||||||
|
if (platforms.length > 4 && crossLinkCount < maxCrossLinks) {
|
||||||
|
// 在同级之间添加协同连接
|
||||||
|
for (let i = 0; i < firstLevelIndices.length - 1 && crossLinkCount < maxCrossLinks; i++) {
|
||||||
|
if (Math.random() < 0.4) { // 40%概率
|
||||||
|
const j = i + 1 + Math.floor(Math.random() * 2);
|
||||||
|
if (j < firstLevelIndices.length) {
|
||||||
|
const idxI = firstLevelIndices[i];
|
||||||
|
const idxJ = firstLevelIndices[j];
|
||||||
|
|
||||||
|
if (idxI !== undefined && idxJ !== undefined) {
|
||||||
|
const platformI = platforms[idxI];
|
||||||
|
const platformJ = platforms[idxJ];
|
||||||
|
|
||||||
|
if (platformI && platformJ) {
|
||||||
|
const exists = records.some(r =>
|
||||||
|
r.command_platform === platformI &&
|
||||||
|
r.subordinate_platform === platformJ
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
records.push({
|
||||||
|
id: recordId++,
|
||||||
|
command_platform: platformI,
|
||||||
|
subordinate_platform: platformJ,
|
||||||
|
command_comm: '协同通信',
|
||||||
|
subordinate_comm: '双向通信',
|
||||||
|
scenary_id: 1,
|
||||||
|
});
|
||||||
|
crossLinkCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为GraphContainer
|
||||||
|
const graph = convertRecordsToGraphContainer(records);
|
||||||
|
|
||||||
|
return { records, graph };
|
||||||
|
};
|
||||||
@@ -48,3 +48,22 @@ export interface ScenarioDetailsResponse extends ApiDataResponse<Scenario> {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通信关系记录(对应数据库表结构)
|
||||||
|
*/
|
||||||
|
export interface CommunicationRelationRecord {
|
||||||
|
id: number;
|
||||||
|
command_platform: string; // 指挥平台名称
|
||||||
|
subordinate_platform: string; // 下属平台名称
|
||||||
|
command_comm?: string; // 指挥端通信方式
|
||||||
|
subordinate_comm?: string; // 下属端通信方式
|
||||||
|
scenary_id?: number; // 场景ID
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取场景所有通信关系的响应
|
||||||
|
*/
|
||||||
|
export interface CommunicationRelationsResponse extends ApiDataResponse<CommunicationRelationRecord[]> {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user