diff --git a/modeler/src/views/decision/communication/communication.vue b/modeler/src/views/decision/communication/communication.vue
index 36f0909..b5b6549 100644
--- a/modeler/src/views/decision/communication/communication.vue
+++ b/modeler/src/views/decision/communication/communication.vue
@@ -21,6 +21,14 @@
+
+
+ 随机生成
+
+
+
+ 从后端加载
+
保存
@@ -50,14 +58,14 @@ 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';
@@ -66,6 +74,8 @@ import PlatformCard from './platform-card.vue';
import NodesCard from './nodes-card.vue';
import { findOneScenarioById, saveScenario } 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() {
@@ -229,6 +241,8 @@ export default defineComponent({
graph: nodeGraph,
relations: []
};
+ console.log(currentScenario.value);
+
currentScenarioEditing.value = true;
createElements();
};
@@ -261,6 +275,7 @@ export default defineComponent({
}, 100); // 延迟一会儿,免得连线错位
}
}
+
}, 100);
});
};
@@ -415,6 +430,149 @@ 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' });
+
+ // TODO: 等待后端提供真实接口后替换为实际API调用
+ // const response = await findCommunicationRelations(currentScenario.value.id);
+ // const relations = response.data || [];
+
+ // 临时使用模拟数据
+ console.log('使用模拟通信关系数据');
+ const mockRelations: CommunicationRecord[] = [
+ { id: 1, command_platform: '指挥中心', subordinate_platform: '预警机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
+ { id: 2, command_platform: '指挥中心', subordinate_platform: '驱逐舰', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
+ { id: 3, command_platform: '指挥中心', subordinate_platform: '战斗机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
+ { id: 4, command_platform: '预警机', subordinate_platform: '电子战飞机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
+ { id: 5, command_platform: '驱逐舰', subordinate_platform: '潜艇', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
+ { id: 6, command_platform: '战斗机', subordinate_platform: '无人机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id },
+ ];
+
+ console.log('模拟的通信记录:', mockRelations);
+
+ // 使用模拟数据进行转换
+ const convertedGraph = convertRecordsToGraphContainer(mockRelations);
+
+ 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 +602,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..e5b55d8
--- /dev/null
+++ b/modeler/src/views/decision/communication/data-converter.ts
@@ -0,0 +1,906 @@
+/*
+ * 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 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_X: number;
+ COL_SPACING: number;
+ MIN_ROW_SPACING: number;
+ START_Y: number;
+}
+
+interface NodePosition {
+ x: number;
+ y: number;
+}
+
+interface SubtreeRange {
+ minY: number;
+ maxY: number;
+}
+
+// ==================== 主函数:数据转换 ====================
+
+/**
+ * 从数据库记录转换为 GraphContainer 格式
+ * @param records 数据库通信关系记录数组
+ * @returns GraphContainer 格式的节点和边数据
+ */
+export const convertRecordsToGraphContainer = (records: CommunicationRecord[]): GraphContainer => {
+ if (!records || records.length === 0) {
+ return { nodes: [], edges: [] };
+ }
+
+ // 收集所有唯一的平台名称
+ const platformSet = new Set();
+ records.forEach(record => {
+ platformSet.add(record.command_platform);
+ platformSet.add(record.subordinate_platform);
+ });
+
+ const allPlatforms = Array.from(platformSet);
+
+ // 构建父子映射关系
+ const { parentToChildren, childToParent } = buildParentChildMappings(records);
+
+ // 检测连通分量(处理断链情况)
+ const connectedComponents = findConnectedComponents(allPlatforms, parentToChildren, childToParent);
+
+ console.log(`检测到 ${connectedComponents.length} 个连通分量`);
+
+ // 为每个连通分量生成布局
+ const allNodes: GraphTaskElement[] = [];
+ const allEdges: GraphEdgeElement[] = [];
+
+ let xOffset = 50; // 第一个分量的起始X坐标
+
+ connectedComponents.forEach((component, componentIndex) => {
+ console.log(`处理第 ${componentIndex + 1} 个连通分量,包含 ${component.size} 个节点`);
+
+ // 为该分量确定根节点
+ const rootPlatformName = determineRootNode(Array.from(component));
+
+ if (!rootPlatformName) {
+ console.warn(`第 ${componentIndex + 1} 个分量未找到根节点,跳过`);
+ return;
+ }
+
+ // BFS遍历确定该分量中每个节点的层级
+ const levelMap = computeNodeLevels(
+ Array.from(component),
+ rootPlatformName,
+ parentToChildren,
+ childToParent
+ );
+
+ // 按层级分组
+ const levelsMap = groupNodesByLevel(levelMap);
+
+ // 布局参数 - 紧凑型布局
+ const layoutParams: LayoutParams = {
+ ROOT_X: xOffset, // 当前分量的起始X坐标
+ COL_SPACING: 280, // 列间距
+ MIN_ROW_SPACING: 140, // 最小行间距
+ START_Y: 80, // 起始Y坐标
+ };
+
+ // 计算节点位置
+ const nodePositions = calculateNodePositions(
+ levelsMap,
+ parentToChildren,
+ layoutParams
+ );
+
+ // 创建节点对象
+ const nodeMap = createNodeObjects(levelsMap, nodePositions);
+
+ // 创建根节点
+ createRootNode(nodeMap, rootPlatformName, parentToChildren);
+
+ // 过滤出属于当前分量的边
+ 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()));
+ xOffset += (maxLevel + 1) * layoutParams.COL_SPACING + 150; // 分量之间留150px间隔
+ });
+
+ return {
+ nodes: allNodes,
+ edges: allEdges,
+ };
+};
+
+// ==================== 辅助函数:检测连通分量 ====================
+
+/**
+ * 使用并查集或DFS检测图中的连通分量
+ */
+const findConnectedComponents = (
+ platforms: string[],
+ parentToChildren: Map,
+ childToParent: Map
+): Set[] => {
+ 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 => dfs(child, component));
+
+ // 访问父节点
+ const parent = childToParent.get(platform);
+ if (parent && !visited.has(parent)) {
+ dfs(parent, component);
+ }
+ };
+
+ // 对每个未访问的节点启动一次DFS
+ platforms.forEach(platform => {
+ if (!visited.has(platform)) {
+ const component = new Set();
+ dfs(platform, component);
+ components.push(component);
+ }
+ });
+
+ return components;
+};
+
+/**
+ * 为连通分量确定根节点
+ * 优先级:1. 包含"指挥"关键词 2. 入度为0的节点 3. 第一个节点
+ */
+const determineRootNode = (platforms: string[]): string | null => {
+ if (platforms.length === 0) return null;
+
+ // 优先选择包含"指挥"的节点
+ const commandNode = platforms.find(p => p.includes('指挥'));
+ if (commandNode) return commandNode;
+
+ // 其次选择入度为0的节点(没有父节点)
+ // 这里简化处理,直接返回第一个
+ return platforms[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 }> = [];
+
+ // 分离根节点和子节点
+ const childPlatforms = platforms.filter(p => p !== rootPlatformName);
+
+ // 第一层:直接与根节点相连的节点
+ childPlatforms.forEach(platform => {
+ if (childToParent.get(platform) === rootPlatformName) {
+ levelMap.set(platform, 0);
+ processed.add(platform);
+ queue.push({ platform, 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 });
+ }
+ });
+ }
+
+ // 处理未连接的孤立节点
+ childPlatforms.forEach(platform => {
+ if (!processed.has(platform)) {
+ const maxLevel = Math.max(...Array.from(levelMap.values()), -1);
+ 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,
+ params: LayoutParams
+): Map => {
+ const nodePositions = new Map();
+ const subtreeRanges = new Map();
+
+ // 第一步:预计算每列需要的总高度
+ precomputeColumnHeights(levelsMap, parentToChildren, params.MIN_ROW_SPACING, subtreeRanges);
+
+ // 第二步:初步分配位置
+ initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions);
+
+ // 第三步:调整父节点使其位于子节点中心
+ adjustParentsToCenterOfChildren(levelsMap, parentToChildren, nodePositions);
+
+ // 第四步:同级节点防重叠检测与修正
+ preventOverlapsWithinSameLevel(levelsMap, parentToChildren, params.MIN_ROW_SPACING, nodePositions);
+
+ // 第五步:重新调整父节点恢复等腰三角形
+ restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions);
+
+ return nodePositions;
+};
+
+// 预计算每列高度
+const precomputeColumnHeights = (
+ levelsMap: Map,
+ parentToChildren: Map,
+ minRowSpacing: number,
+ subtreeRanges: Map
+): void => {
+ const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a);
+
+ sortedLevels.forEach(level => {
+ const platformsInLevel = levelsMap.get(level)!;
+ let totalHeight = 0;
+
+ platformsInLevel.forEach(platform => {
+ const children = parentToChildren.get(platform) || [];
+
+ if (children.length > 0) {
+ let maxSubtreeHeight = 0;
+
+ children.forEach(child => {
+ const childRange = subtreeRanges.get(child);
+ if (childRange) {
+ const childHeight = childRange.maxY - childRange.minY;
+ maxSubtreeHeight = Math.max(maxSubtreeHeight, childHeight);
+ }
+ });
+
+ const extraSpace = children.length > 1 ? (children.length - 1) * minRowSpacing : 0;
+ const subtreeHeight = maxSubtreeHeight + extraSpace;
+
+ totalHeight += Math.max(minRowSpacing, subtreeHeight);
+ subtreeRanges.set(platform, {
+ minY: totalHeight - subtreeHeight,
+ maxY: totalHeight
+ });
+ } else {
+ totalHeight += minRowSpacing;
+ subtreeRanges.set(platform, {
+ minY: totalHeight - minRowSpacing,
+ maxY: totalHeight
+ });
+ }
+ });
+ });
+};
+
+// 初步分配位置
+const initialPositionAssignment = (
+ levelsMap: Map,
+ parentToChildren: Map,
+ params: LayoutParams,
+ subtreeRanges: Map,
+ nodePositions: Map
+): void => {
+ const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
+ const columnOffsets = new Map();
+
+ forwardLevels.forEach(level => {
+ const platformsInLevel = levelsMap.get(level)!;
+ const x = params.ROOT_X + (level + 1) * params.COL_SPACING;
+ let currentYOffset = columnOffsets.get(level) || params.START_Y;
+
+ platformsInLevel.forEach(platform => {
+ const children = parentToChildren.get(platform) || [];
+ let y: number;
+
+ if (children.length > 0) {
+ // 有子节点的父节点:根据子树高度计算位置
+ const subtreeRange = subtreeRanges.get(platform);
+ if (subtreeRange) {
+ const subtreeHeight = subtreeRange.maxY - subtreeRange.minY;
+ y = currentYOffset + subtreeHeight / 2;
+ // 重要:为下一个节点预留足够的垂直空间
+ currentYOffset += Math.max(subtreeHeight, params.MIN_ROW_SPACING);
+ } else {
+ y = currentYOffset + params.MIN_ROW_SPACING / 2;
+ currentYOffset += params.MIN_ROW_SPACING;
+ }
+ } else {
+ // 叶子节点:占用固定空间
+ y = currentYOffset + params.MIN_ROW_SPACING / 2;
+ currentYOffset += params.MIN_ROW_SPACING;
+ }
+
+ nodePositions.set(platform, { x, y });
+ });
+
+ columnOffsets.set(level, currentYOffset);
+ });
+};
+
+// 调整父节点到子节点中心
+const adjustParentsToCenterOfChildren = (
+ levelsMap: Map,
+ parentToChildren: Map,
+ nodePositions: Map
+): 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 avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length;
+ const pos = nodePositions.get(platform)!;
+ pos.y = avgChildY;
+ nodePositions.set(platform, pos);
+ }
+ }
+ });
+ });
+};
+
+// 防重叠检测与修正(迭代式,确保完全无重叠)
+const preventOverlapsWithinSameLevel = (
+ levelsMap: Map,
+ parentToChildren: Map,
+ minRowSpacing: number,
+ nodePositions: Map
+): void => {
+ const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
+
+ forwardLevels.forEach(level => {
+ const platformsInLevel = levelsMap.get(level)!;
+
+ // 迭代检测直到没有重叠
+ let hasOverlap = true;
+ let iterations = 0;
+ const maxIterations = 20; // 防止无限循环
+
+ while (hasOverlap && iterations < maxIterations) {
+ hasOverlap = false;
+ iterations++;
+
+ // 每次都重新按Y坐标排序
+ const sortedByY = platformsInLevel
+ .map(p => ({ platform: p, pos: nodePositions.get(p)! }))
+ .sort((a, b) => a.pos.y - b.pos.y);
+
+ for (let i = 1; i < sortedByY.length; i++) {
+ const prev = sortedByY[i - 1];
+ const curr = sortedByY[i];
+
+ if (!prev || !curr) continue;
+
+ const actualDistance = curr.pos.y - prev.pos.y;
+
+ if (actualDistance < minRowSpacing) {
+ hasOverlap = true;
+
+ const offset = minRowSpacing - actualDistance;
+ curr.pos.y += offset;
+
+ // 递归调整所有后代节点
+ const adjustDescendants = (platform: string, yOffset: number) => {
+ const children = parentToChildren.get(platform) || [];
+ children.forEach(child => {
+ const childPos = nodePositions.get(child);
+ if (childPos) {
+ childPos.y += yOffset;
+ adjustDescendants(child, yOffset);
+ }
+ });
+ };
+
+ adjustDescendants(curr.platform, offset);
+
+ // 发现重叠并调整后,跳出内层循环,重新开始检测
+ break;
+ }
+ }
+ }
+
+ if (iterations >= maxIterations) {
+ console.warn(`Level ${level}: 防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`);
+ }
+ });
+};
+
+// 恢复等腰三角形
+const restoreIsoscelesTriangles = (
+ levelsMap: Map,
+ parentToChildren: Map,
+ nodePositions: Map
+): 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 avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length;
+ const pos = nodePositions.get(platform)!;
+ pos.y = avgChildY;
+ nodePositions.set(platform, pos);
+ }
+ }
+ });
+ });
+};
+
+// ==================== 辅助函数:创建节点对象 ====================
+
+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
+): void => {
+ if (nodeMap.size === 0 || !rootPlatformName) return;
+
+ const directChildren = parentToChildren.get(rootPlatformName) || [];
+
+ let centerY: number;
+ if (directChildren.length > 0) {
+ const childYPositions = directChildren.map(child => {
+ const childNode = nodeMap.get(child);
+ return childNode ? childNode.position.y : 0;
+ }).filter(y => y > 0);
+
+ if (childYPositions.length > 0) {
+ centerY = childYPositions.reduce((sum, y) => sum + y, 0) / childYPositions.length;
+ } else {
+ const allNodes = Array.from(nodeMap.values());
+ const minY = Math.min(...allNodes.map(n => n.position.y));
+ const maxY = Math.max(...allNodes.map(n => n.position.y));
+ centerY = (minY + maxY) / 2;
+ }
+ } else {
+ const allNodes = Array.from(nodeMap.values());
+ const minY = Math.min(...allNodes.map(n => n.position.y));
+ const maxY = Math.max(...allNodes.map(n => n.position.y));
+ centerY = (minY + maxY) / 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: {
+ x: 50, // ROOT_X
+ y: Math.round(centerY),
+ },
+ 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 推断策略
+ * @returns 图容器对象
+ */
+export const convertPlatformsToGraphContainer = (
+ platforms: PlatformWithComponents[],
+ strategy: InferenceStrategy = 'by-name-pattern'
+): GraphContainer => {
+ const records = convertPlatformsToCommunicationRecords(platforms, strategy);
+ return convertRecordsToGraphContainer(records);
+};
diff --git a/modeler/src/views/decision/communication/random-data-generator.ts b/modeler/src/views/decision/communication/random-data-generator.ts
new file mode 100644
index 0000000..9d81e11
--- /dev/null
+++ b/modeler/src/views/decision/communication/random-data-generator.ts
@@ -0,0 +1,143 @@
+/*
+ * 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 { 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 };
+};