diff --git a/modeler/src/views/decision/communication/api.ts b/modeler/src/views/decision/communication/api.ts index 65a6f1d..881dd56 100644 --- a/modeler/src/views/decision/communication/api.ts +++ b/modeler/src/views/decision/communication/api.ts @@ -8,9 +8,10 @@ */ 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 { BasicResponse } from '@/types'; +import type { BehaviorTree } from '../designer/tree'; const req = HttpRequestClient.create({ baseURL: '/api', @@ -32,6 +33,25 @@ export const findPlatformWithComponents = (id: number): Promise(`/system/firerule/platforms/${id}`); }; +/** + * 获取场景的所有通信关系 + * @param id 场景ID + * @returns 通信关系列表 + */ +export const findRelations = (id: number): Promise => { + return req.get(`/system/scene/getAllRelation/${id}`); +}; + export const saveScenario = (scenario: Scenario): Promise => { return req.postJson(`/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 => { + return req.putJson(`/system/behaviortree`, behaviorTree); }; \ No newline at end of file diff --git a/modeler/src/views/decision/communication/communication.vue b/modeler/src/views/decision/communication/communication.vue index 36f0909..585e6b9 100644 --- a/modeler/src/views/decision/communication/communication.vue +++ b/modeler/src/views/decision/communication/communication.vue @@ -21,6 +21,14 @@
+ + + 随机生成 + + + + 从后端加载 + 保存 @@ -50,22 +58,24 @@ import { useRoute, useRouter } from 'vue-router'; import { message } from 'ant-design-vue'; import { getTeleport } from '@antv/x6-vue-shape'; 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 { safePreventDefault, safeStopPropagation } from '@/utils/event'; import Header from '../header.vue'; import type { Scenario } 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 { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils'; import PlatformCard from './platform-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 { generateRandomCommunicationData } from './random-data-generator'; +import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter'; const TeleportContainer = defineComponent(getTeleport()); @@ -81,6 +91,8 @@ export default defineComponent({ CheckCircleOutlined, CheckOutlined, RollbackOutlined, + ThunderboltOutlined, + DatabaseOutlined, TeleportContainer, }, setup() { @@ -211,25 +223,95 @@ 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; } catch (e: any) { console.error('parse error,cause:', e); } - if (!nodeGraph) { - nodeGraph = { - nodes: [], - edges: [], - }; - } + + // 设置当前场景 currentScenario.value = { ...scenario, - graph: nodeGraph, + graph: nodeGraph || { nodes: [], edges: [] }, relations: [] }; + console.log('选中场景:', currentScenario.value); + 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(); }; @@ -261,6 +343,7 @@ export default defineComponent({ }, 100); // 延迟一会儿,免得连线错位 } } + }, 100); }); }; @@ -277,6 +360,12 @@ export default defineComponent({ nodes: [], }, }; + + // 清空graph中的场景信息 + if (graph.value) { + (graph.value as any).currentScenario = null; + } + currentGraph.value = { edges: [], 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(() => { init(); @@ -444,6 +720,8 @@ export default defineComponent({ handleDrop, isDraggingOver, handleSave, + handleGenerateRandom, + handleLoadFromBackend, handleUpdateElement, handleSelect, }; diff --git a/modeler/src/views/decision/communication/data-converter.ts b/modeler/src/views/decision/communication/data-converter.ts new file mode 100644 index 0000000..01b0880 --- /dev/null +++ b/modeler/src/views/decision/communication/data-converter.ts @@ -0,0 +1,1049 @@ +/* + * This file is part of the kernelstudio package. + * + * (c) 2014-2026 zlin + * + * For the full copyright and license information, please view the LICENSE file + * that was distributed with this source code. + */ + +import type { GraphTaskElement, GraphContainer, GraphEdgeElement } from '../graph'; +import { generateKey } from '@/utils/strings'; + +// ==================== 类型定义 ==================== + +/** + * 布局方向类型 + */ +export type LayoutDirection = 'horizontal' | 'vertical'; + +/** + * 布局配置接口 + */ +export interface LayoutConfig { + direction: LayoutDirection; + rootPosition?: number; // 根节点起始位置 + colSpacing?: number; // 列间距 + rowSpacing?: number; // 行间距 + startPosition?: number; // 起始位置 +} + +/** + * 数据库通信关系记录接口 + */ +export interface CommunicationRecord { + id: number; + command_platform: string; // 指挥平台(源节点名称) + subordinate_platform: string; // 下属平台(目标节点名称) + command_comm?: string; // 指挥通信方式 + subordinate_comm?: string; // 下属通信方式 + scenary_id?: number; // 场景ID +} + +interface ParentChildMappings { + parentToChildren: Map; + childToParent: Map; +} + +interface LayoutParams { + ROOT_POSITION: number; // 根节点起始位置(horizontal时为X, vertical时为Y) + LEVEL_SPACING: number; // 层级间距(horizontal时为X间距, vertical时为Y间距) + SIBLING_SPACING: number; // 同级间距(horizontal时为Y间距, vertical时为X间距) + START_POSITION: number; // 起始位置(horizontal时为Y, vertical时为X) + direction: LayoutDirection; +} + +interface NodePosition { + x: number; + y: number; +} + +interface SubtreeRange { + min: number; + max: number; +} + +/** + * 坐标映射器接口 - 用于抽象水平和垂直布局的坐标转换 + */ +interface CoordinateMapper { + /** 将层级索引映射到主轴坐标 */ + mapLevelToAxis(level: number): number; + /** 将同级索引映射到次轴坐标 */ + mapSiblingIndexToAxis(index: number, totalInLevel?: number): number; + /** 获取节点的主轴坐标 */ + getPrimaryCoord(pos: NodePosition): number; + /** 获取节点的次轴坐标 */ + getSecondaryCoord(pos: NodePosition): number; + /** 设置节点坐标 */ + setCoords(primary: number, secondary: number): NodePosition; + /** 获取子树范围的主轴坐标 */ + getSubtreePrimaryCoord(range: SubtreeRange): { min: number; max: number }; + /** 创建子树范围对象 */ + createSubtreeRange(min: number, max: number): SubtreeRange; +} + +/** + * 创建水平布局的坐标映射器 (X轴表示层级,Y轴表示同级分布) + */ +const createHorizontalMapper = (params: LayoutParams): CoordinateMapper => ({ + mapLevelToAxis: (level) => params.ROOT_POSITION + (level + 1) * params.LEVEL_SPACING, + mapSiblingIndexToAxis: (index, totalInLevel) => { + // 均匀分布在START_POSITION开始的区域内 + return params.START_POSITION + index * params.SIBLING_SPACING + params.SIBLING_SPACING / 2; + }, + getPrimaryCoord: (pos) => pos.x, + getSecondaryCoord: (pos) => pos.y, + setCoords: (primary, secondary) => ({ x: primary, y: secondary }), + getSubtreePrimaryCoord: (range) => ({ min: range.min, max: range.max }), + createSubtreeRange: (min, max) => ({ min, max }), +}); + +/** + * 创建垂直布局的坐标映射器 (Y轴表示层级,X轴表示同级分布) + */ +const createVerticalMapper = (params: LayoutParams): CoordinateMapper => ({ + mapLevelToAxis: (level) => params.ROOT_POSITION + (level + 1) * params.LEVEL_SPACING, + mapSiblingIndexToAxis: (index, totalInLevel) => { + // 均匀分布在START_POSITION开始的区域内 + return params.START_POSITION + index * params.SIBLING_SPACING + params.SIBLING_SPACING / 2; + }, + getPrimaryCoord: (pos) => pos.y, + getSecondaryCoord: (pos) => pos.x, + setCoords: (primary, secondary) => ({ x: secondary, y: primary }), + getSubtreePrimaryCoord: (range) => ({ min: range.min, max: range.max }), + createSubtreeRange: (min, max) => ({ min, max }), +}); + +// ==================== 主函数:数据转换 ==================== + +/** + * 从数据库记录转换为 GraphContainer 格式 + * @param records 数据库通信关系记录数组 + * @param config 布局配置(可选),默认为水平布局 + * @returns GraphContainer 格式的节点和边数据 + */ +export const convertRecordsToGraphContainer = ( + records: CommunicationRecord[], + config?: Partial +): GraphContainer => { + if (!records || records.length === 0) { + return { nodes: [], edges: [] }; + } + + // 合并默认配置 + const finalConfig: LayoutConfig = { + direction: config?.direction || 'horizontal', + rootPosition: typeof config?.rootPosition === 'number' ? config.rootPosition : 50, + colSpacing: typeof config?.colSpacing === 'number' ? config.colSpacing : 280, + rowSpacing: typeof config?.rowSpacing === 'number' ? config.rowSpacing : 140, + startPosition: typeof config?.startPosition === 'number' ? config.startPosition : 80, + }; + + // 收集所有唯一的平台名称(过滤无效值) + const platformSet = new Set(); + records.forEach(record => { + if (record.command_platform && typeof record.command_platform === 'string' && record.command_platform.trim()) { + platformSet.add(record.command_platform); + } + if (record.subordinate_platform && typeof record.subordinate_platform === 'string' && record.subordinate_platform.trim()) { + platformSet.add(record.subordinate_platform); + } + }); + + const allPlatforms = Array.from(platformSet); + + console.log(`收集到 ${allPlatforms.length} 个唯一平台:`, allPlatforms); + + if (allPlatforms.length === 0) { + console.warn('没有有效的平台数据'); + return { nodes: [], edges: [] }; + } + + // 构建父子映射关系 + const { parentToChildren, childToParent } = buildParentChildMappings(records); + + // 检测连通分量(处理断链情况) + const connectedComponents = findConnectedComponents(allPlatforms, parentToChildren, childToParent); + + console.log(`检测到 ${connectedComponents.length} 个连通分量`); + + // 为每个连通分量生成布局 + const allNodes: GraphTaskElement[] = []; + const allEdges: GraphEdgeElement[] = []; + + let offset: number = finalConfig.rootPosition ?? 50; // 第一个分量的起始位置 + + connectedComponents.forEach((component, componentIndex) => { + // 为该分量确定根节点(选择入度为0的节点) + const rootPlatformName = determineRootNode(Array.from(component), childToParent); + + if (!rootPlatformName) { + console.warn(`第 ${componentIndex + 1} 个分量未找到根节点,跳过`); + return; + } + + // BFS遍历确定该分量中每个节点的层级 + const levelMap = computeNodeLevels( + Array.from(component), + rootPlatformName, + parentToChildren, + childToParent + ); + + console.log(` 分量 ${componentIndex + 1} 的层级映射:`, Object.fromEntries(levelMap)); + + // 按层级分组 + const levelsMap = groupNodesByLevel(levelMap); + + console.log(` 分量 ${componentIndex + 1} 的层级分组:`, Object.fromEntries(levelsMap)); + + // 根据方向创建布局参数 + const layoutParams: LayoutParams = { + ROOT_POSITION: offset ?? 50, + LEVEL_SPACING: finalConfig.colSpacing ?? 280, + SIBLING_SPACING: finalConfig.rowSpacing ?? 140, + START_POSITION: finalConfig.startPosition ?? 80, + direction: finalConfig.direction, + }; + + // 创建坐标映射器 + const mapper = finalConfig.direction === 'horizontal' + ? createHorizontalMapper(layoutParams) + : createVerticalMapper(layoutParams); + + // 计算节点位置 + const nodePositions = calculateNodePositions( + levelsMap, + parentToChildren, + childToParent, + layoutParams, + mapper + ); + + // 创建节点对象 + const nodeMap = createNodeObjects(levelsMap, nodePositions); + + console.log(` 分量 ${componentIndex + 1} 创建了 ${nodeMap.size} 个节点:`, Array.from(nodeMap.keys())); + + // 创建根节点(如果不存在则添加) + if (!nodeMap.has(rootPlatformName)) { + console.warn(` 根节点 "${rootPlatformName}" 未在 levelsMap 中找到,手动创建`); + } + createRootNode(nodeMap, rootPlatformName, parentToChildren, layoutParams, mapper); + + console.log(` 最终节点数: ${nodeMap.size}`); + + // 过滤出属于当前分量的边 + const componentEdges = filterEdgesForComponent(records, component); + const edges = createEdges(componentEdges, nodeMap); + + // 添加到总结果 + allNodes.push(...Array.from(nodeMap.values())); + allEdges.push(...edges); + + // 计算下一个分量的偏移量(基于当前分量的宽度) + const maxLevel = Math.max(...Array.from(levelsMap.keys())); + offset += (maxLevel + 1) * layoutParams.LEVEL_SPACING + 150; // 分量之间留150px间隔 + }); + + return { + nodes: allNodes, + edges: allEdges, + }; +}; + +// ==================== 辅助函数:检测连通分量 ==================== + +/** + * 使用并查集或DFS检测图中的连通分量 + */ +const findConnectedComponents = ( + platforms: string[], + parentToChildren: Map, + childToParent: Map +): Set[] => { + // 过滤掉undefined、null和空字符串 + const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0); + + const visited = new Set(); + const components: Set[] = []; + + // DFS遍历找出所有连通分量 + const dfs = (platform: string, component: Set) => { + if (visited.has(platform)) return; + + visited.add(platform); + component.add(platform); + + // 访问子节点 + const children = parentToChildren.get(platform) || []; + children.forEach(child => { + // 只处理有效的子节点 + if (child && typeof child === 'string' && child.trim().length > 0) { + dfs(child, component); + } + }); + + // 访问父节点 + const parent = childToParent.get(platform); + if (parent && !visited.has(parent) && typeof parent === 'string' && parent.trim().length > 0) { + dfs(parent, component); + } + }; + + // 对每个未访问的有效节点启动一次DFS + validPlatforms.forEach(platform => { + if (!visited.has(platform)) { + const component = new Set(); + dfs(platform, component); + if (component.size > 0) { + components.push(component); + } + } + }); + + return components; +}; + +/** + * 为连通分量确定根节点 + * 策略:选择入度为0的节点(没有父节点的节点),如果有多个则选择第一个 + */ +const determineRootNode = ( + platforms: string[], + childToParent: Map +): string | null => { + // 过滤掉undefined、null和空字符串 + const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0); + + if (validPlatforms.length === 0) return null; + + // 优先选择入度为0的节点(没有父节点) + const rootCandidates = validPlatforms.filter(platform => !childToParent.has(platform)); + + if (rootCandidates.length > 0) { + return rootCandidates[0] || null; + } + + // 如果所有节点都有父节点(可能是环形结构),返回第一个有效节点 + return validPlatforms[0] || null; +}; + +/** + * 过滤出属于特定连通分量的边 + */ +const filterEdgesForComponent = ( + records: CommunicationRecord[], + component: Set +): CommunicationRecord[] => { + return records.filter(record => + component.has(record.command_platform) && + component.has(record.subordinate_platform) + ); +}; + +// ==================== 辅助函数:构建父子映射 ==================== + +const buildParentChildMappings = (records: CommunicationRecord[]): ParentChildMappings => { + const parentToChildren = new Map(); + const childToParent = new Map(); + + records.forEach(record => { + if (!parentToChildren.has(record.command_platform)) { + parentToChildren.set(record.command_platform, []); + } + parentToChildren.get(record.command_platform)!.push(record.subordinate_platform); + childToParent.set(record.subordinate_platform, record.command_platform); + }); + + return { parentToChildren, childToParent }; +}; + +// ==================== 辅助函数:计算节点层级 ==================== + +const computeNodeLevels = ( + platforms: string[], + rootPlatformName: string, + parentToChildren: Map, + childToParent: Map +): Map => { + const levelMap = new Map(); + const processed = new Set(); + const queue: Array<{ platform: string; level: number }> = []; + + // 找出所有入度为0的节点(没有父节点的节点),作为BFS的起点 + const rootNodes = platforms.filter(p => !childToParent.has(p)); + + console.log(` BFS起点(入度为0的节点): [${rootNodes.join(', ')}]`); + + // 将所有根节点加入队列,层级为0 + rootNodes.forEach(root => { + levelMap.set(root, 0); + processed.add(root); + queue.push({ platform: root, level: 0 }); + }); + + // BFS遍历后续层级 + while (queue.length > 0) { + const { platform: currentPlatform, level: currentLevel } = queue.shift()!; + + const children = parentToChildren.get(currentPlatform) || []; + children.forEach(childPlatform => { + if (!processed.has(childPlatform)) { + levelMap.set(childPlatform, currentLevel + 1); + processed.add(childPlatform); + queue.push({ platform: childPlatform, level: currentLevel + 1 }); + } + }); + } + + // 理论上不应该有未处理的节点,因为所有节点都应该从某个根节点可达 + // 如果有,说明图中存在环或者数据有问题 + const unprocessed = platforms.filter(p => !processed.has(p)); + if (unprocessed.length > 0) { + console.warn(` 警告:以下节点未被BFS遍历到(可能存在环或数据问题): [${unprocessed.join(', ')}]`); + // 给这些节点分配一个合理的层级(基于它们的父节点) + unprocessed.forEach(platform => { + const parent = childToParent.get(platform); + if (parent && levelMap.has(parent)) { + levelMap.set(platform, levelMap.get(parent)! + 1); + } else { + // 如果连父节点都没有,放到最后一层 + const maxLevel = Math.max(...Array.from(levelMap.values()), 0); + levelMap.set(platform, maxLevel + 1); + } + }); + } + + return levelMap; +}; + +// ==================== 辅助函数:按层级分组 ==================== + +const groupNodesByLevel = (levelMap: Map): Map => { + const levelsMap = new Map(); + levelMap.forEach((level, platform) => { + if (!levelsMap.has(level)) { + levelsMap.set(level, []); + } + levelsMap.get(level)!.push(platform); + }); + return levelsMap; +}; + +// ==================== 辅助函数:计算节点位置 ==================== + +const calculateNodePositions = ( + levelsMap: Map, + parentToChildren: Map, + childToParent: Map, + params: LayoutParams, + mapper: CoordinateMapper +): Map => { + const nodePositions = new Map(); + const subtreeRanges = new Map(); + + // 第一步:预计算每列需要的总高度 + precomputeColumnHeights(levelsMap, parentToChildren, params.SIBLING_SPACING, subtreeRanges, mapper); + + // 第二步:初步分配位置 + initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions, mapper); + + // 第三步:调整父节点使其位于子节点中心(处理共享子节点情况) + adjustParentsToCenterOfChildren(levelsMap, parentToChildren, childToParent, nodePositions, mapper); + + // 第四步:全局防重叠检测与修正(跨所有层级) + preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper); + + // 第五步:重新调整父节点恢复等腰三角形 + restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions, mapper); + + // 第六步:最终全局防重叠检查 + preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper); + + return nodePositions; +}; + +// 预计算每列高度 +const precomputeColumnHeights = ( + levelsMap: Map, + parentToChildren: Map, + siblingSpacing: number, + subtreeRanges: Map, + mapper: CoordinateMapper +): void => { + const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a); + + sortedLevels.forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + let totalSize = 0; + + platformsInLevel.forEach(platform => { + const children = parentToChildren.get(platform) || []; + + if (children.length > 0) { + let maxSubtreeSize = 0; + + children.forEach(child => { + const childRange = subtreeRanges.get(child); + if (childRange) { + const range = mapper.getSubtreePrimaryCoord(childRange); + const childSize = range.max - range.min; + maxSubtreeSize = Math.max(maxSubtreeSize, childSize); + } + }); + + const subtreeSize = maxSubtreeSize; + + totalSize += Math.max(siblingSpacing, subtreeSize); + subtreeRanges.set(platform, mapper.createSubtreeRange( + totalSize - subtreeSize, + totalSize + )); + } else { + totalSize += siblingSpacing; + subtreeRanges.set(platform, mapper.createSubtreeRange( + totalSize - siblingSpacing, + totalSize + )); + } + }); + }); +}; + +// 初步分配位置(简化版:所有节点均匀分布,不考虑子树高度) +const initialPositionAssignment = ( + levelsMap: Map, + parentToChildren: Map, + params: LayoutParams, + subtreeRanges: Map, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + + forwardLevels.forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + + platformsInLevel.forEach((platform, index) => { + const primaryCoord = mapper.mapLevelToAxis(level); + const secondaryCoord = mapper.mapSiblingIndexToAxis(index, platformsInLevel.length); + nodePositions.set(platform, mapper.setCoords(primaryCoord, secondaryCoord)); + }); + }); +}; + +// 调整父节点到子节点中心 +const adjustParentsToCenterOfChildren = ( + levelsMap: Map, + parentToChildren: Map, + childToParent: Map, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + + // 从最底层向上遍历,确保子节点位置已经确定后再调整父节点 + [...forwardLevels].reverse().forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + + platformsInLevel.forEach(platform => { + const children = parentToChildren.get(platform) || []; + + if (children.length > 0) { + const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean); + + if (childPositions.length > 0) { + // 计算所有子节点的平均次轴坐标(垂直布局时是X,水平布局时是Y) + const avgSecondaryCoord = childPositions.reduce((sum, pos) => + sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length; + + // 将父节点的次轴坐标移动到子节点的中心 + const pos = nodePositions.get(platform)!; + const primaryCoord = mapper.getPrimaryCoord(pos); + nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord)); + } + } + }); + }); +}; + +// 全局防重叠检测与修正(跨所有层级,迭代式确保完全无重叠) +const preventGlobalOverlaps = ( + parentToChildren: Map, + siblingSpacing: number, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + // 收集所有节点并按主轴坐标分组(同层的节点) + const nodesByLevel = new Map>(); + + nodePositions.forEach((pos, platform) => { + const primaryCoord = mapper.getPrimaryCoord(pos); + if (!nodesByLevel.has(primaryCoord)) { + nodesByLevel.set(primaryCoord, []); + } + nodesByLevel.get(primaryCoord)!.push({ platform, pos }); + }); + + // 对每一层进行防重叠处理 + nodesByLevel.forEach(nodes => { + let hasOverlap = true; + let iterations = 0; + const maxIterations = 30; // 防止无限循环 + + while (hasOverlap && iterations < maxIterations) { + hasOverlap = false; + iterations++; + + // 按次轴坐标排序 + nodes.sort((a, b) => mapper.getSecondaryCoord(a.pos) - mapper.getSecondaryCoord(b.pos)); + + for (let i = 1; i < nodes.length; i++) { + const prev = nodes[i - 1]; + const curr = nodes[i]; + + if (!prev || !curr) continue; + + const actualDistance = mapper.getSecondaryCoord(curr.pos) - mapper.getSecondaryCoord(prev.pos); + + if (actualDistance < siblingSpacing) { + hasOverlap = true; + + const offset = siblingSpacing - actualDistance; + const currSecondary = mapper.getSecondaryCoord(curr.pos); + const currPrimary = mapper.getPrimaryCoord(curr.pos); + curr.pos = mapper.setCoords(currPrimary, currSecondary + offset); + + // 递归调整所有后代节点 + const adjustDescendants = (platform: string, secondaryOffset: number) => { + const children = parentToChildren.get(platform) || []; + children.forEach(child => { + const childPos = nodePositions.get(child); + if (childPos) { + const childPrimary = mapper.getPrimaryCoord(childPos); + const childSecondary = mapper.getSecondaryCoord(childPos); + const newPos = mapper.setCoords(childPrimary, childSecondary + secondaryOffset); + nodePositions.set(child, newPos); + adjustDescendants(child, secondaryOffset); + } + }); + }; + + adjustDescendants(curr.platform, offset); + + // 发现重叠并调整后,跳出内层循环,重新开始检测 + break; + } + } + } + + if (iterations >= maxIterations) { + console.warn(`某层防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`); + } + }); +}; + +// 恢复等腰三角形 +const restoreIsoscelesTriangles = ( + levelsMap: Map, + parentToChildren: Map, + nodePositions: Map, + mapper: CoordinateMapper +): void => { + const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); + + [...forwardLevels].reverse().forEach(level => { + const platformsInLevel = levelsMap.get(level)!; + + platformsInLevel.forEach(platform => { + const children = parentToChildren.get(platform) || []; + + if (children.length > 0) { + const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean); + + if (childPositions.length > 0) { + const avgSecondaryCoord = childPositions.reduce((sum, pos) => + sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length; + const pos = nodePositions.get(platform)!; + const primaryCoord = mapper.getPrimaryCoord(pos); + nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord)); + } + } + }); + }); +}; + +// ==================== 辅助函数:创建节点对象 ==================== + +const createNodeObjects = ( + levelsMap: Map, + nodePositions: Map +): Map => { + const nodeMap = new Map(); + + levelsMap.forEach((platformsInLevel, level) => { + platformsInLevel.forEach(platform => { + const pos = nodePositions.get(platform)!; + const componentId = 1; + + const node: GraphTaskElement = { + id: 0, + key: generateKey(), + type: 'scenario', + name: platform, + platformId: 0, + scenarioId: 0, + components: [ + { + id: componentId, + name: `${platform}_comm`, + type: 'communication', + description: `通信组件` + } + ], + template: 0, + templateType: null, + category: null, + multiable: false, + group: null, + description: platform, + order: 0, + position: { + x: Math.round(pos.x), + y: Math.round(pos.y), + }, + width: 250, + height: 145, + inputs: null, + outputs: null, + parameters: [], + variables: [], + }; + + nodeMap.set(platform, node); + }); + }); + + return nodeMap; +}; + +// ==================== 辅助函数:创建根节点 ==================== + +const createRootNode = ( + nodeMap: Map, + rootPlatformName: string, + parentToChildren: Map, + params: LayoutParams, + mapper: CoordinateMapper +): void => { + if (nodeMap.size === 0 || !rootPlatformName) return; + + const directChildren = parentToChildren.get(rootPlatformName) || []; + + let centerSecondaryCoord: number; + if (directChildren.length > 0) { + const childSecondaryPositions = directChildren.map(child => { + const childNode = nodeMap.get(child); + return childNode ? mapper.getSecondaryCoord(childNode.position) : 0; + }).filter(coord => coord > 0); + + if (childSecondaryPositions.length > 0) { + centerSecondaryCoord = childSecondaryPositions.reduce((sum, coord) => sum + coord, 0) / childSecondaryPositions.length; + } else { + const allNodes = Array.from(nodeMap.values()); + const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position)); + const minCoord = Math.min(...coords); + const maxCoord = Math.max(...coords); + centerSecondaryCoord = (minCoord + maxCoord) / 2; + } + } else { + const allNodes = Array.from(nodeMap.values()); + const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position)); + const minCoord = Math.min(...coords); + const maxCoord = Math.max(...coords); + centerSecondaryCoord = (minCoord + maxCoord) / 2; + } + + const rootNode: GraphTaskElement = { + id: 0, + key: generateKey(), + type: 'scenario', + name: rootPlatformName, + platformId: 0, + scenarioId: 0, + components: [ + { + id: 1, + name: `${rootPlatformName}_comm`, + type: 'communication', + description: `通信组件` + } + ], + template: 0, + templateType: null, + category: null, + multiable: false, + group: null, + description: rootPlatformName, + order: 0, + position: mapper.setCoords(params.ROOT_POSITION, Math.round(centerSecondaryCoord)), + width: 250, + height: 145, + inputs: null, + outputs: null, + parameters: [], + variables: [], + }; + + nodeMap.set(rootPlatformName, rootNode); +}; + +// ==================== 辅助函数:创建边 ==================== + +const createEdges = ( + records: CommunicationRecord[], + nodeMap: Map +): GraphEdgeElement[] => { + const edges: GraphEdgeElement[] = []; + + records.forEach((record, index) => { + const sourceNode = nodeMap.get(record.command_platform); + const targetNode = nodeMap.get(record.subordinate_platform); + + if (sourceNode && targetNode && sourceNode.key && targetNode.key) { + const sourceCompId = sourceNode.components?.[0]?.id || 1; + const targetCompId = targetNode.components?.[0]?.id || 1; + + edges.push({ + id: index + 1, + key: generateKey(), + source: sourceNode.key, + target: targetNode.key, + sourcePort: `out-${sourceCompId}`, + targetPort: `in-${targetCompId}`, + attrs: {}, + router: { name: 'normal' }, + connector: { name: 'smooth' }, + }); + } + }); + + return edges; +}; + +// ==================== 新增功能:平台数据转通信关系 ==================== + +/** + * 平台组件接口(来自后端API) + */ +export interface PlatformComponent { + id: number; + name: string | null; + type: string | null; + description: string | null; + platformId: number; + [key: string]: unknown; +} + +/** + * 带组件的平台接口(来自后端API) + */ +export interface PlatformWithComponents { + id: number; + name: string | null; + description: string | null; + scenarioId: number; + components: PlatformComponent[]; + [key: string]: unknown; +} + +/** + * 智能推断通信关系的策略类型 + */ +type InferenceStrategy = + | 'by-name-pattern' // 根据名称模式推断(如包含"command"、"cmd"等关键词) + | 'all-to-first' // 所有节点连接到第一个节点 + | 'chain' // 链式连接(A→B→C→D...) + | 'star'; // 星型连接(中心节点连接所有其他节点) + +/** + * 将平台列表转换为通信关系记录 + * @param platforms 平台列表 + * @param strategy 推断策略,默认为 'by-name-pattern' + * @returns 通信关系记录数组 + */ +export const convertPlatformsToCommunicationRecords = ( + platforms: PlatformWithComponents[], + strategy: InferenceStrategy = 'by-name-pattern' +): CommunicationRecord[] => { + if (!platforms || platforms.length === 0) { + return []; + } + + const records: CommunicationRecord[] = []; + + switch (strategy) { + case 'by-name-pattern': + inferByNamPattern(platforms, records); + break; + case 'all-to-first': + connectAllToFirst(platforms, records); + break; + case 'chain': + createChainConnection(platforms, records); + break; + case 'star': + createStarConnection(platforms, records); + break; + default: + console.warn(`未知的推断策略: ${strategy},使用默认策略 'by-name-pattern'`); + inferByNamPattern(platforms, records); + } + + return records; +}; + +/** + * 根据名称模式推断通信关系 + * - 识别指挥节点(包含"command"、"cmd"、"指挥"等关键词) + * - 其他节点连接到最近的指挥节点 + */ +const inferByNamPattern = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + // 识别指挥节点 + const commandKeywords = ['command', 'cmd', '指挥', 'chief', 'leader']; + const commandNodes = platforms.filter(p => { + const name = (p.name || '').toLowerCase(); + const desc = (p.description || '').toLowerCase(); + return commandKeywords.some(kw => name.includes(kw) || desc.includes(kw)); + }); + + // 非指挥节点 + const otherNodes = platforms.filter(p => !commandNodes.includes(p)); + + // 如果有指挥节点,其他节点连接到最近的指挥节点 + if (commandNodes.length > 0) { + // 按ID排序,取第一个作为主要指挥中心 + const mainCommand = commandNodes.sort((a, b) => a.id - b.id)[0]; + + if (!mainCommand) return; // 安全检查 + + // 其他指挥节点也连接到主指挥 + commandNodes.slice(1).forEach(cmd => { + records.push({ + id: records.length + 1, + command_platform: mainCommand.name || `platform_${mainCommand.id}`, + subordinate_platform: cmd.name || `platform_${cmd.id}`, + scenary_id: mainCommand.scenarioId, + }); + }); + + // 非指挥节点连接到主指挥 + otherNodes.forEach(node => { + records.push({ + id: records.length + 1, + command_platform: mainCommand.name || `platform_${mainCommand.id}`, + subordinate_platform: node.name || `platform_${node.id}`, + scenary_id: mainCommand.scenarioId, + }); + }); + } else { + // 没有指挥节点,使用链式连接 + createChainConnection(platforms, records); + } +}; + +/** + * 所有节点连接到第一个节点 + */ +const connectAllToFirst = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + if (platforms.length < 2) return; + + const first = platforms[0]; + if (!first) return; // 安全检查 + + const firstName = first.name || `platform_${first.id}`; + + platforms.slice(1).forEach(platform => { + records.push({ + id: records.length + 1, + command_platform: firstName, + subordinate_platform: platform.name || `platform_${platform.id}`, + scenary_id: first.scenarioId, + }); + }); +}; + +/** + * 创建链式连接 A→B→C→D... + */ +const createChainConnection = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + for (let i = 0; i < platforms.length - 1; i++) { + const current = platforms[i]; + const next = platforms[i + 1]; + + if (!current || !next) continue; // 安全检查 + + records.push({ + id: records.length + 1, + command_platform: current.name || `platform_${current.id}`, + subordinate_platform: next.name || `platform_${next.id}`, + scenary_id: current.scenarioId, + }); + } +}; + +/** + * 创建星型连接(中间节点连接所有其他节点) + */ +const createStarConnection = ( + platforms: PlatformWithComponents[], + records: CommunicationRecord[] +): void => { + if (platforms.length < 2) return; + + // 选择中间的节点作为中心 + const centerIndex = Math.floor(platforms.length / 2); + const center = platforms[centerIndex]; + + if (!center) return; // 安全检查 + + const centerName = center.name || `platform_${center.id}`; + + platforms.forEach((platform, idx) => { + if (idx !== centerIndex) { + records.push({ + id: records.length + 1, + command_platform: centerName, + subordinate_platform: platform.name || `platform_${platform.id}`, + scenary_id: center.scenarioId, + }); + } + }); +}; + +/** + * 便捷函数:直接将平台列表转换为图容器 + * @param platforms 平台列表 + * @param strategy 推断策略 + * @param layoutConfig 布局配置(可选) + * @returns 图容器对象 + */ +export const convertPlatformsToGraphContainer = ( + platforms: PlatformWithComponents[], + strategy: InferenceStrategy = 'by-name-pattern', + layoutConfig?: Partial +): GraphContainer => { + const records = convertPlatformsToCommunicationRecords(platforms, strategy); + return convertRecordsToGraphContainer(records, layoutConfig); +}; diff --git a/modeler/src/views/decision/communication/node.vue b/modeler/src/views/decision/communication/node.vue index cc074e7..32305fe 100644 --- a/modeler/src/views/decision/communication/node.vue +++ b/modeler/src/views/decision/communication/node.vue @@ -1,5 +1,9 @@