Merge branch 'feature-transform-node-data'

This commit is contained in:
2026-04-14 17:30:54 +08:00
24 changed files with 2406 additions and 37 deletions

View File

@@ -8,7 +8,7 @@
*/
import { HttpRequestClient } from '@/utils/request';
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest } from './types';
import type { Scenario, ScenarioDetailsResponse, ScenarioPageableResponse, ScenarioRequest, CommunicationRelationsResponse } from './types';
import type { PlatformWithComponentsResponse } from '../types';
import type { BasicResponse } from '@/types';
import type { BehaviorTree } from '../designer/tree';
@@ -33,6 +33,15 @@ export const findPlatformWithComponents = (id: number): Promise<PlatformWithComp
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> => {
return req.postJson<BasicResponse>(`/system/scene/saveSceneConfig`,scenario);
};

View File

@@ -21,6 +21,14 @@
<div class="ks-model-builder-content" style="width: calc(100% - 250px);">
<div class="ks-model-builder-actions">
<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">
<CheckOutlined />
<span>保存</span>
@@ -50,22 +58,24 @@ import { useRoute, useRouter } from 'vue-router';
import { message } from 'ant-design-vue';
import { getTeleport } from '@antv/x6-vue-shape';
import { Graph, Node, type NodeProperties } from '@antv/x6';
import { CheckCircleOutlined, CheckOutlined, RollbackOutlined, SaveOutlined } from '@ant-design/icons-vue';
import { CheckCircleOutlined, CheckOutlined, DatabaseOutlined, RollbackOutlined, SaveOutlined, ThunderboltOutlined } from '@ant-design/icons-vue';
import { Wrapper } from '@/components/wrapper';
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
import Header from '../header.vue';
import type { Scenario } from './types';
import type { PlatformWithComponents } from '../types';
import { createLineOptions, type GraphContainer, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
import { createLineOptions, type GraphContainer, type GraphEdgeElement, type GraphTaskElement, resolveGraph, useGraphCanvas } from '../graph';
import { registerScenarioElement } from './register';
import { createGraphScenarioElement, createGraphTaskElementFromScenario } from './utils';
import PlatformCard from './platform-card.vue';
import NodesCard from './nodes-card.vue';
import { findOneScenarioById, saveScenario, getAllBehaviorTreesBySceneId } from './api';
import { findOneScenarioById, saveScenario, findRelations } 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() {
@@ -218,36 +230,67 @@ export default defineComponent({
} catch (e: any) {
console.error('parse error,cause:', e);
}
if (!nodeGraph) {
nodeGraph = {
nodes: [],
edges: [],
};
}
// 设置当前场景
currentScenario.value = {
...scenario,
graph: nodeGraph,
graph: nodeGraph || { nodes: [], edges: [] },
relations: []
};
console.log('选中场景:', currentScenario.value);
currentScenarioEditing.value = true;
// 场景ID存储到graph对象中供子组件访问
if (graph.value) {
(graph.value as any).currentScenario = currentScenario.value;
// 加载该场景下的行为树列表
// 如果场景ID且没有已保存的图数据,尝试从后端加载通信关系
if (scenario.id > 0 && !nodeGraph) {
try {
const response = await getAllBehaviorTreesBySceneId(scenario.id);
if (response.code === 200 && response.data) {
(graph.value as any).behaviorTrees = response.data;
console.log(`加载场景${scenario.id}的行为树列表:`, response.data.length, '个');
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 {
(graph.value as any).behaviorTrees = [];
console.warn('获取行为树列表失败:', response.msg);
message.warning({ content: '该场景暂无通信关系数据', key: 'loading-relations' });
}
} catch (error) {
console.error('获取行为树列表失败:', error);
(graph.value as any).behaviorTrees = [];
console.error('从后端加载通信关系失败:', error);
message.error({ content: '加载通信关系失败,请手动点击"从后端加载"', key: 'loading-relations' });
}
}
@@ -282,6 +325,7 @@ export default defineComponent({
}, 100); // 延迟一会儿,免得连线错位
}
}
}, 100);
});
};
@@ -442,6 +486,193 @@ export default defineComponent({
});
};
// 随机生成节点流图
const handleGenerateRandom = () => {
if (!graph.value) {
message.error('画布未初始化');
return;
}
try {
// 生成随机数据
const { records, graph: randomGraph } = generateRandomCommunicationData(30);
console.log('生成的随机数据:', records);
console.log('转换后的图数据:', randomGraph);
// 清空现有内容
graph.value.clearCells();
// 设置当前场景
if (!currentScenario.value) {
currentScenario.value = {
id: 0,
name: `随机场景_${Date.now()}`,
description: '自动生成的测试场景',
communicationGraph: null,
relations: [],
graph: randomGraph,
};
} else {
currentScenario.value.graph = randomGraph;
currentScenario.value.communicationGraph = JSON.stringify(randomGraph);
}
// 渲染节点
setTimeout(() => {
if (randomGraph.nodes) {
randomGraph.nodes.forEach(ele => {
const node = createGraphScenarioElement(ele as GraphTaskElement);
graph.value?.addNode(node as Node);
});
}
// 延迟添加边,确保节点已渲染
setTimeout(() => {
if (randomGraph.edges) {
randomGraph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
});
}
// 自动适应视图
fitToScreen();
message.success(`已生成 ${randomGraph.nodes.length} 个节点和 ${randomGraph.edges.length} 条连接线`);
}, 100);
}, 50);
} catch (error) {
console.error('随机生成时出错:', error);
message.error('生成失败,请重试');
}
};
// 从后端加载平台数据并转换为通信关系图(当前使用模拟数据)
const handleLoadFromBackend = async () => {
if (!graph.value || !currentScenario.value) {
message.error('请先选择场景');
return;
}
try {
message.loading({ content: '正在加载通信关系数据...', key: 'loading' });
// 调用真实API获取通信关系
console.log(`正在从后端加载场景 ${currentScenario.value.id} 的通信关系...`);
const response = await findRelations(currentScenario.value.id);
console.log('API完整响应:', response);
console.log('response.data类型:', typeof response.data, Array.isArray(response.data) ? 'Array' : 'Object');
// API返回的是 CommunicationRelationRecord[],与 CommunicationRecord 结构兼容
// 处理可能的多种返回格式
let relations: any[] = [];
if (Array.isArray(response.data)) {
relations = response.data;
} else if (response.data && Array.isArray((response.data as any).data)) {
relations = (response.data as any).data;
} else if (response.data && Array.isArray((response.data as any).rows)) {
relations = (response.data as any).rows;
} else if (response.data && Array.isArray((response.data as any).list)) {
relations = (response.data as any).list;
}
console.log('解析后的通信关系数量:', relations.length);
if (relations.length > 0) {
console.log('第一条记录:', JSON.stringify(relations[0], null, 2));
}
// 后端返回的是驼峰命名,需要转换为下划线命名以匹配前端类型
const normalizedRelations = relations.map((item: any) => ({
id: item.id,
command_platform: item.commandPlatform || item.command_platform,
subordinate_platform: item.subordinatePlatform || item.subordinate_platform,
command_comm: item.commandComm || item.command_comm,
subordinate_comm: item.subordinateComm || item.subordinate_comm,
scenary_id: item.scenaryId || item.scenary_id,
}));
console.log('标准化后的第一条记录:', normalizedRelations[0]);
if (normalizedRelations.length === 0) {
console.warn('API未返回任何通信关系数据使用模拟数据作为fallback');
// Fallback到模拟数据保留以便测试
relations.push(
{ id: 6, command_platform: 'chief', subordinate_platform: 'task1_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 7, command_platform: 'chief', subordinate_platform: 'task2_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 8, command_platform: 'chief', subordinate_platform: 'task3_commander', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 9, command_platform: 'task1_commander', subordinate_platform: 'platform1', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 10, command_platform: 'task1_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 11, command_platform: 'task1_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 12, command_platform: 'task1_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 13, command_platform: 'task1_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 14, command_platform: 'task2_commander', subordinate_platform: 'platform3', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 15, command_platform: 'task2_commander', subordinate_platform: 'platform5', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 16, command_platform: 'task2_commander', subordinate_platform: 'platform4', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 17, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 18, command_platform: 'task3_commander', subordinate_platform: 'platform6', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 19, command_platform: 'task3_commander', subordinate_platform: 'platform7', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
{ id: 20, command_platform: 'task3_commander', subordinate_platform: 'platform8', command_comm: 'radio', subordinate_comm: 'radio', scenary_id: currentScenario.value.id },
);
}
console.log('最终使用的通信记录:', normalizedRelations);
// 使用数据进行转换
const convertedGraph = convertRecordsToGraphContainer(normalizedRelations);
console.log('转换后的图数据:', convertedGraph);
// 清空现有内容
graph.value.clearCells();
// 更新当前场景
currentScenario.value.graph = convertedGraph;
currentScenario.value.communicationGraph = JSON.stringify(convertedGraph);
// 渲染节点
setTimeout(() => {
if (convertedGraph.nodes) {
convertedGraph.nodes.forEach(ele => {
const node = createGraphScenarioElement(ele as GraphTaskElement);
graph.value?.addNode(node as Node);
});
}
// 延迟添加边,确保节点已渲染
setTimeout(() => {
if (convertedGraph.edges) {
convertedGraph.edges.forEach(edgeData => {
graph.value?.addEdge({
...edgeData,
...createLineOptions(),
});
});
}
// 自动适应视图
fitToScreen();
message.success({
content: `已从后端加载 ${convertedGraph.nodes.length} 个平台和 ${convertedGraph.edges.length} 条连接关系`,
key: 'loading'
});
}, 100);
}, 50);
} catch (error) {
console.error('从后端加载时出错:', error);
message.error({
content: error instanceof Error ? error.message : '加载失败,请重试',
key: 'loading'
});
}
};
// 初始化
onMounted(() => {
init();
@@ -471,6 +702,8 @@ export default defineComponent({
handleDrop,
isDraggingOver,
handleSave,
handleGenerateRandom,
handleLoadFromBackend,
handleUpdateElement,
handleSelect,
};

File diff suppressed because it is too large Load Diff

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

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[]> {
}