添加行为决策树管理的场景数据到节点数据的自动转换逻辑

This commit is contained in:
2026-04-14 17:27:31 +08:00
parent e1fe2cf7da
commit 5a8c707340
4 changed files with 456 additions and 191 deletions

View File

@@ -8,7 +8,7 @@
*/ */
import { HttpRequestClient } from '@/utils/request'; import { HttpRequestClient } from '@/utils/request';
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest } from './types'; import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest, CommunicationRelationsResponse } from './types';
import type { PlatformWithComponentsResponse } from '../types'; import type { PlatformWithComponentsResponse } from '../types';
import type { BasicResponse } from '@/types'; import type { BasicResponse } from '@/types';
@@ -32,6 +32,15 @@ export const findPlatformWithComponents = (id: number): Promise<PlatformWithComp
return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`); return req.get<PlatformWithComponentsResponse>(`/system/firerule/platforms/${id}`);
}; };
/**
* 获取场景的所有通信关系
* @param id 场景ID
* @returns 通信关系列表
*/
export const findRelations = (id: number): Promise<CommunicationRelationsResponse> => {
return req.get<CommunicationRelationsResponse>(`/system/scene/getAllRelation/${id}`);
};
export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => { export const saveScenario = (scenario: Scenario): Promise<BasicResponse> => {
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario); return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
}; };

View File

@@ -72,7 +72,7 @@ import { createGraphScenarioElement, createGraphTaskElementFromScenario } from '
import PlatformCard from './platform-card.vue'; import PlatformCard from './platform-card.vue';
import NodesCard from './nodes-card.vue'; import NodesCard from './nodes-card.vue';
import { findOneScenarioById, saveScenario } from './api'; import { findOneScenarioById, saveScenario, findRelations } from './api';
import { resolveConnectionRelation } from './relation'; import { resolveConnectionRelation } from './relation';
import { generateRandomCommunicationData } from './random-data-generator'; import { generateRandomCommunicationData } from './random-data-generator';
import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter'; import { convertRecordsToGraphContainer, type CommunicationRecord } from './data-converter';
@@ -223,27 +223,77 @@ export default defineComponent({
} }
}; };
const handleSelect = (scenario: Scenario) => { const handleSelect = async (scenario: Scenario) => {
let nodeGraph: GraphContainer | null = null; let nodeGraph: GraphContainer | null = null;
try { try {
nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer; nodeGraph = JSON.parse(scenario.communicationGraph as unknown as string) as unknown as GraphContainer;
} catch (e: any) { } catch (e: any) {
console.error('parse error,cause:', e); console.error('parse error,cause:', e);
} }
if (!nodeGraph) {
nodeGraph = { // 设置当前场景
nodes: [],
edges: [],
};
}
currentScenario.value = { currentScenario.value = {
...scenario, ...scenario,
graph: nodeGraph, graph: nodeGraph || { nodes: [], edges: [] },
relations: [] relations: []
}; };
console.log(currentScenario.value); console.log('选中场景:', currentScenario.value);
currentScenarioEditing.value = true; currentScenarioEditing.value = true;
// 如果场景有ID且没有已保存的图数据尝试从后端加载通信关系
if (scenario.id > 0 && !nodeGraph) {
try {
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' });
}
} catch (error) {
console.error('从后端加载通信关系失败:', error);
message.error({ content: '加载通信关系失败,请手动点击"从后端加载"', key: 'loading-relations' });
}
}
createElements(); createElements();
}; };
@@ -505,25 +555,69 @@ export default defineComponent({
try { try {
message.loading({ content: '正在加载通信关系数据...', key: 'loading' }); message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
// TODO: 等待后端提供真实接口后替换为实际API调用 // 调用真实API获取通信关系
// const response = await findCommunicationRelations(currentScenario.value.id); console.log(`正在从后端加载场景 ${currentScenario.value.id} 的通信关系...`);
// const relations = response.data || []; const response = await findRelations(currentScenario.value.id);
// 临时使用模拟数据 console.log('API完整响应:', response);
console.log('使用模拟通信关系数据'); console.log('response.data类型:', typeof response.data, Array.isArray(response.data) ? 'Array' : 'Object');
const mockRelations: CommunicationRecord[] = [
{ id: 1, command_platform: '指挥中心', subordinate_platform: '预警机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id }, // API返回的是 CommunicationRelationRecord[],与 CommunicationRecord 结构兼容
{ 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 }, let relations: any[] = [];
{ id: 4, command_platform: '预警机', subordinate_platform: '电子战飞机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id }, if (Array.isArray(response.data)) {
{ id: 5, command_platform: '驱逐舰', subordinate_platform: '潜艇', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id }, relations = response.data;
{ id: 6, command_platform: '战斗机', subordinate_platform: '无人机', command_comm: undefined, subordinate_comm: undefined, scenary_id: currentScenario.value.id }, } 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('模拟的通信记录:', mockRelations); console.log('最终使用的通信记录:', normalizedRelations);
// 使用模拟数据进行转换 // 使用数据进行转换
const convertedGraph = convertRecordsToGraphContainer(mockRelations); const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
console.log('转换后的图数据:', convertedGraph); console.log('转换后的图数据:', convertedGraph);

View File

@@ -12,6 +12,22 @@ import { generateKey } from '@/utils/strings';
// ==================== 类型定义 ==================== // ==================== 类型定义 ====================
/**
* 布局方向类型
*/
export type LayoutDirection = 'horizontal' | 'vertical';
/**
* 布局配置接口
*/
export interface LayoutConfig {
direction: LayoutDirection;
rootPosition?: number; // 根节点起始位置
colSpacing?: number; // 列间距
rowSpacing?: number; // 行间距
startPosition?: number; // 起始位置
}
/** /**
* 数据库通信关系记录接口 * 数据库通信关系记录接口
*/ */
@@ -30,10 +46,11 @@ interface ParentChildMappings {
} }
interface LayoutParams { interface LayoutParams {
ROOT_X: number; ROOT_POSITION: number; // 根节点起始位置(horizontal时为X, vertical时为Y)
COL_SPACING: number; LEVEL_SPACING: number; // 层级间距(horizontal时为X间距, vertical时为Y间距)
MIN_ROW_SPACING: number; SIBLING_SPACING: number; // 同级间距(horizontal时为Y间距, vertical时为X间距)
START_Y: number; START_POSITION: number; // 起始位置(horizontal时为Y, vertical时为X)
direction: LayoutDirection;
} }
interface NodePosition { interface NodePosition {
@@ -42,31 +59,107 @@ interface NodePosition {
} }
interface SubtreeRange { interface SubtreeRange {
minY: number; min: number;
maxY: 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 格式 * 从数据库记录转换为 GraphContainer 格式
* @param records 数据库通信关系记录数组 * @param records 数据库通信关系记录数组
* @param config 布局配置(可选),默认为水平布局
* @returns GraphContainer 格式的节点和边数据 * @returns GraphContainer 格式的节点和边数据
*/ */
export const convertRecordsToGraphContainer = (records: CommunicationRecord[]): GraphContainer => { export const convertRecordsToGraphContainer = (
records: CommunicationRecord[],
config?: Partial<LayoutConfig>
): GraphContainer => {
if (!records || records.length === 0) { if (!records || records.length === 0) {
return { nodes: [], edges: [] }; 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<string>(); const platformSet = new Set<string>();
records.forEach(record => { records.forEach(record => {
platformSet.add(record.command_platform); if (record.command_platform && typeof record.command_platform === 'string' && record.command_platform.trim()) {
platformSet.add(record.subordinate_platform); 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); 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 { parentToChildren, childToParent } = buildParentChildMappings(records);
@@ -79,13 +172,11 @@ export const convertRecordsToGraphContainer = (records: CommunicationRecord[]):
const allNodes: GraphTaskElement[] = []; const allNodes: GraphTaskElement[] = [];
const allEdges: GraphEdgeElement[] = []; const allEdges: GraphEdgeElement[] = [];
let xOffset = 50; // 第一个分量的起始X坐标 let offset: number = finalConfig.rootPosition ?? 50; // 第一个分量的起始位置
connectedComponents.forEach((component, componentIndex) => { connectedComponents.forEach((component, componentIndex) => {
console.log(`处理第 ${componentIndex + 1} 个连通分量,包含 ${component.size} 个节点`); // 为该分量确定根节点选择入度为0的节点
const rootPlatformName = determineRootNode(Array.from(component), childToParent);
// 为该分量确定根节点
const rootPlatformName = determineRootNode(Array.from(component));
if (!rootPlatformName) { if (!rootPlatformName) {
console.warn(`${componentIndex + 1} 个分量未找到根节点,跳过`); console.warn(`${componentIndex + 1} 个分量未找到根节点,跳过`);
@@ -100,29 +191,48 @@ export const convertRecordsToGraphContainer = (records: CommunicationRecord[]):
childToParent childToParent
); );
console.log(` 分量 ${componentIndex + 1} 的层级映射:`, Object.fromEntries(levelMap));
// 按层级分组 // 按层级分组
const levelsMap = groupNodesByLevel(levelMap); const levelsMap = groupNodesByLevel(levelMap);
// 布局参数 - 紧凑型布局 console.log(` 分量 ${componentIndex + 1} 的层级分组:`, Object.fromEntries(levelsMap));
// 根据方向创建布局参数
const layoutParams: LayoutParams = { const layoutParams: LayoutParams = {
ROOT_X: xOffset, // 当前分量的起始X坐标 ROOT_POSITION: offset ?? 50,
COL_SPACING: 280, // 列间距 LEVEL_SPACING: finalConfig.colSpacing ?? 280,
MIN_ROW_SPACING: 140, // 最小行间距 SIBLING_SPACING: finalConfig.rowSpacing ?? 140,
START_Y: 80, // 起始Y坐标 START_POSITION: finalConfig.startPosition ?? 80,
direction: finalConfig.direction,
}; };
// 创建坐标映射器
const mapper = finalConfig.direction === 'horizontal'
? createHorizontalMapper(layoutParams)
: createVerticalMapper(layoutParams);
// 计算节点位置 // 计算节点位置
const nodePositions = calculateNodePositions( const nodePositions = calculateNodePositions(
levelsMap, levelsMap,
parentToChildren, parentToChildren,
layoutParams childToParent,
layoutParams,
mapper
); );
// 创建节点对象 // 创建节点对象
const nodeMap = createNodeObjects(levelsMap, nodePositions); const nodeMap = createNodeObjects(levelsMap, nodePositions);
// 创建根节点 console.log(` 分量 ${componentIndex + 1} 创建了 ${nodeMap.size} 个节点:`, Array.from(nodeMap.keys()));
createRootNode(nodeMap, rootPlatformName, parentToChildren);
// 创建根节点(如果不存在则添加)
if (!nodeMap.has(rootPlatformName)) {
console.warn(` 根节点 "${rootPlatformName}" 未在 levelsMap 中找到,手动创建`);
}
createRootNode(nodeMap, rootPlatformName, parentToChildren, layoutParams, mapper);
console.log(` 最终节点数: ${nodeMap.size}`);
// 过滤出属于当前分量的边 // 过滤出属于当前分量的边
const componentEdges = filterEdgesForComponent(records, component); const componentEdges = filterEdgesForComponent(records, component);
@@ -134,7 +244,7 @@ export const convertRecordsToGraphContainer = (records: CommunicationRecord[]):
// 计算下一个分量的偏移量(基于当前分量的宽度) // 计算下一个分量的偏移量(基于当前分量的宽度)
const maxLevel = Math.max(...Array.from(levelsMap.keys())); const maxLevel = Math.max(...Array.from(levelsMap.keys()));
xOffset += (maxLevel + 1) * layoutParams.COL_SPACING + 150; // 分量之间留150px间隔 offset += (maxLevel + 1) * layoutParams.LEVEL_SPACING + 150; // 分量之间留150px间隔
}); });
return { return {
@@ -153,6 +263,9 @@ const findConnectedComponents = (
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
childToParent: Map<string, string> childToParent: Map<string, string>
): Set<string>[] => { ): Set<string>[] => {
// 过滤掉undefined、null和空字符串
const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0);
const visited = new Set<string>(); const visited = new Set<string>();
const components: Set<string>[] = []; const components: Set<string>[] = [];
@@ -165,21 +278,28 @@ const findConnectedComponents = (
// 访问子节点 // 访问子节点
const children = parentToChildren.get(platform) || []; const children = parentToChildren.get(platform) || [];
children.forEach(child => dfs(child, component)); children.forEach(child => {
// 只处理有效的子节点
if (child && typeof child === 'string' && child.trim().length > 0) {
dfs(child, component);
}
});
// 访问父节点 // 访问父节点
const parent = childToParent.get(platform); const parent = childToParent.get(platform);
if (parent && !visited.has(parent)) { if (parent && !visited.has(parent) && typeof parent === 'string' && parent.trim().length > 0) {
dfs(parent, component); dfs(parent, component);
} }
}; };
// 对每个未访问的节点启动一次DFS // 对每个未访问的有效节点启动一次DFS
platforms.forEach(platform => { validPlatforms.forEach(platform => {
if (!visited.has(platform)) { if (!visited.has(platform)) {
const component = new Set<string>(); const component = new Set<string>();
dfs(platform, component); dfs(platform, component);
components.push(component); if (component.size > 0) {
components.push(component);
}
} }
}); });
@@ -188,18 +308,26 @@ const findConnectedComponents = (
/** /**
* 为连通分量确定根节点 * 为连通分量确定根节点
* 优先级1. 包含"指挥"关键词 2. 入度为0的节点 3. 第一个节点 * 策略选择入度为0的节点没有父节点的节点如果有多个则选择第一个
*/ */
const determineRootNode = (platforms: string[]): string | null => { const determineRootNode = (
if (platforms.length === 0) return null; platforms: string[],
childToParent: Map<string, string>
): string | null => {
// 过滤掉undefined、null和空字符串
const validPlatforms = platforms.filter(p => p && typeof p === 'string' && p.trim().length > 0);
// 优先选择包含"指挥"的节点 if (validPlatforms.length === 0) return null;
const commandNode = platforms.find(p => p.includes('指挥'));
if (commandNode) return commandNode;
// 其次选择入度为0的节点没有父节点 // 优先选择入度为0的节点没有父节点
// 这里简化处理,直接返回第一个 const rootCandidates = validPlatforms.filter(platform => !childToParent.has(platform));
return platforms[0] || null;
if (rootCandidates.length > 0) {
return rootCandidates[0] || null;
}
// 如果所有节点都有父节点(可能是环形结构),返回第一个有效节点
return validPlatforms[0] || null;
}; };
/** /**
@@ -244,16 +372,16 @@ const computeNodeLevels = (
const processed = new Set<string>(); const processed = new Set<string>();
const queue: Array<{ platform: string; level: number }> = []; const queue: Array<{ platform: string; level: number }> = [];
// 分离根节点和子节 // 找出所有入度为0的节点没有父节点的节点作为BFS的起
const childPlatforms = platforms.filter(p => p !== rootPlatformName); const rootNodes = platforms.filter(p => !childToParent.has(p));
// 第一层:直接与根节点相连的节点 console.log(` BFS起点入度为0的节点: [${rootNodes.join(', ')}]`);
childPlatforms.forEach(platform => {
if (childToParent.get(platform) === rootPlatformName) { // 将所有根节点加入队列层级为0
levelMap.set(platform, 0); rootNodes.forEach(root => {
processed.add(platform); levelMap.set(root, 0);
queue.push({ platform, level: 0 }); processed.add(root);
} queue.push({ platform: root, level: 0 });
}); });
// BFS遍历后续层级 // BFS遍历后续层级
@@ -270,13 +398,23 @@ const computeNodeLevels = (
}); });
} }
// 处理未连接的孤立节点 // 理论上不应该有未处理的节点,因为所有节点都应该从某个根节点可达
childPlatforms.forEach(platform => { // 如果有,说明图中存在环或者数据有问题
if (!processed.has(platform)) { const unprocessed = platforms.filter(p => !processed.has(p));
const maxLevel = Math.max(...Array.from(levelMap.values()), -1); if (unprocessed.length > 0) {
levelMap.set(platform, maxLevel + 1); 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; return levelMap;
}; };
@@ -299,25 +437,30 @@ const groupNodesByLevel = (levelMap: Map<string, number>): Map<number, string[]>
const calculateNodePositions = ( const calculateNodePositions = (
levelsMap: Map<number, string[]>, levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
params: LayoutParams childToParent: Map<string, string>,
params: LayoutParams,
mapper: CoordinateMapper
): Map<string, NodePosition> => { ): Map<string, NodePosition> => {
const nodePositions = new Map<string, NodePosition>(); const nodePositions = new Map<string, NodePosition>();
const subtreeRanges = new Map<string, SubtreeRange>(); const subtreeRanges = new Map<string, SubtreeRange>();
// 第一步:预计算每列需要的总高度 // 第一步:预计算每列需要的总高度
precomputeColumnHeights(levelsMap, parentToChildren, params.MIN_ROW_SPACING, subtreeRanges); precomputeColumnHeights(levelsMap, parentToChildren, params.SIBLING_SPACING, subtreeRanges, mapper);
// 第二步:初步分配位置 // 第二步:初步分配位置
initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions); initialPositionAssignment(levelsMap, parentToChildren, params, subtreeRanges, nodePositions, mapper);
// 第三步:调整父节点使其位于子节点中心 // 第三步:调整父节点使其位于子节点中心(处理共享子节点情况)
adjustParentsToCenterOfChildren(levelsMap, parentToChildren, nodePositions); adjustParentsToCenterOfChildren(levelsMap, parentToChildren, childToParent, nodePositions, mapper);
// 第四步:同级节点防重叠检测与修正 // 第四步:全局防重叠检测与修正(跨所有层级)
preventOverlapsWithinSameLevel(levelsMap, parentToChildren, params.MIN_ROW_SPACING, nodePositions); preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper);
// 第五步:重新调整父节点恢复等腰三角形 // 第五步:重新调整父节点恢复等腰三角形
restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions); restoreIsoscelesTriangles(levelsMap, parentToChildren, nodePositions, mapper);
// 第六步:最终全局防重叠检查
preventGlobalOverlaps(parentToChildren, params.SIBLING_SPACING, nodePositions, mapper);
return nodePositions; return nodePositions;
}; };
@@ -326,90 +469,68 @@ const calculateNodePositions = (
const precomputeColumnHeights = ( const precomputeColumnHeights = (
levelsMap: Map<number, string[]>, levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
minRowSpacing: number, siblingSpacing: number,
subtreeRanges: Map<string, SubtreeRange> subtreeRanges: Map<string, SubtreeRange>,
mapper: CoordinateMapper
): void => { ): void => {
const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a); const sortedLevels = Array.from(levelsMap.keys()).sort((a, b) => b - a);
sortedLevels.forEach(level => { sortedLevels.forEach(level => {
const platformsInLevel = levelsMap.get(level)!; const platformsInLevel = levelsMap.get(level)!;
let totalHeight = 0; let totalSize = 0;
platformsInLevel.forEach(platform => { platformsInLevel.forEach(platform => {
const children = parentToChildren.get(platform) || []; const children = parentToChildren.get(platform) || [];
if (children.length > 0) { if (children.length > 0) {
let maxSubtreeHeight = 0; let maxSubtreeSize = 0;
children.forEach(child => { children.forEach(child => {
const childRange = subtreeRanges.get(child); const childRange = subtreeRanges.get(child);
if (childRange) { if (childRange) {
const childHeight = childRange.maxY - childRange.minY; const range = mapper.getSubtreePrimaryCoord(childRange);
maxSubtreeHeight = Math.max(maxSubtreeHeight, childHeight); const childSize = range.max - range.min;
maxSubtreeSize = Math.max(maxSubtreeSize, childSize);
} }
}); });
const extraSpace = children.length > 1 ? (children.length - 1) * minRowSpacing : 0; const subtreeSize = maxSubtreeSize;
const subtreeHeight = maxSubtreeHeight + extraSpace;
totalHeight += Math.max(minRowSpacing, subtreeHeight); totalSize += Math.max(siblingSpacing, subtreeSize);
subtreeRanges.set(platform, { subtreeRanges.set(platform, mapper.createSubtreeRange(
minY: totalHeight - subtreeHeight, totalSize - subtreeSize,
maxY: totalHeight totalSize
}); ));
} else { } else {
totalHeight += minRowSpacing; totalSize += siblingSpacing;
subtreeRanges.set(platform, { subtreeRanges.set(platform, mapper.createSubtreeRange(
minY: totalHeight - minRowSpacing, totalSize - siblingSpacing,
maxY: totalHeight totalSize
}); ));
} }
}); });
}); });
}; };
// 初步分配位置 // 初步分配位置(简化版:所有节点均匀分布,不考虑子树高度)
const initialPositionAssignment = ( const initialPositionAssignment = (
levelsMap: Map<number, string[]>, levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
params: LayoutParams, params: LayoutParams,
subtreeRanges: Map<string, SubtreeRange>, subtreeRanges: Map<string, SubtreeRange>,
nodePositions: Map<string, NodePosition> nodePositions: Map<string, NodePosition>,
mapper: CoordinateMapper
): void => { ): void => {
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
const columnOffsets = new Map<number, number>();
forwardLevels.forEach(level => { forwardLevels.forEach(level => {
const platformsInLevel = levelsMap.get(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 => { platformsInLevel.forEach((platform, index) => {
const children = parentToChildren.get(platform) || []; const primaryCoord = mapper.mapLevelToAxis(level);
let y: number; const secondaryCoord = mapper.mapSiblingIndexToAxis(index, platformsInLevel.length);
nodePositions.set(platform, mapper.setCoords(primaryCoord, secondaryCoord));
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);
}); });
}; };
@@ -417,10 +538,13 @@ const initialPositionAssignment = (
const adjustParentsToCenterOfChildren = ( const adjustParentsToCenterOfChildren = (
levelsMap: Map<number, string[]>, levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
nodePositions: Map<string, NodePosition> childToParent: Map<string, string>,
nodePositions: Map<string, NodePosition>,
mapper: CoordinateMapper
): void => { ): void => {
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
// 从最底层向上遍历,确保子节点位置已经确定后再调整父节点
[...forwardLevels].reverse().forEach(level => { [...forwardLevels].reverse().forEach(level => {
const platformsInLevel = levelsMap.get(level)!; const platformsInLevel = levelsMap.get(level)!;
@@ -431,64 +555,78 @@ const adjustParentsToCenterOfChildren = (
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean); const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
if (childPositions.length > 0) { if (childPositions.length > 0) {
const avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length; // 计算所有子节点的平均次轴坐标垂直布局时是X水平布局时是Y
const avgSecondaryCoord = childPositions.reduce((sum, pos) =>
sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length;
// 将父节点的次轴坐标移动到子节点的中心
const pos = nodePositions.get(platform)!; const pos = nodePositions.get(platform)!;
pos.y = avgChildY; const primaryCoord = mapper.getPrimaryCoord(pos);
nodePositions.set(platform, pos); nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord));
} }
} }
}); });
}); });
}; };
// 防重叠检测与修正(迭代式确保完全无重叠) // 全局防重叠检测与修正(跨所有层级,迭代式确保完全无重叠)
const preventOverlapsWithinSameLevel = ( const preventGlobalOverlaps = (
levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
minRowSpacing: number, siblingSpacing: number,
nodePositions: Map<string, NodePosition> nodePositions: Map<string, NodePosition>,
mapper: CoordinateMapper
): void => { ): void => {
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); // 收集所有节点并按主轴坐标分组(同层的节点)
const nodesByLevel = new Map<number, Array<{ platform: string; pos: NodePosition }>>();
forwardLevels.forEach(level => { nodePositions.forEach((pos, platform) => {
const platformsInLevel = levelsMap.get(level)!; 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 hasOverlap = true;
let iterations = 0; let iterations = 0;
const maxIterations = 20; // 防止无限循环 const maxIterations = 30; // 防止无限循环
while (hasOverlap && iterations < maxIterations) { while (hasOverlap && iterations < maxIterations) {
hasOverlap = false; hasOverlap = false;
iterations++; iterations++;
// 每次都重新按Y坐标排序 // 按次轴坐标排序
const sortedByY = platformsInLevel nodes.sort((a, b) => mapper.getSecondaryCoord(a.pos) - mapper.getSecondaryCoord(b.pos));
.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++) { for (let i = 1; i < nodes.length; i++) {
const prev = sortedByY[i - 1]; const prev = nodes[i - 1];
const curr = sortedByY[i]; const curr = nodes[i];
if (!prev || !curr) continue; if (!prev || !curr) continue;
const actualDistance = curr.pos.y - prev.pos.y; const actualDistance = mapper.getSecondaryCoord(curr.pos) - mapper.getSecondaryCoord(prev.pos);
if (actualDistance < minRowSpacing) { if (actualDistance < siblingSpacing) {
hasOverlap = true; hasOverlap = true;
const offset = minRowSpacing - actualDistance; const offset = siblingSpacing - actualDistance;
curr.pos.y += offset; const currSecondary = mapper.getSecondaryCoord(curr.pos);
const currPrimary = mapper.getPrimaryCoord(curr.pos);
curr.pos = mapper.setCoords(currPrimary, currSecondary + offset);
// 递归调整所有后代节点 // 递归调整所有后代节点
const adjustDescendants = (platform: string, yOffset: number) => { const adjustDescendants = (platform: string, secondaryOffset: number) => {
const children = parentToChildren.get(platform) || []; const children = parentToChildren.get(platform) || [];
children.forEach(child => { children.forEach(child => {
const childPos = nodePositions.get(child); const childPos = nodePositions.get(child);
if (childPos) { if (childPos) {
childPos.y += yOffset; const childPrimary = mapper.getPrimaryCoord(childPos);
adjustDescendants(child, yOffset); const childSecondary = mapper.getSecondaryCoord(childPos);
const newPos = mapper.setCoords(childPrimary, childSecondary + secondaryOffset);
nodePositions.set(child, newPos);
adjustDescendants(child, secondaryOffset);
} }
}); });
}; };
@@ -502,7 +640,7 @@ const preventOverlapsWithinSameLevel = (
} }
if (iterations >= maxIterations) { if (iterations >= maxIterations) {
console.warn(`Level ${level}: 防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`); console.warn(`某层防重叠达到最大迭代次数(${maxIterations}),可能存在未解决的重叠`);
} }
}); });
}; };
@@ -511,7 +649,8 @@ const preventOverlapsWithinSameLevel = (
const restoreIsoscelesTriangles = ( const restoreIsoscelesTriangles = (
levelsMap: Map<number, string[]>, levelsMap: Map<number, string[]>,
parentToChildren: Map<string, string[]>, parentToChildren: Map<string, string[]>,
nodePositions: Map<string, NodePosition> nodePositions: Map<string, NodePosition>,
mapper: CoordinateMapper
): void => { ): void => {
const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b); const forwardLevels = Array.from(levelsMap.keys()).sort((a, b) => a - b);
@@ -525,10 +664,11 @@ const restoreIsoscelesTriangles = (
const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean); const childPositions = children.map(child => nodePositions.get(child)).filter(Boolean);
if (childPositions.length > 0) { if (childPositions.length > 0) {
const avgChildY = childPositions.reduce((sum, pos) => sum + pos!.y, 0) / childPositions.length; const avgSecondaryCoord = childPositions.reduce((sum, pos) =>
sum + mapper.getSecondaryCoord(pos!), 0) / childPositions.length;
const pos = nodePositions.get(platform)!; const pos = nodePositions.get(platform)!;
pos.y = avgChildY; const primaryCoord = mapper.getPrimaryCoord(pos);
nodePositions.set(platform, pos); nodePositions.set(platform, mapper.setCoords(primaryCoord, avgSecondaryCoord));
} }
} }
}); });
@@ -594,32 +734,36 @@ const createNodeObjects = (
const createRootNode = ( const createRootNode = (
nodeMap: Map<string, GraphTaskElement>, nodeMap: Map<string, GraphTaskElement>,
rootPlatformName: string, rootPlatformName: string,
parentToChildren: Map<string, string[]> parentToChildren: Map<string, string[]>,
params: LayoutParams,
mapper: CoordinateMapper
): void => { ): void => {
if (nodeMap.size === 0 || !rootPlatformName) return; if (nodeMap.size === 0 || !rootPlatformName) return;
const directChildren = parentToChildren.get(rootPlatformName) || []; const directChildren = parentToChildren.get(rootPlatformName) || [];
let centerY: number; let centerSecondaryCoord: number;
if (directChildren.length > 0) { if (directChildren.length > 0) {
const childYPositions = directChildren.map(child => { const childSecondaryPositions = directChildren.map(child => {
const childNode = nodeMap.get(child); const childNode = nodeMap.get(child);
return childNode ? childNode.position.y : 0; return childNode ? mapper.getSecondaryCoord(childNode.position) : 0;
}).filter(y => y > 0); }).filter(coord => coord > 0);
if (childYPositions.length > 0) { if (childSecondaryPositions.length > 0) {
centerY = childYPositions.reduce((sum, y) => sum + y, 0) / childYPositions.length; centerSecondaryCoord = childSecondaryPositions.reduce((sum, coord) => sum + coord, 0) / childSecondaryPositions.length;
} else { } else {
const allNodes = Array.from(nodeMap.values()); const allNodes = Array.from(nodeMap.values());
const minY = Math.min(...allNodes.map(n => n.position.y)); const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position));
const maxY = Math.max(...allNodes.map(n => n.position.y)); const minCoord = Math.min(...coords);
centerY = (minY + maxY) / 2; const maxCoord = Math.max(...coords);
centerSecondaryCoord = (minCoord + maxCoord) / 2;
} }
} else { } else {
const allNodes = Array.from(nodeMap.values()); const allNodes = Array.from(nodeMap.values());
const minY = Math.min(...allNodes.map(n => n.position.y)); const coords = allNodes.map(n => mapper.getSecondaryCoord(n.position));
const maxY = Math.max(...allNodes.map(n => n.position.y)); const minCoord = Math.min(...coords);
centerY = (minY + maxY) / 2; const maxCoord = Math.max(...coords);
centerSecondaryCoord = (minCoord + maxCoord) / 2;
} }
const rootNode: GraphTaskElement = { const rootNode: GraphTaskElement = {
@@ -644,10 +788,7 @@ const createRootNode = (
group: null, group: null,
description: rootPlatformName, description: rootPlatformName,
order: 0, order: 0,
position: { position: mapper.setCoords(params.ROOT_POSITION, Math.round(centerSecondaryCoord)),
x: 50, // ROOT_X
y: Math.round(centerY),
},
width: 250, width: 250,
height: 145, height: 145,
inputs: null, inputs: null,
@@ -895,12 +1036,14 @@ const createStarConnection = (
* 便捷函数:直接将平台列表转换为图容器 * 便捷函数:直接将平台列表转换为图容器
* @param platforms 平台列表 * @param platforms 平台列表
* @param strategy 推断策略 * @param strategy 推断策略
* @param layoutConfig 布局配置(可选)
* @returns 图容器对象 * @returns 图容器对象
*/ */
export const convertPlatformsToGraphContainer = ( export const convertPlatformsToGraphContainer = (
platforms: PlatformWithComponents[], platforms: PlatformWithComponents[],
strategy: InferenceStrategy = 'by-name-pattern' strategy: InferenceStrategy = 'by-name-pattern',
layoutConfig?: Partial<LayoutConfig>
): GraphContainer => { ): GraphContainer => {
const records = convertPlatformsToCommunicationRecords(platforms, strategy); const records = convertPlatformsToCommunicationRecords(platforms, strategy);
return convertRecordsToGraphContainer(records); return convertRecordsToGraphContainer(records, layoutConfig);
}; };

View File

@@ -48,3 +48,22 @@ export interface ScenarioDetailsResponse extends ApiDataResponse<Scenario> {
} }
/**
* 通信关系记录(对应数据库表结构)
*/
export interface CommunicationRelationRecord {
id: number;
command_platform: string; // 指挥平台名称
subordinate_platform: string; // 下属平台名称
command_comm?: string; // 指挥端通信方式
subordinate_comm?: string; // 下属端通信方式
scenary_id?: number; // 场景ID
}
/**
* 获取场景所有通信关系的响应
*/
export interface CommunicationRelationsResponse extends ApiDataResponse<CommunicationRelationRecord[]> {
}