Files
auto-solution/modeler/src/views/decision/communication/node.vue

453 lines
12 KiB
Vue
Raw Normal View History

2026-02-08 15:59:14 +08:00
<template>
<div>
<a-dropdown
:trigger="['contextmenu']"
:getPopupContainer="getPopupContainer"
@openChange="handleVisibleChange"
2026-02-08 17:57:40 +08:00
>
<a-card
:class="[
'ks-scenario-node',
`ks-scenario-${element?.category ?? 'model'}-node`
]"
hoverable
>
<template #title>
<a-space>
<span class="ks-scenario-node-title">{{ element?.description ?? element?.name ?? '-' }}</span>
</a-space>
</template>
<!-- 节点内容区域 -->
<div class="w-full">
<div class="ks-scenario-node-content">
2026-03-15 20:20:56 +08:00
<div
v-for="(item, index) in element?.components || []"
:key="item.id || index"
class="ks-scenario-node-row"
2026-03-15 20:20:56 +08:00
>
<div
:data-port="`in-${item.id || index}`"
:port="`in-${item.id || index}`"
:title="`入桩: ${item.name}`"
class="port port-in"
magnet="passive"
:data-item="JSON.stringify(item)"
>
<div class="triangle-left"></div>
</div>
<!-- child名称 -->
<div class="ks-scenario-node-name">
{{ substring(item.description ?? item.name, 20) }}
</div>
<!-- 右侧出桩只能作为连线源 -->
<div
:data-port="`out-${item.id || index}`"
:port="`out-${item.id || index}`"
:title="`出桩: ${item.name}`"
class="port port-out"
magnet="active"
:data-item="JSON.stringify(item)"
>
<div class="triangle-right" ></div>
</div>
2026-03-15 20:20:56 +08:00
</div>
</div>
</div>
</a-card>
<template #overlay>
<a-menu @click="handleMenuClick">
<a-sub-menu key="mount">
<template #icon>
<LinkOutlined />
</template>
<template #title>挂载</template>
<a-menu-item
v-for="tree in availableTrees"
:key="`tree-${tree.id}`"
:disabled="isTreeMounted(tree.id)"
@click="() => handleMountTree(tree)"
>
<template #icon>
<CheckOutlined v-if="isTreeMounted(tree.id)" />
</template>
{{ tree.name }}
</a-menu-item>
<a-menu-item v-if="availableTrees.length === 0" disabled>
暂无可用行为树
</a-menu-item>
</a-sub-menu>
<a-menu-divider />
<a-menu-item key="delete">
<template #icon>
<DeleteOutlined />
</template>
删除
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</div>
2026-02-08 15:59:14 +08:00
</template>
<script lang="ts">
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
2026-03-15 19:32:20 +08:00
import { elementProps, type ModelElement } from '../graph';
import { DeleteOutlined, LinkOutlined, CheckOutlined, SettingOutlined } from '@ant-design/icons-vue';
2026-02-08 15:59:14 +08:00
import type { Graph } from '@antv/x6';
import { message } from 'ant-design-vue';
2026-02-08 22:31:13 +08:00
import { substring } from '@/utils/strings';
import { updateBehaviorTree, updateBehaviorTreeIdOfPlatform } from './api';
import type { BehaviorTree } from '../designer/tree';
2026-02-08 15:59:14 +08:00
export default defineComponent({
2026-02-08 17:57:40 +08:00
name: 'ModelElement',
2026-02-08 15:59:14 +08:00
components: {
SettingOutlined,
DeleteOutlined,
LinkOutlined,
CheckOutlined,
2026-02-08 15:59:14 +08:00
},
props: elementProps,
setup(_props) {
2026-02-08 17:57:40 +08:00
const element = ref<ModelElement | null>(
_props.node ? (_props.node.getData() as ModelElement) : null,
2026-02-08 15:59:14 +08:00
);
const updateKey = ref(0);
const isMenuVisible = ref(false);
// 挂载行为树相关状态
const availableTrees = ref<BehaviorTree[]>([]);
// 获取 popup 容器
const getPopupContainer = () => {
if (typeof document !== 'undefined') {
return document.body;
}
return undefined;
};
2026-02-08 15:59:14 +08:00
2026-02-08 17:57:40 +08:00
// 获取画布实例
2026-02-08 15:59:14 +08:00
const getGraph = (): Graph | null => {
return _props.graph as Graph || null;
};
2026-02-08 17:57:40 +08:00
// 监听节点数据变化
2026-02-08 15:59:14 +08:00
const handleDataChange = () => {
if (_props.node) {
2026-02-08 17:57:40 +08:00
element.value = _props.node.getData() as ModelElement;
2026-02-08 15:59:14 +08:00
} else {
element.value = null;
}
updateKey.value++;
};
// 获取行为树名称
const getBehaviorTreeName = (treeId: number | undefined | null): string => {
if (!treeId) return '';
const tree = availableTrees.value.find(t => t.id === treeId);
return tree?.name || `行为树${treeId}`;
};
// 判断行为树是否已挂载到当前节点
const isTreeMounted = (treeId: number): boolean => {
if (!element.value) return false;
const currentTreeId = (element.value as any).behaviortreeId as number | undefined;
return currentTreeId === treeId;
};
// 处理挂载行为树 - 当右键菜单打开时从graph中读取已缓存的行为树列表
2026-02-08 15:59:14 +08:00
const handleVisibleChange = (visible: boolean) => {
isMenuVisible.value = visible;
if (!visible || !element.value) return;
// 从graph对象中获取已缓存的行为树列表
const graph = _props.graph as any;
if (graph?.behaviorTrees) {
availableTrees.value = graph.behaviorTrees;
console.log('从缓存中读取行为树列表:', availableTrees.value.length, '个');
} else {
availableTrees.value = [];
console.warn('未找到缓存的行为树列表');
}
2026-02-08 15:59:14 +08:00
};
const handleMenuClick = ({ key }: { key: string }) => {
if (key === 'delete') {
handleDelete();
}
};
// 处理挂载具体的行为树
const handleMountTree = async (tree: BehaviorTree) => {
if (!element.value) return;
try {
// 更新节点的behaviortreeId属性
const updatedElement = { ...(element.value as any), behaviortreeId: tree.id };
// 调用后端API同时更新平台表的 behaviortreeId 和行为树表的 platformId
const platformIdValue = (element.value as any).platformId as number;
const [platformRes, treeRes] = await Promise.all([
updateBehaviorTreeIdOfPlatform({ id: platformIdValue, behaviortreeId: tree.id }),
updateBehaviorTree({ ...tree, platformId: platformIdValue }),
]);
if (platformRes.code === 200 && treeRes.code === 200) {
// 更新本地节点数据
if (_props.node) {
_props.node.setData(updatedElement);
}
message.success(`已成功挂载行为树: ${tree.name}`);
} else {
message.error(platformRes.msg || treeRes.msg || '挂载失败');
}
} catch (error) {
console.error('挂载行为树失败:', error);
message.error('挂载行为树失败');
}
};
2026-02-08 15:59:14 +08:00
const handleDelete = () => {
if (!_props.node) return;
const graph = getGraph();
if (graph) {
try {
2026-02-08 17:57:40 +08:00
// 先删除关联边
2026-02-08 15:59:14 +08:00
const connectedEdges = graph.getConnectedEdges(_props.node);
2026-02-08 17:57:40 +08:00
connectedEdges.forEach(edge => graph.removeEdge(edge));
// 再删除节点
2026-02-08 15:59:14 +08:00
graph.removeNode(_props.node);
console.info(`节点 ${_props.node.id} 已删除`);
} catch (error) {
console.error('删除节点失败:', error);
}
}
isMenuVisible.value = false;
};
onMounted(() => {
_props.node?.on('change:data', handleDataChange);
// 监听画布各种事件,操作时立即关闭菜单
const graph = getGraph();
if (graph) {
const closeMenuHandler = () => {
if (isMenuVisible.value) {
isMenuVisible.value = false;
}
};
// 监听多种可能导致菜单位置变化的事件
graph.on('pan', closeMenuHandler);
graph.on('translate', closeMenuHandler);
graph.on('scale', closeMenuHandler);
graph.on('zoom', closeMenuHandler);
graph.on('resize', closeMenuHandler);
}
2026-02-08 15:59:14 +08:00
});
onUnmounted(() => {
_props.node?.off('change:data', handleDataChange);
// 清理事件监听
const graph = getGraph();
if (graph) {
graph.off('pan');
graph.off('translate');
graph.off('scale');
graph.off('zoom');
graph.off('resize');
}
2026-02-08 15:59:14 +08:00
});
return {
element,
2026-02-08 20:57:07 +08:00
substring,
2026-02-08 15:59:14 +08:00
handleMenuClick,
handleVisibleChange,
availableTrees,
getBehaviorTreeName,
isTreeMounted,
handleMountTree,
getPopupContainer,
2026-02-08 15:59:14 +08:00
};
},
});
</script>
<style lang="less">
2026-03-15 19:32:20 +08:00
.ks-scenario-node {
2026-02-09 14:53:05 +08:00
background: linear-gradient(150deg, rgba(108, 99, 255) 1%, rgba(108, 99, 255) 100%);
2026-02-08 17:57:40 +08:00
border-radius: 8px;
2026-02-08 15:59:14 +08:00
width: 100%;
height: 100%;
cursor: pointer;
2026-02-08 22:31:13 +08:00
position: relative;
2026-02-09 14:53:05 +08:00
background: #1e2533;
border: 1px solid #4a7aff;
2026-02-09 19:53:17 +08:00
border: 2px solid #000000;
2026-02-09 14:53:05 +08:00
&:hover {
2026-02-09 19:53:17 +08:00
border: 2px solid #4a7aff;
2026-02-09 14:53:05 +08:00
box-shadow: 0 0 10px rgba(74, 122, 255, 0.3);
}
2026-02-08 15:59:14 +08:00
.ant-card-head {
border: 0;
2026-03-13 10:15:14 +08:00
height: 28px;
2026-02-09 19:53:17 +08:00
min-height: 25px;
2026-02-08 15:59:14 +08:00
border-radius: 0;
2026-02-09 14:53:05 +08:00
color: #fff;
2026-02-08 15:59:14 +08:00
font-size: 12px;
font-weight: normal;
2026-02-08 17:57:40 +08:00
padding: 0 20px;
2026-03-13 10:15:14 +08:00
//background: linear-gradient(to bottom, #3a4c70, #2d3a56);
2026-02-09 14:53:05 +08:00
border-top-left-radius: 8px;
border-top-right-radius: 8px;
background: linear-gradient(to bottom, rgba(108, 99, 255, 0.15), rgba(108, 99, 255, 0.05));
2026-03-13 10:15:14 +08:00
//background: url('@/assets/icons/bg-node-head.png') center / 100% 100%;
2026-02-09 14:53:05 +08:00
//background: linear-gradient(to bottom, rgb(234 234 234 / 20%), rgb(191 191 191 / 58%));
2026-03-15 20:20:56 +08:00
background: url('@/assets/icons/card-head-red.png') center / 100% 100%;
2026-02-08 17:57:40 +08:00
}
2026-03-15 19:32:20 +08:00
.ks-scenario-node-icon {
2026-02-08 17:57:40 +08:00
width: 15px;
height: 15px;
display: block;
position: absolute;
left: 8px;
2026-03-13 10:15:14 +08:00
top: 6px;
background: url('@/assets/icons/icon-node.svg') center / 100% 100%;
2026-02-08 17:57:40 +08:00
}
2026-03-15 19:32:20 +08:00
.ks-scenario-node-title {
2026-03-13 10:15:14 +08:00
font-size: 12px;
2026-02-09 14:53:05 +08:00
color: #fff;
2026-03-13 10:15:14 +08:00
margin-top: -7px;
display: block;
2026-02-08 15:59:14 +08:00
}
.ant-card-body {
2026-02-09 14:53:05 +08:00
color: #f5f5f5;
2026-02-09 19:53:17 +08:00
height: calc(100% - 25px);
2026-02-08 15:59:14 +08:00
border-radius: 0;
font-size: 12px;
2026-03-13 10:40:44 +08:00
padding: 10px 30px !important;
2026-03-13 17:05:08 +08:00
//border-top: 1px solid rgba(108, 99, 255, 0.5);
2026-02-08 20:57:07 +08:00
overflow: hidden;
2026-02-08 21:36:39 +08:00
text-overflow: ellipsis;
white-space: nowrap;
2026-02-09 14:53:05 +08:00
box-shadow: 0 0 10px rgba(74, 122, 255, 0.3);
2026-02-09 15:35:20 +08:00
white-space: normal; // 恢复默认的换行行为
word-wrap: break-word; // 允许长单词换行
word-break: break-all; // 允许在任意字符处换行
line-height: 1.4; // 增加行高提升可读性
box-shadow: 0 0 10px rgba(74, 122, 255, 0.3);
2026-02-08 15:59:14 +08:00
}
2026-02-08 17:57:40 +08:00
// 连接桩容器样式
2026-03-15 19:32:20 +08:00
.ks-scenario-node-content {
2026-02-08 17:57:40 +08:00
width: 100%;
display: flex;
flex-direction: column;
2026-03-15 20:20:56 +08:00
gap: 6px;
2026-02-08 17:57:40 +08:00
}
2026-03-15 19:32:20 +08:00
.ks-scenario-node-row {
2026-02-08 17:57:40 +08:00
width: 100%;
display: flex;
align-items: center;
position: relative;
2026-02-09 14:53:05 +08:00
min-height: 24px;
2026-02-08 17:57:40 +08:00
}
.port {
width: 12px;
height: 12px;
border-radius: 50%;
cursor: crosshair;
flex-shrink: 0;
2026-02-09 14:53:05 +08:00
box-shadow: 0 0 0 2px rgb(108, 99, 255, 0.8);
z-index: 10;
2026-02-08 17:57:40 +08:00
magnet: true;
2026-03-13 10:15:14 +08:00
position: relative;
.triangle-left {
width: 0;
height: 0;
2026-03-13 17:05:08 +08:00
border-top: 4px solid transparent;
border-right: 5px solid #5da1df;
border-bottom: 4px solid transparent;
2026-03-13 10:15:14 +08:00
position: absolute;
left: -8px;
2026-03-13 17:05:08 +08:00
top: 0.5px;
magnet: passive;
2026-03-13 10:15:14 +08:00
}
/* 右三角形 */
.triangle-right {
width: 0;
height: 0;
2026-03-13 17:05:08 +08:00
border-top: 4px solid transparent;
border-left: 5px solid #5da1df;
border-bottom: 4px solid transparent;
2026-03-13 10:15:14 +08:00
position: absolute;
right: -8px;
2026-03-13 17:05:08 +08:00
top: 0.5px;
magnet: passive;
2026-03-13 10:15:14 +08:00
}
2026-02-08 17:57:40 +08:00
}
// 左侧入桩样式
.port-in {
2026-03-13 17:05:08 +08:00
//background-color: #3c82f6;
2026-02-09 14:53:05 +08:00
margin-right: 8px;
2026-02-08 17:57:40 +08:00
//border: 1px solid #093866;
magnet: passive;
box-shadow: none;
2026-03-13 17:05:08 +08:00
width: 13px;
height: 13px;
2026-02-08 17:57:40 +08:00
display: block;
2026-03-13 17:05:08 +08:00
//background: url('@/assets/icons/point.svg') center / 100% 100%;
border: 2px solid #5da1df;
2026-02-08 20:57:07 +08:00
position: absolute;
2026-02-09 19:53:17 +08:00
//top: 7px;
2026-03-15 20:20:56 +08:00
left: -17px;
2026-02-08 17:57:40 +08:00
}
.port-out {
2026-02-09 14:53:05 +08:00
margin-left: 8px;
2026-02-08 17:57:40 +08:00
margin-right: 5px;
magnet: active;
box-shadow: none;
2026-03-13 17:05:08 +08:00
width: 13px;
height: 13px;
2026-02-08 17:57:40 +08:00
display: block;
2026-03-13 17:05:08 +08:00
border: 2px solid #5da1df;
background:#5da1df;
2026-02-08 20:57:07 +08:00
position: absolute;
2026-03-15 20:20:56 +08:00
right: -17px;
2026-02-08 17:57:40 +08:00
}
// 节点文本样式
2026-03-15 19:32:20 +08:00
.ks-scenario-node-name {
2026-02-09 14:53:05 +08:00
flex: 1;
2026-02-08 17:57:40 +08:00
line-height: 24px;
overflow: hidden;
text-overflow: ellipsis;
2026-03-13 17:05:08 +08:00
}
2026-02-08 17:57:40 +08:00
}
2026-02-08 15:59:14 +08:00
</style>