Initial commit
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>博弈竞赛环境</title>
|
<title>决策管理</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app" class="w-full h-full"></div>
|
<div id="app" class="w-full h-full"></div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { type RouteRecordRaw } from 'vue-router';
|
import { type RouteRecordRaw } from 'vue-router';
|
||||||
import { routers } from '@/views/ai/router';
|
import { routers } from '@/views/behaviour/router';
|
||||||
|
|
||||||
export const routes: RouteRecordRaw[] = [
|
export const routes: RouteRecordRaw[] = [
|
||||||
...routers,
|
...routers,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { NullableString } from '@/types';
|
import type { NullableString } from '@/types';
|
||||||
|
import type { ModelParameters } from '@/views/ai/model/types';
|
||||||
|
|
||||||
export interface DraggableElement {
|
export interface DraggableElement {
|
||||||
id: number | null,
|
id: number | null,
|
||||||
@@ -20,3 +21,41 @@ export interface DraggableElement {
|
|||||||
children: DraggableElement[]
|
children: DraggableElement[]
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ModelElementPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelElementEdge {
|
||||||
|
key: NullableString;
|
||||||
|
sourceKey: NullableString;
|
||||||
|
sourceName: NullableString;
|
||||||
|
targetKey: NullableString;
|
||||||
|
targetName: NullableString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelBaseElement {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
position: ModelElementPosition;
|
||||||
|
category: NullableString;
|
||||||
|
element?: DraggableElement;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelElement extends ModelBaseElement {
|
||||||
|
// 连线
|
||||||
|
edges: ModelElementEdge[];
|
||||||
|
// 模型参数设置
|
||||||
|
parameters: ModelParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedGraphData {
|
||||||
|
nodes: ModelElement[];
|
||||||
|
edges: any[];
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Edge, Graph, Path, Selection } from '@antv/x6';
|
import { Edge, Graph, Path, Selection } from '@antv/x6';
|
||||||
import type { ModelElement } from '../model/types';
|
import type { ModelElement } from './element';
|
||||||
import type { Connecting } from '@antv/x6/lib/graph/options';
|
import type { Connecting } from '@antv/x6/lib/graph/options';
|
||||||
import {createLineOptions} from './line'
|
import { createLineOptions } from './line';
|
||||||
|
|
||||||
Graph.registerConnector(
|
Graph.registerConnector(
|
||||||
'sequenceFlowConnector',
|
'sequenceFlowConnector',
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import { type Dom, Graph, Node } from '@antv/x6';
|
|||||||
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
||||||
import { createGraphCanvas } from './graph';
|
import { createGraphCanvas } from './graph';
|
||||||
import { EventListener } from '@/utils/event';
|
import { EventListener } from '@/utils/event';
|
||||||
import type { ModelElement } from '../model/types';
|
import type { ModelElement } from './element';
|
||||||
|
|
||||||
// import {createLineOptions} from './line'
|
// import {createLineOptions} from './line'
|
||||||
|
|
||||||
export interface UseGraphCanvas {
|
export interface UseGraphCanvas {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
import { defineComponent, onMounted, onUnmounted, ref } from 'vue';
|
||||||
import { elementProps } from './props';
|
import { elementProps } from './props';
|
||||||
import type { ModelElement } from '../model/types';
|
import type { ModelElement } from './element';
|
||||||
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||||
import type { Graph } from '@antv/x6';
|
import type { Graph } from '@antv/x6';
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
import { Graph, Node } from '@antv/x6';
|
import { Graph, Node } from '@antv/x6';
|
||||||
import { type ExtractPropTypes, type PropType } from 'vue';
|
import { type ExtractPropTypes, type PropType } from 'vue';
|
||||||
import type { ModelElement } from '../model/types';
|
import type { ModelElement } from './element';
|
||||||
|
|
||||||
export const elementProps = {
|
export const elementProps = {
|
||||||
node: {
|
node: {
|
||||||
|
|||||||
@@ -10,20 +10,9 @@
|
|||||||
export const menuMap = [
|
export const menuMap = [
|
||||||
{
|
{
|
||||||
key: '0',
|
key: '0',
|
||||||
title: '工程空间',
|
title: '指挥决策规则库管理',
|
||||||
children: [
|
|
||||||
{
|
|
||||||
key: '0-1',
|
|
||||||
title: '工程管理',
|
|
||||||
path: '/app/ai/project/management',
|
path: '/app/ai/project/management',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: '0-2',
|
|
||||||
title: '新建/自定义',
|
|
||||||
path: '/app/ai/project/create',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: '1',
|
key: '1',
|
||||||
title: '智能体构建工具',
|
title: '智能体构建工具',
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
<a-layout-header class="ks-layout-header">
|
<a-layout-header class="ks-layout-header">
|
||||||
<a-flex>
|
<a-flex>
|
||||||
<div class="ks-layout-header-logo">
|
<div class="ks-layout-header-logo">
|
||||||
<router-link :to="{path: '/app/ai/project/management'}">博弈竞赛环境</router-link>
|
<router-link :to="{path: '/app/ai/project/management'}">决策管理</router-link>
|
||||||
</div>
|
|
||||||
<div class="ks-layout-header-actions">
|
|
||||||
<span class="dev top active">开发</span>
|
|
||||||
<span class="training bottom">训练</span>
|
|
||||||
<span class="apps top">博弈对抗</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- <div class="ks-layout-header-actions">-->
|
||||||
|
<!-- <span class="dev top active">开发</span>-->
|
||||||
|
<!-- <span class="training bottom">训练</span>-->
|
||||||
|
<!-- <span class="apps top">博弈对抗</span>-->
|
||||||
|
<!-- </div>-->
|
||||||
<div class="ks-layout-header-right">
|
<div class="ks-layout-header-right">
|
||||||
<a-space size="large">
|
<a-space size="large">
|
||||||
<span><QuestionCircleOutlined /> 帮助文档</span>
|
<span><QuestionCircleOutlined /> 帮助文档</span>
|
||||||
|
|||||||
@@ -4,16 +4,16 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
<a-flex>
|
<a-flex>
|
||||||
<span class="icon"></span>
|
<span class="icon"></span>
|
||||||
<span class="text">工程空间</span>
|
<!-- <span class="text">工程空间</span>-->
|
||||||
</a-flex>
|
</a-flex>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="w-full p-3">
|
<!-- <div class="w-full p-3">-->
|
||||||
<a-flex class="w-full">
|
<!-- <a-flex class="w-full">-->
|
||||||
<a-input style="width: 70%" placeholder="导入"></a-input>
|
<!-- <a-input style="width: 70%" placeholder="导入"></a-input>-->
|
||||||
<a-button style="margin-left: auto">导入</a-button>
|
<!-- <a-button style="margin-left: auto">导入</a-button>-->
|
||||||
</a-flex>
|
<!-- </a-flex>-->
|
||||||
</div>
|
<!-- </div>-->
|
||||||
|
|
||||||
<a-menu
|
<a-menu
|
||||||
v-model:openKeys="openKeys"
|
v-model:openKeys="openKeys"
|
||||||
|
|||||||
16
modeler/src/views/behaviour/config.ts
Normal file
16
modeler/src/views/behaviour/config.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* 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 const menuMap = [
|
||||||
|
{
|
||||||
|
key: '0',
|
||||||
|
title: '指挥决策规则库管理',
|
||||||
|
path: '/app/behaviour/rules',
|
||||||
|
},
|
||||||
|
];
|
||||||
61
modeler/src/views/behaviour/graph/element.ts
Normal file
61
modeler/src/views/behaviour/graph/element.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
* 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 { ModelParameters } from '@/views/ai/model/types';
|
||||||
|
|
||||||
|
export interface DraggableElement {
|
||||||
|
id: number | null,
|
||||||
|
key?: NullableString,
|
||||||
|
name: NullableString,
|
||||||
|
description: NullableString,
|
||||||
|
category: NullableString,
|
||||||
|
draggable: boolean,
|
||||||
|
parent?: DraggableElement,
|
||||||
|
children: DraggableElement[]
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelElementPosition {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelElementEdge {
|
||||||
|
key: NullableString;
|
||||||
|
sourceKey: NullableString;
|
||||||
|
sourceName: NullableString;
|
||||||
|
targetKey: NullableString;
|
||||||
|
targetName: NullableString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelBaseElement {
|
||||||
|
key: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
position: ModelElementPosition;
|
||||||
|
category: NullableString;
|
||||||
|
element?: DraggableElement;
|
||||||
|
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ModelElement extends ModelBaseElement {
|
||||||
|
// 连线
|
||||||
|
edges: ModelElementEdge[];
|
||||||
|
// 模型参数设置
|
||||||
|
parameters: ModelParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SavedGraphData {
|
||||||
|
nodes: ModelElement[];
|
||||||
|
edges: any[];
|
||||||
|
}
|
||||||
177
modeler/src/views/behaviour/graph/graph.ts
Normal file
177
modeler/src/views/behaviour/graph/graph.ts
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Edge, Graph, Path, Selection } from '@antv/x6';
|
||||||
|
import type { ModelElement } from './element';
|
||||||
|
import type { Connecting } from '@antv/x6/lib/graph/options';
|
||||||
|
import { createLineOptions } from './line';
|
||||||
|
|
||||||
|
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> => {
|
||||||
|
const lineOptions = createLineOptions();
|
||||||
|
return {
|
||||||
|
snap: true, // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
|
||||||
|
allowBlank: false, // 是否允许连接到画布空白位置的点,默认为 true
|
||||||
|
allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
|
||||||
|
highlight: true, // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
|
||||||
|
connector: 'smooth',
|
||||||
|
connectionPoint: 'anchor', // 指定连接点,默认值为 boundary。
|
||||||
|
anchor: 'center',
|
||||||
|
// validateMagnet({ magnet }) {
|
||||||
|
// return magnet.getAttribute('port-group') !== 'top'
|
||||||
|
// },
|
||||||
|
// 验证连接
|
||||||
|
createEdge(this: Graph) {
|
||||||
|
const edge: Edge = this.createEdge({
|
||||||
|
shape: 'edge',
|
||||||
|
...lineOptions, // 应用动画配置
|
||||||
|
attrs: lineOptions.attrs,
|
||||||
|
animation: lineOptions.animation,
|
||||||
|
markup: lineOptions.markup,
|
||||||
|
})
|
||||||
|
return edge;
|
||||||
|
},
|
||||||
|
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 ModelElement;
|
||||||
|
|
||||||
|
// 根节点不能作为子节点
|
||||||
|
if (targetData.type === 'startEvent') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 新增核心逻辑:检查源节点是否已有出边(已连接其他节点)
|
||||||
|
// const hasOutgoingEdge = this.getOutgoingEdges(sourceCell);
|
||||||
|
// if (hasOutgoingEdge && hasOutgoingEdge.length > 1) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 检查是否已存在相同连接
|
||||||
|
// const edges: Edge[] = this.getEdges();
|
||||||
|
// const existingConnection = edges.find(edge =>
|
||||||
|
// edge.getSourceCell() === sourceCell &&
|
||||||
|
// edge.getTargetCell() === targetCell,
|
||||||
|
// );
|
||||||
|
//
|
||||||
|
// return !existingConnection;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
modifiers: 'ctrl',
|
||||||
|
factor: 1.1,
|
||||||
|
maxScale: 1.5,
|
||||||
|
minScale: 0.5,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return graph;
|
||||||
|
|
||||||
|
};
|
||||||
250
modeler/src/views/behaviour/graph/hooks.ts
Normal file
250
modeler/src/views/behaviour/graph/hooks.ts
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
/*
|
||||||
|
* 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, Node } from '@antv/x6';
|
||||||
|
import type { NodeViewPositionEventArgs } from '@antv/x6/es/view/node/type';
|
||||||
|
import { createGraphCanvas } from './graph';
|
||||||
|
import { EventListener } from '@/utils/event';
|
||||||
|
import type { ModelElement } from './element';
|
||||||
|
|
||||||
|
// import {createLineOptions} from './line'
|
||||||
|
|
||||||
|
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('edge:removed', edge); // 添加此行
|
||||||
|
});
|
||||||
|
|
||||||
|
graph.value.on('edge:added', ({ edge }) => {
|
||||||
|
// const lineOptions = createLineOptions();
|
||||||
|
// edge.setAttrs(lineOptions.attrs);
|
||||||
|
// edge.set(lineOptions.animation);
|
||||||
|
// edge.setMarkup(lineOptions.markup);
|
||||||
|
emitGraphEvent('edge:added', 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 ModelElement;
|
||||||
|
const targetData = targetNode.getData() as ModelElement;
|
||||||
|
|
||||||
|
// 将连线存储到节点数据中
|
||||||
|
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.replaceData({ ...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/behaviour/graph/index.ts
Normal file
11
modeler/src/views/behaviour/graph/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';
|
||||||
47
modeler/src/views/behaviour/graph/line.ts
Normal file
47
modeler/src/views/behaviour/graph/line.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 { Shape } from '@antv/x6';
|
||||||
|
|
||||||
|
export const createLineOptions = (): any => {
|
||||||
|
return {
|
||||||
|
markup: [
|
||||||
|
{
|
||||||
|
tagName: 'circle',
|
||||||
|
selector: 'marker',
|
||||||
|
attrs: {
|
||||||
|
stroke: 'none',
|
||||||
|
r: 3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...Shape.Edge.getMarkup() as any,
|
||||||
|
],
|
||||||
|
attrs: {
|
||||||
|
line: {
|
||||||
|
stroke: '#5da0df',
|
||||||
|
strokeWidth: 2,
|
||||||
|
strokeDasharray: ' ',
|
||||||
|
strokeDashoffset: 0,
|
||||||
|
},
|
||||||
|
marker: {
|
||||||
|
fill: '#5da0df',
|
||||||
|
atConnectionRatio: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
animation: [
|
||||||
|
[
|
||||||
|
{ 'attrs/marker/atConnectionRatio': 1 },
|
||||||
|
{
|
||||||
|
duration: 2000,
|
||||||
|
iterations: Infinity,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
322
modeler/src/views/behaviour/graph/node.vue
Normal file
322
modeler/src/views/behaviour/graph/node.vue
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
<template>
|
||||||
|
<a-dropdown :trigger="['contextmenu']" @openChange="handleVisibleChange">
|
||||||
|
<a-card
|
||||||
|
:class="[
|
||||||
|
'ks-designer-node',
|
||||||
|
`ks-designer-${element?.category ?? 'model'}-node`
|
||||||
|
]"
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<template #title>
|
||||||
|
<a-space>
|
||||||
|
<span class="ks-designer-node-icon"></span>
|
||||||
|
<span class="ks-designer-node-title">{{ element?.name ?? '-' }}</span>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 节点内容区域 -->
|
||||||
|
<div class="w-full" v-if="element?.category !== 'component'">
|
||||||
|
<div class="ks-designer-node-content">
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in element?.element?.children || []"
|
||||||
|
:key="item.id || index"
|
||||||
|
class="ks-designer-node-row"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:data-port="`in-${item.id || index}`"
|
||||||
|
:title="`入桩: ${item.name}`"
|
||||||
|
class="port port-in"
|
||||||
|
magnet="passive"
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<!-- child名称 -->
|
||||||
|
<div class="ks-designer-node-name">
|
||||||
|
{{ item.name }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧出桩:只能作为连线源 -->
|
||||||
|
<div
|
||||||
|
:data-port="`out-${item.id || index}`"
|
||||||
|
:title="`出桩: ${item.name}`"
|
||||||
|
class="port port-out"
|
||||||
|
magnet="active"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!(element?.element?.children && element?.element?.children?.length > 0)" class="ks-designer-node-row">
|
||||||
|
<div class="port port-in" data-port="in-0" magnet="passive"></div>
|
||||||
|
<div class="ks-designer-node-name">
|
||||||
|
{{ element?.name ?? '-' }}
|
||||||
|
</div>
|
||||||
|
<div class="port port-out" data-port="out-0" magnet="active"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="w-full" v-else>
|
||||||
|
<p>隐藏纬度: {{ element?.parameters?.hiddenLatitude ?? '-' }}</p>
|
||||||
|
<p>激活函数: {{ element?.parameters?.activationFunction ?? '-' }}</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 { ModelElement } from './element';
|
||||||
|
import { DeleteOutlined, SettingOutlined } from '@ant-design/icons-vue';
|
||||||
|
import type { Graph } from '@antv/x6';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'ModelElement',
|
||||||
|
components: {
|
||||||
|
SettingOutlined,
|
||||||
|
DeleteOutlined,
|
||||||
|
},
|
||||||
|
props: elementProps,
|
||||||
|
setup(_props) {
|
||||||
|
const element = ref<ModelElement | null>(
|
||||||
|
_props.node ? (_props.node.getData() as ModelElement) : 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 ModelElement;
|
||||||
|
} else {
|
||||||
|
element.value = null;
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
isMenuVisible.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
_props.node?.on('change:data', handleDataChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
_props.node?.off('change:data', handleDataChange);
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
element,
|
||||||
|
handleMenuClick,
|
||||||
|
handleVisibleChange,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less">
|
||||||
|
.ks-designer-node {
|
||||||
|
background: linear-gradient(150deg, #093866 1%, #1f69b3 55%);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.ant-card-head {
|
||||||
|
border: 0;
|
||||||
|
height: 38px;
|
||||||
|
min-height: 38px;
|
||||||
|
border-radius: 0;
|
||||||
|
color: #ddd;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: normal;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
left: 8px;
|
||||||
|
top: 13px;
|
||||||
|
background: url('@/assets/icons/model-4.svg') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-title {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
color: #fff;
|
||||||
|
height: calc(100% - 38px);
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 8px 15px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-top: 1px solid #195693;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ks-designer-task-node {
|
||||||
|
background: linear-gradient(150deg, #20421b 1%, #4a6646 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #466741;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/m-02.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ks-designer-input-node {
|
||||||
|
background: linear-gradient(150deg, #083058 1%, #1e5d9b 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #105ca7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/icon-model-input.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ks-designer-action-node {
|
||||||
|
background: linear-gradient(150deg, #343207 1%, #485010 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #59550e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/bg-fk-point.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.ks-designer-component-node {
|
||||||
|
background: linear-gradient(150deg, #06226b 1%, #1a43a7 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #26448c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ks-designer-control-node {
|
||||||
|
background: linear-gradient(150deg, #1d4f32 1%, #326a5d 55%);
|
||||||
|
|
||||||
|
.ant-card-body {
|
||||||
|
border-top: 1px solid #326a5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ks-designer-node-icon {
|
||||||
|
background: url('@/assets/icons/bg-model-builder-card-title.png') center / 100% 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接桩容器样式
|
||||||
|
.ks-designer-node-content {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px; // 每个child行之间的间距
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每个child行(包含左右桩+文本)
|
||||||
|
.ks-designer-node-row {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
position: relative;
|
||||||
|
height: 24px; // 固定行高,保证桩对齐
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接桩基础样式
|
||||||
|
.port {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: crosshair;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 0 0 2px rgb(74 114 214 / 80%);
|
||||||
|
z-index: 10; // 确保桩在最上层
|
||||||
|
// X6 标记为可连线的磁体
|
||||||
|
magnet: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 左侧入桩样式
|
||||||
|
.port-in {
|
||||||
|
background-color: #093866; // 青色:入桩
|
||||||
|
margin-right: 8px; // 与文本的间距
|
||||||
|
//border: 1px solid #093866;
|
||||||
|
// X6 只能作为连线目标(入)
|
||||||
|
magnet: passive;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
background: url('@/assets/icons/point.svg') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧出桩样式
|
||||||
|
.port-out {
|
||||||
|
margin-left: 8px; // 与文本的间距
|
||||||
|
margin-right: 5px;
|
||||||
|
// X6 只能作为连线源(出)
|
||||||
|
magnet: active;
|
||||||
|
box-shadow: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: block;
|
||||||
|
background: url('@/assets/icons/arrow-right.svg') center / 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 节点文本样式
|
||||||
|
.ks-designer-node-name {
|
||||||
|
flex: 1; // 占满中间空间
|
||||||
|
line-height: 24px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
51
modeler/src/views/behaviour/graph/ports.ts
Normal file
51
modeler/src/views/behaviour/graph/ports.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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const createPort = (name: string = 'top', args: Record<any, any> = { dx: 0 }) => {
|
||||||
|
return {
|
||||||
|
position: { name: name, args: args },
|
||||||
|
attrs: {
|
||||||
|
circle: {
|
||||||
|
r: 4, // 大小
|
||||||
|
magnet: true,
|
||||||
|
stroke: '#1b5e9f', // 边框颜色
|
||||||
|
strokeWidth: 1, // 边框大小
|
||||||
|
fill: '#3578bf', // 填充颜色
|
||||||
|
style: {
|
||||||
|
visibility: 'visible', // 是否可见
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createPorts = (top: boolean = true, right: boolean = true, bottom: boolean = true, left: boolean = true) => {
|
||||||
|
const groups: any = {};
|
||||||
|
const items: any = [];
|
||||||
|
if (top) {
|
||||||
|
groups['top'] = createPort('top');
|
||||||
|
items.push({ group: 'top', id: 'top' });
|
||||||
|
}
|
||||||
|
if (right) {
|
||||||
|
groups['right'] = createPort('right');
|
||||||
|
items.push({ group: 'right', id: 'right' });
|
||||||
|
}
|
||||||
|
if (bottom) {
|
||||||
|
groups['bottom'] = createPort('bottom');
|
||||||
|
items.push({ group: 'bottom', id: 'bottom' });
|
||||||
|
}
|
||||||
|
if (left) {
|
||||||
|
groups['left'] = createPort('left');
|
||||||
|
items.push({ group: 'left', id: 'left' });
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
groups: groups,
|
||||||
|
items: items,
|
||||||
|
};
|
||||||
|
};
|
||||||
29
modeler/src/views/behaviour/graph/props.ts
Normal file
29
modeler/src/views/behaviour/graph/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 { ModelElement } from './element';
|
||||||
|
|
||||||
|
export const elementProps = {
|
||||||
|
node: {
|
||||||
|
type: Object as PropType<Node>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
graph: {
|
||||||
|
type: Object as PropType<Graph>,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
element: {
|
||||||
|
type: Object as PropType<ModelElement>,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ElementPropsType = ExtractPropTypes<typeof elementProps>
|
||||||
50
modeler/src/views/behaviour/graph/register.ts
Normal file
50
modeler/src/views/behaviour/graph/register.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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';
|
||||||
|
|
||||||
|
export const registerNodeElement = () => {
|
||||||
|
console.info('registerNodeElement');
|
||||||
|
register({
|
||||||
|
shape: 'model',
|
||||||
|
component: ModelElement,
|
||||||
|
width: 120,
|
||||||
|
attrs: {
|
||||||
|
body: {
|
||||||
|
stroke: 'transparent',
|
||||||
|
strokeWidth: 0,
|
||||||
|
fill: 'transparent',
|
||||||
|
rx: 4,
|
||||||
|
ry: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dragging: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
// 配置端口识别规则,
|
||||||
|
portMarkup: [
|
||||||
|
{
|
||||||
|
tagName: 'div',
|
||||||
|
selector: 'port-body',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// 告诉 X6 如何识别 Vue 组件内的端口
|
||||||
|
portAttribute: 'data-port',
|
||||||
|
// ports: createPorts(),
|
||||||
|
});
|
||||||
|
};
|
||||||
77
modeler/src/views/behaviour/header.vue
Normal file
77
modeler/src/views/behaviour/header.vue
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout-header class="ks-layout-header">
|
||||||
|
<a-flex>
|
||||||
|
<div class="ks-layout-header-logo">
|
||||||
|
<router-link :to="{path: '/app/ai/project/management'}">决策管理</router-link>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="ks-layout-header-actions">-->
|
||||||
|
<!-- <span class="dev top active">开发</span>-->
|
||||||
|
<!-- <span class="training bottom">训练</span>-->
|
||||||
|
<!-- <span class="apps top">博弈对抗</span>-->
|
||||||
|
<!-- </div>-->
|
||||||
|
<div class="ks-layout-header-right">
|
||||||
|
<a-space size="large">
|
||||||
|
<span><QuestionCircleOutlined /> 帮助文档</span>
|
||||||
|
<span>{{ currentDateTime }}</span>
|
||||||
|
</a-space>
|
||||||
|
<a-space style="margin-left: 20px;cursor: pointer">
|
||||||
|
<a-dropdown trigger="click">
|
||||||
|
<a-avatar style="background: #132f6c">
|
||||||
|
{{ displayName?.charAt(0) }}
|
||||||
|
</a-avatar>
|
||||||
|
<template #overlay>
|
||||||
|
<a-menu @click="handleLogout">
|
||||||
|
<a-menu-item key="logout">
|
||||||
|
退出登录
|
||||||
|
</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</template>
|
||||||
|
</a-dropdown>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</a-flex>
|
||||||
|
</a-layout-header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, onUnmounted, ref } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { QuestionCircleOutlined } from '@ant-design/icons-vue';
|
||||||
|
import { formatDatetime } from '@/utils/datetime';
|
||||||
|
import { useUserSession } from '@/hooks';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const session = useUserSession();
|
||||||
|
const avatar = ref<string | null>(null);
|
||||||
|
|
||||||
|
avatar.value = session?.details.value?.user?.avatar ?? null;
|
||||||
|
|
||||||
|
const displayName = computed((): string => {
|
||||||
|
let value = session?.details.value?.user?.nickName ?? session?.details.value?.user?.userName;
|
||||||
|
if (value) {
|
||||||
|
return value as string;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentDateTime = ref('');
|
||||||
|
|
||||||
|
const updateCurrentDateTime = () => {
|
||||||
|
currentDateTime.value = formatDatetime(new Date());
|
||||||
|
};
|
||||||
|
|
||||||
|
updateCurrentDateTime();
|
||||||
|
const timer = setInterval(updateCurrentDateTime, 1000);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
session.logout().then(() => {
|
||||||
|
router.push({
|
||||||
|
path: '/signin',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timer);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
47
modeler/src/views/behaviour/layout.vue
Normal file
47
modeler/src/views/behaviour/layout.vue
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<template>
|
||||||
|
<Wrapper>
|
||||||
|
<a-layout :class="['bg-transparent', collapsed ? 'sidebar-collapsed' : '']" style="background: transparent;transition: all 0.2s, background 0s;">
|
||||||
|
<Header />
|
||||||
|
<a-layout class="ks-layout-body">
|
||||||
|
<slot name="body">
|
||||||
|
<Sidebar v-if="!collapsed"/>
|
||||||
|
<a-layout-content class="ks-layout-main">
|
||||||
|
<div class="ks-layout-container">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</a-layout-content>
|
||||||
|
</slot>
|
||||||
|
</a-layout>
|
||||||
|
</a-layout>
|
||||||
|
|
||||||
|
<a-float-button
|
||||||
|
shape="circle"
|
||||||
|
type="primary"
|
||||||
|
:style="{
|
||||||
|
left: '30px',
|
||||||
|
}"
|
||||||
|
@click="()=> collapsed = !collapsed"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<a-tooltip>
|
||||||
|
<template #title>
|
||||||
|
{{collapsed ? '展开' : '收起'}}菜单
|
||||||
|
</template>
|
||||||
|
<MenuUnfoldOutlined v-if="collapsed"/>
|
||||||
|
<MenuFoldOutlined v-else/>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
</a-float-button>
|
||||||
|
|
||||||
|
</Wrapper>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import {ref} from 'vue'
|
||||||
|
import {MenuUnfoldOutlined, MenuFoldOutlined} from '@ant-design/icons-vue';
|
||||||
|
import Sidebar from './sidebar.vue';
|
||||||
|
import Header from './header.vue';
|
||||||
|
import { Wrapper } from '@/components/wrapper';
|
||||||
|
|
||||||
|
const collapsed = ref<boolean>(false);
|
||||||
|
</script>
|
||||||
58
modeler/src/views/behaviour/menu.vue
Normal file
58
modeler/src/views/behaviour/menu.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<template v-if="menu.children && menu.children.length > 0">
|
||||||
|
<a-sub-menu :key="menu.key">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ menu.title }}</span>
|
||||||
|
</template>
|
||||||
|
<Menu
|
||||||
|
v-for="child in menu.children"
|
||||||
|
:key="child.key"
|
||||||
|
:menu="child"
|
||||||
|
@click="handleChildClick"
|
||||||
|
/>
|
||||||
|
</a-sub-menu>
|
||||||
|
</template>
|
||||||
|
<a-menu-item
|
||||||
|
v-else
|
||||||
|
:key="menu.key"
|
||||||
|
@click="handleLeafClick"
|
||||||
|
>
|
||||||
|
<span class="ks-menu-item-label">{{ menu.title }}</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
// 定义菜单项的类型(与父组件保持一致)
|
||||||
|
export interface MenuItem {
|
||||||
|
key: string;
|
||||||
|
title: string;
|
||||||
|
path?: string;
|
||||||
|
type?: string;
|
||||||
|
children?: MenuItem[];
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义组件的 props
|
||||||
|
const props = defineProps<{
|
||||||
|
menu: MenuItem; // 单个菜单项数据
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// 定义组件的事件
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'click', menuItem: MenuItem): void; // 点击事件,传递菜单项
|
||||||
|
}>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理叶子菜单点击
|
||||||
|
*/
|
||||||
|
const handleLeafClick = () => {
|
||||||
|
emit('click', props.menu);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理子菜单点击(递归传递)
|
||||||
|
*/
|
||||||
|
const handleChildClick = (menuItem: MenuItem) => {
|
||||||
|
emit('click', menuItem);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
47
modeler/src/views/behaviour/router.ts
Normal file
47
modeler/src/views/behaviour/router.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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 RouteRecordRaw, type RouteRecordRedirect } from 'vue-router';
|
||||||
|
|
||||||
|
export const routers: RouteRecordRaw[] = [
|
||||||
|
{
|
||||||
|
name: 'index',
|
||||||
|
path: '/',
|
||||||
|
redirect: '/app/behaviour/rules',
|
||||||
|
meta: {
|
||||||
|
hidden: true,
|
||||||
|
},
|
||||||
|
} as RouteRecordRedirect,
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'signin',
|
||||||
|
path: '/signin',
|
||||||
|
meta: {
|
||||||
|
title: '登录',
|
||||||
|
},
|
||||||
|
component: () => import('@/views/signin.vue'),
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
meta: {
|
||||||
|
title: '指挥决策规则库管理',
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
name: 'app-behaviour-rules',
|
||||||
|
path: '/app/behaviour/rules',
|
||||||
|
meta: {
|
||||||
|
title: '指挥决策规则库管理',
|
||||||
|
},
|
||||||
|
component: () => import('@/views/behaviour/rules/management.vue'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
20
modeler/src/views/behaviour/rules/management.vue
Normal file
20
modeler/src/views/behaviour/rules/management.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<Layout>
|
||||||
|
|
||||||
|
<a-card class="ks-page-card ks-cards-wrapper">
|
||||||
|
|
||||||
|
<template #title>
|
||||||
|
<a-space>
|
||||||
|
<span class="point"></span>
|
||||||
|
<span class="text">指挥决策规则库管理</span>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</a-card>
|
||||||
|
|
||||||
|
</Layout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Layout from '../layout.vue'
|
||||||
|
</script>
|
||||||
169
modeler/src/views/behaviour/sidebar.vue
Normal file
169
modeler/src/views/behaviour/sidebar.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<a-layout-sider class="ks-layout-sidebar" width="250">
|
||||||
|
<a-card class="ks-layout-sidebar-card">
|
||||||
|
<template #title>
|
||||||
|
<a-flex>
|
||||||
|
<span class="icon"></span>
|
||||||
|
<!-- <span class="text">工程空间</span>-->
|
||||||
|
</a-flex>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-menu
|
||||||
|
v-model:openKeys="openKeys"
|
||||||
|
v-model:selectedKeys="selectedKeys"
|
||||||
|
class="ks-sidebar-menu"
|
||||||
|
mode="inline"
|
||||||
|
>
|
||||||
|
<!-- 遍历所有根菜单,区分父菜单和一级可点击菜单 -->
|
||||||
|
<template v-for="menuItem in menuList" :key="menuItem.key">
|
||||||
|
<!-- 有子菜单的根菜单 -->
|
||||||
|
<a-sub-menu v-if="menuItem.children && menuItem.children.length > 0" :key="menuItem.key">
|
||||||
|
<template #title>
|
||||||
|
<span>{{ menuItem.title }}</span>
|
||||||
|
</template>
|
||||||
|
<!-- 导入独立的递归组件 -->
|
||||||
|
<MenuItemRecursive
|
||||||
|
v-for="child in menuItem.children"
|
||||||
|
:key="child.key"
|
||||||
|
:menu="child"
|
||||||
|
@click="handleClick"
|
||||||
|
/>
|
||||||
|
</a-sub-menu>
|
||||||
|
|
||||||
|
<!-- 无子菜单的一级可点击菜单 -->
|
||||||
|
<a-menu-item
|
||||||
|
v-else
|
||||||
|
@click="() => handleClick(menuItem)"
|
||||||
|
>
|
||||||
|
<span class="ks-menu-item-label">{{ menuItem.title }}</span>
|
||||||
|
</a-menu-item>
|
||||||
|
</template>
|
||||||
|
</a-menu>
|
||||||
|
</a-card>
|
||||||
|
</a-layout-sider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import MenuItemRecursive, { type MenuItem } from './menu.vue';
|
||||||
|
import { menuMap } from './config';
|
||||||
|
|
||||||
|
// 获取当前路由实例
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// 选中的叶子节点key集合(仅保留叶子节点key)
|
||||||
|
const selectedKeys = ref<string[]>([]);
|
||||||
|
// 展开的父菜单key集合
|
||||||
|
const openKeys = ref<string[]>([]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建菜单数据:过滤隐藏项,补全路由标题
|
||||||
|
*/
|
||||||
|
const menuList = computed<MenuItem[]>(() => {
|
||||||
|
// 递归处理菜单(支持多级)
|
||||||
|
const processMenu = (menu: MenuItem): MenuItem => {
|
||||||
|
// 过滤子菜单的隐藏项
|
||||||
|
const children = menu.children?.map(child => processMenu(child)).filter(child => !child.hidden) || [];
|
||||||
|
// 从路由匹配标题(如需启用,取消注释并调整逻辑)
|
||||||
|
// const matchedRoute = routes.find(item => item.path === menu.path);
|
||||||
|
// const title = matchedRoute?.meta?.title || menu.title;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...menu,
|
||||||
|
title: menu.title, // 暂时用原标题,如需路由标题可替换上面注释的行
|
||||||
|
hidden: menu.hidden || false,
|
||||||
|
children: children.length > 0 ? children : undefined, // 空数组转为undefined,简化渲染逻辑
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理根菜单并过滤隐藏项
|
||||||
|
return menuMap
|
||||||
|
.map(menu => processMenu(menu))
|
||||||
|
.filter(menu => !menu.hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据当前路由路径匹配对应的菜单key
|
||||||
|
* @param path 当前路由路径
|
||||||
|
* @returns 选中的key和需要展开的key
|
||||||
|
*/
|
||||||
|
const getMenuKeyByPath = (path: string): { selectedKey: string[], openKey: string[] } => {
|
||||||
|
const selectedKey: string[] = [];
|
||||||
|
const openKey: string[] = [];
|
||||||
|
|
||||||
|
// 递归查找匹配的菜单
|
||||||
|
const findMenu = (menuItems: MenuItem[], parentKeys: string[] = []): boolean => {
|
||||||
|
for (const menu of menuItems) {
|
||||||
|
// 如果当前菜单是父菜单,先记录展开key
|
||||||
|
if (menu.children && menu.children.length > 0) {
|
||||||
|
const newParentKeys = [...parentKeys, menu.key];
|
||||||
|
// 递归查找子菜单
|
||||||
|
const isMatched = findMenu(menu.children, newParentKeys);
|
||||||
|
if (isMatched) {
|
||||||
|
openKey.push(...newParentKeys);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 叶子节点,匹配路径
|
||||||
|
if (menu.path === path) {
|
||||||
|
selectedKey.push(menu.key);
|
||||||
|
openKey.push(...parentKeys);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始查找根菜单
|
||||||
|
const isFound = findMenu(menuList.value);
|
||||||
|
|
||||||
|
// 未匹配到则默认选中第一个可点击菜单
|
||||||
|
if (!isFound && menuList.value.length > 0) {
|
||||||
|
const firstLeaf = (() => {
|
||||||
|
const findFirstLeaf = (items: MenuItem[]): MenuItem | null => {
|
||||||
|
for (const item of items) {
|
||||||
|
if (!item.children || item.children.length === 0) return item;
|
||||||
|
const leaf = findFirstLeaf(item.children);
|
||||||
|
if (leaf) return leaf;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return findFirstLeaf(menuList.value);
|
||||||
|
})();
|
||||||
|
if (firstLeaf) selectedKey.push(firstLeaf.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { selectedKey, openKey };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化菜单选中/展开状态
|
||||||
|
*/
|
||||||
|
const initMenuState = (path: string) => {
|
||||||
|
const { selectedKey, openKey } = getMenuKeyByPath(path);
|
||||||
|
selectedKeys.value = selectedKey;
|
||||||
|
openKeys.value = [...new Set(openKey)]; // 去重,避免重复展开
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 菜单点击事件处理(兼容一级/多级子菜单)
|
||||||
|
*/
|
||||||
|
const handleClick = (menuItem: MenuItem) => {
|
||||||
|
router.push({ path: menuItem.path });
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面初始化时执行
|
||||||
|
initMenuState(route.path);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 监听路由变化,自动更新菜单选中/展开状态
|
||||||
|
*/
|
||||||
|
watch(
|
||||||
|
() => route.path,
|
||||||
|
(newPath) => initMenuState(newPath),
|
||||||
|
{ immediate: true, deep: false }, // 无需deep,path是字符串
|
||||||
|
);
|
||||||
|
</script>
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ks-signin-wrapper">
|
<div class="ks-signin-wrapper">
|
||||||
<a-card class="ks-signin-card">
|
<a-card class="ks-signin-card">
|
||||||
<template #title>博弈竞赛环境</template>
|
<template #title>决策管理</template>
|
||||||
<a-form
|
<a-form
|
||||||
:model="formState"
|
:model="formState"
|
||||||
name="basic"
|
name="basic"
|
||||||
|
|||||||
Reference in New Issue
Block a user