feat: 增加通信关系图随机生成及后端数据加载功能

- 新增 data-converter.ts,实现通信记录到GraphContainer 的转换及自动布局算法
- 新增random-data-generator.ts,用于生成测试用的随机通信拓扑数据
- communication.vue中集成“随机生成”与“从后端加载”按钮及相关逻辑
- 支持多连通分量检测、层级布局计算及节点防重叠处理
This commit is contained in:
2026-04-14 11:39:22 +08:00
parent d8c238a9bb
commit 85b4a7dd2f
3 changed files with 1211 additions and 2 deletions

View File

@@ -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,14 +58,14 @@ 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';
@@ -66,6 +74,8 @@ 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 } 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() {
@@ -229,6 +241,8 @@ export default defineComponent({
graph: nodeGraph, graph: nodeGraph,
relations: [] relations: []
}; };
console.log(currentScenario.value);
currentScenarioEditing.value = true; currentScenarioEditing.value = true;
createElements(); createElements();
}; };
@@ -261,6 +275,7 @@ export default defineComponent({
}, 100); // 延迟一会儿,免得连线错位 }, 100); // 延迟一会儿,免得连线错位
} }
} }
}, 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(() => { onMounted(() => {
init(); init();
@@ -444,6 +602,8 @@ export default defineComponent({
handleDrop, handleDrop,
isDraggingOver, isDraggingOver,
handleSave, handleSave,
handleGenerateRandom,
handleLoadFromBackend,
handleUpdateElement, handleUpdateElement,
handleSelect, handleSelect,
}; };

View File

@@ -0,0 +1,906 @@
/*
* 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 { 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<string, string[]>;
childToParent: Map<string, string>;
}
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<string>();
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<string, string[]>,
childToParent: Map<string, string>
): Set<string>[] => {
const visited = new Set<string>();
const components: Set<string>[] = [];
// DFS遍历找出所有连通分量
const dfs = (platform: string, component: Set<string>) => {
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<string>();
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<string>
): CommunicationRecord[] => {
return records.filter(record =>
component.has(record.command_platform) &&
component.has(record.subordinate_platform)
);
};
// ==================== 辅助函数:构建父子映射 ====================
const buildParentChildMappings = (records: CommunicationRecord[]): ParentChildMappings => {
const parentToChildren = new Map<string, string[]>();
const childToParent = new Map<string, string>();
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<string, string[]>,
childToParent: Map<string, string>
): Map<string, number> => {
const levelMap = new Map<string, number>();
const processed = new Set<string>();
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<string, number>): Map<number, string[]> => {
const levelsMap = new Map<number, string[]>();
levelMap.forEach((level, platform) => {
if (!levelsMap.has(level)) {
levelsMap.set(level, []);
}
levelsMap.get(level)!.push(platform);
});
return levelsMap;
};
// ==================== 辅助函数:计算节点位置 ====================
const calculateNodePositions = (
levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>,
params: LayoutParams
): Map<string, NodePosition> => {
const nodePositions = new Map<string, NodePosition>();
const subtreeRanges = new Map<string, SubtreeRange>();
// 第一步:预计算每列需要的总高度
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<number, string[]>,
parentToChildren: Map<string, string[]>,
minRowSpacing: number,
subtreeRanges: Map<string, SubtreeRange>
): 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<number, string[]>,
parentToChildren: Map<string, string[]>,
params: LayoutParams,
subtreeRanges: Map<string, SubtreeRange>,
nodePositions: Map<string, NodePosition>
): void => {
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
const columnOffsets = new Map<number, number>();
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<number, string[]>,
parentToChildren: Map<string, string[]>,
nodePositions: Map<string, NodePosition>
): 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<number, string[]>,
parentToChildren: Map<string, string[]>,
minRowSpacing: number,
nodePositions: Map<string, NodePosition>
): 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<number, string[]>,
parentToChildren: Map<string, string[]>,
nodePositions: Map<string, NodePosition>
): 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<number, string[]>,
nodePositions: Map<string, NodePosition>
): Map<string, GraphTaskElement> => {
const nodeMap = new Map<string, GraphTaskElement>();
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<string, GraphTaskElement>,
rootPlatformName: string,
parentToChildren: Map<string, string[]>
): 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<string, GraphTaskElement>
): 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);
};

View File

@@ -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 };
};