Initial commit
This commit is contained in:
61
modeler/src/views/decision/actions.vue
Normal file
61
modeler/src/views/decision/actions.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="ks-model-builder-actions">
|
||||
<a-space>
|
||||
<a-tooltip placement="top">
|
||||
<template #title>
|
||||
返回
|
||||
</template>
|
||||
<a-button class="ks-model-builder-goback" size="small" @click="goback">
|
||||
<RollbackOutlined />
|
||||
<span>返回</span>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<a-tooltip v-if="graph && node" placement="top">
|
||||
<template #title>
|
||||
保存
|
||||
</template>
|
||||
<a-popconfirm
|
||||
title="确定保存?"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<a-button class="ks-model-builder-save" size="small">
|
||||
<CheckOutlined />
|
||||
<span>保存</span>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { CheckOutlined, RollbackOutlined } from '@ant-design/icons-vue';
|
||||
import { elementProps } from './builder/props';
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
CheckOutlined,
|
||||
RollbackOutlined,
|
||||
},
|
||||
props: elementProps,
|
||||
emits: ['save'],
|
||||
setup(props, ctx) {
|
||||
|
||||
const handleSave = () => {
|
||||
ctx.emit('save');
|
||||
};
|
||||
|
||||
const goback = () => {
|
||||
|
||||
};
|
||||
|
||||
return {
|
||||
graph: props.graph,
|
||||
node: props.node,
|
||||
handleSave,
|
||||
goback,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
51
modeler/src/views/decision/api.ts
Normal file
51
modeler/src/views/decision/api.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { HttpRequestClient } from '@/utils/request';
|
||||
import type { NodeTemplateQuery, NodeTemplatesResponse, TreeModelDetailsResponse, TreeModelGraph, TreeModelsResponse } from './types';
|
||||
import type { ApiPaginationQuery, BasicResponse } from '@/types';
|
||||
|
||||
const req = HttpRequestClient.create<BasicResponse>({
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
export const findTemplatesByQuery = (query: Partial<NodeTemplateQuery>): Promise<NodeTemplatesResponse> => {
|
||||
return req.postJson('/node-templates', query);
|
||||
};
|
||||
|
||||
export const findTreesByQuery = (query: Partial<ApiPaginationQuery>): Promise<TreeModelsResponse> => {
|
||||
return req.get<TreeModelsResponse>('/system/behaviortree/list', query);
|
||||
};
|
||||
|
||||
export const findOneTreeById = (id: number): Promise<TreeModelDetailsResponse> => {
|
||||
return req.get(`/behavior-trees/${id}`);
|
||||
};
|
||||
|
||||
export const updateTree = (rt: Partial<TreeModelGraph>): Promise<BasicResponse> => {
|
||||
return req.postJson('/behavior-trees', rt);
|
||||
};
|
||||
|
||||
export const createTree = (rt: Partial<TreeModelGraph>): Promise<BasicResponse> => {
|
||||
return req.putJson('/behavior-trees', rt);
|
||||
};
|
||||
|
||||
|
||||
// export const findOneTreeById = (id: number): Promise<TreeModelDetailsResponse> => {
|
||||
// return req.postJson(`/tree-details`,{
|
||||
// tree_id: id
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// export const updateTree = (rt: Partial<TreeModelGraph>): Promise<BasicResponse> => {
|
||||
// return req.postJson('/update-tree', rt);
|
||||
// };
|
||||
//
|
||||
// export const createTree = (rt: Partial<TreeModelGraph>): Promise<BasicResponse> => {
|
||||
// return req.postJson('/update-tree', rt);
|
||||
// };
|
||||
178
modeler/src/views/decision/builder/graph.ts
Normal file
178
modeler/src/views/decision/builder/graph.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { Clipboard, Edge, Graph, History, Keyboard, Path, Selection, Snapline, Transform } from '@antv/x6';
|
||||
import type { BaseElement } from '../types';
|
||||
import type { Connecting } from '@antv/x6/lib/graph/options';
|
||||
|
||||
Graph.registerConnector(
|
||||
'sequenceFlowConnector',
|
||||
(s, e) => {
|
||||
const offset = 4;
|
||||
const deltaY = Math.abs(e.y - s.y);
|
||||
const control = Math.floor((deltaY / 3) * 2);
|
||||
|
||||
const v1 = { x: s.x, y: s.y + offset + control };
|
||||
const v2 = { x: e.x, y: e.y - offset - control };
|
||||
|
||||
return Path.parse(
|
||||
`
|
||||
M ${s.x} ${s.y}
|
||||
L ${s.x} ${s.y + offset}
|
||||
C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${e.x} ${e.y - offset}
|
||||
L ${e.x} ${e.y}
|
||||
`,
|
||||
).serialize();
|
||||
},
|
||||
true,
|
||||
);
|
||||
|
||||
|
||||
export const createGraphConnectingAttributes = (): Partial<Connecting> => {
|
||||
return {
|
||||
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
|
||||
allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true
|
||||
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
|
||||
highlight: true, // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
|
||||
connector: 'sequenceFlowConnector',
|
||||
connectionPoint: 'boundary', // 指定连接点,默认值为 boundary。
|
||||
anchor: 'center',
|
||||
|
||||
router: 'manhattan',
|
||||
// connector: {
|
||||
// name: 'rounded',
|
||||
// args: {
|
||||
// radius: 8,
|
||||
// },
|
||||
// },
|
||||
// connectionPoint: 'anchor',
|
||||
|
||||
// validateMagnet({ magnet }) {
|
||||
// return magnet.getAttribute('port-group') !== 'top'
|
||||
// },
|
||||
// 验证连接
|
||||
validateConnection(this: Graph, { sourceCell, targetCell }) {
|
||||
console.error('validateConnection');
|
||||
if (!sourceCell || !targetCell) return false;
|
||||
|
||||
// 核心逻辑:禁止节点连接自己(自环)
|
||||
if (sourceCell === targetCell) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// const sourceData = sourceCell.getData() as GraphElement;
|
||||
const targetData = targetCell.getData() as BaseElement;
|
||||
|
||||
// 根节点不能作为子节点
|
||||
if (targetData.type === 'root') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否已存在相同连接(保留原有逻辑)
|
||||
const edges: Edge[] = this.getEdges();
|
||||
const existingConnection = edges.find(edge =>
|
||||
edge.getSourceCell() === sourceCell &&
|
||||
edge.getTargetCell() === targetCell,
|
||||
);
|
||||
|
||||
return !existingConnection;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createGraphCanvas = (container: HTMLDivElement, readonly: boolean = false): Graph => {
|
||||
const graph = new Graph({
|
||||
container: container,
|
||||
grid: {
|
||||
size: 20,
|
||||
visible: true,
|
||||
type: 'dot',
|
||||
// color: '#e5e7eb'
|
||||
},
|
||||
// 确保启用了异步渲染
|
||||
async: true,
|
||||
panning: {
|
||||
enabled: true,
|
||||
eventTypes: ['leftMouseDown', 'mouseWheel'],
|
||||
},
|
||||
mousewheel: {
|
||||
enabled: true,
|
||||
zoomAtMousePosition: true,
|
||||
modifiers: 'ctrl',
|
||||
minScale: 0.5,
|
||||
maxScale: 3,
|
||||
},
|
||||
highlighting: {
|
||||
magnetAdsorbed: {
|
||||
name: 'stroke',
|
||||
args: {
|
||||
attrs: {
|
||||
fill: '#fff',
|
||||
stroke: '#31d0c6',
|
||||
strokeWidth: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
connecting: createGraphConnectingAttributes(),
|
||||
|
||||
scaling: {
|
||||
min: 0.5,
|
||||
max: 2,
|
||||
},
|
||||
|
||||
// 背景配置
|
||||
background: {},
|
||||
|
||||
// 交互配置
|
||||
interacting: (cellView) => {
|
||||
if (readonly) {
|
||||
return false; // 只读模式下禁用所有交互
|
||||
}
|
||||
|
||||
// 确保边(edge)的顶点交互权限开启
|
||||
if (cellView.cell.isEdge()) {
|
||||
return {
|
||||
vertexAddable: true, // 允许添加顶点
|
||||
vertexMovable: true, // 允许移动顶点
|
||||
vertexDeletable: true, // 允许删除顶点
|
||||
edgeMovable: true, // 允许整体拖动连线
|
||||
arrowheadMovable: true, // 允许拖动箭头调整端点
|
||||
};
|
||||
}
|
||||
|
||||
// 节点的交互配置(保持不变)
|
||||
return {
|
||||
nodeMovable: true,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
graph.use(
|
||||
new Selection({
|
||||
multiple: true,
|
||||
rubberEdge: true,
|
||||
rubberNode: true,
|
||||
modifiers: 'shift',
|
||||
rubberband: true,
|
||||
}),
|
||||
).use(
|
||||
new Transform({
|
||||
resizing: false,
|
||||
rotating: false,
|
||||
}),
|
||||
)
|
||||
.use(new Snapline())
|
||||
.use(new Keyboard())
|
||||
.use(new Clipboard())
|
||||
.use(new History());
|
||||
|
||||
return graph;
|
||||
|
||||
};
|
||||
239
modeler/src/views/decision/builder/hooks.ts
Normal file
239
modeler/src/views/decision/builder/hooks.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { computed, type ComputedRef, ref, type Ref } from 'vue';
|
||||
import { type Dom, Graph } from '@antv/x6';
|
||||
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
||||
import { createGraphCanvas } from './graph';
|
||||
import { EventListener } from '@/utils/event';
|
||||
|
||||
export interface UseGraphCanvas {
|
||||
container: Ref<HTMLDivElement | null>;
|
||||
readonly: boolean;
|
||||
eventListener: EventListener;
|
||||
currentZoom: Ref<number>;
|
||||
graph: ComputedRef<Graph>;
|
||||
zoomIn: () => void;
|
||||
zoomOut: () => void;
|
||||
fitToScreen: () => void;
|
||||
centerContent: () => void;
|
||||
resizeCanvas: () => void;
|
||||
handleGraphEvent: (name: string, fn: Function) => void;
|
||||
emitGraphEvent: (name: string, options?: any) => void;
|
||||
createCanvas: (c: HTMLDivElement) => Graph;
|
||||
}
|
||||
|
||||
export const useGraphCanvas = (readonly: boolean = false): UseGraphCanvas => {
|
||||
|
||||
const graph = ref<Graph | null>(null);
|
||||
const container = ref<HTMLDivElement | null>(null);
|
||||
const eventListener = new EventListener();
|
||||
const currentZoom = ref<number>(0);
|
||||
|
||||
const handleGraphEvent = (name: string, fn: Function) => {
|
||||
eventListener.on(name, fn);
|
||||
};
|
||||
|
||||
const emitGraphEvent = (name: string, options?: any): void => {
|
||||
eventListener.emit(name, options);
|
||||
};
|
||||
|
||||
const zoomIn = (): void => {
|
||||
if (graph.value) {
|
||||
const zoom = graph.value.zoom();
|
||||
graph.value.zoom(zoom + 0.1);
|
||||
}
|
||||
};
|
||||
|
||||
const zoomOut = (): void => {
|
||||
if (graph.value) {
|
||||
const zoom = graph.value.zoom();
|
||||
graph.value.zoom(Math.max(0.1, zoom - 0.1));
|
||||
}
|
||||
};
|
||||
|
||||
const fitToScreen = () => {
|
||||
if (graph.value) {
|
||||
graph.value.zoomToFit({ padding: 20 });
|
||||
}
|
||||
};
|
||||
|
||||
const centerContent = () => {
|
||||
if (graph.value) {
|
||||
graph.value.centerContent();
|
||||
}
|
||||
};
|
||||
|
||||
const resizeCanvas = () => {
|
||||
if (graph.value) {
|
||||
graph.value?.resize();
|
||||
}
|
||||
};
|
||||
|
||||
const initEvents = () => {
|
||||
if (!graph.value) {
|
||||
return;
|
||||
}
|
||||
graph.value.on('keydown', ({ e }: any) => {
|
||||
if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
const cells = graph.value?.getSelectedCells();
|
||||
if (cells && cells.length) {
|
||||
graph.value?.removeCells(cells);
|
||||
// 通知父节点更新状态
|
||||
emitGraphEvent('cells:removed', cells);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
graph.value.on('scale', (ctx) => {
|
||||
currentZoom.value = ctx.sx;
|
||||
emitGraphEvent('scale', ctx);
|
||||
});
|
||||
|
||||
// 监听画布空白点击
|
||||
graph.value.on('blank:click', () => {
|
||||
graph.value?.cleanSelection();
|
||||
emitGraphEvent('blank:click');
|
||||
});
|
||||
|
||||
// 监听连线删除
|
||||
graph.value.on('edge:removed', ({ edge }) => {
|
||||
emitGraphEvent('cells:removed', edge); // 添加此行
|
||||
});
|
||||
|
||||
graph.value.on('edge:contextmenu', (ctx) => {
|
||||
emitGraphEvent('edge:contextmenu', ctx);
|
||||
});
|
||||
|
||||
graph.value.on('edge:connect:abort', () => {
|
||||
// 当连线被拖拽到空白区域后释放,自动移除这条无效连线
|
||||
const edges = graph.value?.getEdges();
|
||||
const invalidEdges = (edges ?? []).filter(edge =>
|
||||
!edge.getSourceCell() || !edge.getTargetCell(),
|
||||
);
|
||||
if (invalidEdges.length) {
|
||||
graph.value?.removeCells(invalidEdges);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听连接失败
|
||||
graph.value.on('edge:connect:invalid', () => {
|
||||
emitGraphEvent('edge:connect:invalid');
|
||||
});
|
||||
|
||||
graph.value.on('edge:click', (edge) => {
|
||||
emitGraphEvent('edge:connect:invalid', edge);
|
||||
});
|
||||
|
||||
graph.value.on('edge:connected', ({ edge }) => {
|
||||
// const sourceNode = edge.getSourceCell() as Node;
|
||||
// const targetNode = edge.getTargetCell() as Node;
|
||||
|
||||
// if (sourceNode && targetNode) {
|
||||
// const sourceData = sourceNode.getData() as TaskNodeElement;
|
||||
// const targetData = targetNode.getData() as TaskNodeElement;
|
||||
//
|
||||
// // 将连线存储到节点数据中
|
||||
// const sourceEdges = sourceData.edges || [];
|
||||
// const existingEdge = sourceEdges.find(e => e.targetKey === targetNode.id);
|
||||
//
|
||||
// if (!existingEdge) {
|
||||
// sourceEdges.push({
|
||||
// key: edge.id,
|
||||
// sourceKey: sourceNode.id,
|
||||
// sourceName: sourceData.name,
|
||||
// targetKey: targetNode.id,
|
||||
// targetName: targetData.name,
|
||||
// });
|
||||
// sourceNode.setData({ ...sourceData, edges: sourceEdges });
|
||||
// }
|
||||
// }
|
||||
|
||||
edge.attr('line/stroke', '#3b82f6'); // 显式设置颜色
|
||||
edge.attr('line/strokeWidth', 2); // 显式设置宽度
|
||||
// edge.refresh() // 刷新连线
|
||||
// graph.paint() // 重绘画布
|
||||
|
||||
emitGraphEvent('edge:connected', edge);
|
||||
});
|
||||
|
||||
graph.value.on('node:click', (ctx: NodeViewPositionEventArgs<Dom.ClickEvent>) => {
|
||||
console.info('node click', ctx);
|
||||
emitGraphEvent('node:click', ctx);
|
||||
});
|
||||
|
||||
// 监听节点选中事件
|
||||
graph.value.on('node:selected', ({ node }) => {
|
||||
console.info('node select', node);
|
||||
emitGraphEvent('node:selected', node);
|
||||
});
|
||||
|
||||
// 监听节点取消选中
|
||||
graph.value.on('node:unselected', () => {
|
||||
emitGraphEvent('node:unselected');
|
||||
});
|
||||
|
||||
// 监听节点鼠标移入,显示连接点
|
||||
graph.value.on('node:mouseenter', (ctx) => {
|
||||
emitGraphEvent('node:mouseenter', ctx);
|
||||
});
|
||||
|
||||
// 监听节点鼠标移出,隐藏连接点
|
||||
graph.value.on('node:mouseleave', (ctx) => {
|
||||
emitGraphEvent('node:mouseleave', ctx);
|
||||
});
|
||||
|
||||
// 监听节点状态变化
|
||||
graph.value.on('node:change:data', (ctx) => {
|
||||
const edges = graph.value?.getIncomingEdges(ctx.node);
|
||||
const { status } = ctx.node.getData();
|
||||
edges?.forEach((edge) => {
|
||||
if (status === 'running') {
|
||||
edge.attr('line/strokeDasharray', 5);
|
||||
edge.attr('line/style/animation', 'running-line 30s infinite linear');
|
||||
} else {
|
||||
edge.attr('line/strokeDasharray', '');
|
||||
edge.attr('line/style/animation', '');
|
||||
}
|
||||
});
|
||||
emitGraphEvent('node:change:data', ctx);
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
const createCanvas = (c: HTMLDivElement): Graph => {
|
||||
container.value = c;
|
||||
graph.value = createGraphCanvas(c, readonly);
|
||||
initEvents();
|
||||
|
||||
emitGraphEvent('created', graph.value);
|
||||
|
||||
return graph.value as Graph;
|
||||
};
|
||||
|
||||
const graphInstance = computed(() => graph.value);
|
||||
|
||||
return {
|
||||
container,
|
||||
readonly,
|
||||
graph: graphInstance,
|
||||
eventListener,
|
||||
currentZoom,
|
||||
|
||||
emitGraphEvent,
|
||||
handleGraphEvent,
|
||||
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitToScreen,
|
||||
centerContent,
|
||||
resizeCanvas,
|
||||
createCanvas,
|
||||
} as UseGraphCanvas;
|
||||
};
|
||||
11
modeler/src/views/decision/builder/index.ts
Normal file
11
modeler/src/views/decision/builder/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export { createGraphCanvas } from './graph';
|
||||
export * from './hooks';
|
||||
194
modeler/src/views/decision/builder/node.vue
Normal file
194
modeler/src/views/decision/builder/node.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
|
||||
<a-card :class="['ks-designer-node', `ks-designer-node-${element?.type}`]" hoverable>
|
||||
<template #title>
|
||||
{{ element?.name ?? '-' }}
|
||||
</template>
|
||||
<div class="w-full">
|
||||
<p>{{ element?.description ?? '-' }}</p>
|
||||
</div>
|
||||
</a-card>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleMenuClick">
|
||||
<a-menu-item key="delete">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
删除
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { elementProps } from './props';
|
||||
import type { SettingTaskNodeElement } from '../types';
|
||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||
import type { Graph } from '@antv/x6';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'SettingTaskNodeElement',
|
||||
components: {
|
||||
SettingOutlined,
|
||||
DeleteOutlined,
|
||||
},
|
||||
props: elementProps,
|
||||
setup(_props) {
|
||||
// 初始化 element,保证类型是 SettingTaskNodeElement 或 null
|
||||
const element = ref<SettingTaskNodeElement | null>(
|
||||
_props.node ? (_props.node.getData() as SettingTaskNodeElement) : null,
|
||||
);
|
||||
const updateKey = ref(0);
|
||||
const isMenuVisible = ref(false);
|
||||
|
||||
// 获取节点所在的画布实例
|
||||
const getGraph = (): Graph | null => {
|
||||
return _props.graph as Graph || null;
|
||||
};
|
||||
|
||||
const handleDataChange = () => {
|
||||
if (_props.node) {
|
||||
element.value = _props.node.getData() as SettingTaskNodeElement;
|
||||
} else {
|
||||
element.value = null;
|
||||
}
|
||||
console.info('handleDataChange', element.value);
|
||||
updateKey.value++;
|
||||
};
|
||||
|
||||
const handleVisibleChange = (visible: boolean) => {
|
||||
isMenuVisible.value = visible;
|
||||
};
|
||||
|
||||
const handleMenuClick = ({ key }: { key: string }) => {
|
||||
if (key === 'delete') {
|
||||
handleDelete();
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
if (!_props.node) return;
|
||||
|
||||
const graph = getGraph();
|
||||
if (graph) {
|
||||
try {
|
||||
// 获取与该节点关联的所有边
|
||||
const connectedEdges = graph.getConnectedEdges(_props.node);
|
||||
|
||||
// 先移除关联的边
|
||||
connectedEdges.forEach(edge => {
|
||||
graph.removeEdge(edge);
|
||||
});
|
||||
|
||||
// 再移除节点本身
|
||||
graph.removeNode(_props.node);
|
||||
|
||||
console.info(`节点 ${_props.node.id} 已删除`);
|
||||
} catch (error) {
|
||||
console.error('删除节点失败:', error);
|
||||
}
|
||||
} else {
|
||||
console.error('无法获取 Graph 实例');
|
||||
}
|
||||
|
||||
// 关闭右键菜单
|
||||
isMenuVisible.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
console.info('node onMounted');
|
||||
_props.node?.on('change:data', handleDataChange);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
console.info('node onUnmounted');
|
||||
_props.node?.off('change:data', handleDataChange);
|
||||
});
|
||||
|
||||
return {
|
||||
element,
|
||||
handleMenuClick,
|
||||
handleVisibleChange,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.x6-widget-selection-box {
|
||||
border: 1px dashed #7a6986;
|
||||
box-shadow: 2px 2px 5px #000000;
|
||||
}
|
||||
|
||||
.ks-designer-node {
|
||||
background: #1b3875;
|
||||
//background: url('@/assets/icons/bg-node.png') center / 100% 100%;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 1px 2px -2px rgb(0 0 0), 0 3px 6px 0 rgb(0 0 0 / 60%), 0 5px 12px 4px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
border: 0;
|
||||
height: 30px;
|
||||
min-height: 30px;
|
||||
border-radius: 0;
|
||||
color: #ddd;
|
||||
font-size: 12px;
|
||||
font-weight: normal;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
color: #fff;
|
||||
height: calc(100% - 30px);
|
||||
background: #24417e;
|
||||
border-radius: 0;
|
||||
font-size: 12px;
|
||||
padding: 15px !important;
|
||||
//overflow: hidden;
|
||||
//white-space: nowrap;
|
||||
//text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
//&.ks-designer-node-root{
|
||||
//background: #645525;
|
||||
//.ant-card-body{
|
||||
// background: #726334;
|
||||
//}
|
||||
//}
|
||||
&.ks-designer-node-select {
|
||||
background: #255464;
|
||||
|
||||
.ant-card-body {
|
||||
background: #1c4654;
|
||||
}
|
||||
}
|
||||
|
||||
&.ks-designer-node-precondition,
|
||||
&.ks-designer-node-parallel,
|
||||
&.ks-designer-node-sequence {
|
||||
background: #4c5a9d;
|
||||
|
||||
.ant-card-body {
|
||||
background: #3f4d8d;
|
||||
}
|
||||
}
|
||||
|
||||
&.ks-designer-node-action {
|
||||
background: #645525;
|
||||
|
||||
.ant-card-body {
|
||||
background: #726334;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
43
modeler/src/views/decision/builder/ports.ts
Normal file
43
modeler/src/views/decision/builder/ports.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
export const createPort = (name: string = 'top', args: Record<any, any> = { dx: 0 }) => {
|
||||
return {
|
||||
position: { name: name, args: args },
|
||||
attrs: {
|
||||
circle: {
|
||||
r: 3, // 大小
|
||||
magnet: true,
|
||||
stroke: '#999', // 边框颜色
|
||||
strokeWidth: 1, // 边框大小
|
||||
fill: '#ffffff', // 填充颜色
|
||||
style: {
|
||||
visibility: 'visible', // 是否可见
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createPorts = () => {
|
||||
return {
|
||||
groups: {
|
||||
top: createPort('top'),
|
||||
right: createPort('right'),
|
||||
bottom: createPort('bottom'),
|
||||
left: createPort('left'),
|
||||
},
|
||||
items: [
|
||||
{ group: 'top', id: 'top' },
|
||||
{ group: 'right', id: 'right' },
|
||||
{ group: 'bottom', id: 'bottom' },
|
||||
{ group: 'left', id: 'left' },
|
||||
],
|
||||
};
|
||||
};
|
||||
29
modeler/src/views/decision/builder/props.ts
Normal file
29
modeler/src/views/decision/builder/props.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { Graph, Node } from '@antv/x6';
|
||||
import { type ExtractPropTypes, type PropType } from 'vue';
|
||||
import type { BaseElement } from '../types';
|
||||
|
||||
export const elementProps = {
|
||||
node: {
|
||||
type: Object as PropType<Node>,
|
||||
required: true,
|
||||
},
|
||||
graph: {
|
||||
type: Object as PropType<Graph>,
|
||||
required: true,
|
||||
},
|
||||
element: {
|
||||
type: Object as PropType<BaseElement>,
|
||||
required: false,
|
||||
},
|
||||
};
|
||||
|
||||
export type ElementPropsType = ExtractPropTypes<typeof elementProps>
|
||||
34
modeler/src/views/decision/builder/register.ts
Normal file
34
modeler/src/views/decision/builder/register.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 zlin <admin@kernelstudio.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE file
|
||||
* that was distributed with this source code.
|
||||
*/
|
||||
|
||||
import { register } from '@antv/x6-vue-shape';
|
||||
import ModelElement from './node.vue';
|
||||
import { createPorts } from './ports';
|
||||
|
||||
export const registerNodeElement = (type: string = 'task') => {
|
||||
console.info('registerNodeElement');
|
||||
register({
|
||||
shape: type,
|
||||
component: ModelElement,
|
||||
width: 120,
|
||||
attrs: {
|
||||
body: {
|
||||
stroke: 'transparent',
|
||||
strokeWidth: 0,
|
||||
fill: 'transparent',
|
||||
rx: 4,
|
||||
ry: 4,
|
||||
},
|
||||
},
|
||||
dragging: {
|
||||
enabled: true,
|
||||
},
|
||||
ports: createPorts(),
|
||||
});
|
||||
};
|
||||
41
modeler/src/views/decision/constants.ts
Normal file
41
modeler/src/views/decision/constants.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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 { NodeTemplateData, NodeTemplateQuery, TreeModelsData } from './types';
|
||||
import type { ApiPagination, ApiPaginationQuery } from '@/types';
|
||||
|
||||
export const defaultPagination = {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
total_pages: 0,
|
||||
} as ApiPagination;
|
||||
|
||||
export const defaultTreeModelsData = {
|
||||
trees: [],
|
||||
pagination: { ...defaultPagination },
|
||||
} as TreeModelsData;
|
||||
|
||||
export const defaultPaginationRequest = {
|
||||
page: 1,
|
||||
limit: 10,
|
||||
keyword: null,
|
||||
} as ApiPaginationQuery;
|
||||
|
||||
export const defaultNodeTemplateQuery = {
|
||||
page: 1,
|
||||
limit: 1000,
|
||||
keyword: null,
|
||||
include_params: true,
|
||||
} as NodeTemplateQuery;
|
||||
|
||||
export const defaultNodeTemplateData = {
|
||||
templates: [],
|
||||
total: 0,
|
||||
} as NodeTemplateData;
|
||||
375
modeler/src/views/decision/designer.vue
Normal file
375
modeler/src/views/decision/designer.vue
Normal file
@@ -0,0 +1,375 @@
|
||||
<template>
|
||||
<Wrapper>
|
||||
<a-layout class="bg-transparent" style="background: transparent">
|
||||
<Header />
|
||||
<a-layout class="ks-layout-body">
|
||||
<div class="ks-model-builder-body">
|
||||
<div class="ks-model-builder-left">
|
||||
<TressCard
|
||||
@select-tree="handleSelectTree"
|
||||
/>
|
||||
<NodesCard
|
||||
@drag-item-start="handleDragStart"
|
||||
@drag-item-end="handleDragEnd"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ks-model-builder-content">
|
||||
<div class="ks-model-builder-actions">
|
||||
<a-space>
|
||||
<a-tooltip v-if="graph && currentTreeModelGraph" placement="top">
|
||||
<template #title>
|
||||
保存
|
||||
</template>
|
||||
<a-popconfirm
|
||||
title="确定保存?"
|
||||
@confirm="handleSave"
|
||||
>
|
||||
<a-button class="ks-model-builder-save" size="small">
|
||||
<CheckOutlined />
|
||||
<span>保存</span>
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-tooltip>
|
||||
</a-space>
|
||||
</div>
|
||||
<!-- 画布容器,添加拖放事件 -->
|
||||
<div
|
||||
ref="canvas"
|
||||
class="ks-model-builder-canvas"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDrop"
|
||||
@dragover.prevent
|
||||
></div>
|
||||
<TeleportContainer />
|
||||
</div>
|
||||
<Properties
|
||||
v-if="graph"
|
||||
:element="selectedNodeTaskElement"
|
||||
:graph="graph as any"
|
||||
:node="selectedModelNode as any"
|
||||
@update-element="handleUpdateElement" />
|
||||
</div>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</Wrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
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 { Wrapper } from '@/components/wrapper';
|
||||
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
||||
import Header from './header.vue';
|
||||
import Properties from './properties.vue';
|
||||
import { useGraphCanvas } from './builder/hooks';
|
||||
import { registerNodeElement } from './builder/register';
|
||||
import type { NodeGraph, NodeTemplate, SettingTaskNodeElement, TreeModel, TreeModelGraph } from './types';
|
||||
import { createTree, findOneTreeById, updateTree } from './api';
|
||||
import { createTaskNodeElement, createTaskNodeElementFromTemplate, hasElements, hasRootElementNode, resolveNodeGraph } from './utils/node';
|
||||
import TressCard from './trees-card.vue';
|
||||
import NodesCard from './nodes-card.vue';
|
||||
|
||||
const TeleportContainer = defineComponent(getTeleport());
|
||||
|
||||
registerNodeElement();
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
TressCard,
|
||||
NodesCard,
|
||||
Wrapper,
|
||||
Header,
|
||||
Properties,
|
||||
SaveOutlined,
|
||||
CheckCircleOutlined,
|
||||
CheckOutlined,
|
||||
RollbackOutlined,
|
||||
TeleportContainer,
|
||||
},
|
||||
setup() {
|
||||
const canvas = ref<HTMLDivElement | null>(null);
|
||||
const graph = ref<Graph | null>(null);
|
||||
const currentZoom = ref<number>(1);
|
||||
const draggedNodeData = ref<NodeTemplate | null>(null);
|
||||
const isDraggingOver = ref(false);
|
||||
const currentTreeModelGraph = ref<TreeModelGraph | null>(null);
|
||||
const selectedModelNode = ref<Node<NodeProperties> | null>(null);
|
||||
const selectedNodeTaskElement = ref<SettingTaskNodeElement | null>(null);
|
||||
const changed = ref<boolean>(false);
|
||||
|
||||
const {
|
||||
handleGraphEvent,
|
||||
createCanvas,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitToScreen,
|
||||
centerContent,
|
||||
resizeCanvas,
|
||||
} = useGraphCanvas();
|
||||
|
||||
// 处理拖动开始
|
||||
const handleDragStart = (nm: NodeTemplate) => {
|
||||
draggedNodeData.value = nm;
|
||||
};
|
||||
|
||||
// 处理拖动结束
|
||||
const handleDragEnd = () => {
|
||||
isDraggingOver.value = false;
|
||||
};
|
||||
|
||||
// 处理拖动进入
|
||||
const handleDragEnter = (e: DragEvent) => {
|
||||
safePreventDefault(e);
|
||||
safeStopPropagation(e);
|
||||
isDraggingOver.value = true;
|
||||
};
|
||||
|
||||
// 处理拖动离开
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
safePreventDefault(e);
|
||||
safeStopPropagation(e);
|
||||
|
||||
if (canvas.value && e.relatedTarget &&
|
||||
typeof e.relatedTarget === 'object' &&
|
||||
'nodeType' in e.relatedTarget) {
|
||||
// 使用 Element 类型而不是 x6 的 Node 类型
|
||||
if (!canvas.value.contains(e.relatedTarget as Element)) {
|
||||
isDraggingOver.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 处理放置
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
console.info('handleDrop', e);
|
||||
safePreventDefault(e);
|
||||
safeStopPropagation(e);
|
||||
isDraggingOver.value = false;
|
||||
|
||||
if (!currentTreeModelGraph.value) {
|
||||
message.error('请先选择或者创建行为树.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!graph.value || !canvas.value || !draggedNodeData.value) {
|
||||
message.error('无法放置节点,缺少必要数据');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取拖动的数据
|
||||
const template = draggedNodeData.value as NodeTemplate;
|
||||
|
||||
if (!hasElements(graph.value as Graph) && template.type !== 'root') {
|
||||
message.error('请先添加根节点.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasRootElementNode(graph.value as Graph) && template.type === 'root') {
|
||||
message.error('根节点已经存在.');
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算相对于画布的位置(考虑缩放)
|
||||
const rect = canvas.value.getBoundingClientRect();
|
||||
const scale = currentZoom.value || 1;
|
||||
const x = (e.clientX - rect.left) / scale;
|
||||
const y = (e.clientY - rect.top) / scale;
|
||||
|
||||
console.log('放置节点:', { ...template, x, y });
|
||||
|
||||
// 创建节点数据
|
||||
const settingTaskElement: SettingTaskNodeElement = createTaskNodeElementFromTemplate(template, { x, y });
|
||||
// 创建节点
|
||||
const settingTaskNode = createTaskNodeElement(settingTaskElement);
|
||||
console.info('create settingTaskNode: ', settingTaskElement, settingTaskNode);
|
||||
|
||||
// 将节点添加到画布
|
||||
graph.value?.addNode(settingTaskNode as any);
|
||||
console.log('节点已添加到画布:', settingTaskNode.id);
|
||||
|
||||
// 重置拖动数据
|
||||
draggedNodeData.value = null;
|
||||
} catch (error) {
|
||||
console.error('放置节点时出错:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const createElements = () => {
|
||||
if (graph.value && currentTreeModelGraph.value) {
|
||||
graph.value.clearCells();
|
||||
setTimeout(() => {
|
||||
nextTick(() => {
|
||||
try {
|
||||
if (currentTreeModelGraph.value && !currentTreeModelGraph.value?.graph) {
|
||||
currentTreeModelGraph.value.graph = {
|
||||
nodes: [],
|
||||
edges: [],
|
||||
};
|
||||
}
|
||||
const nodes = currentTreeModelGraph.value?.graph?.nodes ?? [];
|
||||
const edges = currentTreeModelGraph.value?.graph?.edges ?? [];
|
||||
|
||||
nodes.forEach(n => {
|
||||
const node = createTaskNodeElement(n);
|
||||
graph.value?.addNode(node as Node);
|
||||
});
|
||||
edges.forEach(g => {
|
||||
graph.value?.addEdge(g as any);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn('createElements', e);
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化X6画布
|
||||
const initGraph = () => {
|
||||
if (!canvas.value) {
|
||||
console.error('画布容器不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
graph.value = createCanvas(canvas.value);
|
||||
console.log('画布初始化成功');
|
||||
createElements();
|
||||
|
||||
// 监听缩放变化
|
||||
handleGraphEvent('scale', ({ sx }: { sx: number }) => {
|
||||
currentZoom.value = sx;
|
||||
});
|
||||
|
||||
handleGraphEvent('node:click', (args: any) => {
|
||||
const node = args.node as Node<NodeProperties>;
|
||||
const newElement = node.getData() as SettingTaskNodeElement;
|
||||
|
||||
selectedModelNode.value = node;
|
||||
selectedNodeTaskElement.value = JSON.parse(JSON.stringify(newElement || {})) as SettingTaskNodeElement;
|
||||
});
|
||||
|
||||
// 监听节点鼠标事件,显示/隐藏连接点
|
||||
handleGraphEvent('node:mouseenter', (_ctx: any) => {
|
||||
});
|
||||
|
||||
handleGraphEvent('node:mouseleave', (_ctx: any) => {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('初始化画布失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
nextTick(() => {
|
||||
resizeCanvas();
|
||||
});
|
||||
};
|
||||
|
||||
const init = () => {
|
||||
console.info('init');
|
||||
nextTick(() => {
|
||||
initGraph();
|
||||
window.addEventListener('resize', handleResize);
|
||||
console.log('节点挂载完成');
|
||||
});
|
||||
};
|
||||
|
||||
const handleUpdateElement = (element: SettingTaskNodeElement) => {
|
||||
// 更新选中的节点数据
|
||||
if (selectedModelNode.value) {
|
||||
selectedModelNode.value.replaceData(element);
|
||||
}
|
||||
console.info('handleUpdateElement', element);
|
||||
// 更新本地引用
|
||||
selectedNodeTaskElement.value = element;
|
||||
changed.value = true;
|
||||
};
|
||||
|
||||
const handleSelectTree = (treeModel: TreeModel) => {
|
||||
console.error('handleSelectTree', treeModel);
|
||||
findOneTreeById(treeModel.id).then(r => {
|
||||
if (r.data) {
|
||||
currentTreeModelGraph.value = r.data;
|
||||
createElements();
|
||||
} else {
|
||||
message.error(r.message ?? '行为树不存在.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const graphData: NodeGraph = resolveNodeGraph(graph.value as Graph);
|
||||
console.info('handleSave', graphData);
|
||||
if (!currentTreeModelGraph.value) {
|
||||
message.error('当前决策树不存在');
|
||||
return;
|
||||
}
|
||||
const newModel: TreeModelGraph = {
|
||||
...currentTreeModelGraph.value,
|
||||
graph: graphData,
|
||||
};
|
||||
let res = null;
|
||||
if (currentTreeModelGraph.value.id > 0) {
|
||||
res = createTree(newModel);
|
||||
} else {
|
||||
res = updateTree(newModel);
|
||||
}
|
||||
res.then(r => {
|
||||
if (r.code === 200) {
|
||||
message.success(r.message ?? '操作成功.');
|
||||
} else {
|
||||
message.error(r.message ?? '操作失败.');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
// 清理
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('resize', handleResize);
|
||||
if (graph.value) {
|
||||
try {
|
||||
graph.value.clearCells();
|
||||
} catch (error) {
|
||||
console.warn('销毁画布时出错:', error);
|
||||
}
|
||||
graph.value = null;
|
||||
console.log('画布已销毁');
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
currentTreeModelGraph,
|
||||
selectedNodeTaskElement,
|
||||
selectedModelNode,
|
||||
graph,
|
||||
canvas,
|
||||
zoomIn,
|
||||
zoomOut,
|
||||
fitToScreen,
|
||||
centerContent,
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
handleDragEnter,
|
||||
handleDragLeave,
|
||||
handleDrop,
|
||||
isDraggingOver,
|
||||
handleSave,
|
||||
handleUpdateElement,
|
||||
handleSelectTree,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
32
modeler/src/views/decision/header.vue
Normal file
32
modeler/src/views/decision/header.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<a-layout-header class="ks-layout-header">
|
||||
<a-flex>
|
||||
<div class="ks-layout-header-logo">
|
||||
<router-link :to="{path: '/app/decision/designer'}">行为决策树管理</router-link>
|
||||
</div>
|
||||
<div class="ks-layout-header-right">
|
||||
<a-space size="large">
|
||||
<span>{{ currentDateTime }}</span>
|
||||
</a-space>
|
||||
</div>
|
||||
</a-flex>
|
||||
</a-layout-header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onUnmounted, ref } from 'vue';
|
||||
import { formatDatetime } from '@/utils/datetime';
|
||||
|
||||
const currentDateTime = ref('');
|
||||
|
||||
const updateCurrentDateTime = () => {
|
||||
currentDateTime.value = formatDatetime(new Date());
|
||||
};
|
||||
|
||||
updateCurrentDateTime();
|
||||
const timer = setInterval(updateCurrentDateTime, 1000);
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(timer);
|
||||
});
|
||||
</script>
|
||||
24
modeler/src/views/decision/layout.vue
Normal file
24
modeler/src/views/decision/layout.vue
Normal file
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<Wrapper>
|
||||
<a-layout class="bg-transparent" style="background: transparent">
|
||||
<Header />
|
||||
<a-layout class="ks-layout-body">
|
||||
<slot name="body">
|
||||
<a-layout-sider class="ks-layout-sidebar" width="300">
|
||||
<slot name="sidebar" />
|
||||
</a-layout-sider>
|
||||
<a-layout-content class="ks-layout-main">
|
||||
<div class="ks-layout-container">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</a-layout-content>
|
||||
</slot>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</Wrapper>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Wrapper } from '@/components/wrapper';
|
||||
import Header from './header.vue';
|
||||
</script>
|
||||
217
modeler/src/views/decision/nodes-card.vue
Normal file
217
modeler/src/views/decision/nodes-card.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<a-collapse v-model:activeKey="activeKey" :accordion="true">
|
||||
<a-collapse-panel key="1">
|
||||
<template #header>
|
||||
<span class="ks-model-builder-title-icon icon-model"></span>控制节点
|
||||
</template>
|
||||
<div class="w-full h-full">
|
||||
<a-row>
|
||||
<a-col v-for="nm in controlTemplates" :span="12">
|
||||
<div
|
||||
:key="nm.id"
|
||||
:data-type="nm.type"
|
||||
class="ks-model-drag-item"
|
||||
@dragend="handleDragEnd"
|
||||
@dragstart="handleDragStart($event, nm)"
|
||||
>
|
||||
<img :alt="nm.name ?? ''" class="icon" src="@/assets/icons/model-4.svg" />
|
||||
<span class="desc">{{ nm.name }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="2">
|
||||
<template #header>
|
||||
<span class="ks-model-builder-title-icon icon-model"></span>条件节点
|
||||
</template>
|
||||
<div class="w-full h-full">
|
||||
<a-row>
|
||||
<a-col v-for="nm in conditionTemplates" :span="12">
|
||||
<div
|
||||
:key="nm.id"
|
||||
:data-type="nm.type"
|
||||
class="ks-model-drag-item"
|
||||
@dragend="handleDragEnd"
|
||||
@dragstart="handleDragStart($event, nm)"
|
||||
>
|
||||
<img :alt="nm.name ?? ''" class="icon" src="@/assets/icons/model-4.svg" />
|
||||
<span class="desc">{{ nm.name }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="3">
|
||||
<template #header>
|
||||
<span class="ks-model-builder-title-icon icon-model"></span>行为节点
|
||||
</template>
|
||||
<div class="w-full h-full">
|
||||
<a-row>
|
||||
<a-col v-for="nm in actionsTemplates" :span="12">
|
||||
<div
|
||||
:key="nm.id"
|
||||
:data-type="nm.type"
|
||||
class="ks-model-drag-item"
|
||||
@dragend="handleDragEnd"
|
||||
@dragstart="handleDragStart($event, nm)"
|
||||
>
|
||||
<img :alt="nm.name ?? ''" class="icon" src="@/assets/icons/model-4.svg" />
|
||||
<span class="desc">{{ nm.name }}</span>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
|
||||
<!-- <a-card class="ks-model-builder-card">-->
|
||||
<!-- <template #title>-->
|
||||
<!-- <span class="ks-model-builder-title-icon icon-model"></span>控制节点-->
|
||||
<!-- </template>-->
|
||||
<!-- <div-->
|
||||
<!-- v-for="nm in controlTemplates"-->
|
||||
<!-- :key="nm.id"-->
|
||||
<!-- :data-type="nm.type"-->
|
||||
<!-- class="ks-model-drag-item"-->
|
||||
<!-- >-->
|
||||
<!-- <img class="icon" src="@/assets/icons/model-4.svg" :alt="nm.name ?? ''"/>-->
|
||||
<!-- <span class="desc">{{ nm.name }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-card>-->
|
||||
|
||||
<!-- <a-card class="ks-model-builder-card">-->
|
||||
<!-- <template #title>-->
|
||||
<!-- <span class="ks-model-builder-title-icon icon-model"></span>条件节点-->
|
||||
<!-- </template>-->
|
||||
<!-- <div-->
|
||||
<!-- v-for="nm in conditionTemplates"-->
|
||||
<!-- :key="nm.id"-->
|
||||
<!-- :data-type="nm.type"-->
|
||||
<!-- class="ks-model-drag-item"-->
|
||||
<!-- >-->
|
||||
<!-- <img class="icon" src="@/assets/icons/model-4.svg" :alt="nm.name ?? ''"/>-->
|
||||
<!-- <span class="desc">{{ nm.name }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-card>-->
|
||||
|
||||
<!-- <a-card class="ks-model-builder-card">-->
|
||||
<!-- <template #title>-->
|
||||
<!-- <span class="ks-model-builder-title-icon icon-model"></span>行为节点-->
|
||||
<!-- </template>-->
|
||||
<!-- <div-->
|
||||
<!-- v-for="nm in actionsTemplates"-->
|
||||
<!-- :key="nm.id"-->
|
||||
<!-- :data-type="nm.type"-->
|
||||
<!-- class="ks-model-drag-item"-->
|
||||
<!-- >-->
|
||||
<!-- <img class="icon" src="@/assets/icons/model-4.svg" :alt="nm.name ?? ''"/>-->
|
||||
<!-- <span class="desc">{{ nm.name }}</span>-->
|
||||
<!-- </div>-->
|
||||
<!-- </a-card>-->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
import { defaultNodeTemplateData, defaultNodeTemplateQuery } from './constants';
|
||||
import type { NodeTemplate, NodeTemplateData, NodeTemplateQuery } from './types';
|
||||
import { findTemplatesByQuery } from './api';
|
||||
import { safePreventDefault, safeStopPropagation } from '@/utils/event';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['drag-item-start', 'drag-item-end'],
|
||||
setup(_props, { emit }) {
|
||||
|
||||
const activeKey = ref<number>(1);
|
||||
const templateData = ref<NodeTemplateData>({ ...defaultNodeTemplateData });
|
||||
const templateQuery = ref<NodeTemplateQuery>({ ...defaultNodeTemplateQuery });
|
||||
const isDraggingOver = ref(false);
|
||||
const draggedNodeData = ref<NodeTemplate | null>(null);
|
||||
|
||||
// 控制节点
|
||||
const controlTemplates = ref<NodeTemplate[]>([]);
|
||||
// 条件节点
|
||||
const conditionTemplates = ref<NodeTemplate[]>([]);
|
||||
// 行为节点
|
||||
const actionsTemplates = ref<NodeTemplate[]>([]);
|
||||
|
||||
const loadTress = () => {
|
||||
controlTemplates.value = [];
|
||||
conditionTemplates.value = [];
|
||||
actionsTemplates.value = [];
|
||||
|
||||
findTemplatesByQuery(templateQuery.value).then(r => {
|
||||
templateData.value = r.data;
|
||||
if (r.data.templates) {
|
||||
r.data.templates.forEach(tpl => {
|
||||
if (tpl.type === 'action') {
|
||||
if (tpl.parameter_defs && tpl.parameter_defs.length > 0) {
|
||||
actionsTemplates.value.push(tpl);
|
||||
}
|
||||
} else if (tpl.type === 'parallel' || tpl.type === 'sequence' || tpl.type === 'precondition') {
|
||||
conditionTemplates.value.push(tpl);
|
||||
} else {
|
||||
controlTemplates.value.push(tpl);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDragStart = (e: DragEvent, nm: NodeTemplate) => {
|
||||
draggedNodeData.value = nm;
|
||||
|
||||
if (e.dataTransfer) {
|
||||
e.dataTransfer.setData('text/plain', JSON.stringify(draggedNodeData.value));
|
||||
e.dataTransfer.effectAllowed = 'copyMove';
|
||||
|
||||
const dragPreview = document.createElement('div');
|
||||
dragPreview.textContent = draggedNodeData.value.name || '';
|
||||
dragPreview.style.cssText = `
|
||||
position: absolute;
|
||||
top: -1000px;
|
||||
padding: 6px 12px;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
`;
|
||||
document.body.appendChild(dragPreview);
|
||||
e.dataTransfer.setDragImage(dragPreview, dragPreview.offsetWidth / 2, dragPreview.offsetHeight / 2);
|
||||
emit('drag-item-start', nm, isDraggingOver.value, e);
|
||||
console.log('开始拖动:', nm);
|
||||
setTimeout(() => document.body.removeChild(dragPreview), 0);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = (e: DragEvent) => {
|
||||
safePreventDefault(e);
|
||||
safeStopPropagation(e);
|
||||
isDraggingOver.value = false;
|
||||
console.log('拖动结束');
|
||||
emit('drag-item-end', isDraggingOver.value, e);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadTress();
|
||||
});
|
||||
|
||||
return {
|
||||
activeKey,
|
||||
|
||||
controlTemplates,
|
||||
conditionTemplates,
|
||||
actionsTemplates,
|
||||
|
||||
handleDragStart,
|
||||
handleDragEnd,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
211
modeler/src/views/decision/properties.vue
Normal file
211
modeler/src/views/decision/properties.vue
Normal file
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<div class="ks-model-builder-right">
|
||||
<template v-if="currentElement">
|
||||
|
||||
<a-tabs v-model:activeKey="activeTopTabsKey" class="ks-model-builder-tabs settings-tab">
|
||||
<template #leftExtra>
|
||||
<span class="ks-model-builder-title-icon icon-input"></span>
|
||||
</template>
|
||||
<a-tab-pane key="1" tab="节点属性">
|
||||
<a-form
|
||||
autocomplete="off"
|
||||
layout="vertical"
|
||||
name="basic"
|
||||
style="padding-bottom:15px;"
|
||||
>
|
||||
<a-form-item label="节点名称">
|
||||
<a-input v-model:value="currentElement.name" :placeholder="currentElement.name" size="small" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="节点介绍">
|
||||
<a-textarea v-model:value="currentElement.description" :placeholder="currentElement.description" size="small" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider />
|
||||
|
||||
<a-form-item label="输入">
|
||||
<a-textarea v-model:value="currentElement.inputs" size="small" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="输出">
|
||||
<a-textarea v-model:value="currentElement.outputs" size="small" />
|
||||
</a-form-item>
|
||||
|
||||
<a-divider v-if="currentElement.settings && currentElement.settings.length > 0" />
|
||||
|
||||
<a-form-item v-for="setting in currentElement.settings" :label="setting.description">
|
||||
<a-input-number v-if="setting.data_type === 'double'" v-model:value="setting.default_value" :placeholder="setting.description" size="small" style="width:100%;" />
|
||||
<a-input v-else v-model:value="setting.default_value" :placeholder="setting.description" size="small" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- <a-tab-pane key="2" tab="外观">-->
|
||||
|
||||
<!-- </a-tab-pane>-->
|
||||
<!-- <a-tab-pane key="3" tab="系统">-->
|
||||
|
||||
<!-- </a-tab-pane>-->
|
||||
</a-tabs>
|
||||
|
||||
<a-tabs v-model:activeKey="activeBottomTabsKey" class="ks-model-builder-tabs parameters-tabs">
|
||||
<template #leftExtra>
|
||||
<span class="ks-model-builder-title-icon icon-input"></span>
|
||||
</template>
|
||||
<a-tab-pane key="1" tab="节点变量">
|
||||
<div class="w-full">
|
||||
<a-space>
|
||||
<a-button size="small" type="primary" @click="addVariable">添加</a-button>
|
||||
</a-space>
|
||||
<a-table
|
||||
:columns="actionSpaceColumns"
|
||||
:dataSource="currentElement.variables"
|
||||
:pagination="false"
|
||||
:scroll="{ x: 500 }"
|
||||
class="mt-1"
|
||||
row-key="key"
|
||||
size="small"
|
||||
style="overflow-y:auto;height:35vh;"
|
||||
>
|
||||
<template #bodyCell="{column, record, index}">
|
||||
<template v-if="column.dataIndex === 'index'">
|
||||
{{ index + 1 }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === '_actions'">
|
||||
<a-button
|
||||
class="btn-link-delete"
|
||||
danger
|
||||
size="small"
|
||||
type="text"
|
||||
@click="()=> removeVariable(record)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-input v-model:value="record[column.dataIndex]" size="small" />
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
|
||||
</template>
|
||||
|
||||
<a-tabs v-else :activeKey="'0'" class="ks-model-builder-tabs parameters-tabs empty">
|
||||
<template #leftExtra>
|
||||
<span class="ks-model-builder-title-icon icon-input"></span>
|
||||
</template>
|
||||
<a-tab-pane :key="'0'" tab="请选择或者创建决策树">
|
||||
<a-empty>
|
||||
<template #description>
|
||||
请选择或者创建决策树
|
||||
</template>
|
||||
</a-empty>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, type PropType, ref, watch } from 'vue';
|
||||
import { CheckOutlined } from '@ant-design/icons-vue';
|
||||
import type { ElementVariable, SettingTaskNodeElement } from './types';
|
||||
import type { Graph, Node, NodeProperties } from '@antv/x6';
|
||||
import { generateKey } from '@/utils/strings';
|
||||
|
||||
const actionSpaceColumns = [
|
||||
{ title: '序号', dataIndex: 'index', key: 'index', width: 40 },
|
||||
{ title: '变量名', dataIndex: 'name', key: 'name', width: 80 },
|
||||
{ title: '参数值', dataIndex: 'value', key: 'value', width: 80 },
|
||||
{ title: '单位', dataIndex: 'unit', key: 'unit', width: 80 },
|
||||
{ title: '操作', dataIndex: '_actions', key: '_actions', width: 60 },
|
||||
];
|
||||
|
||||
export default defineComponent({
|
||||
components: { CheckOutlined },
|
||||
props: {
|
||||
node: { type: [Object, null] as PropType<Node<NodeProperties> | null | undefined>, required: false },
|
||||
graph: { type: [Object, null] as PropType<Graph | null | undefined>, required: true },
|
||||
},
|
||||
emits: ['update-element'],
|
||||
setup(props, { emit }) {
|
||||
const activeTopTabsKey = ref<string>('1');
|
||||
const activeBottomTabsKey = ref<string>('1');
|
||||
const activeBottomTabs2Key = ref<string>('1');
|
||||
|
||||
const currentNode = ref<Node | null>(props.node ?? null);
|
||||
const currentElement = ref<SettingTaskNodeElement | null>(null);
|
||||
|
||||
const load = () => {
|
||||
};
|
||||
|
||||
const resolveNode = (n?: Node | null | undefined) => {
|
||||
currentNode.value = n ?? null;
|
||||
if (n) {
|
||||
const data = n.getData();
|
||||
currentElement.value = JSON.parse(JSON.stringify(data || {})) as SettingTaskNodeElement;
|
||||
} else {
|
||||
currentElement.value = null;
|
||||
}
|
||||
};
|
||||
|
||||
const addVariable = () => {
|
||||
if (!currentElement.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentElement.value.variables) {
|
||||
currentElement.value.variables = [];
|
||||
}
|
||||
currentElement.value?.variables.push({
|
||||
key: generateKey('variable'),
|
||||
name: null,
|
||||
value: null,
|
||||
defaults: null,
|
||||
unit: null,
|
||||
});
|
||||
};
|
||||
|
||||
const removeVariable = (row: ElementVariable) => {
|
||||
if (currentElement.value && currentElement.value.variables) {
|
||||
const filteredVars = currentElement.value.variables.filter(v => v.key !== row.key);
|
||||
currentElement.value.variables = [...filteredVars];
|
||||
}
|
||||
};
|
||||
|
||||
const updateNode = () => {
|
||||
if (currentNode.value && currentElement.value) {
|
||||
// 深拷贝当前元素数据
|
||||
const newElement = JSON.parse(JSON.stringify(currentElement.value)) as SettingTaskNodeElement;
|
||||
// 更新节点数据
|
||||
currentNode.value.replaceData(newElement);
|
||||
// 触发事件通知父组件
|
||||
emit('update-element', newElement);
|
||||
}
|
||||
};
|
||||
|
||||
watch(
|
||||
() => props.node,
|
||||
(n?: Node | null | undefined) => resolveNode(n),
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
watch(() => currentElement.value, () => updateNode(), { deep: true });
|
||||
|
||||
onMounted(() => load());
|
||||
|
||||
return {
|
||||
actionSpaceColumns,
|
||||
activeTopTabsKey,
|
||||
activeBottomTabsKey,
|
||||
activeBottomTabs2Key,
|
||||
currentElement,
|
||||
addVariable,
|
||||
removeVariable,
|
||||
// currentElementParameters,
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
98
modeler/src/views/decision/trees-card.vue
Normal file
98
modeler/src/views/decision/trees-card.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<template>
|
||||
<a-collapse v-model:activeKey="activeKey" :accordion="false">
|
||||
<a-collapse-panel key="1">
|
||||
<template #header>
|
||||
<span class="ks-model-builder-title-icon icon-model"></span>我的行为树
|
||||
</template>
|
||||
<a-list :data-source="treeModelsData.trees || []" size="small" style="min-height: 25vh">
|
||||
<template #renderItem="{ item }">
|
||||
<a-tooltip placement="right">
|
||||
<template #title>
|
||||
{{ item.description }}
|
||||
</template>
|
||||
<a-list-item @click="()=> handleSelect(item)">
|
||||
{{ item.name }}
|
||||
</a-list-item>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-list>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, onMounted, ref } from 'vue';
|
||||
import { defaultPaginationRequest, defaultTreeModelsData } from './constants';
|
||||
import { PlusOutlined } from '@ant-design/icons-vue';
|
||||
import type { ApiPaginationQuery } from '@/types';
|
||||
import type { TreeModel, TreeModelsData } from './types';
|
||||
import { findTreesByQuery } from './api';
|
||||
|
||||
export default defineComponent({
|
||||
emits: ['select-tree'],
|
||||
components: {
|
||||
PlusOutlined,
|
||||
},
|
||||
setup(_props, { emit }) {
|
||||
const treeModelsData = ref<TreeModelsData>({ ...defaultTreeModelsData });
|
||||
const treeModelsQuery = ref<ApiPaginationQuery>({ ...defaultPaginationRequest });
|
||||
const activeKey = ref<number>(1);
|
||||
const loadTress = () => {
|
||||
findTreesByQuery(treeModelsQuery.value).then(r => {
|
||||
treeModelsData.value = r.data;
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '名称',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
];
|
||||
|
||||
const handleSelect = (record: TreeModel) => {
|
||||
emit('select-tree', record);
|
||||
};
|
||||
|
||||
const customRow = (record: TreeModel) => {
|
||||
return {
|
||||
onClick: (event: any) => {
|
||||
emit('select-tree', record, event);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadTress();
|
||||
});
|
||||
|
||||
return {
|
||||
activeKey,
|
||||
treeModelsData,
|
||||
treeModelsQuery,
|
||||
loadTress,
|
||||
columns,
|
||||
customRow,
|
||||
handleSelect,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.create-tree-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
padding: 5px 5px;
|
||||
cursor: pointer;
|
||||
color: rgb(130 196 233);
|
||||
|
||||
&:hover {
|
||||
background: #0d2d4e;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
13
modeler/src/views/decision/types/index.ts
Normal file
13
modeler/src/views/decision/types/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './tree';
|
||||
export * from './template';
|
||||
export * from './parameter';
|
||||
export * from './node';
|
||||
73
modeler/src/views/decision/types/node.ts
Normal file
73
modeler/src/views/decision/types/node.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 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 { NullableString } from '@/types';
|
||||
import type { NodeSetting } from './parameter';
|
||||
|
||||
export type ElementStatus = 'default' | 'success' | 'failed' | 'running' | string | null
|
||||
|
||||
export interface ElementPosition {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface ElementVariable {
|
||||
key: NullableString;
|
||||
name: NullableString;
|
||||
value: NullableString;
|
||||
defaults: NullableString;
|
||||
unit: NullableString;
|
||||
}
|
||||
|
||||
export interface BaseElement {
|
||||
id: number;
|
||||
key: NullableString;
|
||||
type: NullableString;
|
||||
status: ElementStatus;
|
||||
}
|
||||
|
||||
export interface TaskNodeRect {
|
||||
width?: number;
|
||||
height?: number;
|
||||
x?: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export interface TaskNodeElement extends BaseElement {
|
||||
template: number;
|
||||
name: NullableString;
|
||||
description: NullableString;
|
||||
width: number;
|
||||
height: number;
|
||||
position: ElementPosition;
|
||||
inputs: any;
|
||||
outputs: any;
|
||||
variables: ElementVariable[];
|
||||
parameters: Record<any, any>;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SettingTaskNodeElement extends TaskNodeElement {
|
||||
settings: NodeSetting[];
|
||||
}
|
||||
|
||||
|
||||
export interface EdgeNodeElement extends BaseElement {
|
||||
source: NullableString;
|
||||
target: NullableString;
|
||||
attrs: Record<any, any>;
|
||||
router: Record<any, any>;
|
||||
connector: any;
|
||||
}
|
||||
|
||||
export interface NodeGraph {
|
||||
edges: EdgeNodeElement[];
|
||||
nodes: TaskNodeElement[];
|
||||
}
|
||||
20
modeler/src/views/decision/types/parameter.ts
Normal file
20
modeler/src/views/decision/types/parameter.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 { NullableString } from '@/types';
|
||||
|
||||
export interface NodeSetting {
|
||||
id: number;
|
||||
name: NullableString;
|
||||
type: NullableString;
|
||||
default_value: NullableString;
|
||||
data_type: NullableString;
|
||||
required: boolean;
|
||||
description: NullableString;
|
||||
}
|
||||
35
modeler/src/views/decision/types/template.ts
Normal file
35
modeler/src/views/decision/types/template.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 { ApiPaginationQuery, ApiResponse, NullableString } from '@/types';
|
||||
import type { NodeSetting } from './parameter';
|
||||
|
||||
export interface NodeTemplate {
|
||||
id: number;
|
||||
name: NullableString;
|
||||
type: NullableString;
|
||||
english_name: NullableString;
|
||||
description: NullableString;
|
||||
parameter_defs: NodeSetting[];
|
||||
}
|
||||
|
||||
export interface NodeTemplateData {
|
||||
templates: NodeTemplate[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface NodeTemplateQuery extends ApiPaginationQuery {
|
||||
include_params: boolean;
|
||||
type: NullableString;
|
||||
}
|
||||
|
||||
export interface NodeTemplatesResponse extends ApiResponse<NodeTemplateData> {
|
||||
|
||||
}
|
||||
|
||||
92
modeler/src/views/decision/types/tree.ts
Normal file
92
modeler/src/views/decision/types/tree.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* This file is part of the kernelstudio package.
|
||||
*
|
||||
* (c) 2014-2025 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 { ApiErrors, ApiPagination, ApiResponse, NullableString } from '@/types';
|
||||
import type { NodeGraph } from './node';
|
||||
|
||||
export type TreeModelStatus = string | 'active'
|
||||
|
||||
export type TreeNodeType = 'selector' | string;
|
||||
|
||||
// 获取树列表
|
||||
export interface TreeModel {
|
||||
id: number;
|
||||
name: NullableString;
|
||||
english_name: NullableString;
|
||||
description: NullableString;
|
||||
created_at: NullableString;
|
||||
updated_at: NullableString;
|
||||
node_count: number;
|
||||
status: TreeModelStatus;
|
||||
}
|
||||
|
||||
export interface TreeModelGraph extends TreeModel {
|
||||
graph: NodeGraph;
|
||||
}
|
||||
|
||||
// 所有行为树列表
|
||||
export interface TreeModelsData {
|
||||
trees: TreeModel[];
|
||||
pagination: ApiPagination;
|
||||
}
|
||||
|
||||
export interface TreeModelsResponse extends ApiResponse<TreeModelsData> {
|
||||
|
||||
}
|
||||
|
||||
export interface TreeModelDetailsResponse extends ApiResponse<TreeModelGraph> {
|
||||
|
||||
}
|
||||
|
||||
// 创建行为树
|
||||
export interface RootTreeNodeConfig {
|
||||
type: TreeNodeType;
|
||||
instance_name: NullableString;
|
||||
}
|
||||
|
||||
export interface RootTreeNode {
|
||||
id: number;
|
||||
name: NullableString;
|
||||
english_name: NullableString;
|
||||
created_at: NullableString;
|
||||
root_node_id: number;
|
||||
}
|
||||
|
||||
export interface CreateTreeModel {
|
||||
name: NullableString;
|
||||
english_name: NullableString;
|
||||
description: NullableString;
|
||||
template_id: number;
|
||||
root_node_config: RootTreeNodeConfig;
|
||||
}
|
||||
|
||||
export type CreateTreeModelData = RootTreeNode | ApiErrors
|
||||
|
||||
export interface CreateTreeModelResponse extends ApiResponse<CreateTreeModelData> {
|
||||
|
||||
}
|
||||
|
||||
// 行为树详细信息
|
||||
|
||||
export interface TreeNodeDetailRequest {
|
||||
tree_id: number;
|
||||
include_structure: boolean;
|
||||
include_parameters: boolean;
|
||||
}
|
||||
|
||||
export interface TreeNodeStructure {
|
||||
id: number;
|
||||
template_name: NullableString;
|
||||
instance_name: NullableString;
|
||||
children: TreeNodeStructure[];
|
||||
}
|
||||
|
||||
export interface TreeNodeDetail {
|
||||
tree: TreeModel;
|
||||
}
|
||||
146
modeler/src/views/decision/utils/node.ts
Normal file
146
modeler/src/views/decision/utils/node.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 { Edge, Graph, Node } from '@antv/x6';
|
||||
import type { EdgeNodeElement, NodeGraph, NodeTemplate, SettingTaskNodeElement, TaskNodeElement, TaskNodeRect } from '../types';
|
||||
import { generateKey } from '@/utils/strings.ts';
|
||||
|
||||
export const createTaskNodeElementFromTemplate = (
|
||||
template: NodeTemplate,
|
||||
rect?: TaskNodeRect,
|
||||
): SettingTaskNodeElement => {
|
||||
let realRect = { width: 200, height: 100, x: 0, y: 0, ...rect || {} };
|
||||
console.info('rect', rect);
|
||||
return {
|
||||
id: 0,
|
||||
key: generateKey(template.type),
|
||||
status: null,
|
||||
template: template.id,
|
||||
type: template.type,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
position: {
|
||||
x: realRect.x ?? 0,
|
||||
y: realRect.y ?? 0,
|
||||
},
|
||||
width: realRect.width,
|
||||
height: realRect.height,
|
||||
settings: JSON.parse(JSON.stringify(template.parameter_defs ?? [])),
|
||||
inputs: null,
|
||||
outputs: null,
|
||||
parameters: {},
|
||||
variables: [
|
||||
{
|
||||
key: generateKey('var_'),
|
||||
name: '范围',
|
||||
value: '1000',
|
||||
defaults: '1000',
|
||||
unit: 'KM',
|
||||
},
|
||||
{
|
||||
key: generateKey('var_'),
|
||||
name: '武器名称',
|
||||
value: '地对空导弹',
|
||||
defaults: '地对空导弹',
|
||||
unit: '个',
|
||||
},
|
||||
],
|
||||
} as SettingTaskNodeElement;
|
||||
};
|
||||
|
||||
export const createTaskNodeElement = (element: TaskNodeElement, width: number = 200, height: number = 100): any => {
|
||||
return {
|
||||
shape: 'task',
|
||||
id: element.key,
|
||||
position: {
|
||||
x: element.position?.x || 0,
|
||||
y: element.position?.y || 0,
|
||||
},
|
||||
size: {
|
||||
width: element.width ?? width,
|
||||
height: element.height ?? height,
|
||||
},
|
||||
attrs: {
|
||||
label: {
|
||||
text: element.name,
|
||||
},
|
||||
},
|
||||
data: element,
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveNodeTaskElements = (graph: Graph): TaskNodeElement[] => {
|
||||
const taskElements: TaskNodeElement[] = [];
|
||||
if (graph) {
|
||||
const nodes = graph?.getNodes() as Node[];
|
||||
if (nodes) {
|
||||
nodes.forEach(node => {
|
||||
const nodeData = node.getData() as TaskNodeElement;
|
||||
const newElement = {
|
||||
...nodeData,
|
||||
key: node.id,
|
||||
position: node.getPosition(),
|
||||
width: node.getSize().width,
|
||||
height: node.getSize().height,
|
||||
};
|
||||
taskElements.push(newElement);
|
||||
});
|
||||
}
|
||||
}
|
||||
return taskElements;
|
||||
};
|
||||
|
||||
export const resolveNodeEdgeElements = (graph: Graph): EdgeNodeElement[] => {
|
||||
const edgeElements: EdgeNodeElement[] = [];
|
||||
if (graph) {
|
||||
const graphEdges = graph?.getEdges() ?? [] as Edge[];
|
||||
if (graphEdges) {
|
||||
graphEdges.forEach(edge => {
|
||||
const nodeData = edge.getData() as TaskNodeElement;
|
||||
edgeElements.push({
|
||||
id: nodeData?.id ?? 0,
|
||||
key: edge.id,
|
||||
type: 'edge',
|
||||
status: nodeData?.status,
|
||||
source: edge.getSource() ? edge.getSource() as unknown as string : null,
|
||||
target: edge.getSource() ? edge.getTarget() as unknown as string : null,
|
||||
attrs: edge.getAttrs() ?? {},
|
||||
router: edge.getRouter() ?? {},
|
||||
connector: edge.getConnector() ?? null,
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
return edgeElements;
|
||||
};
|
||||
|
||||
export const resolveNodeGraph = (graph: Graph): NodeGraph => {
|
||||
const nodes: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
||||
const edges: EdgeNodeElement[] = resolveNodeEdgeElements(graph);
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
};
|
||||
|
||||
export const hasElements = (graph: Graph): boolean => {
|
||||
if (graph) {
|
||||
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
||||
return taskElements.length > 0;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
export const hasRootElementNode = (graph: Graph): boolean => {
|
||||
if (graph) {
|
||||
const taskElements: TaskNodeElement[] = resolveNodeTaskElements(graph);
|
||||
return taskElements.filter(e => e.type === 'root').length === 1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
Reference in New Issue
Block a user